@@ -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+
276416class ReprTest (unittest .TestCase ):
277417 """
278418 Tests for repr(frame).
0 commit comments