diff --git a/Lib/test/test_frame.py b/Lib/test/test_frame.py
new file mode 100644
index 000000000..ae02e2a59
--- /dev/null
+++ b/Lib/test/test_frame.py
@@ -0,0 +1,814 @@
+import copy
+import operator
+import re
+import sys
+import textwrap
+import threading
+import unittest
+import weakref
+try:
+ import _testcapi
+except ImportError:
+ _testcapi = None
+
+from collections.abc import Mapping
+from test import support
+from test.support import import_helper, threading_helper
+from test.support.script_helper import assert_python_ok
+from test import mapping_tests
+
+
+class ClearTest(unittest.TestCase):
+ """
+ Tests for frame.clear().
+ """
+
+ def inner(self, x=5, **kwargs):
+ 1/0
+
+ def outer(self, **kwargs):
+ try:
+ self.inner(**kwargs)
+ except ZeroDivisionError as e:
+ exc = e
+ return exc
+
+ def clear_traceback_frames(self, tb):
+ """
+ Clear all frames in a traceback.
+ """
+ while tb is not None:
+ tb.tb_frame.clear()
+ tb = tb.tb_next
+
+ def test_clear_locals(self):
+ class C:
+ pass
+ c = C()
+ wr = weakref.ref(c)
+ exc = self.outer(c=c)
+ del c
+ support.gc_collect()
+ # A reference to c is held through the frames
+ self.assertIsNot(None, wr())
+ self.clear_traceback_frames(exc.__traceback__)
+ support.gc_collect()
+ # The reference was released by .clear()
+ self.assertIs(None, wr())
+
+ def test_clear_locals_after_f_locals_access(self):
+ # see gh-113939
+ class C:
+ pass
+
+ wr = None
+ def inner():
+ nonlocal wr
+ c = C()
+ wr = weakref.ref(c)
+ 1/0
+
+ try:
+ inner()
+ except ZeroDivisionError as exc:
+ support.gc_collect()
+ self.assertIsNotNone(wr())
+ exc.__traceback__.tb_next.tb_frame.clear()
+ support.gc_collect()
+ self.assertIsNone(wr())
+
+ def test_clear_does_not_clear_specials(self):
+ class C:
+ pass
+ c = C()
+ exc = self.outer(c=c)
+ del c
+ f = exc.__traceback__.tb_frame
+ f.clear()
+ self.assertIsNot(f.f_code, None)
+ self.assertIsNot(f.f_locals, None)
+ self.assertIsNot(f.f_builtins, None)
+ self.assertIsNot(f.f_globals, None)
+
+ def test_clear_generator(self):
+ endly = False
+ def g():
+ nonlocal endly
+ try:
+ yield
+ self.inner()
+ finally:
+ endly = True
+ gen = g()
+ next(gen)
+ self.assertFalse(endly)
+
+ # Cannot clear a suspended frame
+ with self.assertRaisesRegex(RuntimeError, r'suspended frame'):
+ gen.gi_frame.clear()
+ self.assertFalse(endly)
+
+ def test_clear_executing(self):
+ # Attempting to clear an executing frame is forbidden.
+ try:
+ 1/0
+ except ZeroDivisionError as e:
+ f = e.__traceback__.tb_frame
+ with self.assertRaises(RuntimeError):
+ f.clear()
+ with self.assertRaises(RuntimeError):
+ f.f_back.clear()
+
+ def test_clear_executing_generator(self):
+ # Attempting to clear an executing generator frame is forbidden.
+ endly = False
+ def g():
+ nonlocal endly
+ try:
+ 1/0
+ except ZeroDivisionError as e:
+ f = e.__traceback__.tb_frame
+ with self.assertRaises(RuntimeError):
+ f.clear()
+ with self.assertRaises(RuntimeError):
+ f.f_back.clear()
+ yield f
+ finally:
+ endly = True
+ gen = g()
+ f = next(gen)
+ self.assertFalse(endly)
+ # Cannot clear a suspended frame
+ with self.assertRaisesRegex(RuntimeError, 'suspended frame'):
+ f.clear()
+ self.assertFalse(endly)
+
+ def test_lineno_with_tracing(self):
+ def record_line():
+ f = sys._getframe(1)
+ lines.append(f.f_lineno-f.f_code.co_firstlineno)
+
+ def test(trace):
+ record_line()
+ if trace:
+ sys._getframe(0).f_trace = True
+ record_line()
+ record_line()
+
+ expected_lines = [1, 4, 5]
+ lines = []
+ test(False)
+ self.assertEqual(lines, expected_lines)
+ lines = []
+ test(True)
+ self.assertEqual(lines, expected_lines)
+
+ @support.cpython_only
+ def test_clear_refcycles(self):
+ # .clear() doesn't leave any refcycle behind
+ with support.disable_gc():
+ class C:
+ pass
+ c = C()
+ wr = weakref.ref(c)
+ exc = self.outer(c=c)
+ del c
+ self.assertIsNot(None, wr())
+ self.clear_traceback_frames(exc.__traceback__)
+ self.assertIs(None, wr())
+
+
+class FrameAttrsTest(unittest.TestCase):
+
+ def make_frames(self):
+ def outer():
+ x = 5
+ y = 6
+ def inner():
+ z = x + 2
+ 1/0
+ t = 9
+ 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_clear_locals(self):
+ # Test f_locals after clear() (issue #21897)
+ f, outer, inner = self.make_frames()
+ outer.clear()
+ inner.clear()
+ self.assertEqual(outer.f_locals, {})
+ self.assertEqual(inner.f_locals, {})
+
+ def test_locals_clear_locals(self):
+ # Test f_locals before and after clear() (to exercise caching)
+ f, outer, inner = self.make_frames()
+ self.assertNotEqual(outer.f_locals, {})
+ self.assertNotEqual(inner.f_locals, {})
+ outer.clear()
+ inner.clear()
+ self.assertEqual(outer.f_locals, {})
+ self.assertEqual(inner.f_locals, {})
+
+ @unittest.expectedFailure # TODO: RUSTPYTHON; TypeError: can't delete f_lineno attribute
+ def test_f_lineno_del_segfault(self):
+ f, _, _ = self.make_frames()
+ with self.assertRaises(AttributeError):
+ del f.f_lineno
+
+ def test_f_generator(self):
+ # Test f_generator in different contexts.
+
+ def t0():
+ def nested():
+ frame = sys._getframe()
+ return frame.f_generator
+
+ def gen():
+ yield nested()
+
+ g = gen()
+ try:
+ return next(g)
+ finally:
+ g.close()
+
+ def t1():
+ frame = sys._getframe()
+ return frame.f_generator
+
+ def t2():
+ frame = sys._getframe()
+ yield frame.f_generator
+
+ async def t3():
+ frame = sys._getframe()
+ return frame.f_generator
+
+ # For regular functions f_generator is None
+ self.assertIsNone(t0())
+ self.assertIsNone(t1())
+
+ # For generators f_generator is equal to self
+ g = t2()
+ try:
+ frame_g = next(g)
+ self.assertIs(g, frame_g)
+ finally:
+ g.close()
+
+ # Ditto for coroutines
+ c = t3()
+ try:
+ c.send(None)
+ except StopIteration as ex:
+ self.assertIs(ex.value, c)
+ else:
+ raise AssertionError('coroutine did not exit')
+
+
+class ReprTest(unittest.TestCase):
+ """
+ Tests for repr(frame).
+ """
+
+ @unittest.expectedFailure # TODO: RUSTPYTHON
+ def test_repr(self):
+ def outer():
+ x = 5
+ y = 6
+ def inner():
+ z = x + 2
+ 1/0
+ t = 9
+ return inner()
+
+ offset = outer.__code__.co_firstlineno
+ try:
+ outer()
+ except ZeroDivisionError as e:
+ tb = e.__traceback__
+ frames = []
+ while tb:
+ frames.append(tb.tb_frame)
+ tb = tb.tb_next
+ else:
+ self.fail("should have raised")
+
+ f_this, f_outer, f_inner = frames
+ file_repr = re.escape(repr(__file__))
+ self.assertRegex(repr(f_this),
+ r"^$"
+ % (file_repr, offset + 23))
+ self.assertRegex(repr(f_outer),
+ r"^$"
+ % (file_repr, offset + 7))
+ self.assertRegex(repr(f_inner),
+ r"^$"
+ % (file_repr, offset + 5))
+
+class TestFrameLocals(unittest.TestCase):
+ @unittest.expectedFailure # TODO: RUSTPYTHON
+ def test_scope(self):
+ class A:
+ x = 1
+ sys._getframe().f_locals['x'] = 2
+ sys._getframe().f_locals['y'] = 2
+
+ self.assertEqual(A.x, 2)
+ self.assertEqual(A.y, 2)
+
+ def f():
+ x = 1
+ sys._getframe().f_locals['x'] = 2
+ sys._getframe().f_locals['y'] = 2
+ self.assertEqual(x, 2)
+ self.assertEqual(locals()['y'], 2)
+ f()
+
+ @unittest.expectedFailure # TODO: RUSTPYTHON; AssertionError: 1 != 2
+ def test_closure(self):
+ x = 1
+ y = 2
+
+ def f():
+ z = x + y
+ d = sys._getframe().f_locals
+ self.assertEqual(d['x'], 1)
+ self.assertEqual(d['y'], 2)
+ d['x'] = 2
+ d['y'] = 3
+
+ f()
+ self.assertEqual(x, 2)
+ self.assertEqual(y, 3)
+
+ def test_closure_with_inline_comprehension(self):
+ lambda: k
+ k = 1
+ lst = [locals() for k in [0]]
+ self.assertEqual(lst[0]['k'], 0)
+
+ @unittest.expectedFailure # TODO: RUSTPYTHON; AssertionError: 3 != 4
+ def test_as_dict(self):
+ x = 1
+ y = 2
+ d = sys._getframe().f_locals
+ # self, x, y, d
+ self.assertEqual(len(d), 4)
+ self.assertIs(d['d'], d)
+ self.assertEqual(set(d.keys()), set(['x', 'y', 'd', 'self']))
+ self.assertEqual(len(d.values()), 4)
+ self.assertIn(1, d.values())
+ self.assertEqual(len(d.items()), 4)
+ self.assertIn(('x', 1), d.items())
+ self.assertEqual(d.__getitem__('x'), 1)
+ d.__setitem__('x', 2)
+ self.assertEqual(d['x'], 2)
+ self.assertEqual(d.get('x'), 2)
+ self.assertIs(d.get('non_exist', None), None)
+ self.assertEqual(d.__len__(), 4)
+ self.assertEqual(set([key for key in d]), set(['x', 'y', 'd', 'self']))
+ self.assertIn('x', d)
+ self.assertTrue(d.__contains__('x'))
+
+ self.assertEqual(reversed(d), list(reversed(d.keys())))
+
+ d.update({'x': 3, 'z': 4})
+ self.assertEqual(d['x'], 3)
+ self.assertEqual(d['z'], 4)
+
+ with self.assertRaises(TypeError):
+ d.update([1, 2])
+
+ self.assertEqual(d.setdefault('x', 5), 3)
+ self.assertEqual(d.setdefault('new', 5), 5)
+ self.assertEqual(d['new'], 5)
+
+ with self.assertRaises(KeyError):
+ d['non_exist']
+
+ def test_as_number(self):
+ x = 1
+ y = 2
+ d = sys._getframe().f_locals
+ self.assertIn('z', d | {'z': 3})
+ d |= {'z': 3}
+ self.assertEqual(d['z'], 3)
+ d |= {'y': 3}
+ self.assertEqual(d['y'], 3)
+ with self.assertRaises(TypeError):
+ d |= 3
+ with self.assertRaises(TypeError):
+ _ = d | [3]
+
+ def test_non_string_key(self):
+ d = sys._getframe().f_locals
+ d[1] = 2
+ self.assertEqual(d[1], 2)
+
+ @unittest.expectedFailure # TODO: RUSTPYTHON; UnboundLocalError: local variable 'b' referenced before assignment
+ def test_write_with_hidden(self):
+ def f():
+ f_locals = [sys._getframe().f_locals for b in [0]][0]
+ f_locals['b'] = 2
+ f_locals['c'] = 3
+ self.assertEqual(b, 2)
+ self.assertEqual(c, 3)
+ b = 0
+ c = 0
+ f()
+
+ @unittest.expectedFailure # TODO: RUSTPYTHON; AssertionError: