Skip to content

Commit 42abb05

Browse files
committed
Make the GC clear weakrefs later.
Clear the weakrefs to unreachable objects after finalizers are called.
1 parent f41e9c7 commit 42abb05

File tree

9 files changed

+238
-102
lines changed

9 files changed

+238
-102
lines changed

Lib/_threading_local.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ def local_deleted(_, key=key):
4949
# When the localimpl is deleted, remove the thread attribute.
5050
thread = wrthread()
5151
if thread is not None:
52-
del thread.__dict__[key]
52+
thread.__dict__.pop(key, None)
5353
def thread_deleted(_, idt=idt):
5454
# When the thread is deleted, remove the local dict.
5555
# Note that this is suboptimal if the thread object gets

Lib/test/test_finalization.py

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -280,8 +280,9 @@ def test_simple_resurrect(self):
280280
gc.collect()
281281
self.assert_del_calls(ids)
282282
self.assert_survivors(ids)
283-
# XXX is this desirable?
284-
self.assertIs(wr(), None)
283+
# This used to be None because weakrefs were cleared before
284+
# calling finalizers. Now they are cleared after.
285+
self.assertIsNot(wr(), None)
285286
# When trying to destroy the object a second time, __del__
286287
# isn't called anymore (and the object isn't resurrected).
287288
self.clear_survivors()
@@ -388,8 +389,10 @@ def check_resurrecting_chain(self, classes):
388389
gc.collect()
389390
self.assert_del_calls(ids)
390391
self.assert_survivors(survivor_ids)
391-
# XXX desirable?
392-
self.assertEqual([wr() for wr in wrs], [None] * N)
392+
for wr in wrs:
393+
# These values used to be None because weakrefs were cleared
394+
# before calling finalizers. Now they are cleared after.
395+
self.assertIsNotNone(wr())
393396
self.clear_survivors()
394397
gc.collect()
395398
self.assert_del_calls(ids)

Lib/test/test_gc.py

Lines changed: 22 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -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,14 +295,18 @@ 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

306312
@refcount_test
@@ -652,9 +658,11 @@ def callback(ignored):
652658
gc.collect()
653659
self.assertEqual(len(ouch), 2) # else the callbacks didn't run
654660
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)
661+
# In previous versions of Python, the weakrefs are cleared
662+
# before calling finalizers. Now they are cleared after.
663+
# So when the object is resurrected, the weakref is not
664+
# cleared since it is no longer unreachable.
665+
self.assertIsInstance(x, C1055820)
658666

659667
def test_bug21435(self):
660668
# This is a poor test - its only virtue is that it happened to
@@ -1041,8 +1049,8 @@ class Z:
10411049
# release references and create trash
10421050
del a, wr_cycle
10431051
gc.collect()
1044-
# if called, it means there is a bug in the GC. The weakref should be
1045-
# cleared before Z dies.
1052+
# In older versions of Python, the weakref was cleared by the
1053+
# gc. Now it is not cleared and so the callback is run.
10461054
callback.assert_not_called()
10471055
gc.enable()
10481056

@@ -1324,6 +1332,7 @@ def setUp(self):
13241332
def tearDown(self):
13251333
gc.disable()
13261334

1335+
@unittest.skipIf(Py_GIL_DISABLED, "requires GC generations or increments")
13271336
def test_bug1055820c(self):
13281337
# Corresponds to temp2c.py in the bug report. This is pretty
13291338
# elaborate.
@@ -1399,6 +1408,7 @@ def callback(ignored):
13991408
self.assertEqual(x, None)
14001409

14011410
@gc_threshold(1000, 0, 0)
1411+
@unittest.skipIf(Py_GIL_DISABLED, "requires GC generations or increments")
14021412
def test_bug1055820d(self):
14031413
# Corresponds to temp2d.py in the bug report. This is very much like
14041414
# test_bug1055820c, but uses a __del__ method instead of a weakref

Lib/test/test_io.py

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -807,7 +807,12 @@ def test_closefd_attr(self):
807807
def test_garbage_collection(self):
808808
# FileIO objects are collected, and collecting them flushes
809809
# all data to disk.
810-
with warnings_helper.check_warnings(('', ResourceWarning)):
810+
#
811+
# Note that using warnings_helper.check_warnings() will keep the
812+
# file alive due to the `source` argument to warn(). So, use
813+
# catch_warnings() instead.
814+
with warnings.catch_warnings():
815+
warnings.simplefilter("ignore", ResourceWarning)
811816
f = self.FileIO(os_helper.TESTFN, "wb")
812817
f.write(b"abcxxx")
813818
f.f = f
@@ -1808,7 +1813,11 @@ def test_garbage_collection(self):
18081813
# C BufferedReader objects are collected.
18091814
# The Python version has __del__, so it ends into gc.garbage instead
18101815
self.addCleanup(os_helper.unlink, os_helper.TESTFN)
1811-
with warnings_helper.check_warnings(('', ResourceWarning)):
1816+
# Note that using warnings_helper.check_warnings() will keep the
1817+
# file alive due to the `source` argument to warn(). So, use
1818+
# catch_warnings() instead.
1819+
with warnings.catch_warnings():
1820+
warnings.simplefilter("ignore", ResourceWarning)
18121821
rawio = self.FileIO(os_helper.TESTFN, "w+b")
18131822
f = self.tp(rawio)
18141823
f.f = f
@@ -2157,7 +2166,11 @@ def test_garbage_collection(self):
21572166
# all data to disk.
21582167
# The Python version has __del__, so it ends into gc.garbage instead
21592168
self.addCleanup(os_helper.unlink, os_helper.TESTFN)
2160-
with warnings_helper.check_warnings(('', ResourceWarning)):
2169+
# Note that using warnings_helper.check_warnings() will keep the
2170+
# file alive due to the `source` argument to warn(). So, use
2171+
# catch_warnings() instead.
2172+
with warnings.catch_warnings():
2173+
warnings.simplefilter("ignore", ResourceWarning)
21612174
rawio = self.FileIO(os_helper.TESTFN, "w+b")
21622175
f = self.tp(rawio)
21632176
f.write(b"123xxx")
@@ -4079,7 +4092,8 @@ def test_garbage_collection(self):
40794092
# C TextIOWrapper objects are collected, and collecting them flushes
40804093
# all data to disk.
40814094
# The Python version has __del__, so it ends in gc.garbage instead.
4082-
with warnings_helper.check_warnings(('', ResourceWarning)):
4095+
with warnings.catch_warnings():
4096+
warnings.simplefilter("ignore", ResourceWarning)
40834097
rawio = self.FileIO(os_helper.TESTFN, "wb")
40844098
b = self.BufferedWriter(rawio)
40854099
t = self.TextIOWrapper(b, encoding="ascii")

Lib/test/test_weakref.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1314,7 +1314,7 @@ def check_len_cycles(self, dict_type, cons):
13141314
n2 = len(dct)
13151315
# one item may be kept alive inside the iterator
13161316
self.assertIn(n1, (0, 1))
1317-
self.assertEqual(n2, 0)
1317+
self.assertIn(n2, (0, 1))
13181318

13191319
def test_weak_keyed_len_cycles(self):
13201320
self.check_len_cycles(weakref.WeakKeyDictionary, lambda k: (k, 1))

Modules/_datetimemodule.c

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -218,7 +218,7 @@ clear_current_module(PyInterpreterState *interp, PyObject *expected)
218218
if (PyDict_GetItemRef(dict, INTERP_KEY, &ref) < 0) {
219219
goto error;
220220
}
221-
if (ref != NULL) {
221+
if (ref != NULL && ref != Py_None) {
222222
PyObject *current = NULL;
223223
int rc = PyWeakref_GetRef(ref, &current);
224224
/* We only need "current" for pointer comparison. */

Modules/gc_weakref.txt

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,16 @@
11
Intro
22
=====
33

4+
**************************************************************************
5+
Note: this document was written long ago, before PEP 442 (safe object
6+
finalization) was implemented. While that has changed some things, this
7+
document is still mostly accurate. Just note that the rules being discussed
8+
here apply to the unreachable set of objects *after* non-legacy finalizers
9+
have been called. Also, the clearing of weakrefs has been changed to happen
10+
later in the collection (after running finalizers but before tp_clear is
11+
called).
12+
**************************************************************************
13+
414
The basic rule for dealing with weakref callbacks (and __del__ methods too,
515
for that matter) during cyclic gc:
616

0 commit comments

Comments
 (0)