def test_notifier_can_be_removed(self): def filter_func(name, trait): return name.startswith("num") instance = DummyParent() handler = mock.Mock() call_add_or_remove_notifiers( object=instance, graph=create_graph(create_observer(filter=filter_func)), handler=handler, ) # sanity check instance.number += 1 self.assertEqual(handler.call_count, 1) handler.reset_mock() # when call_add_or_remove_notifiers( object=instance, graph=create_graph(create_observer(filter=filter_func)), handler=handler, remove=True, ) # then instance.number += 1 self.assertEqual(handler.call_count, 0)
def test_maintain_notifier_for_default(self): # Dynamic defaults are not computed when hooking up the notifiers. # By when the default is defined, the maintainer will then hook up # the child observer. foo = ClassWithDefault() graph = create_graph( create_observer(name="instance", notify=True), create_observer(name="value1", notify=True), ) handler = mock.Mock() call_add_or_remove_notifiers( object=foo, graph=graph, handler=handler, ) # sanity check test setup. self.assertNotIn("instance", foo.__dict__) # when foo.instance # this triggers the default to be computed and set # then # setting the default does not trigger notifications self.assertEqual(handler.call_count, 0) # when foo.instance.value1 += 1 # then # the notifier for value1 has been hooked up by the maintainer self.assertEqual(handler.call_count, 1)
def test_maintainer_keep_notify_flag(self): # Test the maintainer will maintain the notify flag for the root # observer in the subgraph. instance = DummyHasTraitsClass() notifier = DummyNotifier() graph = create_graph( self.observer, DummyObserver( notify=False, notifier=notifier, ), ) handler = mock.Mock() call_add_or_remove_notifiers( object=instance, handler=handler, target=instance, graph=graph, remove=False, ) # when instance.add_trait("good_name", Str()) # then # notify flag is set to false, so there are no notifiers added. notifiers = instance._trait("good_name", 2)._notifiers(True) self.assertNotIn(notifier, notifiers)
def test_filter_metadata(self): class Person(HasTraits): n_jobs = Int(status="public") n_children = Int() # no metadata observer = FilteredTraitObserver( filter=MetadataFilter(metadata_name="status", ), notify=True, ) person = Person() handler = mock.Mock() call_add_or_remove_notifiers( object=person, graph=create_graph(observer), handler=handler, ) # when person.n_jobs += 1 # then self.assertEqual(handler.call_count, 1) handler.reset_mock() # when person.n_children += 1 # then self.assertEqual(handler.call_count, 0)
def test_add_trait_remove_trait_then_add_trait_again(self): # Test a scenario when a trait is added, then removed, then added back. # given # trait is optional. It will be added later. graph = create_graph( create_observer(name="new_value", notify=True, optional=True), ) handler = mock.Mock() foo = ClassWithInstance() call_add_or_remove_notifiers( object=foo, graph=graph, handler=handler, remove=False) foo.add_trait("new_value", Int()) foo.new_value += 1 handler.assert_called_once() handler.reset_mock() # when # remove the trait and then add it back foo.remove_trait("new_value") foo.add_trait("new_value", Int()) # then # the handler is now back! The trait was not defined on the class, # so the last 'add_trait' fires a trait_added event. foo.new_value += 1 handler.assert_called_once()
def test_maintain_notifier_change_to_new_value(self): # Test when the container object is changed, the notifiers are # maintained downstream. foo = ClassWithInstance() graph = create_graph( create_observer(name="instance", notify=True), create_observer(name="value1", notify=True), ) handler = mock.Mock() call_add_or_remove_notifiers( object=foo, graph=graph, handler=handler, ) # sanity check foo.instance = ClassWithTwoValue() foo.instance.value1 += 1 self.assertEqual(handler.call_count, 2) # when old_instance = foo.instance foo.instance = ClassWithTwoValue() handler.reset_mock() old_instance.value1 += 1 # then self.assertEqual(handler.call_count, 0) # when foo.instance.value1 += 1 # then self.assertEqual(handler.call_count, 1)
def test_maintain_notifier_change_to_none(self): # Instance may accept None, maintainer should accomodate that # and skip it for the next observer. class UnassumingObserver(DummyObserver): def iter_observables(self, object): if object is None: raise ValueError("This observer cannot handle None.") yield from () foo = ClassWithInstance() graph = create_graph( create_observer(name="instance", notify=True), UnassumingObserver(), ) handler = mock.Mock() call_add_or_remove_notifiers( object=foo, graph=graph, handler=handler, ) foo.instance = ClassWithTwoValue() try: foo.instance = None except Exception: self.fail("Setting instance back to None should not fail.")
def test_add_notifier_atomic(self): class BadNotifier(DummyNotifier): def add_to(self, observable): raise ZeroDivisionError() observable = DummyObservable() good_observer = DummyObserver( notify=True, observables=[observable], next_objects=[mock.Mock()], notifier=DummyNotifier(), maintainer=DummyNotifier(), ) bad_observer = DummyObserver( notify=True, observables=[observable], notifier=BadNotifier(), maintainer=DummyNotifier(), ) graph = create_graph( good_observer, bad_observer, ) # when with self.assertRaises(ZeroDivisionError): call_add_or_remove_notifiers( object=mock.Mock(), graph=graph, ) # then self.assertEqual(observable.notifiers, [])
def test_notifier_extended_trait_change(self): foo = ClassWithInstance() graph = create_graph( create_observer(name="instance", notify=True), create_observer(name="value1", notify=True), ) handler = mock.Mock() call_add_or_remove_notifiers( object=foo, graph=graph, handler=handler, ) self.assertIsNone(foo.instance) # when foo.instance = ClassWithTwoValue() # then ((event, ), _), = handler.call_args_list self.assertEqual(event.object, foo) self.assertEqual(event.name, "instance") self.assertEqual(event.old, None) self.assertEqual(event.new, foo.instance) # when handler.reset_mock() foo.instance.value1 += 1 # then ((event, ), _), = handler.call_args_list self.assertEqual(event.object, foo.instance) self.assertEqual(event.name, "value1") self.assertEqual(event.old, 0) self.assertEqual(event.new, 1)
def test_notify_filter_values_changed(self): instance = DummyParent() # Observes number and number2 observer = create_observer( filter=lambda name, trait: type(trait.trait_type) is Int, ) handler = mock.Mock() call_add_or_remove_notifiers( object=instance, graph=create_graph(observer), handler=handler, ) # when instance.number += 1 # then self.assertEqual(handler.call_count, 1) handler.reset_mock() # when instance.number2 += 1 # then self.assertEqual(handler.call_count, 1)
def test_maintain_notifier_for_removed(self): # Test removing downstream notifier by observing a nested dict # inside another dict instance = ClassWithDict(dict_of_dict={"1": {"2": 2}}) graph = create_graph( create_observer(notify=False, optional=False), create_observer(notify=True, optional=False), ) handler = mock.Mock() call_add_or_remove_notifiers( object=instance.dict_of_dict, graph=graph, handler=handler, ) # sanity check test setup inner_dict = instance.dict_of_dict["1"] inner_dict["3"] = 3 self.assertEqual(handler.call_count, 1) ((event, ), _), = handler.call_args_list self.assertEqual(event.added, {"3": 3}) self.assertEqual(event.removed, {}) handler.reset_mock() # when # Change the content to something else instance.dict_of_dict["1"] = {} # the inner dict is not inside the instance.dict_of_dict any more inner_dict["4"] = 4 # then self.assertEqual(handler.call_count, 0)
def test_maintain_notifier_for_added(self): # Test adding downstream notifier by observing a nested dict # inside another dict instance = ClassWithDict() graph = create_graph( create_observer(notify=False, optional=False), create_observer(notify=True, optional=False), ) handler = mock.Mock() call_add_or_remove_notifiers( object=instance.dict_of_dict, graph=graph, handler=handler, ) # when instance.dict_of_dict.update({"1": {"2": 2}}) # then # ``notify`` is set to False for mutations on the outer dict self.assertEqual(handler.call_count, 0) # when del instance.dict_of_dict["1"]["2"] # then # ``notify`` is set to True for mutations on the inner dict self.assertEqual(handler.call_count, 1) ((event, ), _), = handler.call_args_list self.assertEqual(event.added, {}) self.assertEqual(event.removed, {"2": 2})
def test_observe_remove_notifiers_remove_trait_added(self): graph = create_graph( create_observer(name="value", notify=True, optional=True), ) handler = mock.Mock() foo = ClassWithInstance() # when # The following should cancel each other call_add_or_remove_notifiers( object=foo, graph=graph, handler=handler, remove=False) call_add_or_remove_notifiers( object=foo, graph=graph, handler=handler, remove=True) # when foo.add_trait("value", Int()) # then self.assertEqual(handler.call_count, 0) # when foo.value += 1 # then self.assertEqual(handler.call_count, 0)
def test_get_maintainer_excuse_old_value_with_no_notifiers(self): # The "instance" trait has a default that has not been # materialized prior to the user setting a new value to the trait. # There isn't an old: Uninitialized -> new: Default value change event. # Instead, there is a old: Default -> new value event. # The old default value in this event won't have any notifiers # to be removed, therefore we need to excuse the NotifierNotFound # in the maintainer when it tries to remove notifiers from the old # value. foo = ClassWithDefault() graph = create_graph( create_observer(name="instance", notify=True), create_observer(name="value1", notify=True), ) handler = mock.Mock() call_add_or_remove_notifiers( object=foo, graph=graph, handler=handler, ) try: foo.instance = ClassWithTwoValue() except Exception: self.fail( "Reassigning the instance value should not fail." )
def test_remove_atomic(self): # Test atomicity notifier = DummyNotifier() maintainer = DummyNotifier() observable1 = DummyObservable() observable1.notifiers = [ notifier, maintainer, ] old_observable1_notifiers = observable1.notifiers.copy() observable2 = DummyObservable() observable2.notifiers = [maintainer] old_observable2_notifiers = observable2.notifiers.copy() observable3 = DummyObservable() observable3.notifiers = [ notifier, maintainer, ] old_observable3_notifiers = observable3.notifiers.copy() observer = DummyObserver( notify=True, observables=[ observable1, observable2, observable3, ], notifier=notifier, maintainer=maintainer, ) graph = create_graph( observer, DummyObserver(), # Need a child graph to get maintainer in ) # when with self.assertRaises(NotifierNotFound): call_add_or_remove_notifiers( object=mock.Mock(), graph=graph, remove=True, ) # then # as if nothing has happened, the order might not be maintained though! self.assertCountEqual( observable1.notifiers, old_observable1_notifiers, ) self.assertCountEqual( observable2.notifiers, old_observable2_notifiers, ) self.assertCountEqual( observable3.notifiers, old_observable3_notifiers, )
def test_notifier_trait_added_distinguished(self): # Add two observers, both will have their own additional trait_added # observer. When one is removed, the other one is not affected. graph1 = create_graph( create_observer(name="some_value1", notify=True, optional=True), ) graph2 = create_graph( create_observer(name="some_value2", notify=True, optional=True), ) handler = mock.Mock() foo = ClassWithInstance() # Add two observers call_add_or_remove_notifiers( object=foo, graph=graph1, handler=handler, remove=False) call_add_or_remove_notifiers( object=foo, graph=graph2, handler=handler, remove=False) # when # Now remove the second observer call_add_or_remove_notifiers( object=foo, graph=graph2, handler=handler, remove=True) # the first one should still respond to trait_added event foo.add_trait("some_value1", Int()) foo.some_value1 += 1 # then self.assertEqual(handler.call_count, 1) handler.reset_mock() # when # the second observer has been removed foo.add_trait("some_value2", Int()) foo.some_value2 += 1 # then self.assertEqual(handler.call_count, 0)
def test_trait_added_removed(self): instance = Dummy() integer_observer = create_observer( filter=lambda name, trait: type(trait.trait_type) is Int, notify=True, ) handler = mock.Mock() call_add_or_remove_notifiers( object=instance, graph=create_graph(integer_observer), handler=handler, ) # Add two traits. # If the maintainer from TraitAddedObserver did not restrict its # action to just the added trait, when 'count' is added, the previously # added 'another_number' would have received a second notifier again. # Then it would require two *remove* actions in order to clean up # notifiers on 'another_number'. instance.add_trait("another_number", Int()) instance.add_trait("count", Int()) # when call_add_or_remove_notifiers( object=instance, graph=create_graph(integer_observer), handler=handler, remove=True, ) # then instance.another_number += 1 self.assertEqual(handler.call_count, 0) instance.count += 1 self.assertEqual(handler.call_count, 0)
def test_optional_observers(self): # ListItemObserver.optional is true, meaning it will ignore # incompatible incoming object. instance = ClassWithList() graph = create_graph(ListItemObserver(notify=True, optional=True), ) handler = mock.Mock() call_add_or_remove_notifiers( object=instance.not_a_trait_list, graph=graph, handler=handler, ) instance.not_a_trait_list = CustomList() instance.not_a_trait_list.append(1) self.assertEqual(handler.call_count, 0)
def test_notify_set_change(self): instance = ClassWithSet(values=set()) graph = create_graph(create_observer(notify=True), ) handler = mock.Mock() call_add_or_remove_notifiers( object=instance.values, graph=graph, handler=handler, ) # when instance.values.add(1) # then ((event, ), _), = handler.call_args_list self.assertEqual(event.added, set([1])) self.assertEqual(event.removed, set())
def test_optional_trait_added(self): graph = create_graph( create_observer(name="value", notify=True, optional=True), ) handler = mock.Mock() not_an_has_traits_instance = mock.Mock() # does not complain because optional is set to true try: call_add_or_remove_notifiers( object=not_an_has_traits_instance, graph=graph, handler=handler, ) except Exception: self.fail("Optional flag should have been propagated.")
def test_maintain_notifier(self): # Test maintaining downstream notifier by # observing list of list instance = ClassWithListOfList() graph = create_graph( ListItemObserver(notify=False, optional=False), ListItemObserver(notify=True, optional=False), ) handler = mock.Mock() call_add_or_remove_notifiers( object=instance.list_of_list, graph=graph, handler=handler, ) # when instance.list_of_list.append([]) # then # the first ListItemObserver has notify=False self.assertEqual(handler.call_count, 0) # but the second ListItemObserver is given to the nested list nested_list = instance.list_of_list[0] # when nested_list.append(1) # then ((event, ), _), = handler.call_args_list self.assertIs(event.object, nested_list) self.assertEqual(event.added, [1]) self.assertEqual(event.removed, []) self.assertEqual(event.index, 0) handler.reset_mock() # when # the list is removed, it is not observed instance.list_of_list.pop() nested_list.append(1) # then self.assertEqual(handler.call_count, 0)
def test_notify_custom_trait_dict_change(self): # Test using DictItemObserver for changes on a subclass of TraitDict # that isn't TraitDictObject instance = ClassWithDict(custom_trait_dict=CustomTraitDict()) graph = create_graph(create_observer(notify=True), ) handler = mock.Mock() call_add_or_remove_notifiers( object=instance.custom_trait_dict, graph=graph, handler=handler, ) # when instance.custom_trait_dict.update({"1": 1}) # then ((event, ), _), = handler.call_args_list self.assertEqual(event.added, {"1": 1}) self.assertEqual(event.removed, {})
def test_trait_added_filtered_matched(self): instance = DummyParent() integer_observer = create_observer( filter=lambda name, trait: type(trait.trait_type) is Int, notify=True, ) handler = mock.Mock() call_add_or_remove_notifiers( object=instance, graph=create_graph(integer_observer), handler=handler, ) # when instance.add_trait("another_number", Int()) instance.another_number += 1 # then self.assertEqual(handler.call_count, 1)
def test_notifier_list_change(self): instance = ClassWithList(values=[]) graph = create_graph(ListItemObserver(notify=True, optional=False), ) handler = mock.Mock() call_add_or_remove_notifiers( object=instance.values, graph=graph, handler=handler, ) # when instance.values.append(1) # then ((event, ), _), = handler.call_args_list self.assertIs(event.object, instance.values) self.assertEqual(event.added, [1]) self.assertEqual(event.removed, []) self.assertEqual(event.index, 0)
def test_remove_trait_then_add_trait_again(self): # Test a scenario where a trait exists when the observer is hooked, # but then the trait is removed, and then added back again, the # observer is gone, because the CTrait is gone and the trait_added # event is not fired for something already defined on the class. # given # the trait exists, we can set optional to false. graph = create_graph( create_observer(name="value1", notify=True, optional=False), ) handler = mock.Mock() foo = ClassWithTwoValue() call_add_or_remove_notifiers( object=foo, graph=graph, handler=handler, remove=False) # sanity check, the handler is called when the trait changes. foo.value1 += 1 handler.assert_called_once() handler.reset_mock() # when # remove the trait foo.remove_trait("value1") # then # the handler is gone with the instance trait. foo.value1 += 1 handler.assert_not_called() # when # Add the trait back... foo.add_trait("value1", Int()) # then # won't bring the handler back, because the 'value1' is defined as a # class trait, trait_added is not fired when it is added. foo.value1 += 1 handler.assert_not_called()
def test_observe_respond_to_trait_added(self): graph = create_graph( create_observer(name="value", notify=True, optional=True), ) handler = mock.Mock() foo = ClassWithInstance() # when # does not complain because optional is set to true call_add_or_remove_notifiers(object=foo, graph=graph, handler=handler) # when foo.add_trait("value", Int()) # then self.assertEqual(handler.call_count, 0) # when foo.value += 1 # then self.assertEqual(handler.call_count, 1)
def test_maintainer_trait_added(self): # Test the maintainer is added for the trait_added event. instance = DummyHasTraitsClass() notifier = DummyNotifier() maintainer = DummyNotifier() graph = create_graph( self.observer, DummyObserver( notify=True, notifier=notifier, maintainer=maintainer, ), DummyObserver(), # to get maintainer in ) call_add_or_remove_notifiers( object=instance, handler=instance.dummy_method, target=instance, graph=graph, remove=False, ) # when instance.add_trait("good_name", Str()) # then # the maintainer will have added a notifier because notify flag # is set to true on the single observer being maintained. notifiers = instance._trait("good_name", 2)._notifiers(True) self.assertIn(notifier, notifiers) self.assertIn(maintainer, notifiers) # when instance.add_trait("bad_name", Str()) # then notifiers = instance._trait("bad_name", 2)._notifiers(True) self.assertNotIn(notifier, notifiers) self.assertNotIn(maintainer, notifiers)
def test_notifier_custom_trait_list_change(self): # Test compatibility with any extension of TraitList, not just # TraitListObject instance = ClassWithList() instance.custom_trait_list = CustomTraitList() graph = create_graph(ListItemObserver(notify=True, optional=False), ) handler = mock.Mock() call_add_or_remove_notifiers( object=instance.custom_trait_list, graph=graph, handler=handler, ) # when instance.custom_trait_list.append(1) # then ((event, ), _), = handler.call_args_list self.assertIs(event.object, instance.custom_trait_list) self.assertEqual(event.added, [1]) self.assertEqual(event.removed, []) self.assertEqual(event.index, 0)
def test_trait_added_match_func_correct(self): # Test the match function supplied to TraitAddedObserver is consistent # with the filter. instance = DummyParent() integer_observer = create_observer( filter=lambda name, trait: type(trait.trait_type) is Int, notify=True, ) handler = mock.Mock() call_add_or_remove_notifiers( object=instance, graph=create_graph(integer_observer), handler=handler, ) # when # This trait does not satisfy the filter instance.add_trait("another_number", Float()) instance.another_number += 1 # then self.assertEqual(handler.call_count, 0)
def test_maintain_notifier(self): # Test maintaining downstream notifier class ChildObserver(DummyObserver): def iter_observables(self, object): yield object instance = ClassWithSet() instance.values = set() notifier = DummyNotifier() child_observer = ChildObserver(notifier=notifier) graph = create_graph( create_observer(notify=False, optional=False), child_observer, ) handler = mock.Mock() call_add_or_remove_notifiers( object=instance.values, graph=graph, handler=handler, ) # when observable = DummyObservable() instance.values.add(observable) # then self.assertEqual(observable.notifiers, [notifier]) # when instance.values.remove(observable) # then self.assertEqual(observable.notifiers, [])