Resolve the NotifierNotFound error in testsuite#601
Conversation
|
Okay so if I instead just replace The warning goes away. However this isn't really the right thing I don't think... Also this pattern of hooking/ unhooking listeners this way is still very strange to me because it assumes 1 of two things (at least I think):
The current fix works only because 1) above holds for component. |
I should have paid more attention to this effort earlier. From what I can see, there is actually a bug here. If the listener isnt hooked up correctly at first, we should be able to reproduce this using a regression test. Once we have the regression test, I think we can also be confident about the change we are making. |
| ) | ||
| if new is not None: | ||
| self.plot.observe(self.__mapper_changed, self.axis + "_mapper") | ||
|
|
There was a problem hiding this comment.
semi-relevant (?) ref: enthought/enable#739
See this class' base class, AbstractController's __init__method:
chaco/chaco/abstract_controller.py
Lines 16 to 18 in 3e83091
There was a problem hiding this comment.
Probably. Dynamic trait change handlers (@on_trait_change() and @observe()) need HasTraits.__init__() to run before the machinery is properly set up. Static trait change handlers like _component_changed don't need that machinery to be set up and will be run on the relevant attribute setting.
There was a problem hiding this comment.
ah that is very good to know, thank you!
| @unittest.mock.patch('chaco.tools.range_selection.RangeSelection.deselect') | ||
| def test_notifiers_connected(self, mocked_deselect): | ||
| plot_data = ArrayPlotData() | ||
| arr = np.arange(4.0) | ||
| plot_data.set_data("x", arr) | ||
| plot_data.set_data("y", arr) | ||
|
|
||
| plot = Plot(plot_data) | ||
|
|
||
| renderer = plot.plot(("x", "y"))[0] | ||
| renderer.bounds = [10, 20] | ||
| tool = RangeSelection(renderer) | ||
| renderer.tools.append(tool) | ||
|
|
||
| # attempt to trigger change handler for the index_mapper trait on the | ||
| # RangeSelection tool's plot | ||
| # assign a new mapper with same attrs | ||
| renderer.index_mapper = LinearMapper( | ||
| range=renderer.index_mapper.range, | ||
| stretch_data=renderer.index_mapper.stretch_data | ||
| ) | ||
|
|
||
| mocked_deselect.assert_called_once() |
There was a problem hiding this comment.
This test fails, and the error is still observed if the fix in the file above is replaced with:
@observe("component")
def _component_updated(self, event):
if event.old is not None:
self.plot.observe(
self.__mapper_changed, self.axis + "_mapper", remove=True
)
if event.new is not None:
self.plot.observe(self.__mapper_changed, self.axis + "_mapper")
seems the timing at which notifiers are setup/called is different for the magic named methods vs decorating with observe?
|
|
||
| def _axis_changed(self, old, new): | ||
| if old is not None: | ||
| self.plot.observe( |
There was a problem hiding this comment.
So plot is _plot if it exists or else it is component. If we're hooking things up directly to component, then we're going to have bugs if plot property i.e. _plot is ever set.
There was a problem hiding this comment.
i guess we can use trait from observe and hook up _plot.index_mapper and _plot.value_mapper if _plot exists.
Actually, what happens if we listen to plot.index_mapper and plot.value_mapper?
There was a problem hiding this comment.
Actually, what happens if we listen to plot.index_mapper and plot.value_mapper?
In this case the added test test_notifiers_connected fails with deselect never being called
There was a problem hiding this comment.
I added _plot.index_mapper and _plot.value_mapper to the observe decorator, along with a test (which previously failed) for checking that if we do explicitly set plot, we still get notified of changes.
| "component.index_mapper", | ||
| "component.value_mapper", | ||
| "_plot.index_mapper", | ||
| "_plot.value_mapper" |
There was a problem hiding this comment.
I actually don't think we want this optional=True here. _plot = Trait(None, Any).
Without the optional=True everything seems to work fine, and this might be confusing. whatever _plot gets set to should definitely have index_mapper and value_mapper traits.
It is possible I am missing something here and we need to specify them as optional for some reason, but the _plot trait is guaranteed to exist. If it is None it will not have index_mapper and value_mapper traits, but if it is ever set it should. And it appears that there are not problems encountered when it is None (ie observe isn't complaining raising an error or warning or anything)
There was a problem hiding this comment.
I am happy to undo this revert if needed. @rahulporuri lmk what you think on this
There was a problem hiding this comment.
can't we just use plot.index_mapper and plot.value_mapper?
There was a problem hiding this comment.
I tried doing this but the added test fails
But I can look into it more to try to better understand why thats happening
|
Summary of my latest findings: If I do that and change the definition of the If I do the above but even further define the From the traits docs: I am going to stop investigating this for now, but it is likely worth further discussion. |
It's probably worth asking the traits maintainer what the best solution for this problem is - is the solution we have the best available or are there better alternatives. |
closes #597
The current change is a little confusing but it gets the job done. So the original cause of the error is that
axisis an enum trait which defaults to index. Thus, the first time_axis_changedis called, it is called withold="index",new="value". However, that means that we never set up the observer on the"index_mapper"trait, but we are now trying to remove it. Hence theNotifierNotFounderror.To resolve this, I intended to simply set up that observer initially. I tried doing this by overriding the
__init__method (while calling super), but this timing was incorrect. We hook the observer up to theself.plotobject, but in the__init__this is not the correct object as the plot changes. I then tried using a_plot_changedmethod, but this was also proving unsuccessful. It turns out theplottrait is really just a property that ends up mirroringcomponent. Changing to_component_changeddoes as expected (hooks up listener to the correct object).