Repository: ipython/traitlets
Pull request #482 (2018-03-21 17:25:24)

Fix copy(HasTraits) by copying `_trait_values` dict
- Added also TC.
- Waiting for response on question whether must reset `_cross_validation_lock' lock back to `False` 
  (asked also in #481).

ankostis: All test-cases pass, except from Appveyor on Windows (expecting #483 to merge for that).
minrk: Makes sense. I'm not sure what to do when cross_validation_lock is held. It does make sense to clear it, but what's the state going to be if a HasTraits is copied in the middle of pending changes? Should the cross-validation be forced to finish as part of the copy?
ankostis: Ok, pinned the `_cross_validation_lock=False` when cloning.

> Should the cross-validation be forced to finish as part of the copy?

Maybe raise an exception, to fail early?
ankostis: Btw, I have pushed a relevant commit about `utils.sentinel.Sentinel` ignoring copy/deepcopy.
Sentinels must not change, or cloned traits fail defaults 
(a a really nasty bug discovered a year ago, please don't ask me more).

Patch:
diff --git a/traitlets/traitlets.py b/traitlets/traitlets.py
index 4ad3e95..be2a8f9 100644
--- a/traitlets/traitlets.py
+++ b/traitlets/traitlets.py
@@ -1088,6 +1088,9 @@ class HasTraits(six.with_metaclass(MetaHasTraits, HasDescriptors)):
         # recall of instance_init during __setstate__
         d['_trait_notifiers'] = {}
         d['_trait_validators'] = {}
+        d['_trait_values'] = self._trait_values.copy()
+        d['_cross_validation_lock'] = False  # FIXME: raise if cloning locked!
+
         return d
 
     def __setstate__(self, state):
diff --git a/traitlets/utils/sentinel.py b/traitlets/utils/sentinel.py
index dc57a25..0760bec 100644
--- a/traitlets/utils/sentinel.py
+++ b/traitlets/utils/sentinel.py
@@ -3,6 +3,7 @@
 # Copyright (c) IPython Development Team.
 # Distributed under the terms of the Modified BSD License.
 
+
 class Sentinel(object):
 
     def __init__(self, name, module, docstring=None):
@@ -11,7 +12,11 @@ class Sentinel(object):
         if docstring:
             self.__doc__ = docstring
 
-
     def __repr__(self):
-        return str(self.module)+'.'+self.name
+        return str(self.module) + '.' + self.name
+
+    def __copy__(self):
+        return self
 
+    def __deepcopy__(self, memo):
+        return self


Test patch:
diff --git a/traitlets/tests/test_traitlets.py b/traitlets/tests/test_traitlets.py
index 1e2029a..acb8a63 100644
--- a/traitlets/tests/test_traitlets.py
+++ b/traitlets/tests/test_traitlets.py
@@ -2598,7 +2598,7 @@ def test_override_default():
         a = Unicode('hard default')
         def _a_default(self):
             return 'default method'
-    
+
     C._a_default = lambda self: 'overridden'
     c = C()
     assert c.a == 'overridden'
@@ -2609,7 +2609,7 @@ def test_override_default_decorator():
         @default('a')
         def _a_default(self):
             return 'default method'
-    
+
     C._a_default = lambda self: 'overridden'
     c = C()
     assert c.a == 'overridden'
@@ -2620,8 +2620,21 @@ def test_override_default_instance():
         @default('a')
         def _a_default(self):
             return 'default method'
-    
+
     c = C()
     c._a_default = lambda self: 'overridden'
     assert c.a == 'overridden'
 
+def test_copy_HasTraits():
+    from copy import copy
+
+    class C(HasTraits):
+        a = Int()
+
+    c = C(a=1)
+    assert c.a == 1
+
+    cc = copy(c)
+    cc.a = 2
+    assert cc.a == 2
+    assert c.a == 1