-
Notifications
You must be signed in to change notification settings - Fork 85
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
[PoC][EEP-3] User-facing expression for defining target traits to observe #969
Conversation
For reference, |
Updated main comment to add a section for examples that cannot be supported by the current |
I think we want to commit it (in spite of the usual rules about not committing generated files), and ideally have an For sure we want to include the generated parser in any source distribution, else people installing from PyPI would have to have |
This reverts commit ee6a376.
From offline discussion, the first version is not going to have recursion support. We may have to add it later if it becomes obvious that the feature is well missed. For future reference, this commit removes all the code and tests required for recursion support: ee6a376 Just to note an experience from working on this branch: The consideration to support recursion really drove how this branch evolved. There were two possible implementations at the start. After the attempt to try dealing with path comparisons involving cycles, it became clear that one of the two implementations was a dead end (or just became extremely difficult to work with). That said, it does not require a massive amount of code (excluding test code) to support recursion after I have settled on the one implementation that will support it. |
Another note... because
This wasn't what I initially had planned for...but maybe it is still desirable. This PoC is aiming at discussing the API and experimenting with various implementations/tooling, and we have done both. I will be closing this PR in the next few days if there are no more discussions and experimentation needed here. |
My apologies I did not reply to this. Renaming
|
I tried to put together the expressions here, minus the recursion support, plus the notifiers implemented in other PRs, plus all the plumbing needed on |
Ah, that hadn't occurred to me. I think |
Sounds good to me! |
) | ||
|
||
|
||
def observe(object, expression, handler): |
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.
API note: I think it would make sense to have an overloaded signature: if expression is a string, then we should parse
it to get the expression.
I generally don't like overloaded API's like this, but for a top-level, frequently-used method, I think it is appropriate.
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.
Agreed. It is fairly easy to exercise parse
once only at the layer closest to the user. In this other mammoth fully-integrated branch, I tried to do this once only on HasTraits.observe
instance method:
Lines 2118 to 2142 in 3f71d81
def observe(self, handler, expression, *, remove=False, dispatch="same"): | |
""" Attach or detach event handler that would fire when one or many | |
traits change. | |
Pararameters | |
------------ | |
handler : callable(event) | |
A callable that will receive the change event when the observed | |
trait changes. | |
expression : str or Expression | |
A description of what traits are being observed. | |
remove : boolean, optional | |
Whether to remove the event handler. | |
dispatch : str, optional | |
Option for how the handler is dispatched. | |
""" | |
if isinstance(expression, str): | |
expression = _observers_api.parse(expression) | |
_observers_api.observe( | |
object=self, | |
expression=expression, | |
handler=handler, | |
remove=remove, | |
dispatcher=_observer_dispatchers[dispatch], | |
) |
And then the Expression
gets converted to ObserverGraph
as you go deeper into the stack.
traits/traits/observers/observe.py
Lines 21 to 52 in 3f71d81
def observe( | |
object, expression, handler, | |
*, remove=False, dispatcher=dispatch_same): | |
""" Observer or unobserve traits on an object. | |
Parameters | |
---------- | |
object : IObservable | |
An object to be observed. | |
expression : Expression | |
An object describing what traits are being observed. | |
handler : callable(event) | |
User-defined callable to handle change events. | |
``event`` is an object representing the change. | |
Its type and content depends on the change. | |
remove : boolean, optional | |
If true, remove notifiers. i.e. unobserve the traits. | |
dispatcher : callable(callable, event) | |
Callable for dispatching the user-defined handler, i.e. dispatching | |
callback on a different thread. | |
""" | |
# ``expression`` can be anything with a method ``as_graphs`` that yields | |
# ObserverGraph. Hence ObserverGraph can be supplied as ``expression``. | |
for graph in expression.as_graphs(): | |
_add_or_remove_notifiers( | |
object=object, | |
graph=graph, | |
handler=handler, | |
target=object, | |
dispatcher=dispatcher, | |
remove=remove, | |
) |
i.e. no
isinstance(expression, str)
anywhere else.
I am sorry that the naming has changed in this other massive branch. ObserverGraph
there is ObserverPath
here...
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.
Yep, that looks right.
@@ -0,0 +1,314 @@ | |||
|
|||
|
|||
def _is_not_none(value): |
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.
from traits.trait_base import not_none
|
||
from poc.expressions import trait, recursive, metadata | ||
from poc.observe import ( | ||
_is_not_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.
Similarly, this is available in traits.trait_base
, as not_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.
It's a bit bike-sheddy, but using a simple lambda v: v is not None
seems more readable to me - it's understandable from the immediate context (so the reader has no need to look up the function to check that it's doing what they think it is). The not_none
in traits.trait_base
feels like clutter.
|
||
Parameters | ||
---------- | ||
filter : callable(str, TraitType) -> boolean |
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.
I have been thinking about exposing object
to the user as well. So the callable will have a signature callable(HasTraits, str, TraitType)
. The HasTraits object is available: https://github.com/enthought/traits/pull/942/files#diff-b70bb1540b20e1ffe710b12a7774b069R343-R345
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.
This could be useful, but it makes the requirement below of producing the same result more difficult to guarantee.
In particular if you pass the object you could use this for metadata
(assuming you aren't already somewhere).
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.
Right... in order to cleanup observers/notifiers, the result of this filter
for a given set of input should always be the same. Exposing object
risks violating this requirement and maybe a bug trap.
Yes I agree this metadata
can reuse this filter_
method.
Thank you @corranwebster and @mdickinson for your feedback! These will be taken into account in the future PR targeting #1024, and we can continue with more discussion there. |
Related to #977
This PR is targeting the Observation Mini-Language section in EEP 3. This is another proof-of-concept PR that will be closed without merging. It is open for discussion.
This PR proposes an expression approach for describing what traits are to be observed, without introducing new syntax that are not already part of Python language, and without any need for parsing a string. This also copied over data structures introduced in another proof-of-concept PR (#942).
Benefits:
Cost:
Edited: This PR also includes a text parser to translate the existing mini-language to the expression.
Examples for features supported by
on_trait_change
t("age")
Notify when
age
changes.t("age") | t("number")
Notify when
age
ornumber
changes.Equivalent to
"[age, number]"
in theon_trait_change
mini-language.t("child", notify=False).t("age")
Notify when
age
on achild
changes but only notify whenchild
is reassigned.Equivalent to
"child:age"
in theon_trait_change
mini-language.t("children").list_items().t("age")
Notify for changes to a trait
age
on any items inside a list namedchildren
. Notify for reassignment of the list and mutation of the list as well.This is equivalent to
["children.age", "children_items.age"]
in the on_trait_change mini-language.t("children", notify=False).list_items().t("age")
Similar to the above, but not notify on reassignment on
children
.This is equivalent to
["children_items.age"]
in theon_trait_change
mini-language.t("child").then( t("age") | t("name") )
Notify changes when
child
changes, or when eitherage
orname
on achild
changes.t("root").recursive( t("left") | t("right") ).t("value")
This matches a trait named
value
on a trait namedleft
orright
, matched recursively on another trait namedroot
. e.g. it matchesroot.left.left.value
,root.left.right.left.right.right.value
.metadata("name", filter=lambda value: value is not None)
Similar to
HasTraits.traits
filtering, this notifies for changes on traits that has a metadata namedname
with a value that is not None.Examples for features NOT currently supported by
on_trait_change
filter_(lambda name, trait: name.startswith("abc")
Observe traits with a generic filter.
This example is equivalent to "prefix+" in the
on_trait_change
mini-language.t("children").list_items().list_items().list_items()
Observe a nested list of list of list.
t("children").list_items().dict_keys().t("mapping").dict_values().t("value")
Observe a trait named "value", inside objects which are values of a dict named "mapping", which is inside objects which are dictionary keys, inside a list named "children".
(ignore the fact one should not use mutables as dictionary keys)
Then with the
observe
decorator supersedingon_trait_change
, the usage may look like this:and
Discussion:
t
too brief?items()
which is equivalent tolist_items() | dict_items() | set_items()
, is it worth adding though?t("child").t("attr1")
is supported, is it worth adding another top-level function for parsing a dotted string such as"child.attr1"
and just this pattern? This introduces a second way to do the same thing merely for fewer keystrokes, and is a slippery slope for more complicated mini-language all over again. How sorely missed will these dotted strings be?Notes:
given all the deep copies and looping(edited: removed, but the implementation is still drafty). I wanted to test the idea and interface first, so I have not given much thought on how to speed things up. Suggestions for faster implementation are very welcome.Checklist
TestsUpdate API reference (docs/source/traits_api_reference
)Update User manual (docs/source/traits_user_manual
)Update type annotation hints intraits-stubs