Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

A way to hook into frozen class setattr, delattr functions? #378

Closed
joeblackwaslike opened this issue May 7, 2018 · 4 comments
Closed
Labels

Comments

@joeblackwaslike
Copy link

joeblackwaslike commented May 7, 2018

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.

@hynek
Copy link
Member

hynek commented Jun 16, 2018

Um that’s quite the specific wish. You want ostensibly frozen classes?

@hynek
Copy link
Member

hynek commented May 14, 2020

You may be interested in #645 – would that help you?

@hynek
Copy link
Member

hynek commented Jul 10, 2020

…and implemented in #660. Feedback welcome.

@hynek
Copy link
Member

hynek commented Jul 22, 2020

I believe this is fixed.

@hynek hynek closed this as completed Jul 22, 2020
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

3 participants