-
Notifications
You must be signed in to change notification settings - Fork 16
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
feat: Support bubbling up of events from evented children on dataclasses #298
base: main
Are you sure you want to change the base?
Conversation
Codecov ReportAll modified and coverable lines are covered by tests ✅
Additional details and impacted files@@ Coverage Diff @@
## main #298 +/- ##
=========================================
Coverage 100.00% 100.00%
=========================================
Files 21 21
Lines 2062 2103 +41
=========================================
+ Hits 2062 2103 +41 ☔ View full report in Codecov by Sentry. |
CodSpeed Performance ReportMerging #298 will not alter performanceComparing Summary
|
@brisvag, you expressed interest in this a while back. Not sure if it's still something you care about, but have a look at the API above if you're inclined. @Czaki, if you want to review as well, it's welcome. @ndxmrb as well, since you recently expressed interest, let me know if it would work for you. |
Hey! Excited to see this picked up again :) I did a quick-and-dirty test with old code from napari/napari#4474. Maybe I converted things wrong, but I don't think it's working with containers a the moment. I wasn't sure how to use the EventedModel (if it's possible) with nested eventes, so I switched to dataclasses like in your example. from dataclasses import dataclass, field
from psygnal import evented
from psygnal.containers import EventedList, EventedDict, EventedSet
@evented
@dataclass
class A:
x: int = 1
@evented(connect_child_events=True)
@dataclass
class M:
a: EventedList = field(default_factory=lambda: EventedList([1, 2, 3]))
b: EventedDict = field(default_factory=lambda: EventedDict({'a': 1, 'b': 2}))
c: EventedSet = field(default_factory=lambda: EventedSet({'x', 'y', 'z'}))
d: A = field(default_factory=A)
m = M()
m.events.connect(print)
m.a[1] = 12
m.a = [0, 2]
m.b['a'] = 3
m.b = {'x': 11}
m.c.add('w')
m.c = {1}
m.d.x = 12
m.d = {'x': 1} I would expect 8 events, but I only get 5: EmissionInfo(
signal=<_DataclassFieldSignalInstance 'a' on <SignalGroup 'MSignalGroup' with 4 signals>>,
args=([0, 2], EventedList([1, 12, 3])),
attr_name=None
)
EmissionInfo(
signal=<_DataclassFieldSignalInstance 'b' on <SignalGroup 'MSignalGroup' with 4 signals>>,
args=({'x': 11}, EventedDict({'a': 3, 'b': 2})),
attr_name=None
)
EmissionInfo(
signal=<_DataclassFieldSignalInstance 'c' on <SignalGroup 'MSignalGroup' with 4 signals>>,
args=({1}, EventedSet({'w', 'z', 'x', 'y'})),
attr_name=None
)
EmissionInfo(
signal=<SignalRelay on A(x=12)>,
args=(EmissionInfo(signal=<_DataclassFieldSignalInstance 'x' on <SignalGroup 'ASignalGroup' with 1 signals>>, args=(12, 1), attr_name=None),),
attr_name='d'
)
EmissionInfo(
signal=<_DataclassFieldSignalInstance 'd' on <SignalGroup 'MSignalGroup' with 4 signals>>,
args=({'x': 1}, A(x=12)),
attr_name=None
) |
K thanks. This was mostly focusing on the models themselves... I'll go back and check all the evented containers too. (We do eventually want them to work) |
ok @brisvag, evented containers should be working now. Added a test at from dataclasses import dataclass, field
from rich import print
from psygnal import evented
from psygnal.containers import EventedDict, EventedList, EventedSet
@evented
@dataclass
class A:
x: int = 1
@evented(connect_child_events=True)
@dataclass
class M:
a: EventedList = field(default_factory=lambda: EventedList([1, 2, 3]))
b: EventedDict = field(default_factory=lambda: EventedDict({"a": 1, "b": 2}))
c: EventedSet = field(default_factory=lambda: EventedSet({"x", "y", "z"}))
d: A = field(default_factory=A)
m = M()
m.events.connect(lambda e: print(e.flatten()))
m.a[1] = 12
m.a = [0, 2]
m.b["a"] = 3
m.b = {"x": 11}
m.c.add("w")
m.c = {1}
m.d.x = 12
m.d = {"x": 1} prints EmissionInfo(
signal=<SignalInstance 'changed' on <SignalGroup 'ListEvents' with 9 signals>>,
args=(1, 2, 12),
loc=('a', 'changed')
)
EmissionInfo(
signal=<_DataclassFieldSignalInstance 'a' on <SignalGroup 'MSignalGroup' with 4 signals>>,
args=([0, 2], EventedList([1, 12, 3])),
loc=('a',)
)
EmissionInfo(
signal=<SignalInstance 'changing' on <SignalGroup 'DictEvents' with 6 signals>>,
args=('a',),
loc=('b', 'changing')
)
EmissionInfo(
signal=<SignalInstance 'changed' on <SignalGroup 'DictEvents' with 6 signals>>,
args=('a', 1, 3),
loc=('b', 'changed')
)
EmissionInfo(
signal=<_DataclassFieldSignalInstance 'b' on <SignalGroup 'MSignalGroup' with 4 signals>>,
args=({'x': 11}, EventedDict({'a': 3, 'b': 2})),
loc=('b',)
)
EmissionInfo(
signal=<SignalInstance 'items_changed' on <SignalGroup 'SetEvents' with 1 signals>>,
args=(('w',), ()),
loc=('c', 'items_changed')
)
EmissionInfo(
signal=<_DataclassFieldSignalInstance 'c' on <SignalGroup 'MSignalGroup' with 4 signals>>,
args=({1}, EventedSet({'z', 'w', 'x', 'y'})),
loc=('c',)
)
EmissionInfo(
signal=<_DataclassFieldSignalInstance 'x' on <SignalGroup 'ASignalGroup' with 1 signals>>,
args=(12, 1),
loc=('d', 'x')
)
EmissionInfo(
signal=<_DataclassFieldSignalInstance 'd' on <SignalGroup 'MSignalGroup' with 4 signals>>,
args=({'x': 1}, A(x=12)),
loc=('d',)
) |
(todo: bubble up from evented containers inside of evented containers) |
Great! How does one enable this for |
with patience young padawan 😂 🧘 |
a more general and less sarcastic answer: @dataclass
class MyThing:
events: ClassVar[SignalGroupDescriptor] = SignalGroupDescriptor() and that one object is able to inspect the class on which it is defined and create a SignalGroup with events for all the fields. The psygnal.EventedModel still uses the older napari-style metaclass structure, and therefore has some degree of logic duplication. So, in the short time, implementing this for EventedModels means just going through and doing the corresponding stuff in the EventedModel that I've done here for SignalGroupDescriptor. Ultimately, it would be even better to get EventedModel just using SignalGroupDescriptor itself. That's possible, but we need to maintain support (at least at first) for all the property stuff on evented model. There too, I think it would be good to check in with what pydantic has done in v2 to support properties, and ideally deprecate the features in psygnal's evented model. anyway, that's the general requirement |
I was hoping for Maybe I didn't fully understand the details of your explanation above, but I guess this is about computed fields? So providing something like the |
Exactly, pydantic v1 didn't allow It was a nice feature to have, but it's also just one more thing to "keep working" as pydantic updates itself. Now that pydantic has added support for property.setter (via their computed properties), I think it would be great to get rid of the extra logic to "allow_property_setters" in psygnal's EventedModel. and (you have it right) simply figure out how to pass on the
I promise it will! Doing it on dataclasses first is just a good way to set up all the machinery inside of psygnal, and adding it to the EventedModel won't be too difficult. will happen soon |
mostly a note to self: this is more or less ready to go, but i want to reconsider what we're putting in the |
@tlambert03 just stumbled on this. It's exciting! Let me know if you want to pair up at some point in case you want someone to talk things out with. |
@jni, the primary thing that I need to decide here is the API of that for example, here is the same example as above, but with comments putting "what happened" into natural language. Note the slight difference depending on the type of signal ( # item 1 of attribute a changed from 2 to 12
m.a[1] = 12
EmissionInfo(
signal=<SignalInstance 'changed' on <SignalGroup 'ListEvents' with 9 signals>>,
args=(1, 2, 12),
loc=('a', 'changed')
)
# attribute a changed from [1, 12, 3] to [0, 2]
m.a = [0, 2]
EmissionInfo(
signal=<_DataclassFieldSignalInstance 'a' on <SignalGroup 'MSignalGroup' with 4 signals>>,
args=([0, 2], EventedList([1, 12, 3])),
loc=('a',)
)
# item 'a' of attribute b changed from 1 to 3
m.b["a"] = 3
EmissionInfo(
signal=<SignalInstance 'changing' on <SignalGroup 'DictEvents' with 6 signals>>,
args=('a',),
loc=('b', 'changing')
)
EmissionInfo(
signal=<SignalInstance 'changed' on <SignalGroup 'DictEvents' with 6 signals>>,
args=('a', 1, 3),
loc=('b', 'changed')
)
# attribute b changed from {'a': 1, 'b': 2} to {'x': 11}
m.b = {"x": 11}
EmissionInfo(
signal=<_DataclassFieldSignalInstance 'b' on <SignalGroup 'MSignalGroup' with 4 signals>>,
args=({'x': 11}, EventedDict({'a': 3, 'b': 2})),
loc=('b',)
)
# attribute c had item 'w' added to the set
m.c.add("w")
EmissionInfo(
signal=<SignalInstance 'items_changed' on <SignalGroup 'SetEvents' with 1 signals>>,
args=(('w',), ()),
loc=('c', 'items_changed')
)
# attribute c was changed from {'z', 'w', 'x', 'y'} to {1}
m.c = {1}
EmissionInfo(
signal=<_DataclassFieldSignalInstance 'c' on <SignalGroup 'MSignalGroup' with 4 signals>>,
args=({1}, EventedSet({'z', 'w', 'x', 'y'})),
loc=('c',)
)
# attribute x on attribute d changed from 1 to 12
m.d.x = 12
EmissionInfo(
signal=<_DataclassFieldSignalInstance 'x' on <SignalGroup 'ASignalGroup' with 1 signals>>,
args=(12, 1),
loc=('d', 'x')
)
# attribute d changed from {'x': 12} to {'x': 1}
m.d = {"x": 1}
EmissionInfo(
signal=<_DataclassFieldSignalInstance 'd' on <SignalGroup 'MSignalGroup' with 4 signals>>,
args=({'x': 1}, A(x=12)),
loc=('d',)
) I want to unify those things somehow so that the consumer doesn't need to do too much conditional logic every time to determine what actually happened. It might require changing the signals on the containers (which would be breaking) or just changing the semantics of your thoughts on that would be very welcome |
I felt the same way. Additionally, for example in
vs
I need to infer from Disclaimer: I stiched the second example together from the others above, so it might be wrong... Also, I saw that on the first of your examples we get |
also "evented" (as determined by the `is_evented` function in this module, | ||
which returns True if the class has been decorated with `@evented`, or if it | ||
has a SignalGroupDescriptor) to the group on the parent object. By default | ||
False. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
... True?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
(Which btw I think should be the default... I think it takes a lot of experience with events to even notice that there's no bubbling up by default, let alone grok it. I think most users would expect bubbling to automagically work.)
""" | ||
|
||
signal: SignalInstance | ||
args: tuple[Any, ...] | ||
loc: str | int | None | tuple[int | str, ...] = None |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looking at the earlier example, which had attr_name
here, I find that easier to think about than loc
. This is especially true if you add dictionaries into the mix — so now we don't know from the type whether this is an index, a key, or an attribute name. What do you think about having three optionals:
- attr
- index
- key
Of course, now I move on to read about "flatten"... 😅
You could make a lightweight "Loc" object that could contain each of the keys, then you could have a sequence of Loc objects? The nice thing about this option is that you could write an accessor that gets you the right object given the parent and the sequence of locs.
I'm also not super keen on the "loc" name. "from"? "emitted_by"? "child"?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What about "origin"?
I think "loc" would be fine though...
picking up from #169 ... starting over since lots has changed
prints