@@ -262,9 +262,11 @@ class Cyclic(tuple):
262262 # finalizer.
263263 def __del__(self):
264264
265- # 5. Create a weakref to `func` now. If we had created
266- # it earlier, it would have been cleared by the
267- # garbage collector before calling the finalizers.
265+ # 5. Create a weakref to `func` now. In previous
266+ # versions of Python, this would avoid having it
267+ # cleared by the garbage collector before calling
268+ # the finalizers. Now, weakrefs get cleared after
269+ # calling finalizers.
268270 self[1].ref = weakref.ref(self[0])
269271
270272 # 6. Drop the global reference to `latefin`. The only
@@ -293,16 +295,40 @@ def func():
293295 # which will find `cyc` and `func` as garbage.
294296 gc.collect()
295297
296- # 9. Previously, this would crash because `func_qualname`
297- # had been NULL-ed out by func_clear().
298+ # 9. Previously, this would crash because the weakref
299+ # created in the finalizer revealed the function after
300+ # `tp_clear` was called and `func_qualname`
301+ # had been NULL-ed out by func_clear(). Now, we clear
302+ # weakrefs to unreachable objects before calling `tp_clear`
303+ # but after calling finalizers.
298304 print(f"{func=}")
299305 """
300- # We're mostly just checking that this doesn't crash.
301306 rc , stdout , stderr = assert_python_ok ("-c" , code )
302307 self .assertEqual (rc , 0 )
303- self .assertRegex (stdout , rb"""\A\s*func=<function at \S+>\s*\z""" )
308+ # The `func` global is None because the weakref was cleared.
309+ self .assertRegex (stdout , rb"""\A\s*func=None""" )
304310 self .assertFalse (stderr )
305311
312+ def test_datetime_weakref_cycle (self ):
313+ # https://github.com/python/cpython/issues/132413
314+ # If the weakref used by the datetime extension gets cleared by the GC (due to being
315+ # in an unreachable cycle) then datetime functions would crash (get_module_state()
316+ # was returning a NULL pointer). This bug is fixed by clearing weakrefs without
317+ # callbacks *after* running finalizers.
318+ code = """if 1:
319+ import _datetime
320+ class C:
321+ def __del__(self):
322+ print('__del__ called')
323+ _datetime.timedelta(days=1) # crash?
324+
325+ l = [C()]
326+ l.append(l)
327+ """
328+ rc , stdout , stderr = assert_python_ok ("-c" , code )
329+ self .assertEqual (rc , 0 )
330+ self .assertEqual (stdout .strip (), b'__del__ called' )
331+
306332 @refcount_test
307333 def test_frame (self ):
308334 def f ():
@@ -652,9 +678,8 @@ def callback(ignored):
652678 gc .collect ()
653679 self .assertEqual (len (ouch ), 2 ) # else the callbacks didn't run
654680 for x in ouch :
655- # If the callback resurrected one of these guys, the instance
656- # would be damaged, with an empty __dict__.
657- self .assertEqual (x , None )
681+ # The weakref should be cleared before executing the callback.
682+ self .assertIsNone (x )
658683
659684 def test_bug21435 (self ):
660685 # This is a poor test - its only virtue is that it happened to
@@ -821,11 +846,15 @@ def test_get_stats(self):
821846 self .assertEqual (len (stats ), 3 )
822847 for st in stats :
823848 self .assertIsInstance (st , dict )
824- self .assertEqual (set (st ),
825- {"collected" , "collections" , "uncollectable" , "candidates" , "duration" })
849+ self .assertEqual (
850+ set (st ),
851+ {"collected" , "collections" , "uncollectable" , "candidates" , "duration" }
852+ )
826853 self .assertGreaterEqual (st ["collected" ], 0 )
827854 self .assertGreaterEqual (st ["collections" ], 0 )
828855 self .assertGreaterEqual (st ["uncollectable" ], 0 )
856+ self .assertGreaterEqual (st ["candidates" ], 0 )
857+ self .assertGreaterEqual (st ["duration" ], 0 )
829858 # Check that collection counts are incremented correctly
830859 if gc .isenabled ():
831860 self .addCleanup (gc .enable )
@@ -836,11 +865,25 @@ def test_get_stats(self):
836865 self .assertEqual (new [0 ]["collections" ], old [0 ]["collections" ] + 1 )
837866 self .assertEqual (new [1 ]["collections" ], old [1 ]["collections" ])
838867 self .assertEqual (new [2 ]["collections" ], old [2 ]["collections" ])
868+ self .assertGreater (new [0 ]["duration" ], old [0 ]["duration" ])
869+ self .assertEqual (new [1 ]["duration" ], old [1 ]["duration" ])
870+ self .assertEqual (new [2 ]["duration" ], old [2 ]["duration" ])
871+ for stat in ["collected" , "uncollectable" , "candidates" ]:
872+ self .assertGreaterEqual (new [0 ][stat ], old [0 ][stat ])
873+ self .assertEqual (new [1 ][stat ], old [1 ][stat ])
874+ self .assertEqual (new [2 ][stat ], old [2 ][stat ])
839875 gc .collect (2 )
840- new = gc .get_stats ()
841- self .assertEqual (new [0 ]["collections" ], old [0 ]["collections" ] + 1 )
876+ old , new = new , gc .get_stats ()
877+ self .assertEqual (new [0 ]["collections" ], old [0 ]["collections" ])
842878 self .assertEqual (new [1 ]["collections" ], old [1 ]["collections" ])
843879 self .assertEqual (new [2 ]["collections" ], old [2 ]["collections" ] + 1 )
880+ self .assertEqual (new [0 ]["duration" ], old [0 ]["duration" ])
881+ self .assertEqual (new [1 ]["duration" ], old [1 ]["duration" ])
882+ self .assertGreater (new [2 ]["duration" ], old [2 ]["duration" ])
883+ for stat in ["collected" , "uncollectable" , "candidates" ]:
884+ self .assertEqual (new [0 ][stat ], old [0 ][stat ])
885+ self .assertEqual (new [1 ][stat ], old [1 ][stat ])
886+ self .assertGreaterEqual (new [2 ][stat ], old [2 ][stat ])
844887
845888 def test_freeze (self ):
846889 gc .freeze ()
@@ -1156,6 +1199,37 @@ def test_something(self):
11561199 """ )
11571200 assert_python_ok ("-c" , source )
11581201
1202+ def test_do_not_cleanup_type_subclasses_before_finalization (self ):
1203+ # See https://github.com/python/cpython/issues/135552
1204+ # If we cleanup weakrefs for tp_subclasses before calling
1205+ # the finalizer (__del__) then the line `fail = BaseNode.next.next`
1206+ # should fail because we are trying to access a subclass
1207+ # attribute. But subclass type cache was not properly invalidated.
1208+ code = """
1209+ class BaseNode:
1210+ def __del__(self):
1211+ BaseNode.next = BaseNode.next.next
1212+ fail = BaseNode.next.next
1213+
1214+ class Node(BaseNode):
1215+ pass
1216+
1217+ BaseNode.next = Node()
1218+ BaseNode.next.next = Node()
1219+ """
1220+ # this test checks garbage collection while interp
1221+ # finalization
1222+ assert_python_ok ("-c" , textwrap .dedent (code ))
1223+
1224+ code_inside_function = textwrap .dedent (F"""
1225+ def test():
1226+ { textwrap .indent (code , ' ' )}
1227+
1228+ test()
1229+ """ )
1230+ # this test checks regular garbage collection
1231+ assert_python_ok ("-c" , code_inside_function )
1232+
11591233
11601234 @unittest .skipUnless (Py_GIL_DISABLED , "requires free-threaded GC" )
11611235 @unittest .skipIf (_testinternalcapi is None , "requires _testinternalcapi" )
@@ -1260,9 +1334,11 @@ def test_collect(self):
12601334 # Check that we got the right info dict for all callbacks
12611335 for v in self .visit :
12621336 info = v [2 ]
1263- self .assertTrue ("generation" in info )
1264- self .assertTrue ("collected" in info )
1265- self .assertTrue ("uncollectable" in info )
1337+ self .assertIn ("generation" , info )
1338+ self .assertIn ("collected" , info )
1339+ self .assertIn ("uncollectable" , info )
1340+ self .assertIn ("candidates" , info )
1341+ self .assertIn ("duration" , info )
12661342
12671343 def test_collect_generation (self ):
12681344 self .preclean ()
@@ -1450,6 +1526,7 @@ def callback(ignored):
14501526 self .assertEqual (x , None )
14511527
14521528 @gc_threshold (1000 , 0 , 0 )
1529+ @unittest .skipIf (Py_GIL_DISABLED , "requires GC generations or increments" )
14531530 def test_bug1055820d (self ):
14541531 # Corresponds to temp2d.py in the bug report. This is very much like
14551532 # test_bug1055820c, but uses a __del__ method instead of a weakref
0 commit comments