python-attrs / attrs

Python Classes Without Boilerplate
https://www.attrs.org/
MIT License
5.25k stars 367 forks source link

Attrs support for publish/subscribe #1100

Open bdanofsky opened 1 year ago

bdanofsky commented 1 year ago

Attrs has nice support for validation and conversion. That same concept could/should be extended to post setattr() phase of attribute updates. This would allow for publish / subscribe systems on attribute updates.

In theory this should be a trivial feature enhancement to allow an additional hook.

hynek commented 1 year ago

Are you sure it's necessary to run after and can't be just part of on_setattr?

bdanofsky commented 1 year ago

I think the answer is no but let me spell out my use case: 1) User sets a new attribute value (call this attrs attribute abc) 2) Publish is called when setattr is called on abc a. stack is inspected to determine if abc depends on other attrs attributes b. If abc depends on other attrs attributes then their relationship is stored as part of my publish/subscribe system. example: abc = 8 * xyz # where xyz is another attrs attribute. c. Lookup if another attrs attribute depends on abc if so then that attribute is updated. example: qrs = abc / 128 # when abc updates then qrs updates with new value.
This is done doing exec('qrs = abc/128') Note in step C there is an implied getattr() on abc which requires the new value

if Publish is called before abc actually has a new value then all the dependencies (in my example qrs) will be assigned based on the old value.

I was able to work around this issue by using a custom setattr . FYI that workflow in attrs is a bit buggy or was when I implemented the code. That said if you feel this feature is not something you wish to support I understand.
It seems like an obvious feature though.

jamesmurphy-mc commented 1 year ago

I think the issue here is that on_setattr doesn't provide any mechanism to do something after a new value is set, only to intercept a value before it is set. There are many things one might want to do immediately after setting a value including:

Instead of specific pub/sub support, would it make sense to add a post_setattr hook? So the semantics would be


def set_dirty(obj, attribute, value):
    obj._dirty=True

@define
class A:
    x : int = field(default=0, post_setattr=set_dirty)
    _dirty: bool = False

# Mostly equivalent to
x_attr = ...
psa_attrs = {'x':  (x_attr, set_dirty)}
class A:
    def __init__(x: int=0, dirty=False):
        self.x = x
        self._dirty=dirty

    def __setattr__(self, name, val):
        object.__setattr__(self, name, val)
        try:
            a, hook = psa_attrs[name]
        except KeyError:
            pass
        else:
            hook(self, a, val)
bdanofsky commented 1 year ago

I'd be very happy with a post_setattr feature.