From d01fbce154886caa1f890ce5c1ead396a0252ab1 Mon Sep 17 00:00:00 2001 From: hakarrr Date: Sat, 24 Oct 2026 19:02:07 +0530 Subject: [PATCH 1/4] Fix key assignment and YAMLSerializer representation --- benedict/dicts/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/benedict/dicts/__init__.py b/benedict/dicts/__init__.py index 92eca8da..22a77b67 100644 --- a/benedict/dicts/__init__.py +++ b/benedict/dicts/__init__.py @@ -92,7 +92,7 @@ def __deepcopy__(self, memo: dict[int, Any]) -> Self: keypath_separator=self._keypath_separator, ) for key, value in self.items(): - obj[key] = _clone(value, memo=memo) + obj[_clone(key, memo=memo)] = _clone(value, memo=memo) if self._frozen: obj.freeze() return obj From a12402a798db60527c0ba80ddac60f5beb4a1a95 Mon Sep 17 00:00:00 2001 From: hakarrr Date: Sat, 24 Oct 2026 19:02:24 +0530 Subject: [PATCH 2/4] Deep-copy BaseDict keys with the value memo --- benedict/dicts/base/base_dict.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/benedict/dicts/base/base_dict.py b/benedict/dicts/base/base_dict.py index 204fdcf0..766d50bb 100644 --- a/benedict/dicts/base/base_dict.py +++ b/benedict/dicts/base/base_dict.py @@ -85,7 +85,7 @@ def __contains__(self, o: object) -> bool: def __deepcopy__(self, memo: dict[int, Any]) -> Self: obj = self.__class__() for key, value in self.items(): - obj[key] = _clone(value, memo=memo) + obj[_clone(key, memo=memo)] = _clone(value, memo=memo) if self._frozen: obj.freeze() return obj From ae5cb8cfb493f1e89dcf6383479c2cbd209672d7 Mon Sep 17 00:00:00 2001 From: hakarrr Date: Sat, 24 Oct 2026 19:04:26 +0530 Subject: [PATCH 3/4] Cover copied key aliases, wrapped dictionaries and frozen settings --- tests/dicts/test_deepcopy_keys.py | 61 +++++++++++++++++++++++++++++++ 1 file changed, 61 insertions(+) create mode 100644 tests/dicts/test_deepcopy_keys.py diff --git a/tests/dicts/test_deepcopy_keys.py b/tests/dicts/test_deepcopy_keys.py new file mode 100644 index 00000000..5b589817 --- /dev/null +++ b/tests/dicts/test_deepcopy_keys.py @@ -0,0 +1,61 @@ +from __future__ import annotations + +import copy +import unittest + +from benedict import benedict +from benedict.dicts.base import BaseDict + + +class deepcopy_keys_test_case(unittest.TestCase): + def test_base_deepcopy_custom_keys(self) -> None: + class Key: + def __init__(self) -> None: + self.values: list[int] = [] + + for wrapped in (False, True): + for frozen in (False, True): + with self.subTest(wrapped=wrapped, frozen=frozen): + key = Key() + source = {key: [key]} + original = BaseDict(source) if wrapped else BaseDict() + if not wrapped: + original[key] = source[key] + if frozen: + original.freeze() + cloned = copy.deepcopy(original) + cloned_key = next(iter(cloned)) + self.assertIsNot(cloned_key, key) + self.assertIs(cloned[cloned_key][0], cloned_key) + self.assertEqual(cloned.frozen, frozen) + cloned_key.values.append(1) + self.assertEqual(key.values, []) + self.assertIs(next(iter(original)), key) + + def test_benedict_deepcopy_custom_keys(self) -> None: + class Key: + def __init__(self) -> None: + self.values: list[int] = [] + + for separator in (None, "/"): + for frozen in (False, True): + with self.subTest(separator=separator, frozen=frozen): + key = Key() + original = benedict( + {key: [key]}, + keypath_separator=separator, + keyattr_enabled=False, + keyattr_dynamic=True, + ) + if frozen: + original.freeze() + cloned = original.deepcopy() + cloned_key = next(iter(cloned)) + self.assertIsNot(cloned_key, key) + self.assertIs(cloned[cloned_key][0], cloned_key) + self.assertEqual(cloned.frozen, frozen) + self.assertEqual(cloned.keypath_separator, separator) + self.assertFalse(cloned.keyattr_enabled) + self.assertTrue(cloned.keyattr_dynamic) + cloned_key.values.append(1) + self.assertEqual(key.values, []) From 8a3fdc034f77eb38bfb7e862650385b66f59e808 Mon Sep 17 00:00:00 2001 From: generaltony007 <191973953+hakarrr@users.noreply.github.com> Date: Mon, 2 Nov 2026 15:52:13 +0530 Subject: [PATCH 4/4] Preserve wrapper identity in deepcopy backreferences --- benedict/dicts/__init__.py | 1 + benedict/dicts/base/base_dict.py | 1 + tests/dicts/test_deepcopy_keys.py | 107 ++++++++++++++++++++++++++++++ 3 files changed, 109 insertions(+) diff --git a/benedict/dicts/__init__.py b/benedict/dicts/__init__.py index 22a77b67..f79ac5e9 100644 --- a/benedict/dicts/__init__.py +++ b/benedict/dicts/__init__.py @@ -91,6 +91,7 @@ def __deepcopy__(self, memo: dict[int, Any]) -> Self: keyattr_dynamic=self._keyattr_dynamic, keypath_separator=self._keypath_separator, ) + memo[id(self)] = obj for key, value in self.items(): obj[_clone(key, memo=memo)] = _clone(value, memo=memo) if self._frozen: diff --git a/benedict/dicts/base/base_dict.py b/benedict/dicts/base/base_dict.py index 766d50bb..203f297c 100644 --- a/benedict/dicts/base/base_dict.py +++ b/benedict/dicts/base/base_dict.py @@ -84,6 +84,7 @@ def __contains__(self, o: object) -> bool: def __deepcopy__(self, memo: dict[int, Any]) -> Self: obj = self.__class__() + memo[id(self)] = obj for key, value in self.items(): obj[_clone(key, memo=memo)] = _clone(value, memo=memo) if self._frozen: diff --git a/tests/dicts/test_deepcopy_keys.py b/tests/dicts/test_deepcopy_keys.py index 5b589817..174fe7cc 100644 --- a/tests/dicts/test_deepcopy_keys.py +++ b/tests/dicts/test_deepcopy_keys.py @@ -59,3 +59,110 @@ def __init__(self) -> None: self.assertTrue(cloned.keyattr_dynamic) cloned_key.values.append(1) self.assertEqual(key.values, []) + + +class deepcopy_backreference_test_case(unittest.TestCase): + def test_key_backreference_uses_returned_clone(self) -> None: + class Key: + pass + + for cls in (BaseDict, benedict): + for wrapped in (False, True): + for frozen in (False, True): + with self.subTest(cls=cls, wrapped=wrapped, frozen=frozen): + key = Key() + original = cls({}) if wrapped else cls() + key.parent = original + original[key] = [key] + if frozen: + original.freeze() + memo = {} + cloned = copy.deepcopy(original, memo) + cloned_key = next(iter(cloned)) + self.assertIs(cloned_key.parent, cloned) + self.assertIs(cloned[cloned_key][0], cloned_key) + self.assertIs(memo[id(original)], cloned) + self.assertEqual(cloned.frozen, frozen) + self.assertIs(key.parent, original) + + def test_value_object_backreference_uses_returned_clone(self) -> None: + class Value: + pass + + for cls in (BaseDict, benedict): + with self.subTest(cls=cls): + original = cls() + value = Value() + value.parent = original + original["value"] = value + cloned = copy.deepcopy(original) + self.assertIs(cloned["value"].parent, cloned) + self.assertIsNot(cloned["value"], value) + + def test_mutual_key_owner_references(self) -> None: + class Key: + pass + + for cls in (BaseDict, benedict): + with self.subTest(cls=cls): + first, second = cls(), cls() + key1, key2 = Key(), Key() + key1.owner, key1.other = first, second + key2.owner, key2.other = second, first + first[key1], second[key2] = 1, 2 + copied_first, copied_second = copy.deepcopy([first, second]) + copied_key1 = next(iter(copied_first)) + copied_key2 = next(iter(copied_second)) + self.assertIs(copied_key1.owner, copied_first) + self.assertIs(copied_key1.other, copied_second) + self.assertIs(copied_key2.owner, copied_second) + self.assertIs(copied_key2.other, copied_first) + + def test_wrapped_mapping_memo_is_not_claimed_by_wrapper(self) -> None: + for cls in (BaseDict, benedict): + with self.subTest(cls=cls): + backing = {"value": [1]} + original = cls(backing) + backing_clone = {"separate": True} + memo = {id(backing): backing_clone} + cloned = copy.deepcopy(original, memo) + self.assertIs(memo[id(backing)], backing_clone) + self.assertIs(memo[id(original)], cloned) + self.assertIsNot(cloned, backing_clone) + self.assertEqual(cloned, original) + self.assertIsNot(cloned["value"], backing["value"]) + + def test_mapping_self_cycles_still_rejected(self) -> None: + for cls in (BaseDict, benedict): + with self.subTest(cls=cls): + source = cls() + source["self"] = source + with self.assertRaisesRegex(ValueError, "cyclic.*not supported"): + cls(source) + self.assertEqual(cls._unwrapping_ids, set()) + + def test_benedict_backreference_preserves_options(self) -> None: + class Key: + pass + + for separator in (None, "/"): + for frozen in (False, True): + with self.subTest(separator=separator, frozen=frozen): + original = benedict( + keypath_separator=separator, + keyattr_enabled=False, + keyattr_dynamic=True, + ) + key = Key() + key.parent = original + original[key] = key + if frozen: + original.freeze() + cloned = original.deepcopy() + cloned_key = next(iter(cloned)) + self.assertIs(cloned_key.parent, cloned) + self.assertIs(cloned[cloned_key], cloned_key) + self.assertEqual(cloned.keypath_separator, separator) + self.assertFalse(cloned.keyattr_enabled) + self.assertTrue(cloned.keyattr_dynamic) + self.assertEqual(cloned.frozen, frozen)