]> git.ipfire.org Git - thirdparty/Python/cpython.git/commitdiff
gh-141510: Fix copy.deepcopy() for recursive frozendict (#145027)
authorVictor Stinner <vstinner@python.org>
Sat, 21 Feb 2026 15:30:40 +0000 (16:30 +0100)
committerGitHub <noreply@github.com>
Sat, 21 Feb 2026 15:30:40 +0000 (15:30 +0000)
Lib/copy.py
Lib/test/test_copy.py

index 33dabb3395a7c0adfba3a48915aada7c09e5464f..6149301ad1389e24a2a8616b03b521417546347a 100644 (file)
@@ -204,7 +204,17 @@ def _deepcopy_dict(x, memo, deepcopy=deepcopy):
 d[dict] = _deepcopy_dict
 
 def _deepcopy_frozendict(x, memo, deepcopy=deepcopy):
-    y = _deepcopy_dict(x, memo, deepcopy)
+    y = {}
+    for key, value in x.items():
+        y[deepcopy(key, memo)] = deepcopy(value, memo)
+
+    # We're not going to put the frozendict in the memo, but it's still
+    # important we check for it, in case the frozendict contains recursive
+    # mutable structures.
+    try:
+        return memo[id(x)]
+    except KeyError:
+        pass
     return frozendict(y)
 d[frozendict] = _deepcopy_frozendict
 
index 858e5e089d5abac5a58aff4a0376193a89391ce8..98f56b5ae87f964432e23aa96db4db74b8ae1081 100644 (file)
@@ -432,6 +432,23 @@ class TestCopy(unittest.TestCase):
         self.assertIsNot(x, y)
         self.assertIsNot(x["foo"], y["foo"])
 
+        # recursive frozendict
+        x = frozendict(foo=[])
+        x['foo'].append(x)
+        y = copy.deepcopy(x)
+        self.assertEqual(y.keys(), x.keys())
+        self.assertIsNot(x, y)
+        self.assertIsNot(x["foo"], y["foo"])
+        self.assertIs(y['foo'][0], y)
+
+        x = frozendict(foo=[])
+        x['foo'].append(x)
+        x = x['foo']
+        y = copy.deepcopy(x)
+        self.assertIsNot(x, y)
+        self.assertIsNot(x[0], y[0])
+        self.assertIs(y[0]['foo'], y)
+
     @support.skip_emscripten_stack_overflow()
     @support.skip_wasi_stack_overflow()
     def test_deepcopy_reflexive_dict(self):