From 59a7471cd5ea5bf5ad9b344b781055c65d813698 Mon Sep 17 00:00:00 2001 From: Bobronium Date: Thu, 4 Dec 2025 10:12:25 +0100 Subject: [PATCH 1/5] gh-142254: rewrite `test_deepcopy_keepalive`and `_dont_memo_immutable` --- Lib/test/test_copy.py | 46 ++++++++++++++++++++++++++++--------------- 1 file changed, 30 insertions(+), 16 deletions(-) diff --git a/Lib/test/test_copy.py b/Lib/test/test_copy.py index cfef24727e8c82..442374a8532fdb 100644 --- a/Lib/test/test_copy.py +++ b/Lib/test/test_copy.py @@ -1,4 +1,5 @@ """Unit tests for the copy module.""" +import sys import copy import copyreg @@ -434,25 +435,38 @@ def test_deepcopy_reflexive_dict(self): self.assertEqual(len(y), 1) def test_deepcopy_keepalive(self): - memo = {} - x = [] - y = copy.deepcopy(x, memo) - self.assertIs(memo[id(memo)][0], x) + mutable = [] + def count_refs(): + return sys.getrefcount(mutable) + class C: + def __deepcopy__(self, memo): + self.recorded = count_refs() + return self + x = [mutable, 42, observer := C()] + y = copy.deepcopy(x) + support.gc_collect() # For PyPy or other GCs. + # Additional keepalive ref dropped after copy: + self.assertGreater(observer.recorded, count_refs()) + self.assertEqual(observer.recorded - count_refs(), 1) def test_deepcopy_dont_memo_immutable(self): - memo = {} - x = [1, 2, 3, 4] - y = copy.deepcopy(x, memo) - self.assertEqual(y, x) - # There's the entry for the new list, and the keep alive. - self.assertEqual(len(memo), 2) - - memo = {} - x = [(1, 2)] - y = copy.deepcopy(x, memo) - self.assertEqual(y, x) + def run_case(immutable): + def count_refs(): + return sys.getrefcount(immutable) + class C: + def __deepcopy__(self, memo): + self.recorded = count_refs() + return self + x = [immutable, 42, observer := C()] + y = copy.deepcopy(x) + support.gc_collect() # For PyPy or other GCs + self.assertIs(y[0], x[0]) + self.assertEqual(count_refs(), observer.recorded) + with self.subTest(kind="string"): + run_case(f"mortal_immutable_{id(object())}") # Tuples with immutable contents are immutable for deepcopy. - self.assertEqual(len(memo), 2) + with self.subTest(kind="tuple_with_string"): + run_case((f"mortal_immutable_{id(object())}",)) def test_deepcopy_inst_vanilla(self): class C: From 9a4153faf46fcf9f9b4abe691d94fd8be99c7878 Mon Sep 17 00:00:00 2001 From: Bobronium Date: Fri, 5 Dec 2025 10:49:36 +0100 Subject: [PATCH 2/5] gh-142254: rewrite test without relying on `sys.getrefcount()` These now pass on 3.10-3.15 as well as PyPy 3.11. --- Lib/test/test_copy.py | 59 ++++++++++++++++++++++++------------------- 1 file changed, 33 insertions(+), 26 deletions(-) diff --git a/Lib/test/test_copy.py b/Lib/test/test_copy.py index 442374a8532fdb..5bd67fcff63725 100644 --- a/Lib/test/test_copy.py +++ b/Lib/test/test_copy.py @@ -435,38 +435,45 @@ def test_deepcopy_reflexive_dict(self): self.assertEqual(len(y), 1) def test_deepcopy_keepalive(self): - mutable = [] - def count_refs(): - return sys.getrefcount(mutable) - class C: + class Target: + pass + class Dropper: + def __init__(self, to_clear): + self.to_clear = to_clear def __deepcopy__(self, memo): - self.recorded = count_refs() + self.to_clear.clear() return self - x = [mutable, 42, observer := C()] - y = copy.deepcopy(x) + class Checker: + def __init__(self, ref): + self.ref = ref + self.was_alive = None + def __deepcopy__(self, memo): + support.gc_collect() # For PyPy or other GCs. + self.was_alive = self.ref() is not None + return self + weak_ref = weakref.ref(target := Target()) + holder = [target] + del target # holder[0] is now only strong ref + container = [holder, Dropper(holder), *((1,) * 1000), (checker := Checker(weak_ref))] + copy.deepcopy(container) + self.assertTrue(checker.was_alive) support.gc_collect() # For PyPy or other GCs. - # Additional keepalive ref dropped after copy: - self.assertGreater(observer.recorded, count_refs()) - self.assertEqual(observer.recorded - count_refs(), 1) + self.assertIsNone(weak_ref()) def test_deepcopy_dont_memo_immutable(self): - def run_case(immutable): - def count_refs(): - return sys.getrefcount(immutable) - class C: - def __deepcopy__(self, memo): - self.recorded = count_refs() - return self - x = [immutable, 42, observer := C()] - y = copy.deepcopy(x) - support.gc_collect() # For PyPy or other GCs - self.assertIs(y[0], x[0]) - self.assertEqual(count_refs(), observer.recorded) - with self.subTest(kind="string"): - run_case(f"mortal_immutable_{id(object())}") + class ByRef: + def __init__(self): + self.copied = 0 + def __deepcopy__(self, memo): + self.copied += 1 + return self + y = copy.deepcopy(x := [br := ByRef(), br]) + self.assertEqual(br.copied, 2) + self.assertEqual(y, x) # Tuples with immutable contents are immutable for deepcopy. - with self.subTest(kind="tuple_with_string"): - run_case((f"mortal_immutable_{id(object())}",)) + y = copy.deepcopy(x := [(br := ByRef()), (br,)]) + self.assertEqual(br.copied, 2) + self.assertEqual(y, x) def test_deepcopy_inst_vanilla(self): class C: From 996366e6d08d02f5fd7346cfd3f5bc687f233fa5 Mon Sep 17 00:00:00 2001 From: Bobronium Date: Fri, 5 Dec 2025 10:51:58 +0100 Subject: [PATCH 3/5] gh-142254: remove unused import --- Lib/test/test_copy.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/Lib/test/test_copy.py b/Lib/test/test_copy.py index 5bd67fcff63725..a725840d75e82f 100644 --- a/Lib/test/test_copy.py +++ b/Lib/test/test_copy.py @@ -1,6 +1,4 @@ """Unit tests for the copy module.""" -import sys - import copy import copyreg import weakref From 0ac3e4aaf34dc47aba559c2041195c50b94bcc72 Mon Sep 17 00:00:00 2001 From: Bobronium Date: Fri, 5 Dec 2025 16:31:37 +0100 Subject: [PATCH 4/5] Rewert blank line removal --- Lib/test/test_copy.py | 1 + 1 file changed, 1 insertion(+) diff --git a/Lib/test/test_copy.py b/Lib/test/test_copy.py index a725840d75e82f..3e5ae93e462556 100644 --- a/Lib/test/test_copy.py +++ b/Lib/test/test_copy.py @@ -1,4 +1,5 @@ """Unit tests for the copy module.""" + import copy import copyreg import weakref From 93d6e7ed048f5dacf179b31bb37e68f74202e063 Mon Sep 17 00:00:00 2001 From: Bobronium Date: Fri, 5 Dec 2025 16:47:24 +0100 Subject: [PATCH 5/5] Apply review suggestions --- Lib/test/test_copy.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/Lib/test/test_copy.py b/Lib/test/test_copy.py index 3e5ae93e462556..f1cdd3a9535bfd 100644 --- a/Lib/test/test_copy.py +++ b/Lib/test/test_copy.py @@ -453,7 +453,9 @@ def __deepcopy__(self, memo): weak_ref = weakref.ref(target := Target()) holder = [target] del target # holder[0] is now only strong ref - container = [holder, Dropper(holder), *((1,) * 1000), (checker := Checker(weak_ref))] + large_tuple = ((1,) * 1000) + checker = Checker(weak_ref) + container = [holder, Dropper(holder), *large_tuple, checker] copy.deepcopy(container) self.assertTrue(checker.was_alive) support.gc_collect() # For PyPy or other GCs. @@ -466,11 +468,14 @@ def __init__(self): def __deepcopy__(self, memo): self.copied += 1 return self - y = copy.deepcopy(x := [br := ByRef(), br]) + br = ByRef() + y = copy.deepcopy(x := [br, br]) self.assertEqual(br.copied, 2) self.assertEqual(y, x) # Tuples with immutable contents are immutable for deepcopy. - y = copy.deepcopy(x := [(br := ByRef()), (br,)]) + t = (ByRef(),) + x = [t, t] + y = copy.deepcopy(x) self.assertEqual(br.copied, 2) self.assertEqual(y, x)