Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions Doc/library/weakref.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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::

Expand Down
5 changes: 5 additions & 0 deletions Doc/whatsnew/3.16.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions Include/internal/pycore_frame.h
Original file line number Diff line number Diff line change
Expand Up @@ -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];
};
Expand Down
198 changes: 198 additions & 0 deletions Lib/test/test_frame.py
Original file line number Diff line number Diff line change
Expand Up @@ -273,6 +273,204 @@ async def t3():
raise AssertionError('coroutine did not exit')


class WeakRefTest(unittest.TestCase):
"""
Frames support weak references (gh-102960).
"""

def make_frame(self):

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we really need so many tests for this? A single test which verifies that frame objects are weakrefable and that when a frame dies the weakrefs gets cleared should be enough for this.

# 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):
f = self.make_frame()
ref = weakref.ref(f)
self.assertIs(ref(), f)
del f
support.gc_collect()
self.assertIsNone(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]())

def test_weakref_callback(self):
called = []
f = self.make_frame()
ref = weakref.ref(f, called.append)
del f
support.gc_collect()
self.assertEqual(called, [ref])
self.assertIsNone(ref())

def test_weakref_proxy(self):
f = self.make_frame()
proxy = weakref.proxy(f)
self.assertEqual(proxy.f_lineno, f.f_lineno)
self.assertIs(proxy.f_code, f.f_code)
del f
support.gc_collect()
with self.assertRaises(ReferenceError):
proxy.f_lineno

def test_multiple_weakrefs(self):
f = self.make_frame()
called = []
refs = [weakref.ref(f) for _ in range(3)]
refs += [weakref.ref(f, called.append) for _ in range(2)]
# Callback-less weakrefs to the same object are shared.
self.assertIs(refs[0], refs[1])
self.assertIs(refs[0], refs[2])
self.assertEqual(weakref.getweakrefcount(f), 3)
# Weakrefs hash and compare through their referent while it is
# alive, so compare identities instead.
self.assertEqual({id(r) for r in weakref.getweakrefs(f)},
{id(refs[0]), id(refs[3]), id(refs[4])})
del f
support.gc_collect()
for ref in refs:
self.assertIsNone(ref())
self.assertEqual({id(r) for r in called}, {id(refs[3]), id(refs[4])})

@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_weak_value_dictionary(self):
wvd = weakref.WeakValueDictionary()
def _fill():
for i, frame in enumerate(self.make_traceback_frames()):
wvd[i] = frame
self.assertEqual(len(wvd), 3)
_fill()
support.gc_collect()
self.assertEqual(len(wvd), 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_coroutine_frame(self):
async def coro():
return sys._getframe()
c = coro()
ref = None
try:
c.send(None)
except StopIteration as ex:
ref = weakref.ref(ex.value)
self.assertIsNotNone(ref, 'coroutine did not exit')
del c
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).
Expand Down
2 changes: 1 addition & 1 deletion Lib/test/test_sys.py
Original file line number Diff line number Diff line change
Expand Up @@ -1700,7 +1700,7 @@ def func():
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'))
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
:ref:`Frame objects <frame-objects>` now support :mod:`weak references
<weakref>`. Patch by Łukasz Langa.
6 changes: 5 additions & 1 deletion Objects/frameobject.c
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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 */
Expand Down Expand Up @@ -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;
}

Expand Down
Loading