-
-
Notifications
You must be signed in to change notification settings - Fork 424
Description
Hello,
I'm using attrs with subclasses of persistent.Persistent, which allows for persisting python objects in ZODB (Zope DB http://www.zodb.org). This is a pretty popular library, which inherently would benefit from being able to persist somewhat immutable objects, think LineItems in an Order for a Shop.
Quick test, failing
import attr
from persistent import Persistent
@attr.s(frozen=True)
class TestAttrs(Persistent):
val: str = attr.ib(default='TestDC')
import ZODB
from ZODB.MappingStorage import MappingStorage
db = ZODB.DB(MappingStorage())
conn = db.open()
root = conn.root()
root['test'] = TestAttrs('test value')
import transaction
transaction.commit()Which currently results in the following:
/Library/Frameworks/Python.framework/Versions/3.7/lib/python3.7/site-packages/attr/_make.py in _frozen_setattrs(self, name, value)
386 Attached to frozen classes as __setattr__.
387 """
--> 388 raise FrozenInstanceError()
389
390
FrozenInstanceError:
After some research, I was able to determine that for full functionality, ZODB persistent objects need the ability to write to attributes prefixed with _p_ representing persistent state in the db, and _v_ for volatile attributes that won't be persisted, used for instance with a caching decorator, etc.
With that information, I wrote a decorator that monkeypatches attr._make._frozen_setattrs and attr._make._frozen_delattrs to allow for modifying attributes beginning with _p_ and _v_.
import sys
import attr._make
from attr.exceptions import FrozenInstanceError
_WHITELISTED_PREFIXES = ('_p_', '_v_')
def frozen_wrapper(func, override):
def wrapper(self, *args):
name = args[0]
if name.startswith(_WHITELISTED_PREFIXES):
sup = super(type(self), self)
return getattr(sup, override)(*args)
else:
return func(self, *args)
return wrapper
sys.modules['attr._make']._frozen_setattrs = frozen_wrapper(
sys.modules['attr._make']._frozen_setattrs, '__setattr__')
sys.modules['attr._make']._frozen_delattrs = frozen_wrapper(
sys.modules['attr._make']._frozen_delattrs, '__delattr__')The following test now works
import attr
from persistent import Persistent
@attr.s(frozen=True)
class TestAttrs(Persistent):
val: str = attr.ib(default='TestDC')
import ZODB
from ZODB.MappingStorage import MappingStorage
db = ZODB.DB(MappingStorage())
conn = db.open()
root = conn.root()
root['test'] = TestAttrs('test value')
import transaction
transaction.commit()So obviously this is pretty hacky and I would love if there was an officially supported, generic way of hooking into the frozen class's __setattr__ and __delattr__ methods.
Although I think a module level tuple of whitelisted/blacklisted prefixes would work well in this case, I was thinking something similar to this will likely be requested in the future again, to work with another package, so providing a more flexible hook might be best.
Perhaps the ability to provide a decorator, or replacement functions would provide as much functionality as anyone would need down the line. I can volunteer my time to help implement this, but would like to open this issue to see what other people think about all of this. Such an interface could be generic and allow for added hooks, configuration in the future.