Skip to content

Commit 6ae740c

Browse files
ambvclaude
andauthored
gh-102960: Make frames weak-referenceable (#152838)
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>
1 parent b704b64 commit 6ae740c

7 files changed

Lines changed: 159 additions & 4 deletions

File tree

Doc/library/weakref.rst

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -63,12 +63,15 @@ exposed by the :mod:`!weakref` module for the benefit of advanced uses.
6363
Not all objects can be weakly referenced. Objects which support weak references
6464
include class instances, functions written in Python (but not in C), instance methods,
6565
sets, frozensets, some :term:`file objects <file object>`, :term:`generators <generator>`,
66-
type objects, sockets, arrays, deques, regular expression pattern objects, and code
67-
objects.
66+
type objects, sockets, arrays, deques, regular expression pattern objects, code
67+
objects, and frame objects.
6868

6969
.. versionchanged:: 3.2
7070
Added support for thread.lock, threading.Lock, and code objects.
7171

72+
.. versionchanged:: 3.16
73+
Added support for frame objects.
74+
7275
Several built-in types such as :class:`list` and :class:`dict` do not directly
7376
support weak references but can add support through subclassing::
7477

Doc/whatsnew/3.16.rst

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,11 @@ New features
7575
Other language changes
7676
======================
7777

78+
* :ref:`Frame objects <frame-objects>` now support :mod:`weak references
79+
<weakref>`. This allows associating extra data with active frames,
80+
for example in debuggers, without keeping the frames (and everything
81+
they reference) alive indefinitely.
82+
(Contributed by Łukasz Langa in :gh:`102960`.)
7883

7984

8085
New modules

Include/internal/pycore_frame.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ struct _frame {
3434
* "support" for the borrowed references, ensuring that they remain valid.
3535
*/
3636
PyObject *f_overwritten_fast_locals;
37+
PyObject *f_weakreflist; /* List of weak references */
3738
/* The frame data, if this frame object owns the frame */
3839
PyObject *_f_frame_data[1];
3940
};

Lib/test/test_frame.py

Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -273,6 +273,146 @@ async def t3():
273273
raise AssertionError('coroutine did not exit')
274274

275275

276+
class WeakRefTest(unittest.TestCase):
277+
"""
278+
Frames support weak references (gh-102960).
279+
"""
280+
281+
def make_frame(self):
282+
# Return the frame object of a finished function call. Unlike
283+
# frames extracted from a traceback, it isn't part of a reference
284+
# cycle, so it dies as soon as the last reference is dropped.
285+
def func():
286+
return sys._getframe()
287+
return func()
288+
289+
def make_traceback_frames(self):
290+
def outer():
291+
def inner():
292+
1/0
293+
return inner()
294+
try:
295+
outer()
296+
except ZeroDivisionError as e:
297+
tb = e.__traceback__
298+
frames = []
299+
while tb:
300+
frames.append(tb.tb_frame)
301+
tb = tb.tb_next
302+
return frames
303+
304+
def test_weakref_basic(self):
305+
called = []
306+
f = self.make_frame()
307+
ref = weakref.ref(f)
308+
cb_ref = weakref.ref(f, called.append)
309+
self.assertIs(ref(), f)
310+
self.assertIs(cb_ref(), f)
311+
del f
312+
support.gc_collect()
313+
self.assertIsNone(ref())
314+
self.assertIsNone(cb_ref())
315+
self.assertEqual(called, [cb_ref])
316+
317+
@support.thread_unsafe("relies on gc.collect() reclaiming its cycles")
318+
def test_weakref_live_frame(self):
319+
refs = []
320+
def func():
321+
frame = sys._getframe()
322+
refs.append(weakref.ref(frame))
323+
self.assertIs(refs[0](), frame)
324+
func()
325+
support.gc_collect()
326+
self.assertIsNone(refs[0]())
327+
328+
@support.thread_unsafe("relies on gc.collect() reclaiming its cycles")
329+
def test_weak_key_dictionary(self):
330+
wkd = weakref.WeakKeyDictionary()
331+
def _fill():
332+
for i, frame in enumerate(self.make_traceback_frames()):
333+
wkd[frame] = i
334+
self.assertEqual(len(wkd), 3)
335+
_fill()
336+
support.gc_collect()
337+
self.assertEqual(len(wkd), 0)
338+
339+
@support.thread_unsafe("relies on gc.collect() reclaiming its cycles")
340+
def test_weakref_traceback_frames(self):
341+
# Frames that participate in reference cycles are cleaned up
342+
# by the cyclic garbage collector.
343+
refs = []
344+
def _make():
345+
for frame in self.make_traceback_frames():
346+
refs.append(weakref.ref(frame))
347+
for ref in refs:
348+
self.assertIsNotNone(ref())
349+
_make()
350+
support.gc_collect()
351+
for ref in refs:
352+
self.assertIsNone(ref())
353+
354+
def test_weakref_generator_frame(self):
355+
def gen():
356+
yield sys._getframe()
357+
g = gen()
358+
frame = next(g)
359+
ref = weakref.ref(frame)
360+
del frame
361+
support.gc_collect()
362+
# The generator keeps its frame alive while suspended.
363+
self.assertIsNotNone(ref())
364+
g.close()
365+
del g
366+
support.gc_collect()
367+
self.assertIsNone(ref())
368+
369+
def test_weakref_after_frame_clear(self):
370+
f = self.make_frame()
371+
ref = weakref.ref(f)
372+
# Clearing the frame's contents must not affect weak references
373+
# to the frame object itself.
374+
f.clear()
375+
self.assertIs(ref(), f)
376+
del f
377+
support.gc_collect()
378+
self.assertIsNone(ref())
379+
380+
@threading_helper.requires_working_threading()
381+
def test_weakref_concurrent(self):
382+
# Exercise concurrent creation and destruction of weak references
383+
# to the same frame, mainly for the free-threaded build.
384+
def gen():
385+
yield sys._getframe()
386+
g = gen()
387+
frame = next(g)
388+
barrier = threading.Barrier(4)
389+
# Collect failures instead of asserting in the workers: exceptions
390+
# raised in threads don't propagate to the unittest result.
391+
failures = []
392+
def work():
393+
barrier.wait()
394+
for _ in range(1000):
395+
ref = weakref.ref(frame)
396+
if ref() is not frame:
397+
failures.append('shared ref dead while frame alive')
398+
# Callback refs are not shared, so this concurrently adds
399+
# to and removes from the frame's weakref list.
400+
cb_ref = weakref.ref(frame, lambda r: None)
401+
if cb_ref() is not frame:
402+
failures.append('callback ref dead while frame alive')
403+
del ref, cb_ref
404+
threads = [threading.Thread(target=work) for _ in range(4)]
405+
with threading_helper.start_threads(threads):
406+
pass
407+
self.assertEqual(failures, [])
408+
ref = weakref.ref(frame)
409+
del frame
410+
g.close()
411+
del g
412+
support.gc_collect()
413+
self.assertIsNone(ref())
414+
415+
276416
class ReprTest(unittest.TestCase):
277417
"""
278418
Tests for repr(frame).

Lib/test/test_sys.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1700,7 +1700,7 @@ def func():
17001700
INTERPRETER_FRAME = '9PihcP'
17011701
else:
17021702
INTERPRETER_FRAME = '9PhcP'
1703-
check(x, size('3PiccPPP' + INTERPRETER_FRAME + 'P'))
1703+
check(x, size('3PiccPPPP' + INTERPRETER_FRAME + 'P'))
17041704
# function
17051705
def func(): pass
17061706
check(func, size('16Pi'))
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
:ref:`Frame objects <frame-objects>` now support :mod:`weak references
2+
<weakref>`. Patch by Łukasz Langa.

Objects/frameobject.c

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
#include "pycore_optimizer.h" // _Py_Executors_InvalidateDependency()
1717
#include "pycore_tuple.h" // _PyTuple_FromPair
1818
#include "pycore_unicodeobject.h" // _PyUnicode_Equal()
19+
#include "pycore_weakref.h" // FT_CLEAR_WEAKREFS()
1920

2021
#include "frameobject.h" // PyFrameLocalsProxyObject
2122
#include "opcode.h" // EXTENDED_ARG
@@ -1931,6 +1932,8 @@ frame_dealloc(PyObject *op)
19311932
_PyObject_GC_UNTRACK(f);
19321933
}
19331934

1935+
FT_CLEAR_WEAKREFS(op, f->f_weakreflist);
1936+
19341937
/* GH-106092: If f->f_frame was on the stack and we reached the maximum
19351938
* nesting depth for deallocations, the trashcan may have delayed this
19361939
* deallocation until after f->f_frame is freed. Avoid dereferencing
@@ -2089,7 +2092,7 @@ PyTypeObject PyFrame_Type = {
20892092
frame_traverse, /* tp_traverse */
20902093
frame_tp_clear, /* tp_clear */
20912094
0, /* tp_richcompare */
2092-
0, /* tp_weaklistoffset */
2095+
OFF(f_weakreflist), /* tp_weaklistoffset */
20932096
0, /* tp_iter */
20942097
0, /* tp_iternext */
20952098
frame_methods, /* tp_methods */
@@ -2125,6 +2128,7 @@ _PyFrame_New_NoTrack(PyCodeObject *code)
21252128
f->f_extra_locals = NULL;
21262129
f->f_locals_cache = NULL;
21272130
f->f_overwritten_fast_locals = NULL;
2131+
f->f_weakreflist = NULL;
21282132
return f;
21292133
}
21302134

0 commit comments

Comments
 (0)