From 54beabc89abb49620527617c5e1a0d53811244b6 2015-03-22 23:47:14 From: Min RK Date: 2015-03-22 23:47:14 Subject: [PATCH] add HasTraits.delay_trait_notifications context manager for delaying trait notifications, used to avoid race conditions in dict ordering when loading from kwargs. --- diff --git a/IPython/utils/tests/test_traitlets.py b/IPython/utils/tests/test_traitlets.py index 30d82a9..83b70cd 100644 --- a/IPython/utils/tests/test_traitlets.py +++ b/IPython/utils/tests/test_traitlets.py @@ -1330,6 +1330,50 @@ def test_pickle_hastraits(): nt.assert_equal(c2.i, c.i) nt.assert_equal(c2.j, c.j) + +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..d35e289 100644 --- a/IPython/utils/traitlets.py +++ b/IPython/utils/traitlets.py @@ -575,8 +575,31 @@ 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.delay_trait_notifications(): + for key, value in iteritems(kw): + setattr(self, key, value) + + @contextlib.contextmanager + def delay_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 + + # trigger delayed notifications + for args in notifications: + self._notify_trait(*args) def _notify_trait(self, name, old_value, new_value):