]> git.ipfire.org Git - thirdparty/Python/cpython.git/commitdiff
gh-102960: Make frames weak-referenceable (#152838)
authorŁukasz Langa <lukasz@langa.pl>
Tue, 14 Jul 2026 23:58:14 +0000 (01:58 +0200)
committerGitHub <noreply@github.com>
Tue, 14 Jul 2026 23:58:14 +0000 (01:58 +0200)
Add an explicit f_weakreflist field to the frame object, following the
same pattern as generators and coroutines, including free-threading-safe
weakref clearing via FT_CLEAR_WEAKREFS() in frame_dealloc().

Py_TPFLAGS_MANAGED_WEAKREF is not used because static builtin types
must not carry it (see init_static_type()) and the pre-header would
cost two extra words per frame instead of one.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Doc/library/weakref.rst
Doc/whatsnew/3.16.rst
Include/internal/pycore_frame.h
Lib/test/test_frame.py
Lib/test/test_sys.py
Misc/NEWS.d/next/Core_and_Builtins/2026-07-01-12-00-00.gh-issue-102960.fR8kJa.rst [new file with mode: 0644]
Objects/frameobject.c

index 952609c0e700e5e4f5233e478c7bb912ccede3da..7cc0c33fda353c462368a1fb18c69285b3a3fcc3 100644 (file)
@@ -63,12 +63,15 @@ exposed by the :mod:`!weakref` module for the benefit of advanced uses.
 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::
 
index fad723ba3f4cb6310cb744ae9d966fb9d4bb295c..b6a5e4c7fbf1b85f7ee841d9f6417bc8d1d84c49 100644 (file)
@@ -75,6 +75,11 @@ New features
 Other language changes
 ======================
 
+* :ref:`Frame objects <frame-objects>` now support :mod:`weak references
+  <weakref>`.  This allows associating extra data with active frames,
+  for example in debuggers, without keeping the frames (and everything
+  they reference) alive indefinitely.
+  (Contributed by Łukasz Langa in :gh:`102960`.)
 
 
 New modules
index 3c9ab99c34ebc64cdd54a52c8335bf2cfed84256..4eefbf89acb85f719213c713eea3351119febcb0 100644 (file)
@@ -34,6 +34,7 @@ struct _frame {
      * "support" for the borrowed references, ensuring that they remain valid.
      */
     PyObject *f_overwritten_fast_locals;
+    PyObject *f_weakreflist;    /* List of weak references */
     /* The frame data, if this frame object owns the frame */
     PyObject *_f_frame_data[1];
 };
index 18ade18d1a1708ceac0c597d45376b169ef137b9..fc50e16575da2b997b2fa966a39b65b93dc943a6 100644 (file)
@@ -273,6 +273,146 @@ class FrameAttrsTest(unittest.TestCase):
             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).
index 1773633730ea001fe0ca38f2b7bc8f4a05bd4fc6..56c5c2aa2025b9a30075ac73e888dfaad3eb8a73 100644 (file)
@@ -1700,7 +1700,7 @@ class SizeofTest(unittest.TestCase):
             INTERPRETER_FRAME = '9PihcP'
         else:
             INTERPRETER_FRAME = '9PhcP'
-        check(x, size('3PiccPPP' + INTERPRETER_FRAME + 'P'))
+        check(x, size('3PiccPPPP' + INTERPRETER_FRAME + 'P'))
         # function
         def func(): pass
         check(func, size('16Pi'))
diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2026-07-01-12-00-00.gh-issue-102960.fR8kJa.rst b/Misc/NEWS.d/next/Core_and_Builtins/2026-07-01-12-00-00.gh-issue-102960.fR8kJa.rst
new file mode 100644 (file)
index 0000000..fec1eeb
--- /dev/null
@@ -0,0 +1,2 @@
+:ref:`Frame objects <frame-objects>` now support :mod:`weak references
+<weakref>`.  Patch by Łukasz Langa.
index e7ac59379dcfbcce95e162de2d3b4518eb81efd9..c50cbeaada3c406cec5a824e5cd72464908a3d01 100644 (file)
@@ -16,6 +16,7 @@
 #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
@@ -1931,6 +1932,8 @@ frame_dealloc(PyObject *op)
         _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
@@ -2089,7 +2092,7 @@ PyTypeObject PyFrame_Type = {
     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 */
@@ -2125,6 +2128,7 @@ _PyFrame_New_NoTrack(PyCodeObject *code)
     f->f_extra_locals = NULL;
     f->f_locals_cache = NULL;
     f->f_overwritten_fast_locals = NULL;
+    f->f_weakreflist = NULL;
     return f;
 }