Skip to content

Commit

Permalink
Fix an observe-decorated _name_changed method (#1560)
Browse files Browse the repository at this point in the history
* Add tests for listener methods with the _changed name

* Skip observed methods as well as on_trait_change-decorated methods

* Update documentation
  • Loading branch information
mdickinson authored Oct 1, 2021
1 parent 6eda720 commit 2dcf651
Show file tree
Hide file tree
Showing 3 changed files with 75 additions and 0 deletions.
11 changes: 11 additions & 0 deletions docs/source/traits_user_manual/listening.rst
Original file line number Diff line number Diff line change
Expand Up @@ -539,6 +539,17 @@ The arguments that are passed to the trait attribute change notification
method depend on the method signature and on which type of static notification
handler it is.

.. note::
The :func:`~.on_trait_change` and :func:`~.observe` decorators nullify
the effect of special naming. A method that looks like::

@observe("foo")
def _foo_changed(self, event):
do_something_with(event)

will only be called once when ``foo`` changes, as a result of the
``observe`` decorator.

.. _attribute-specific-handler-signatures:

Attribute-specific Handler Signatures
Expand Down
2 changes: 2 additions & 0 deletions traits/has_traits.py
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,7 @@ def _get_def(class_name, class_dict, bases, method):
(result is not None)
and is_unbound_method_type(result)
and (getattr(result, "on_trait_change", None) is None)
and (getattr(result, "_observe_inputs", None) is None)
):
return result

Expand All @@ -171,6 +172,7 @@ def _get_def(class_name, class_dict, bases, method):
(result is not None)
and is_unbound_method_type(result)
and (getattr(result, "on_trait_change", None) is None)
and (getattr(result, "_observe_inputs", None) is None)
):
return result

Expand Down
62 changes: 62 additions & 0 deletions traits/tests/test_has_traits.py
Original file line number Diff line number Diff line change
Expand Up @@ -604,6 +604,68 @@ class A(HasTraits):
{"foo": A.class_traits()["foo"]},
)

def test_decorated_changed_method(self):
# xref: enthought/traits#527
# Traits should ignore the _changed magic naming.

events = []

class A(HasTraits):
foo = Int()

@on_trait_change("foo")
def _foo_changed(self, obj, name, old, new):
events.append((obj, name, old, new))

a = A()
a.foo = 23
self.assertEqual(
events,
[(a, "foo", 0, 23)],
)

def test_observed_changed_method(self):
events = []

class A(HasTraits):
foo = Int()

@observe("foo")
def _foo_changed(self, event):
events.append(event)

a = A()
a.foo = 23
self.assertEqual(len(events), 1)
event = events[0]
self.assertEqual(event.object, a)
self.assertEqual(event.name, "foo")
self.assertEqual(event.old, 0)
self.assertEqual(event.new, 23)

def test_decorated_changed_method_subclass(self):
# xref: enthought/traits#527
# Traits should ignore the _changed magic naming.

events = []

class A(HasTraits):
foo = Int()

@on_trait_change("foo")
def _foo_changed(self, obj, name, old, new):
events.append((obj, name, old, new))

class B(A):
pass

a = B()
a.foo = 23
self.assertEqual(
events,
[(a, "foo", 0, 23)],
)


class TestObjectNotifiers(unittest.TestCase):
""" Test calling object notifiers. """
Expand Down

0 comments on commit 2dcf651

Please sign in to comment.