@@ -273,6 +273,194 @@ 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+ f = self .make_frame ()
306+ ref = weakref .ref (f )
307+ self .assertIs (ref (), f )
308+ del f
309+ support .gc_collect ()
310+ self .assertIsNone (ref ())
311+
312+ def test_weakref_live_frame (self ):
313+ refs = []
314+ def func ():
315+ frame = sys ._getframe ()
316+ refs .append (weakref .ref (frame ))
317+ self .assertIs (refs [0 ](), frame )
318+ func ()
319+ support .gc_collect ()
320+ self .assertIsNone (refs [0 ]())
321+
322+ def test_weakref_callback (self ):
323+ called = []
324+ f = self .make_frame ()
325+ ref = weakref .ref (f , called .append )
326+ del f
327+ support .gc_collect ()
328+ self .assertEqual (called , [ref ])
329+ self .assertIsNone (ref ())
330+
331+ def test_weakref_proxy (self ):
332+ f = self .make_frame ()
333+ proxy = weakref .proxy (f )
334+ self .assertEqual (proxy .f_lineno , f .f_lineno )
335+ self .assertIs (proxy .f_code , f .f_code )
336+ del f
337+ support .gc_collect ()
338+ with self .assertRaises (ReferenceError ):
339+ proxy .f_lineno
340+
341+ def test_multiple_weakrefs (self ):
342+ f = self .make_frame ()
343+ called = []
344+ refs = [weakref .ref (f ) for _ in range (3 )]
345+ refs += [weakref .ref (f , called .append ) for _ in range (2 )]
346+ # Callback-less weakrefs to the same object are shared.
347+ self .assertIs (refs [0 ], refs [1 ])
348+ self .assertIs (refs [0 ], refs [2 ])
349+ self .assertEqual (weakref .getweakrefcount (f ), 3 )
350+ # Weakrefs hash and compare through their referent while it is
351+ # alive, so compare identities instead.
352+ self .assertEqual ({id (r ) for r in weakref .getweakrefs (f )},
353+ {id (refs [0 ]), id (refs [3 ]), id (refs [4 ])})
354+ del f
355+ support .gc_collect ()
356+ for ref in refs :
357+ self .assertIsNone (ref ())
358+ self .assertEqual ({id (r ) for r in called }, {id (refs [3 ]), id (refs [4 ])})
359+
360+ def test_weak_key_dictionary (self ):
361+ wkd = weakref .WeakKeyDictionary ()
362+ def _fill ():
363+ for i , frame in enumerate (self .make_traceback_frames ()):
364+ wkd [frame ] = i
365+ self .assertEqual (len (wkd ), 3 )
366+ _fill ()
367+ support .gc_collect ()
368+ self .assertEqual (len (wkd ), 0 )
369+
370+ def test_weak_value_dictionary (self ):
371+ wvd = weakref .WeakValueDictionary ()
372+ def _fill ():
373+ for i , frame in enumerate (self .make_traceback_frames ()):
374+ wvd [i ] = frame
375+ self .assertEqual (len (wvd ), 3 )
376+ _fill ()
377+ support .gc_collect ()
378+ self .assertEqual (len (wvd ), 0 )
379+
380+ def test_weakref_traceback_frames (self ):
381+ # Frames that participate in reference cycles are cleaned up
382+ # by the cyclic garbage collector.
383+ refs = []
384+ def _make ():
385+ for frame in self .make_traceback_frames ():
386+ refs .append (weakref .ref (frame ))
387+ for ref in refs :
388+ self .assertIsNotNone (ref ())
389+ _make ()
390+ support .gc_collect ()
391+ for ref in refs :
392+ self .assertIsNone (ref ())
393+
394+ def test_weakref_generator_frame (self ):
395+ def gen ():
396+ yield sys ._getframe ()
397+ g = gen ()
398+ frame = next (g )
399+ ref = weakref .ref (frame )
400+ del frame
401+ support .gc_collect ()
402+ # The generator keeps its frame alive while suspended.
403+ self .assertIsNotNone (ref ())
404+ g .close ()
405+ del g
406+ support .gc_collect ()
407+ self .assertIsNone (ref ())
408+
409+ def test_weakref_coroutine_frame (self ):
410+ async def coro ():
411+ return sys ._getframe ()
412+ c = coro ()
413+ ref = None
414+ try :
415+ c .send (None )
416+ except StopIteration as ex :
417+ ref = weakref .ref (ex .value )
418+ self .assertIsNotNone (ref , 'coroutine did not exit' )
419+ del c
420+ support .gc_collect ()
421+ self .assertIsNone (ref ())
422+
423+ def test_weakref_after_frame_clear (self ):
424+ f = self .make_frame ()
425+ ref = weakref .ref (f )
426+ # Clearing the frame's contents must not affect weak references
427+ # to the frame object itself.
428+ f .clear ()
429+ self .assertIs (ref (), f )
430+ del f
431+ support .gc_collect ()
432+ self .assertIsNone (ref ())
433+
434+ @threading_helper .requires_working_threading ()
435+ def test_weakref_concurrent (self ):
436+ # Exercise concurrent creation and destruction of weak references
437+ # to the same frame, mainly for the free-threaded build.
438+ def gen ():
439+ yield sys ._getframe ()
440+ g = gen ()
441+ frame = next (g )
442+ barrier = threading .Barrier (4 )
443+ def work ():
444+ barrier .wait ()
445+ for _ in range (1000 ):
446+ ref = weakref .ref (frame )
447+ self .assertIs (ref (), frame )
448+ # Callback refs are not shared, so this concurrently adds
449+ # to and removes from the frame's weakref list.
450+ cb_ref = weakref .ref (frame , lambda r : None )
451+ self .assertIs (cb_ref (), frame )
452+ del ref , cb_ref
453+ threads = [threading .Thread (target = work ) for _ in range (4 )]
454+ with threading_helper .start_threads (threads ):
455+ pass
456+ ref = weakref .ref (frame )
457+ del frame
458+ g .close ()
459+ del g
460+ support .gc_collect ()
461+ self .assertIsNone (ref ())
462+
463+
276464class ReprTest (unittest .TestCase ):
277465 """
278466 Tests for repr(frame).
0 commit comments