Show More
@@ -69,6 +69,11 b' class Configurable(HasTraits):' | |||||
69 | self.parent = parent |
|
69 | self.parent = parent | |
70 |
|
70 | |||
71 | config = kwargs.pop('config', None) |
|
71 | config = kwargs.pop('config', None) | |
|
72 | ||||
|
73 | # load kwarg traits, other than config | |||
|
74 | super(Configurable, self).__init__(**kwargs) | |||
|
75 | ||||
|
76 | # load config | |||
72 | if config is not None: |
|
77 | if config is not None: | |
73 | # We used to deepcopy, but for now we are trying to just save |
|
78 | # We used to deepcopy, but for now we are trying to just save | |
74 | # by reference. This *could* have side effects as all components |
|
79 | # by reference. This *could* have side effects as all components | |
@@ -81,9 +86,12 b' class Configurable(HasTraits):' | |||||
81 | else: |
|
86 | else: | |
82 | # allow _config_default to return something |
|
87 | # allow _config_default to return something | |
83 | self._load_config(self.config) |
|
88 | self._load_config(self.config) | |
84 | # This should go second so individual keyword arguments override |
|
89 | ||
85 | # the values in config. |
|
90 | # Ensure explicit kwargs are applied after loading config. | |
86 | super(Configurable, self).__init__(**kwargs) |
|
91 | # This is usually redundant, but ensures config doesn't override | |
|
92 | # explicitly assigned values. | |||
|
93 | for key, value in kwargs.items(): | |||
|
94 | setattr(self, key, value) | |||
87 |
|
95 | |||
88 | #------------------------------------------------------------------------- |
|
96 | #------------------------------------------------------------------------- | |
89 | # Static trait notifiations |
|
97 | # Static trait notifiations | |
@@ -130,17 +138,20 b' class Configurable(HasTraits):' | |||||
130 | section_names = self.section_names() |
|
138 | section_names = self.section_names() | |
131 |
|
139 | |||
132 | my_config = self._find_my_config(cfg) |
|
140 | my_config = self._find_my_config(cfg) | |
133 | for name, config_value in iteritems(my_config): |
|
141 | ||
134 | if name in traits: |
|
142 | # hold trait notifications until after all config has been loaded | |
135 | if isinstance(config_value, LazyConfigValue): |
|
143 | with self.hold_trait_notifications(): | |
136 | # ConfigValue is a wrapper for using append / update on containers |
|
144 | for name, config_value in iteritems(my_config): | |
137 | # without having to copy the |
|
145 | if name in traits: | |
138 | initial = getattr(self, name) |
|
146 | if isinstance(config_value, LazyConfigValue): | |
139 | config_value = config_value.get_value(initial) |
|
147 | # ConfigValue is a wrapper for using append / update on containers | |
140 | # We have to do a deepcopy here if we don't deepcopy the entire |
|
148 | # without having to copy the initial value | |
141 | # config object. If we don't, a mutable config_value will be |
|
149 | initial = getattr(self, name) | |
142 | # shared by all instances, effectively making it a class attribute. |
|
150 | config_value = config_value.get_value(initial) | |
143 | setattr(self, name, deepcopy(config_value)) |
|
151 | # We have to do a deepcopy here if we don't deepcopy the entire | |
|
152 | # config object. If we don't, a mutable config_value will be | |||
|
153 | # shared by all instances, effectively making it a class attribute. | |||
|
154 | setattr(self, name, deepcopy(config_value)) | |||
144 |
|
155 | |||
145 | def _config_changed(self, name, old, new): |
|
156 | def _config_changed(self, name, old, new): | |
146 | """Update all the class traits having ``config=True`` as metadata. |
|
157 | """Update all the class traits having ``config=True`` as metadata. |
@@ -1330,6 +1330,78 b' def test_pickle_hastraits():' | |||||
1330 | nt.assert_equal(c2.i, c.i) |
|
1330 | nt.assert_equal(c2.i, c.i) | |
1331 | nt.assert_equal(c2.j, c.j) |
|
1331 | nt.assert_equal(c2.j, c.j) | |
1332 |
|
1332 | |||
|
1333 | ||||
|
1334 | def test_hold_trait_notifications(): | |||
|
1335 | changes = [] | |||
|
1336 | class Test(HasTraits): | |||
|
1337 | a = Integer(0) | |||
|
1338 | def _a_changed(self, name, old, new): | |||
|
1339 | changes.append((old, new)) | |||
|
1340 | ||||
|
1341 | t = Test() | |||
|
1342 | with t.hold_trait_notifications(): | |||
|
1343 | with t.hold_trait_notifications(): | |||
|
1344 | t.a = 1 | |||
|
1345 | nt.assert_equal(t.a, 1) | |||
|
1346 | nt.assert_equal(changes, []) | |||
|
1347 | t.a = 2 | |||
|
1348 | nt.assert_equal(t.a, 2) | |||
|
1349 | with t.hold_trait_notifications(): | |||
|
1350 | t.a = 3 | |||
|
1351 | nt.assert_equal(t.a, 3) | |||
|
1352 | nt.assert_equal(changes, []) | |||
|
1353 | t.a = 4 | |||
|
1354 | nt.assert_equal(t.a, 4) | |||
|
1355 | nt.assert_equal(changes, []) | |||
|
1356 | t.a = 4 | |||
|
1357 | nt.assert_equal(t.a, 4) | |||
|
1358 | nt.assert_equal(changes, []) | |||
|
1359 | nt.assert_equal(changes, [(0,1), (1,2), (2,3), (3,4)]) | |||
|
1360 | ||||
|
1361 | ||||
|
1362 | class OrderTraits(HasTraits): | |||
|
1363 | notified = Dict() | |||
|
1364 | ||||
|
1365 | a = Unicode() | |||
|
1366 | b = Unicode() | |||
|
1367 | c = Unicode() | |||
|
1368 | d = Unicode() | |||
|
1369 | e = Unicode() | |||
|
1370 | f = Unicode() | |||
|
1371 | g = Unicode() | |||
|
1372 | h = Unicode() | |||
|
1373 | i = Unicode() | |||
|
1374 | j = Unicode() | |||
|
1375 | k = Unicode() | |||
|
1376 | l = Unicode() | |||
|
1377 | ||||
|
1378 | def _notify(self, name, old, new): | |||
|
1379 | """check the value of all traits when each trait change is triggered | |||
|
1380 | ||||
|
1381 | This verifies that the values are not sensitive | |||
|
1382 | to dict ordering when loaded from kwargs | |||
|
1383 | """ | |||
|
1384 | # check the value of the other traits | |||
|
1385 | # when a given trait change notification fires | |||
|
1386 | self.notified[name] = { | |||
|
1387 | c: getattr(self, c) for c in 'abcdefghijkl' | |||
|
1388 | } | |||
|
1389 | ||||
|
1390 | def __init__(self, **kwargs): | |||
|
1391 | self.on_trait_change(self._notify) | |||
|
1392 | super(OrderTraits, self).__init__(**kwargs) | |||
|
1393 | ||||
|
1394 | def test_notification_order(): | |||
|
1395 | d = {c:c for c in 'abcdefghijkl'} | |||
|
1396 | obj = OrderTraits() | |||
|
1397 | nt.assert_equal(obj.notified, {}) | |||
|
1398 | obj = OrderTraits(**d) | |||
|
1399 | notifications = { | |||
|
1400 | c: d for c in 'abcdefghijkl' | |||
|
1401 | } | |||
|
1402 | nt.assert_equal(obj.notified, notifications) | |||
|
1403 | ||||
|
1404 | ||||
1333 | class TestEventful(TestCase): |
|
1405 | class TestEventful(TestCase): | |
1334 |
|
1406 | |||
1335 | def test_list(self): |
|
1407 | def test_list(self): |
@@ -575,8 +575,36 b' class HasTraits(py3compat.with_metaclass(MetaHasTraits, object)):' | |||||
575 | # Allow trait values to be set using keyword arguments. |
|
575 | # Allow trait values to be set using keyword arguments. | |
576 | # We need to use setattr for this to trigger validation and |
|
576 | # We need to use setattr for this to trigger validation and | |
577 | # notifications. |
|
577 | # notifications. | |
578 | for key, value in iteritems(kw): |
|
578 | ||
579 | setattr(self, key, value) |
|
579 | with self.hold_trait_notifications(): | |
|
580 | for key, value in iteritems(kw): | |||
|
581 | setattr(self, key, value) | |||
|
582 | ||||
|
583 | @contextlib.contextmanager | |||
|
584 | def hold_trait_notifications(self): | |||
|
585 | """Context manager for bundling trait change notifications | |||
|
586 | ||||
|
587 | Use this when doing multiple trait assignments (init, config), | |||
|
588 | to avoid race conditions in trait notifiers requesting other trait values. | |||
|
589 | All trait notifications will fire after all values have been assigned. | |||
|
590 | """ | |||
|
591 | _notify_trait = self._notify_trait | |||
|
592 | notifications = [] | |||
|
593 | self._notify_trait = lambda *a: notifications.append(a) | |||
|
594 | ||||
|
595 | try: | |||
|
596 | yield | |||
|
597 | finally: | |||
|
598 | self._notify_trait = _notify_trait | |||
|
599 | if isinstance(_notify_trait, types.MethodType): | |||
|
600 | # FIXME: remove when support is bumped to 3.4. | |||
|
601 | # when original method is restored, | |||
|
602 | # remove the redundant value from __dict__ | |||
|
603 | # (only used to preserve pickleability on Python < 3.4) | |||
|
604 | self.__dict__.pop('_notify_trait', None) | |||
|
605 | # trigger delayed notifications | |||
|
606 | for args in notifications: | |||
|
607 | self._notify_trait(*args) | |||
580 |
|
608 | |||
581 | def _notify_trait(self, name, old_value, new_value): |
|
609 | def _notify_trait(self, name, old_value, new_value): | |
582 |
|
610 |
General Comments 0
You need to be logged in to leave comments.
Login now