Repository: enthought/chaco

Issue:
Clean up NotifierNotFound error causing noise in the test suite 
**Problem Description**
See comment: https://github.com/enthought/chaco/pull/591/files#r606044010

In  #591 a change was made that introduces a very noisy error message in the test suite (no tests fail though).  The causing change was to code that was already clearly broken (looked like a copy paste error) and seems to generally be an improvement, but something is still incorrect.  We try to unhook a listener before it has been unhooked because the magically named static handler is called with both `old` and `new` `!=None` on the first change.  Thus, the `old` object never had its listener hooked up in the first place.  This issue is about avoiding this error here.  However, I will open a separate issue about the general use of this pattern.

For ref here is what you see in the test suite:
<details>

```
.Exception occurred in traits notification handler for object: <chaco.tools.range_selection.RangeSelection object at 0x7fb94d99c9e8>, trait: axis, old value: index, new value: value
Traceback (most recent call last):
  File "/Users/aayres/.edm/envs/chaco-test-3.6-pyqt5/lib/python3.6/site-packages/traits/trait_notifiers.py", line 342, in __call__
    self.handler(*args)
  File "/Users/aayres/Desktop/chaco/chaco/tools/range_selection.py", line 669, in _axis_changed
    self.__mapper_changed, old + "_mapper", remove=True
  File "/Users/aayres/.edm/envs/chaco-test-3.6-pyqt5/lib/python3.6/site-packages/traits/has_traits.py", line 2330, in observe
    dispatcher=_ObserverDispatchers[dispatch],
  File "/Users/aayres/.edm/envs/chaco-test-3.6-pyqt5/lib/python3.6/site-packages/traits/observation/observe.py", line 59, in observe
    remove=remove,
  File "/Users/aayres/.edm/envs/chaco-test-3.6-pyqt5/lib/python3.6/site-packages/traits/observation/_observe.py", line 54, in add_or_remove_notifiers
    callable_()
  File "/Users/aayres/.edm/envs/chaco-test-3.6-pyqt5/lib/python3.6/site-packages/traits/observation/_observe.py", line 94, in __call__
    step()
  File "/Users/aayres/.edm/envs/chaco-test-3.6-pyqt5/lib/python3.6/site-packages/traits/observation/_observe.py", line 118, in _add_or_remove_extra_graphs
    remove=self.remove,
  File "/Users/aayres/.edm/envs/chaco-test-3.6-pyqt5/lib/python3.6/site-packages/traits/observation/_observe.py", line 54, in add_or_remove_notifiers
    callable_()
  File "/Users/aayres/.edm/envs/chaco-test-3.6-pyqt5/lib/python3.6/site-packages/traits/observation/_observe.py", line 94, in __call__
    step()
  File "/Users/aayres/.edm/envs/chaco-test-3.6-pyqt5/lib/python3.6/site-packages/traits/observation/_observe.py", line 150, in _add_or_remove_maintainers
    change_notifier.remove_from(observable)
  File "/Users/aayres/.edm/envs/chaco-test-3.6-pyqt5/lib/python3.6/site-packages/traits/observation/_observer_change_notifier.py", line 123, in remove_from
    raise NotifierNotFound("Notifier not found.")
traits.observation._exceptions.NotifierNotFound: Notifier not found.
Exception occurred in traits notification handler for object: <chaco.tools.range_selection.RangeSelection object at 0x7fb94b547e60>, trait: axis, old value: index, new value: value
Traceback (most recent call last):
  File "/Users/aayres/.edm/envs/chaco-test-3.6-pyqt5/lib/python3.6/site-packages/traits/trait_notifiers.py", line 342, in __call__
    self.handler(*args)
  File "/Users/aayres/Desktop/chaco/chaco/tools/range_selection.py", line 669, in _axis_changed
    self.__mapper_changed, old + "_mapper", remove=True
  File "/Users/aayres/.edm/envs/chaco-test-3.6-pyqt5/lib/python3.6/site-packages/traits/has_traits.py", line 2330, in observe
    dispatcher=_ObserverDispatchers[dispatch],
  File "/Users/aayres/.edm/envs/chaco-test-3.6-pyqt5/lib/python3.6/site-packages/traits/observation/observe.py", line 59, in observe
    remove=remove,
  File "/Users/aayres/.edm/envs/chaco-test-3.6-pyqt5/lib/python3.6/site-packages/traits/observation/_observe.py", line 54, in add_or_remove_notifiers
    callable_()
  File "/Users/aayres/.edm/envs/chaco-test-3.6-pyqt5/lib/python3.6/site-packages/traits/observation/_observe.py", line 94, in __call__
    step()
  File "/Users/aayres/.edm/envs/chaco-test-3.6-pyqt5/lib/python3.6/site-packages/traits/observation/_observe.py", line 118, in _add_or_remove_extra_graphs
    remove=self.remove,
  File "/Users/aayres/.edm/envs/chaco-test-3.6-pyqt5/lib/python3.6/site-packages/traits/observation/_observe.py", line 54, in add_or_remove_notifiers
    callable_()
  File "/Users/aayres/.edm/envs/chaco-test-3.6-pyqt5/lib/python3.6/site-packages/traits/observation/_observe.py", line 94, in __call__
    step()
  File "/Users/aayres/.edm/envs/chaco-test-3.6-pyqt5/lib/python3.6/site-packages/traits/observation/_observe.py", line 150, in _add_or_remove_maintainers
    change_notifier.remove_from(observable)
  File "/Users/aayres/.edm/envs/chaco-test-3.6-pyqt5/lib/python3.6/site-packages/traits/observation/_observer_change_notifier.py", line 123, in remove_from
    raise NotifierNotFound("Notifier not found.")
traits.observation._exceptions.NotifierNotFound: Notifier not found.
Exception occurred in traits notification handler for object: <chaco.tools.range_selection.RangeSelection object at 0x7fb94dc987d8>, trait: axis, old value: index, new value: value
Traceback (most recent call last):
  File "/Users/aayres/.edm/envs/chaco-test-3.6-pyqt5/lib/python3.6/site-packages/traits/trait_notifiers.py", line 342, in __call__
    self.handler(*args)
  File "/Users/aayres/Desktop/chaco/chaco/tools/range_selection.py", line 669, in _axis_changed
    self.__mapper_changed, old + "_mapper", remove=True
  File "/Users/aayres/.edm/envs/chaco-test-3.6-pyqt5/lib/python3.6/site-packages/traits/has_traits.py", line 2330, in observe
    dispatcher=_ObserverDispatchers[dispatch],
  File "/Users/aayres/.edm/envs/chaco-test-3.6-pyqt5/lib/python3.6/site-packages/traits/observation/observe.py", line 59, in observe
    remove=remove,
  File "/Users/aayres/.edm/envs/chaco-test-3.6-pyqt5/lib/python3.6/site-packages/traits/observation/_observe.py", line 54, in add_or_remove_notifiers
    callable_()
  File "/Users/aayres/.edm/envs/chaco-test-3.6-pyqt5/lib/python3.6/site-packages/traits/observation/_observe.py", line 94, in __call__
    step()
  File "/Users/aayres/.edm/envs/chaco-test-3.6-pyqt5/lib/python3.6/site-packages/traits/observation/_observe.py", line 118, in _add_or_remove_extra_graphs
    remove=self.remove,
  File "/Users/aayres/.edm/envs/chaco-test-3.6-pyqt5/lib/python3.6/site-packages/traits/observation/_observe.py", line 54, in add_or_remove_notifiers
    callable_()
  File "/Users/aayres/.edm/envs/chaco-test-3.6-pyqt5/lib/python3.6/site-packages/traits/observation/_observe.py", line 94, in __call__
    step()
  File "/Users/aayres/.edm/envs/chaco-test-3.6-pyqt5/lib/python3.6/site-packages/traits/observation/_observe.py", line 150, in _add_or_remove_maintainers
    change_notifier.remove_from(observable)
  File "/Users/aayres/.edm/envs/chaco-test-3.6-pyqt5/lib/python3.6/site-packages/traits/observation/_observer_change_notifier.py", line 123, in remove_from
    raise NotifierNotFound("Notifier not found.")
traits.observation._exceptions.NotifierNotFound: Notifier not found.
Exception occurred in traits notification handler for object: <chaco.tools.range_selection.RangeSelection object at 0x7fb94dce7728>, trait: axis, old value: index, new value: value
Traceback (most recent call last):
  File "/Users/aayres/.edm/envs/chaco-test-3.6-pyqt5/lib/python3.6/site-packages/traits/trait_notifiers.py", line 342, in __call__
    self.handler(*args)
  File "/Users/aayres/Desktop/chaco/chaco/tools/range_selection.py", line 669, in _axis_changed
    self.__mapper_changed, old + "_mapper", remove=True
  File "/Users/aayres/.edm/envs/chaco-test-3.6-pyqt5/lib/python3.6/site-packages/traits/has_traits.py", line 2330, in observe
    dispatcher=_ObserverDispatchers[dispatch],
  File "/Users/aayres/.edm/envs/chaco-test-3.6-pyqt5/lib/python3.6/site-packages/traits/observation/observe.py", line 59, in observe
    remove=remove,
  File "/Users/aayres/.edm/envs/chaco-test-3.6-pyqt5/lib/python3.6/site-packages/traits/observation/_observe.py", line 54, in add_or_remove_notifiers
    callable_()
  File "/Users/aayres/.edm/envs/chaco-test-3.6-pyqt5/lib/python3.6/site-packages/traits/observation/_observe.py", line 94, in __call__
    step()
  File "/Users/aayres/.edm/envs/chaco-test-3.6-pyqt5/lib/python3.6/site-packages/traits/observation/_observe.py", line 118, in _add_or_remove_extra_graphs
    remove=self.remove,
  File "/Users/aayres/.edm/envs/chaco-test-3.6-pyqt5/lib/python3.6/site-packages/traits/observation/_observe.py", line 54, in add_or_remove_notifiers
    callable_()
  File "/Users/aayres/.edm/envs/chaco-test-3.6-pyqt5/lib/python3.6/site-packages/traits/observation/_observe.py", line 94, in __call__
    step()
  File "/Users/aayres/.edm/envs/chaco-test-3.6-pyqt5/lib/python3.6/site-packages/traits/observation/_observe.py", line 150, in _add_or_remove_maintainers
    change_notifier.remove_from(observable)
  File "/Users/aayres/.edm/envs/chaco-test-3.6-pyqt5/lib/python3.6/site-packages/traits/observation/_observer_change_notifier.py", line 123, in remove_from
    raise NotifierNotFound("Notifier not found.")
traits.observation._exceptions.NotifierNotFound: Notifier not found.
Exception occurred in traits notification handler for object: <chaco.tools.range_selection.RangeSelection object at 0x7fb94dd21b48>, trait: axis, old value: index, new value: value
Traceback (most recent call last):
  File "/Users/aayres/.edm/envs/chaco-test-3.6-pyqt5/lib/python3.6/site-packages/traits/trait_notifiers.py", line 342, in __call__
    self.handler(*args)
  File "/Users/aayres/Desktop/chaco/chaco/tools/range_selection.py", line 669, in _axis_changed
    self.__mapper_changed, old + "_mapper", remove=True
  File "/Users/aayres/.edm/envs/chaco-test-3.6-pyqt5/lib/python3.6/site-packages/traits/has_traits.py", line 2330, in observe
    dispatcher=_ObserverDispatchers[dispatch],
  File "/Users/aayres/.edm/envs/chaco-test-3.6-pyqt5/lib/python3.6/site-packages/traits/observation/observe.py", line 59, in observe
    remove=remove,
  File "/Users/aayres/.edm/envs/chaco-test-3.6-pyqt5/lib/python3.6/site-packages/traits/observation/_observe.py", line 54, in add_or_remove_notifiers
    callable_()
  File "/Users/aayres/.edm/envs/chaco-test-3.6-pyqt5/lib/python3.6/site-packages/traits/observation/_observe.py", line 94, in __call__
    step()
  File "/Users/aayres/.edm/envs/chaco-test-3.6-pyqt5/lib/python3.6/site-packages/traits/observation/_observe.py", line 118, in _add_or_remove_extra_graphs
    remove=self.remove,
  File "/Users/aayres/.edm/envs/chaco-test-3.6-pyqt5/lib/python3.6/site-packages/traits/observation/_observe.py", line 54, in add_or_remove_notifiers
    callable_()
  File "/Users/aayres/.edm/envs/chaco-test-3.6-pyqt5/lib/python3.6/site-packages/traits/observation/_observe.py", line 94, in __call__
    step()
  File "/Users/aayres/.edm/envs/chaco-test-3.6-pyqt5/lib/python3.6/site-packages/traits/observation/_observe.py", line 150, in _add_or_remove_maintainers
    change_notifier.remove_from(observable)
  File "/Users/aayres/.edm/envs/chaco-test-3.6-pyqt5/lib/python3.6/site-packages/traits/observation/_observer_change_notifier.py", line 123, in remove_from
    raise NotifierNotFound("Notifier not found.")
traits.observation._exceptions.NotifierNotFound: Notifier not found.
Exception occurred in traits notification handler for object: <chaco.tools.range_selection.RangeSelection object at 0x7fb94dd4df10>, trait: axis, old value: index, new value: value
Traceback (most recent call last):
  File "/Users/aayres/.edm/envs/chaco-test-3.6-pyqt5/lib/python3.6/site-packages/traits/trait_notifiers.py", line 342, in __call__
    self.handler(*args)
  File "/Users/aayres/Desktop/chaco/chaco/tools/range_selection.py", line 669, in _axis_changed
    self.__mapper_changed, old + "_mapper", remove=True
  File "/Users/aayres/.edm/envs/chaco-test-3.6-pyqt5/lib/python3.6/site-packages/traits/has_traits.py", line 2330, in observe
    dispatcher=_ObserverDispatchers[dispatch],
  File "/Users/aayres/.edm/envs/chaco-test-3.6-pyqt5/lib/python3.6/site-packages/traits/observation/observe.py", line 59, in observe
    remove=remove,
  File "/Users/aayres/.edm/envs/chaco-test-3.6-pyqt5/lib/python3.6/site-packages/traits/observation/_observe.py", line 54, in add_or_remove_notifiers
    callable_()
  File "/Users/aayres/.edm/envs/chaco-test-3.6-pyqt5/lib/python3.6/site-packages/traits/observation/_observe.py", line 94, in __call__
    step()
  File "/Users/aayres/.edm/envs/chaco-test-3.6-pyqt5/lib/python3.6/site-packages/traits/observation/_observe.py", line 118, in _add_or_remove_extra_graphs
    remove=self.remove,
  File "/Users/aayres/.edm/envs/chaco-test-3.6-pyqt5/lib/python3.6/site-packages/traits/observation/_observe.py", line 54, in add_or_remove_notifiers
    callable_()
  File "/Users/aayres/.edm/envs/chaco-test-3.6-pyqt5/lib/python3.6/site-packages/traits/observation/_observe.py", line 94, in __call__
    step()
  File "/Users/aayres/.edm/envs/chaco-test-3.6-pyqt5/lib/python3.6/site-packages/traits/observation/_observe.py", line 150, in _add_or_remove_maintainers
    change_notifier.remove_from(observable)
  File "/Users/aayres/.edm/envs/chaco-test-3.6-pyqt5/lib/python3.6/site-packages/traits/observation/_observer_change_notifier.py", line 123, in remove_from
    raise NotifierNotFound("Notifier not found.")
traits.observation._exceptions.NotifierNotFound: Notifier not found.
Exception occurred in traits notification handler for object: <chaco.tools.range_selection.RangeSelection object at 0x7fb94b547b48>, trait: axis, old value: index, new value: value
Traceback (most recent call last):
  File "/Users/aayres/.edm/envs/chaco-test-3.6-pyqt5/lib/python3.6/site-packages/traits/trait_notifiers.py", line 342, in __call__
    self.handler(*args)
  File "/Users/aayres/Desktop/chaco/chaco/tools/range_selection.py", line 669, in _axis_changed
    self.__mapper_changed, old + "_mapper", remove=True
  File "/Users/aayres/.edm/envs/chaco-test-3.6-pyqt5/lib/python3.6/site-packages/traits/has_traits.py", line 2330, in observe
    dispatcher=_ObserverDispatchers[dispatch],
  File "/Users/aayres/.edm/envs/chaco-test-3.6-pyqt5/lib/python3.6/site-packages/traits/observation/observe.py", line 59, in observe
    remove=remove,
  File "/Users/aayres/.edm/envs/chaco-test-3.6-pyqt5/lib/python3.6/site-packages/traits/observation/_observe.py", line 54, in add_or_remove_notifiers
    callable_()
  File "/Users/aayres/.edm/envs/chaco-test-3.6-pyqt5/lib/python3.6/site-packages/traits/observation/_observe.py", line 94, in __call__
    step()
  File "/Users/aayres/.edm/envs/chaco-test-3.6-pyqt5/lib/python3.6/site-packages/traits/observation/_observe.py", line 118, in _add_or_remove_extra_graphs
    remove=self.remove,
  File "/Users/aayres/.edm/envs/chaco-test-3.6-pyqt5/lib/python3.6/site-packages/traits/observation/_observe.py", line 54, in add_or_remove_notifiers
    callable_()
  File "/Users/aayres/.edm/envs/chaco-test-3.6-pyqt5/lib/python3.6/site-packages/traits/observation/_observe.py", line 94, in __call__
    step()
  File "/Users/aayres/.edm/envs/chaco-test-3.6-pyqt5/lib/python3.6/site-packages/traits/observation/_observe.py", line 150, in _add_or_remove_maintainers
    change_notifier.remove_from(observable)
  File "/Users/aayres/.edm/envs/chaco-test-3.6-pyqt5/lib/python3.6/site-packages/traits/observation/_observer_change_notifier.py", line 123, in remove_from
    raise NotifierNotFound("Notifier not found.")
traits.observation._exceptions.NotifierNotFound: Notifier not found.
Exception occurred in traits notification handler for object: <chaco.tools.range_selection.RangeSelection object at 0x7fb94b547d58>, trait: axis, old value: index, new value: value
Traceback (most recent call last):
  File "/Users/aayres/.edm/envs/chaco-test-3.6-pyqt5/lib/python3.6/site-packages/traits/trait_notifiers.py", line 342, in __call__
    self.handler(*args)
  File "/Users/aayres/Desktop/chaco/chaco/tools/range_selection.py", line 669, in _axis_changed
    self.__mapper_changed, old + "_mapper", remove=True
  File "/Users/aayres/.edm/envs/chaco-test-3.6-pyqt5/lib/python3.6/site-packages/traits/has_traits.py", line 2330, in observe
    dispatcher=_ObserverDispatchers[dispatch],
  File "/Users/aayres/.edm/envs/chaco-test-3.6-pyqt5/lib/python3.6/site-packages/traits/observation/observe.py", line 59, in observe
    remove=remove,
  File "/Users/aayres/.edm/envs/chaco-test-3.6-pyqt5/lib/python3.6/site-packages/traits/observation/_observe.py", line 54, in add_or_remove_notifiers
    callable_()
  File "/Users/aayres/.edm/envs/chaco-test-3.6-pyqt5/lib/python3.6/site-packages/traits/observation/_observe.py", line 94, in __call__
    step()
  File "/Users/aayres/.edm/envs/chaco-test-3.6-pyqt5/lib/python3.6/site-packages/traits/observation/_observe.py", line 118, in _add_or_remove_extra_graphs
    remove=self.remove,
  File "/Users/aayres/.edm/envs/chaco-test-3.6-pyqt5/lib/python3.6/site-packages/traits/observation/_observe.py", line 54, in add_or_remove_notifiers
    callable_()
  File "/Users/aayres/.edm/envs/chaco-test-3.6-pyqt5/lib/python3.6/site-packages/traits/observation/_observe.py", line 94, in __call__
    step()
  File "/Users/aayres/.edm/envs/chaco-test-3.6-pyqt5/lib/python3.6/site-packages/traits/observation/_observe.py", line 150, in _add_or_remove_maintainers
    change_notifier.remove_from(observable)
  File "/Users/aayres/.edm/envs/chaco-test-3.6-pyqt5/lib/python3.6/site-packages/traits/observation/_observer_change_notifier.py", line 123, in remove_from
    raise NotifierNotFound("Notifier not found.")
traits.observation._exceptions.NotifierNotFound: Notifier not found.
...........
----------------------------------------------------------------------
Ran 549 tests in 5.399s

OK (skipped=8)
```

</details>

**Reproduction Steps:**
Run the test suite on master now that #591 has been merged


**Expected behavior:**

Don't see the warnings in the test suite, and handlers get hooked up / removed at the appropriate times




Discussion:

Resolving patch:
diff --git a/chaco/tools/range_selection.py b/chaco/tools/range_selection.py
--- a/chaco/tools/range_selection.py
+++ b/chaco/tools/range_selection.py
@@ -15,6 +15,7 @@
     Instance,
     Int,
     List,
+    observe,
     Property,
     Str,
     Trait,
@@ -676,13 +677,12 @@ def _determine_axis(self):
             else:
                 return 0
 
-    def __mapper_changed(self, event):
-        self.deselect()
-
-    def _axis_changed(self, old, new):
-        if old is not None:
-            self.plot.observe(
-                self.__mapper_changed, old + "_mapper", remove=True
-            )
-        if new is not None:
-            self.plot.observe(self.__mapper_changed, new + "_mapper")
+    @observe([
+        "component.index_mapper",
+        "component.value_mapper",
+        "_plot.index_mapper",
+        "_plot.value_mapper"
+    ])
+    def _axis_mapper_updated(self, event):
+        if event.name == self.axis + "_mapper":
+            self.deselect()


Test patch:
diff --git a/chaco/tools/tests/test_range_selection.py b/chaco/tools/tests/test_range_selection.py
--- a/chaco/tools/tests/test_range_selection.py
+++ b/chaco/tools/tests/test_range_selection.py
@@ -3,6 +3,7 @@
 
 import numpy as np
 
+from chaco.api import LinearMapper
 from chaco.array_plot_data import ArrayPlotData
 from chaco.plot import Plot
 from chaco.tools.range_selection import RangeSelection
@@ -75,3 +76,54 @@ def test_selection_no_warning(self):
         tool.selection = (1.5, 3.5)
         tool.selection = [1.0, 2.0]
         tool.selection = None
+
+    # regression test for enthought/chaco#597
+    @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]
+        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()
+
+    # regression test for enthought/chaco#597
+    @unittest.mock.patch('chaco.tools.range_selection.RangeSelection.deselect')
+    def test_notifiers_connected_specify_plot(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]
+        tool = RangeSelection(renderer)
+        renderer.tools.append(tool)
+
+        new_renderer = plot.plot(("x", "y"), type='scatter')[0]
+        tool.plot = new_renderer
+
+        # attempt to trigger change handler for the index_mapper trait on the
+        # RangeSelection tool's plot
+        # assign a new mapper with same attrs
+        new_renderer.index_mapper = LinearMapper(
+            range=new_renderer.index_mapper.range,
+            stretch_data=new_renderer.index_mapper.stretch_data
+        )
+
+        mocked_deselect.assert_called_once()