-
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
Implement ListItemObserver for observing mutations to a list #1071
Conversation
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.
Found a few small things, otherwise LGTM! Although someone else should review this as well
Attributes | ||
---------- | ||
trait_list : list | ||
The list being mutated. |
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 think it would be good to clarify whether this is a list before or after changes (same in event factory).
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.
Whether the list contains content before or after the change depends on whether the change event is fired before or after the mutation, because it is the same list object. Strictly speaking, the event object does not need to dictate when the notification should be fired. Not sure how I could write this...
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.
We could add to the API document (not here) that notifications are fired after the mutations. What do you think?
i.e. TraitList.notify
are called after the mutation has taken place.
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.
Oh, okay, I thought the behaviour was consistent. I think it would be good to report which events are fired before changes and which are fired after, but I agree that the event itself is not a place for that.
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 am sorry I did not mean to imply there were events fired before changes. All events are fired after changes. Sorry for the confusion there.
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.
Okay, well I would still argue that it would be good to inform the user that the event is fired after changes are made, but then I really don't know what would be the right place to do so.
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.
Yes I agree that the information needs to be somewhere in the documentation. In fact, I would very much like that to be included in the user manual.
While it may be convenient to have that description in the event object parameter section, doing so may introduce restrictions that do not belong to the data structure.
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.
Actually, the first sentence in the current user manual already implies that all change notifications are fired after the change:
https://docs.enthought.com/traits/traits_user_manual/notification.html
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 see that similar implicit mentions are made in the new observer's user manual as well, so it might be enough. I was just a bit worried that because the handlers are now supposed to deal with the actual events, the user's might be paying more attention to the docstrings of those events.
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 might not be amplified enough, but it is a fairly strong feature of Traits that notifications always fire after the value has set.
Codecov Report
@@ Coverage Diff @@
## master #1071 +/- ##
==========================================
- Coverage 76.15% 73.03% -3.13%
==========================================
Files 54 67 +13
Lines 6493 8103 +1610
Branches 1263 1548 +285
==========================================
+ Hits 4945 5918 +973
- Misses 1205 1806 +601
- Partials 343 379 +36
Continue to review full report at Codecov.
|
Thank you @ievacerny for the review! |
) | ||
|
||
|
||
def list_event_factory(trait_list, index, removed, added): |
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'd be tempted to make this a classmethod
of ListChangeEvent
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.
Actually, on second thought, perhaps embrace the ordering and make ListChangeEvent
a named tuple.
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.
The __init__
of ListChangeEvent
is currently the same as the call signature of TraitList.notify
, but they could evolve independently of each other. DictChangeEvent
in #1072 is actually an example where the __init__
of DictChangeEvent
is not the same as the call signature of TraitDict.notify
.
The only reason why this is not a classmethod is to avoid exposing this to the user. ListChangeEvent
is exposed in the public API via the public module events
whereas the event factory isn't. An alternative organization would be to have all the public facing event object defined in a public module, and these event factories defined in another private modules.
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.
The init of ListChangeEvent is currently the same as the call signature of TraitList.notify
That's not true because of the keyword arguments...and of course that's why you asked in the next comment 😅
|
||
|
||
@IObserver.register | ||
class ListItemObserver: |
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.
As in the last PR, can this be a subclass of named tuple?
Also had the thought that even if you don't turn it into a named tuple, using __slots__
here might make sense for efficiency (keep that in the back of your mind, even if it isn't part of this PR).
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 did give this a go, and while it got rid of the __init__
, __hash__
, __eq__
and the properties, the equality check does not quite do what is implemented here. e.g. the following returns true.
ListItemObserver(notify=True, optional=False) == (True, False)
That is not very convenient considering that ListItemObserver
, SetItemObserver
and DictItemObserver
have the same attributes and they should not be equal to each other.
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.
Let's keep __slots__
away from this for now; using __slots__
can have some interesting side-effects (particularly with regard to subclassing). It's an optimization that we should save until we need it (and my bet would be that we won't ever need it).
# ListChangeEvent is exposed in the public API | ||
|
||
|
||
class ListChangeEvent: |
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.
Possibly make this a named tuple?
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.
namedtuple turns this into a sequence-like object. Unlike the observer, this is a public-facing object, so it would be difficult going back. Maybe better to wait until we really need to.
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 agree about avoiding namedtuple; it's an interface that presents multiple ways to do it, which often isn't what you want. It's useful as a transitional type when going from something that was previously a tuple to something closer to a class with attributes, but I don't think it's what we want here. A dataclass may be closer to what we want, but that's not available until Python 3.7.
Values removed from the list. | ||
""" | ||
|
||
def __init__(self, *, trait_list, index, removed, added): |
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.
Is there are particular reason you want these to be keyword only?
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.
Enforcing keyword-only arguments will help prevent bugs like this one: enthought/traitsui#791
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 agree that keyword-only arguments make sense here: it makes the calling code clearer as well, particularly with respect to the ordering of removed
and added
- someone reading the client code doesn't have to check or remember which way around the arguments go.
The keyword-only restriction is also something that's easy to relax if it becomes problematic. It's harder to add the restriction back in if it's not present to begin with.
|
||
Attributes | ||
---------- | ||
trait_list : list |
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.
Also, a design suggestion for this and the other collection change events: it might make sense to try to mimic the API of TraitChangeEvent
. At a minimum:
- using
object
instead oftrait_list
- a dummy attribute
name
which is always"items"
This might allow code to consume both types of event without having to do a type check if it only cares about the object and the name.
That would imply an implicit "ChangeEvent" interface where you expect both "object" and "name" to exist. It is tempting to make new
be the new value of the list (ie. the same as object
) and old
to be a property that lazily reconstructs the old value of the list from the added
and removed
, but that isn't really right from a philosophical point of view.
Really the rest of the information is the "delta", ie. we have
- "who changed": the object that was changed, be it a HasTraits, TraitList, etc.
- "what changed": the name of the thing that changed (the thing that was being observed)
- "how it changed": the nature of the changes that took place (old/new, added/removed, etc.)
Thinking this way may help with the design of these classes.
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.
Hmmm we might need some use cases to drive this. For example, if a handler needs to consume object
and name
, the next thing it does may be to call getattr(object, name)
, and doing that with object
being TraitList
and name
being "items"
is going to fail.
If the handler needs to do something specific about the change details, then it probably needs to know which type of event objects it gets anyway, it could go two routes:
(1) Apply notify=True
only on the type of change that is compatible. Then it only deals with one type of event objects.
(2) Check the event type, and do different things according to the type.
Are there use cases already we can draw conclusions from? Otherwise, leaving these object
and name
undefined for now might actually be more flexible, so we don't have to commit to a value before knowing how they are going to be used. e.g. object
could be the HasTraits
instance that contains the list somewhere (note that the list could be nested). It does not hurt if we end up having both object
and trait_list
pointing to the same thing.
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.
"how it changed": the nature of the changes that took place (old/new, added/removed, etc.)
This is one of the reasons that I think it would make sense to bundle index
, removed
and added
up into a single object, as I've suggested before. Those three pieces of information are always generated together, passed around together and used together.
Maybe open an issue to revisit this once the various related PRs are merged and we have a chance to look at the design as a whole?
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 am open to that discussion and it makes sense to do that at the very end instead of blocking these PRs.
Do you mind opening the said issue please (since I am worried I may misunderstand the scope you are thinking about)?
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.
Do you mind opening the said issue please
Opened #1084.
I have addressed the ones that I could address. There are some pending discussion points and I am not sure what to do with those. |
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.
A few minor comments and change requests.
…ey should not be mutated, it is very unlikely they will be mutated.
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.
Thank you for the review! I hope I have addressed all the comments now.
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.
LGTM
@ievacerny and @corranwebster: you both still have outstanding comments against this PR. Is this okay to merge?
@corranwebster gave his okay on another channel. Merging. |
This PR implements item 8 of #977:
ListChangeEvent
for representing mutations to a list. This object will be given to the user's change handler.ListItemObserver
implements the interface ofIObserver
to:TraitList
. If the incoming object is not a TraitList and the observer is not marked as optional, the observer will complain. This is to help catch usage errors.ListItemObserver
has no knowledge about the nature of the next observers (this is generally true for any observers). If the content of the list is not compatible with the next observer (say the list contains integers), the next observer is expected to complain about it. e.g. seeNamedTraitObserver
in Implement the logic for observing a named trait #1069 which raises an error when the incoming object is not an instance of HasTraits.IObservable
interface inTraitList
. With this, any subclass ofTraitList
, e.g. TraitListObject, can be observed withListItemObserver
(see tests).Checklist
docs/source/traits_api_reference
)Update User manual (docs/source/traits_user_manual
)Update type annotation hints intraits-stubs