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

Implement ObserverGraph with NamedTraitObserver #976

Merged
merged 41 commits into from
Apr 22, 2020
Merged

Conversation

kitchoi
Copy link
Contributor

@kitchoi kitchoi commented Apr 1, 2020

This PR implements a few low-level objects required for implementing the new observer framework described in EEP-3.

Contribute towards #977

This PR mainly introduces the following:

  • ObserverPath ObserverGraph:
    A data structure that represents the attribute paths for traits being observed on an HasTraits instance

The following is included for context, I am happy to move them out to a separate PR

  • IObserver:
    Interface for an observer. Currently it requires __eq__ and __hash__ as are required for ObserverPath ObserverGraph.
  • NamedTraitObserver:
    An observer that can be used as a node in ObserverPath ObserverGraph.

Checklist

  • Tests
  • Update API reference (docs/source/traits_api_reference)
    All the modules introduced here are private. I don't think the API reference includes private modules, but I could be wrong.
  • Update User manual (docs/source/traits_user_manual)
    All the modules introduced here are private.
  • Update type annotation hints in traits-stubs
    Nothing introduced in this PR are trait-ed.

Note:
The proof-of-concept PR is still being reviewed in #942. So there is still a chance this PR will need to be closed altogether or reworked.

@kitchoi kitchoi mentioned this pull request Apr 1, 2020
7 tasks
# Thanks for using Enthought open source!


class ObserverPath:
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't know why I only realized this now: But this object actually represents many "paths", not just one.
An alternative name welcome...

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would ObserverTree be appropriate? ObserverGraph? ObserverTree certainly helps me when looking at the code: then I'm not surprised to see contents for this node along with a list of child nodes.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ObserverGraph? Or even ObserverDGraph? Trees can't have cycles and I am not sure we can confidently rule that possibility out at the moment...

# is also available online at http://www.enthought.com/licenses/BSD.txt
#
# Thanks for using Enthought open source!
from traits.observers._interfaces import IObserver
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On naming, should the module be named _iobserver and we have one module per interface object?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One module per IObserver subclass sounds fine to me, especially since we have a traits.observers subpackage (so we won't be cluttering the main traits package).

So _named_trait_observer seems like a fine name.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I ended up naming this module to _i_observer (not the _iobserver I initially proposed), to be inlined with naming in pyface (maybe elsewhere too).

Comment on lines 89 to 91
# Remove loops
self_nexts = set(path for path in self.nexts if path is not self)
other_nexts = set(path for path in other.nexts if path is not other)
Copy link
Contributor Author

@kitchoi kitchoi Apr 7, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can also handle loops later when we implement the methods for supporting recursion.
Handling just the loop back to self is not enough actually, considering the example of root.[left,right]*.value, there are actually two paths in the loop.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

#969 attempted an alternative for handling cycles. I will remove this loop handling here.

@codecov-io
Copy link

codecov-io commented Apr 14, 2020

Codecov Report

Merging #976 into master will not change coverage.
The diff coverage is n/a.

Impacted file tree graph

@@           Coverage Diff           @@
##           master     #976   +/-   ##
=======================================
  Coverage   74.92%   74.92%           
=======================================
  Files          54       54           
  Lines        6477     6477           
  Branches     1280     1280           
=======================================
  Hits         4853     4853           
  Misses       1253     1253           
  Partials      371      371           

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update 05889d5...05889d5. Read the comment docs.

@kitchoi kitchoi changed the title Implement ObserverPath with NamedTraitObserver Implement ObserverGraph with NamedTraitObserver Apr 17, 2020
def __hash__(self):
""" Return the hash of this ObserverGraph."""
return hash(
(type(self), self.node, frozenset(self.children))
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Previously I had hash(frozenset(self.children)) here, the hash is a bit redundant, and is removed.

@kitchoi
Copy link
Contributor Author

kitchoi commented Apr 17, 2020

Thank you for the review!
The class is now renamed to ObserverGraph and so I also updated the title and description. Ready for re-review.

@kitchoi kitchoi requested a review from mdickinson April 17, 2020 16:24
@mdickinson
Copy link
Member

One question about the use of sets here: will the unpredictability of the set ordering leak into user-visible behaviour (e.g., notifiers firing in different orders from run to run)?

While it's true that well-written code shouldn't care about the order in which listeners are fired, I think we probably do still want to have predictability here.

@mdickinson
Copy link
Member

One other thought on the set ordering: right now that ordering should be genuinely unpredictable from run to run, thanks to the inclusion of the types themselves in the __hash__ algorithms. If we change that (e.g., by replacing the type with the name of the type, so that we have deterministic __hash__) then we'll end up in an arguably worse situation, which is that on any given platform behaviour will be consistent from run to run, but there could be platform differences (e.g., due to the hash on Windows not matching that on macOS/Linux). The latter seems like a recipe for hard-to-diagnose bugs.

@kitchoi
Copy link
Contributor Author

kitchoi commented Apr 20, 2020

Good question. In order to address that, I need to understand if traits make any promises about (1) the order of notifications and (2) whether the order of notifications is deterministic. For (1), what order is promised?

Before dict becomes order, on_trait_change listeners are registered in a dict and have always been iterated through in an order that is not deterministic. But the fact that dict becomes ordered in CPython 3.6/Python 3.7 might mean traits has extended that order guarantee.

@mdickinson
Copy link
Member

mdickinson commented Apr 20, 2020

No promises, except that it's known through folklore that static listeners (those arising from _mytrait_changed magic method names) fire before dynamic ones (those coming from @on_trait_change-decorated methods, or direct on_trait_change calls on objects). I don't know if that's actually written down in the documentation, but if not, it should be. Beyond that, I don't think we have any guarantees.

Nevertheless, we've definitely seen code that happens to work only because listeners happen to be fired in the right order. And with developers often working on macOS or Linux but deploying on Windows, I'd like to be sure that we don't end up in a situation where the behaviour is consistent from run to run on local machines, but differs from platform to platform.

@kitchoi
Copy link
Contributor Author

kitchoi commented Apr 20, 2020

Related doc question: Should it be documented that users should not make assumptions on the order for when the change handlers are called?

e.g. the following may assume _handle_name_change is called after _update_number, and should be discouraged even if it works now:

class Foo(HasTraits):
      @on_trait_change("name")
       def _update_number(self):
             self.number += 1

      @on_trait_change("name")
      def _handle_name_change(self):
             self.packages = [self.name] * self.number

@mdickinson
Copy link
Member

Yes, should definitely be documented, and that would be a nice example to include in that documentation.

@kitchoi
Copy link
Contributor Author

kitchoi commented Apr 20, 2020

And if the order concern is about interactions among change handlers, then I think the ordering of ObserverGraph.children would not cause that, because there is only one handler associated to the root node of the entire tree. But it probably still be a good idea to keep the order as given so we don't lose that information.

I pushed some new commits. See what you think.

@kitchoi
Copy link
Contributor Author

kitchoi commented Apr 20, 2020

I think the ordering of ObserverGraph.children would not cause that, because there is only one handler associated to the root node of the entire tree.

To clarify with an example, the "children" are b and c in the following:

@on_trait_change("a.[b,c]")
def update_handler(self):
    pass

So the ordering will affect whether update_handler is hooked up with b first or c first.

Copy link
Member

@mdickinson mdickinson left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM; one nitpick and one suggestion for a minor change

# is also available online at http://www.enthought.com/licenses/BSD.txt
#
# Thanks for using Enthought open source!
import abc
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just a style nitpick: could we keep a blank line separating the copyright header from the module body?

traits/observers/_named_trait_observer.py Show resolved Hide resolved
)


IObserver.register(NamedTraitObserver)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggestion: use this as a class decorator: @IObserver.register, to better match the familiarity of the way we use @provides.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah that might be more readable too!

@mdickinson
Copy link
Member

LGTM!

@kitchoi
Copy link
Contributor Author

kitchoi commented Apr 22, 2020

Thanks! Merging.

@kitchoi kitchoi merged commit 245dcb3 into master Apr 22, 2020
@kitchoi kitchoi deleted the observer-path branch April 22, 2020 08:29
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants