Skip to content

Commit 86cdb2f

Browse files
authored
Fix that clone_traits doesn't respect dynamic traits (#1846)
Closes #1845 This PR fixes `clone_traits` (and `copy_traits`) to respect dynamic traits. Previously, the `HasTraits.copy_traits` method used a mixture of trait information from `self` (the object being copied _to_) and `other` (the object being copied _from_) to determine how to behave. Since the actual copying was enclosed in a large `try / except` block, any errors arising just resulted in the trait value not being copied. In particular, when cloning a `HasTraits` object with dynamically-added traits: - the set of traits to copy was first determined by inspecting the object, and included those dynamically-added traits - then, in `copy_traits`, we inspect the cloned object that we're copying _to_, which doesn't have the dynamically-added traits, and as a result we fail to copy those trait values. This PR updates the logic to only use the trait information from `other` when deciding what / how to copy, instead of using a mixture of sources. As a result, `clone_traits` should behave as expected on objects with dynamically-added traits. We have a large corpus of existing Traits code, and like all changes to Traits this change risks unintended consequences and surprise breakages of existing code that's inadvertently relying on the existing behaviour. While I think it's the right change to make, I'd advocate not backporting this change to any bugfix branch, and highlighting the change prominently in release notes.
1 parent ce0af93 commit 86cdb2f

File tree

2 files changed

+22
-7
lines changed

2 files changed

+22
-7
lines changed

traits/has_traits.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1554,9 +1554,9 @@ def copy_traits(
15541554
The object whose trait attribute values should be copied.
15551555
traits : list of strings
15561556
A list of names of trait attributes to copy. If None or
1557-
unspecified, the set of names returned by trait_names() is used.
1558-
If 'all' or an empty list, the set of names returned by
1559-
all_trait_names() is used.
1557+
unspecified, the set of names returned by
1558+
other.copyable_trait_names() is used. If 'all' or an empty list,
1559+
the set of names returned by other.all_trait_names() is used.
15601560
memo : dict
15611561
A dictionary of objects that have already been copied.
15621562
copy : None | 'deep' | 'shallow'
@@ -1572,9 +1572,9 @@ def copy_traits(
15721572
"""
15731573

15741574
if traits is None:
1575-
traits = self.copyable_trait_names(**metadata)
1575+
traits = other.copyable_trait_names(**metadata)
15761576
elif (traits == "all") or (len(traits) == 0):
1577-
traits = self.all_trait_names()
1577+
traits = other.all_trait_names()
15781578
if memo is not None:
15791579
memo["traits_to_copy"] = "all"
15801580

@@ -1585,7 +1585,7 @@ def copy_traits(
15851585

15861586
for name in traits:
15871587
try:
1588-
trait = self.trait(name)
1588+
trait = other.trait(name)
15891589
if trait.type in DeferredCopy:
15901590
deferred.append(name)
15911591
continue

traits/tests/test_clone.py

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010

1111
import unittest
1212

13-
from traits.api import HasTraits, Instance, Str, Any, Property
13+
from traits.api import HasTraits, Instance, Int, Str, Any, Property
1414

1515

1616
class Foo(HasTraits):
@@ -280,3 +280,18 @@ def test_Instance_circular_references_deep(self):
280280
# should reference the new clone.
281281
self.assertIsNot(bar_copy.shared, baz.shared)
282282
self.assertIs(bar_copy.shared, baz_copy.shared)
283+
284+
def test_clone_traits_copies_dynamic_traits(self):
285+
# Regression test for https://github.com/enthought/traits/issues/1845
286+
287+
# Given
288+
ref = Foo(s="ref")
289+
ref.add_trait("dynamic", Int(42))
290+
ref.some_other_value = 35
291+
292+
# When
293+
clone = ref.clone_traits()
294+
295+
# Then
296+
self.assertEqual(clone.some_other_value, 35)
297+
self.assertEqual(clone.dynamic, 42)

0 commit comments

Comments
 (0)