From 4f935ba4ad6b6bb16123dacd2768bb9b044752b3 2015-03-23 18:51:20 From: Thomas Kluyver Date: 2015-03-23 18:51:20 Subject: [PATCH] Merge pull request #8114 from minrk/delay_trait_notifications add HasTraits.hold_trait_notifications --- diff --git a/IPython/config/configurable.py b/IPython/config/configurable.py index 3402f07..51a8fd0 100644 --- a/IPython/config/configurable.py +++ b/IPython/config/configurable.py @@ -69,6 +69,11 @@ class Configurable(HasTraits): self.parent = parent config = kwargs.pop('config', None) + + # load kwarg traits, other than config + super(Configurable, self).__init__(**kwargs) + + # load config if config is not None: # We used to deepcopy, but for now we are trying to just save # by reference. This *could* have side effects as all components @@ -81,9 +86,12 @@ class Configurable(HasTraits): else: # allow _config_default to return something self._load_config(self.config) - # This should go second so individual keyword arguments override - # the values in config. - super(Configurable, self).__init__(**kwargs) + + # Ensure explicit kwargs are applied after loading config. + # This is usually redundant, but ensures config doesn't override + # explicitly assigned values. + for key, value in kwargs.items(): + setattr(self, key, value) #------------------------------------------------------------------------- # Static trait notifiations @@ -130,17 +138,20 @@ class Configurable(HasTraits): section_names = self.section_names() my_config = self._find_my_config(cfg) - for name, config_value in iteritems(my_config): - if name in traits: - if isinstance(config_value, LazyConfigValue): - # ConfigValue is a wrapper for using append / update on containers - # without having to copy the - initial = getattr(self, name) - config_value = config_value.get_value(initial) - # We have to do a deepcopy here if we don't deepcopy the entire - # config object. If we don't, a mutable config_value will be - # shared by all instances, effectively making it a class attribute. - setattr(self, name, deepcopy(config_value)) + + # hold trait notifications until after all config has been loaded + with self.hold_trait_notifications(): + for name, config_value in iteritems(my_config): + if name in traits: + if isinstance(config_value, LazyConfigValue): + # ConfigValue is a wrapper for using append / update on containers + # without having to copy the initial value + initial = getattr(self, name) + config_value = config_value.get_value(initial) + # We have to do a deepcopy here if we don't deepcopy the entire + # config object. If we don't, a mutable config_value will be + # shared by all instances, effectively making it a class attribute. + setattr(self, name, deepcopy(config_value)) def _config_changed(self, name, old, new): """Update all the class traits having ``config=True`` as metadata. diff --git a/IPython/utils/tests/test_traitlets.py b/IPython/utils/tests/test_traitlets.py index 30d82a9..1b364f7 100644 --- a/IPython/utils/tests/test_traitlets.py +++ b/IPython/utils/tests/test_traitlets.py @@ -1330,6 +1330,78 @@ def test_pickle_hastraits(): nt.assert_equal(c2.i, c.i) nt.assert_equal(c2.j, c.j) + +def test_hold_trait_notifications(): + changes = [] + class Test(HasTraits): + a = Integer(0) + def _a_changed(self, name, old, new): + changes.append((old, new)) + + t = Test() + with t.hold_trait_notifications(): + with t.hold_trait_notifications(): + t.a = 1 + nt.assert_equal(t.a, 1) + nt.assert_equal(changes, []) + t.a = 2 + nt.assert_equal(t.a, 2) + with t.hold_trait_notifications(): + t.a = 3 + nt.assert_equal(t.a, 3) + nt.assert_equal(changes, []) + t.a = 4 + nt.assert_equal(t.a, 4) + nt.assert_equal(changes, []) + t.a = 4 + nt.assert_equal(t.a, 4) + nt.assert_equal(changes, []) + nt.assert_equal(changes, [(0,1), (1,2), (2,3), (3,4)]) + + +class OrderTraits(HasTraits): + notified = Dict() + + a = Unicode() + b = Unicode() + c = Unicode() + d = Unicode() + e = Unicode() + f = Unicode() + g = Unicode() + h = Unicode() + i = Unicode() + j = Unicode() + k = Unicode() + l = Unicode() + + def _notify(self, name, old, new): + """check the value of all traits when each trait change is triggered + + This verifies that the values are not sensitive + to dict ordering when loaded from kwargs + """ + # check the value of the other traits + # when a given trait change notification fires + self.notified[name] = { + c: getattr(self, c) for c in 'abcdefghijkl' + } + + def __init__(self, **kwargs): + self.on_trait_change(self._notify) + super(OrderTraits, self).__init__(**kwargs) + +def test_notification_order(): + d = {c:c for c in 'abcdefghijkl'} + obj = OrderTraits() + nt.assert_equal(obj.notified, {}) + obj = OrderTraits(**d) + notifications = { + c: d for c in 'abcdefghijkl' + } + nt.assert_equal(obj.notified, notifications) + + class TestEventful(TestCase): def test_list(self): diff --git a/IPython/utils/traitlets.py b/IPython/utils/traitlets.py index 7d3c5f1..f2fb877 100644 --- a/IPython/utils/traitlets.py +++ b/IPython/utils/traitlets.py @@ -575,8 +575,36 @@ class HasTraits(py3compat.with_metaclass(MetaHasTraits, object)): # Allow trait values to be set using keyword arguments. # We need to use setattr for this to trigger validation and # notifications. - for key, value in iteritems(kw): - setattr(self, key, value) + + with self.hold_trait_notifications(): + for key, value in iteritems(kw): + setattr(self, key, value) + + @contextlib.contextmanager + def hold_trait_notifications(self): + """Context manager for bundling trait change notifications + + Use this when doing multiple trait assignments (init, config), + to avoid race conditions in trait notifiers requesting other trait values. + All trait notifications will fire after all values have been assigned. + """ + _notify_trait = self._notify_trait + notifications = [] + self._notify_trait = lambda *a: notifications.append(a) + + try: + yield + finally: + self._notify_trait = _notify_trait + if isinstance(_notify_trait, types.MethodType): + # FIXME: remove when support is bumped to 3.4. + # when original method is restored, + # remove the redundant value from __dict__ + # (only used to preserve pickleability on Python < 3.4) + self.__dict__.pop('_notify_trait', None) + # trigger delayed notifications + for args in notifications: + self._notify_trait(*args) def _notify_trait(self, name, old_value, new_value):