Skip to content

Commit 84bd123

Browse files
committed
Defer clear for weakrefs without callbacks.
This fixes GH-132413 and GH-135552.
1 parent 900022b commit 84bd123

File tree

8 files changed

+47
-31
lines changed

8 files changed

+47
-31
lines changed

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: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -658,9 +658,8 @@ def callback(ignored):
658658
gc.collect()
659659
self.assertEqual(len(ouch), 2) # else the callbacks didn't run
660660
for x in ouch:
661-
# If the callback resurrected one of these guys, the instance
662-
# would be damaged, with an empty __dict__.
663-
self.assertEqual(x, None)
661+
# The weakref should be cleared before executing the callback.
662+
self.assertIsNone(x)
664663

665664
def test_bug21435(self):
666665
# This is a poor test - its only virtue is that it happened to
@@ -1047,8 +1046,8 @@ class Z:
10471046
# release references and create trash
10481047
del a, wr_cycle
10491048
gc.collect()
1050-
# if called, it means there is a bug in the GC. The weakref should be
1051-
# cleared before Z dies.
1049+
# In older versions of Python, the weakref was cleared by the
1050+
# gc. Now it is not cleared and so the callback is run.
10521051
callback.assert_not_called()
10531052
gc.enable()
10541053

@@ -1330,6 +1329,7 @@ def setUp(self):
13301329
def tearDown(self):
13311330
gc.disable()
13321331

1332+
@unittest.skipIf(Py_GIL_DISABLED, "requires GC generations or increments")
13331333
def test_bug1055820c(self):
13341334
# Corresponds to temp2c.py in the bug report. This is pretty
13351335
# elaborate.
@@ -1405,6 +1405,7 @@ def callback(ignored):
14051405
self.assertEqual(x, None)
14061406

14071407
@gc_threshold(1000, 0, 0)
1408+
@unittest.skipIf(Py_GIL_DISABLED, "requires GC generations or increments")
14081409
def test_bug1055820d(self):
14091410
# Corresponds to temp2d.py in the bug report. This is very much like
14101411
# 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
@@ -808,7 +808,12 @@ def test_closefd_attr(self):
808808
def test_garbage_collection(self):
809809
# FileIO objects are collected, and collecting them flushes
810810
# all data to disk.
811-
with warnings_helper.check_warnings(('', ResourceWarning)):
811+
#
812+
# Note that using warnings_helper.check_warnings() will keep the
813+
# file alive due to the `source` argument to warn(). So, use
814+
# catch_warnings() instead.
815+
with warnings.catch_warnings():
816+
warnings.simplefilter("ignore", ResourceWarning)
812817
f = self.FileIO(os_helper.TESTFN, "wb")
813818
f.write(b"abcxxx")
814819
f.f = f
@@ -1809,7 +1814,11 @@ def test_garbage_collection(self):
18091814
# C BufferedReader objects are collected.
18101815
# The Python version has __del__, so it ends into gc.garbage instead
18111816
self.addCleanup(os_helper.unlink, os_helper.TESTFN)
1812-
with warnings_helper.check_warnings(('', ResourceWarning)):
1817+
# Note that using warnings_helper.check_warnings() will keep the
1818+
# file alive due to the `source` argument to warn(). So, use
1819+
# catch_warnings() instead.
1820+
with warnings.catch_warnings():
1821+
warnings.simplefilter("ignore", ResourceWarning)
18131822
rawio = self.FileIO(os_helper.TESTFN, "w+b")
18141823
f = self.tp(rawio)
18151824
f.f = f
@@ -2158,7 +2167,11 @@ def test_garbage_collection(self):
21582167
# all data to disk.
21592168
# The Python version has __del__, so it ends into gc.garbage instead
21602169
self.addCleanup(os_helper.unlink, os_helper.TESTFN)
2161-
with warnings_helper.check_warnings(('', ResourceWarning)):
2170+
# Note that using warnings_helper.check_warnings() will keep the
2171+
# file alive due to the `source` argument to warn(). So, use
2172+
# catch_warnings() instead.
2173+
with warnings.catch_warnings():
2174+
warnings.simplefilter("ignore", ResourceWarning)
21622175
rawio = self.FileIO(os_helper.TESTFN, "w+b")
21632176
f = self.tp(rawio)
21642177
f.write(b"123xxx")
@@ -4080,7 +4093,8 @@ def test_garbage_collection(self):
40804093
# C TextIOWrapper objects are collected, and collecting them flushes
40814094
# all data to disk.
40824095
# The Python version has __del__, so it ends in gc.garbage instead.
4083-
with warnings_helper.check_warnings(('', ResourceWarning)):
4096+
with warnings.catch_warnings():
4097+
warnings.simplefilter("ignore", ResourceWarning)
40844098
rawio = self.FileIO(os_helper.TESTFN, "wb")
40854099
b = self.BufferedWriter(rawio)
40864100
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

Python/gc.c

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -920,14 +920,8 @@ handle_weakref_callbacks(PyGC_Head *unreachable, PyGC_Head *old)
920920
// is called, the cache may not be correctly invalidated. That
921921
// can lead to segfaults since the caches can refer to deallocated
922922
// objects. Delaying the clear of weakrefs until *after*
923-
// finalizers have been called fixes that bug. However, that
924-
// deferral could introduce other problems if some finalizer
925-
// code expects that the weakrefs will be cleared first. So, we
926-
// have the PyType_Check() test below to only defer the clear of
927-
// weakrefs to types. That solves the issue for tp_subclasses.
928-
// In a future version of Python, we should likely defer the
929-
// weakref clear for all objects, not just types.
930-
if (wr->wr_callback != NULL || !PyType_Check(wr->wr_object)) {
923+
// finalizers have been called fixes that bug.
924+
if (wr->wr_callback != NULL) {
931925
// _PyWeakref_ClearRef clears the weakref but leaves the
932926
// callback pointer intact. Obscure: it also changes *wrlist.
933927
_PyObject_ASSERT((PyObject *)wr, wr->wr_object == op);

Python/gc_free_threading.c

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1532,14 +1532,8 @@ find_weakref_callbacks(struct collection_state *state)
15321532
// is called, the cache may not be correctly invalidated. That
15331533
// can lead to segfaults since the caches can refer to deallocated
15341534
// objects. Delaying the clear of weakrefs until *after*
1535-
// finalizers have been called fixes that bug. However, that
1536-
// deferral could introduce other problems if some finalizer
1537-
// code expects that the weakrefs will be cleared first. So, we
1538-
// have the PyType_Check() test below to only defer the clear of
1539-
// weakrefs to types. That solves the issue for tp_subclasses.
1540-
// In a future version of Python, we should likely defer the
1541-
// weakref clear for all objects, not just types.
1542-
if (wr->wr_callback != NULL || !PyType_Check(wr->wr_object)) {
1535+
// finalizers have been called fixes that bug.
1536+
if (wr->wr_callback != NULL) {
15431537
// _PyWeakref_ClearRef clears the weakref but leaves the
15441538
// callback pointer intact. Obscure: it also changes *wrlist.
15451539
_PyObject_ASSERT((PyObject *)wr, wr->wr_object == op);

0 commit comments

Comments
 (0)