Not all objects can be weakly referenced. Objects which support weak references
include class instances, functions written in Python (but not in C), instance methods,
sets, frozensets, some :term:`file objects <file object>`, :term:`generators <generator>`,
-type objects, sockets, arrays, deques, regular expression pattern objects, and code
-objects.
+type objects, sockets, arrays, deques, regular expression pattern objects, code
+objects, and frame objects.
.. versionchanged:: 3.2
Added support for thread.lock, threading.Lock, and code objects.
+.. versionchanged:: 3.16
+ Added support for frame objects.
+
Several built-in types such as :class:`list` and :class:`dict` do not directly
support weak references but can add support through subclassing::
raise AssertionError('coroutine did not exit')
+class WeakRefTest(unittest.TestCase):
+ """
+ Frames support weak references (gh-102960).
+ """
+
+ def make_frame(self):
+ # Return the frame object of a finished function call. Unlike
+ # frames extracted from a traceback, it isn't part of a reference
+ # cycle, so it dies as soon as the last reference is dropped.
+ def func():
+ return sys._getframe()
+ return func()
+
+ def make_traceback_frames(self):
+ def outer():
+ def inner():
+ 1/0
+ return inner()
+ try:
+ outer()
+ except ZeroDivisionError as e:
+ tb = e.__traceback__
+ frames = []
+ while tb:
+ frames.append(tb.tb_frame)
+ tb = tb.tb_next
+ return frames
+
+ def test_weakref_basic(self):
+ called = []
+ f = self.make_frame()
+ ref = weakref.ref(f)
+ cb_ref = weakref.ref(f, called.append)
+ self.assertIs(ref(), f)
+ self.assertIs(cb_ref(), f)
+ del f
+ support.gc_collect()
+ self.assertIsNone(ref())
+ self.assertIsNone(cb_ref())
+ self.assertEqual(called, [cb_ref])
+
+ @support.thread_unsafe("relies on gc.collect() reclaiming its cycles")
+ def test_weakref_live_frame(self):
+ refs = []
+ def func():
+ frame = sys._getframe()
+ refs.append(weakref.ref(frame))
+ self.assertIs(refs[0](), frame)
+ func()
+ support.gc_collect()
+ self.assertIsNone(refs[0]())
+
+ @support.thread_unsafe("relies on gc.collect() reclaiming its cycles")
+ def test_weak_key_dictionary(self):
+ wkd = weakref.WeakKeyDictionary()
+ def _fill():
+ for i, frame in enumerate(self.make_traceback_frames()):
+ wkd[frame] = i
+ self.assertEqual(len(wkd), 3)
+ _fill()
+ support.gc_collect()
+ self.assertEqual(len(wkd), 0)
+
+ @support.thread_unsafe("relies on gc.collect() reclaiming its cycles")
+ def test_weakref_traceback_frames(self):
+ # Frames that participate in reference cycles are cleaned up
+ # by the cyclic garbage collector.
+ refs = []
+ def _make():
+ for frame in self.make_traceback_frames():
+ refs.append(weakref.ref(frame))
+ for ref in refs:
+ self.assertIsNotNone(ref())
+ _make()
+ support.gc_collect()
+ for ref in refs:
+ self.assertIsNone(ref())
+
+ def test_weakref_generator_frame(self):
+ def gen():
+ yield sys._getframe()
+ g = gen()
+ frame = next(g)
+ ref = weakref.ref(frame)
+ del frame
+ support.gc_collect()
+ # The generator keeps its frame alive while suspended.
+ self.assertIsNotNone(ref())
+ g.close()
+ del g
+ support.gc_collect()
+ self.assertIsNone(ref())
+
+ def test_weakref_after_frame_clear(self):
+ f = self.make_frame()
+ ref = weakref.ref(f)
+ # Clearing the frame's contents must not affect weak references
+ # to the frame object itself.
+ f.clear()
+ self.assertIs(ref(), f)
+ del f
+ support.gc_collect()
+ self.assertIsNone(ref())
+
+ @threading_helper.requires_working_threading()
+ def test_weakref_concurrent(self):
+ # Exercise concurrent creation and destruction of weak references
+ # to the same frame, mainly for the free-threaded build.
+ def gen():
+ yield sys._getframe()
+ g = gen()
+ frame = next(g)
+ barrier = threading.Barrier(4)
+ # Collect failures instead of asserting in the workers: exceptions
+ # raised in threads don't propagate to the unittest result.
+ failures = []
+ def work():
+ barrier.wait()
+ for _ in range(1000):
+ ref = weakref.ref(frame)
+ if ref() is not frame:
+ failures.append('shared ref dead while frame alive')
+ # Callback refs are not shared, so this concurrently adds
+ # to and removes from the frame's weakref list.
+ cb_ref = weakref.ref(frame, lambda r: None)
+ if cb_ref() is not frame:
+ failures.append('callback ref dead while frame alive')
+ del ref, cb_ref
+ threads = [threading.Thread(target=work) for _ in range(4)]
+ with threading_helper.start_threads(threads):
+ pass
+ self.assertEqual(failures, [])
+ ref = weakref.ref(frame)
+ del frame
+ g.close()
+ del g
+ support.gc_collect()
+ self.assertIsNone(ref())
+
+
class ReprTest(unittest.TestCase):
"""
Tests for repr(frame).
#include "pycore_optimizer.h" // _Py_Executors_InvalidateDependency()
#include "pycore_tuple.h" // _PyTuple_FromPair
#include "pycore_unicodeobject.h" // _PyUnicode_Equal()
+#include "pycore_weakref.h" // FT_CLEAR_WEAKREFS()
#include "frameobject.h" // PyFrameLocalsProxyObject
#include "opcode.h" // EXTENDED_ARG
_PyObject_GC_UNTRACK(f);
}
+ FT_CLEAR_WEAKREFS(op, f->f_weakreflist);
+
/* GH-106092: If f->f_frame was on the stack and we reached the maximum
* nesting depth for deallocations, the trashcan may have delayed this
* deallocation until after f->f_frame is freed. Avoid dereferencing
frame_traverse, /* tp_traverse */
frame_tp_clear, /* tp_clear */
0, /* tp_richcompare */
- 0, /* tp_weaklistoffset */
+ OFF(f_weakreflist), /* tp_weaklistoffset */
0, /* tp_iter */
0, /* tp_iternext */
frame_methods, /* tp_methods */
f->f_extra_locals = NULL;
f->f_locals_cache = NULL;
f->f_overwritten_fast_locals = NULL;
+ f->f_weakreflist = NULL;
return f;
}