mirror of https://github.com/python/cpython
590 lines
18 KiB
Python
590 lines
18 KiB
Python
import copy
|
|
import gc
|
|
import operator
|
|
import re
|
|
import sys
|
|
import textwrap
|
|
import threading
|
|
import types
|
|
import unittest
|
|
import weakref
|
|
try:
|
|
import _testcapi
|
|
except ImportError:
|
|
_testcapi = None
|
|
|
|
from test import support
|
|
from test.support import import_helper, threading_helper, Py_GIL_DISABLED
|
|
from test.support.script_helper import assert_python_ok
|
|
|
|
|
|
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, {})
|
|
|
|
def test_f_lineno_del_segfault(self):
|
|
f, _, _ = self.make_frames()
|
|
with self.assertRaises(AttributeError):
|
|
del f.f_lineno
|
|
|
|
|
|
class ReprTest(unittest.TestCase):
|
|
"""
|
|
Tests for repr(frame).
|
|
"""
|
|
|
|
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"^<frame at 0x[0-9a-fA-F]+, file %s, line %d, code test_repr>$"
|
|
% (file_repr, offset + 23))
|
|
self.assertRegex(repr(f_outer),
|
|
r"^<frame at 0x[0-9a-fA-F]+, file %s, line %d, code outer>$"
|
|
% (file_repr, offset + 7))
|
|
self.assertRegex(repr(f_inner),
|
|
r"^<frame at 0x[0-9a-fA-F]+, file %s, line %d, code inner>$"
|
|
% (file_repr, offset + 5))
|
|
|
|
class TestFrameLocals(unittest.TestCase):
|
|
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()
|
|
|
|
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_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)
|
|
|
|
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()
|
|
|
|
def test_local_objects(self):
|
|
o = object()
|
|
k = '.'.join(['a', 'b', 'c'])
|
|
f_locals = sys._getframe().f_locals
|
|
f_locals['o'] = f_locals['k']
|
|
self.assertEqual(o, 'a.b.c')
|
|
|
|
def test_copy(self):
|
|
x = 0
|
|
d = sys._getframe().f_locals
|
|
d_copy = d.copy()
|
|
self.assertIsInstance(d_copy, dict)
|
|
self.assertEqual(d_copy['x'], 0)
|
|
d_copy['x'] = 1
|
|
self.assertEqual(x, 0)
|
|
|
|
def test_update_with_self(self):
|
|
def f():
|
|
f_locals = sys._getframe().f_locals
|
|
f_locals.update(f_locals)
|
|
f_locals.update(f_locals)
|
|
f_locals.update(f_locals)
|
|
f()
|
|
|
|
def test_repr(self):
|
|
x = 1
|
|
# Introduce a reference cycle
|
|
frame = sys._getframe()
|
|
self.assertEqual(repr(frame.f_locals), repr(dict(frame.f_locals)))
|
|
|
|
def test_delete(self):
|
|
x = 1
|
|
d = sys._getframe().f_locals
|
|
with self.assertRaises(TypeError):
|
|
del d['x']
|
|
|
|
with self.assertRaises(AttributeError):
|
|
d.clear()
|
|
|
|
with self.assertRaises(AttributeError):
|
|
d.pop('x')
|
|
|
|
@support.cpython_only
|
|
def test_sizeof(self):
|
|
proxy = sys._getframe().f_locals
|
|
support.check_sizeof(self, proxy, support.calcobjsize("P"))
|
|
|
|
def test_unsupport(self):
|
|
x = 1
|
|
d = sys._getframe().f_locals
|
|
with self.assertRaises(TypeError):
|
|
copy.copy(d)
|
|
|
|
with self.assertRaises(TypeError):
|
|
copy.deepcopy(d)
|
|
|
|
|
|
class TestFrameCApi(unittest.TestCase):
|
|
def test_basic(self):
|
|
x = 1
|
|
ctypes = import_helper.import_module('ctypes')
|
|
PyEval_GetFrameLocals = ctypes.pythonapi.PyEval_GetFrameLocals
|
|
PyEval_GetFrameLocals.restype = ctypes.py_object
|
|
frame_locals = PyEval_GetFrameLocals()
|
|
self.assertTrue(type(frame_locals), dict)
|
|
self.assertEqual(frame_locals['x'], 1)
|
|
frame_locals['x'] = 2
|
|
self.assertEqual(x, 1)
|
|
|
|
PyEval_GetFrameGlobals = ctypes.pythonapi.PyEval_GetFrameGlobals
|
|
PyEval_GetFrameGlobals.restype = ctypes.py_object
|
|
frame_globals = PyEval_GetFrameGlobals()
|
|
self.assertTrue(type(frame_globals), dict)
|
|
self.assertIs(frame_globals, globals())
|
|
|
|
PyEval_GetFrameBuiltins = ctypes.pythonapi.PyEval_GetFrameBuiltins
|
|
PyEval_GetFrameBuiltins.restype = ctypes.py_object
|
|
frame_builtins = PyEval_GetFrameBuiltins()
|
|
self.assertEqual(frame_builtins, __builtins__)
|
|
|
|
PyFrame_GetLocals = ctypes.pythonapi.PyFrame_GetLocals
|
|
PyFrame_GetLocals.argtypes = [ctypes.py_object]
|
|
PyFrame_GetLocals.restype = ctypes.py_object
|
|
frame = sys._getframe()
|
|
f_locals = PyFrame_GetLocals(frame)
|
|
self.assertTrue(f_locals['x'], 1)
|
|
f_locals['x'] = 2
|
|
self.assertEqual(x, 2)
|
|
|
|
|
|
class TestIncompleteFrameAreInvisible(unittest.TestCase):
|
|
|
|
def test_issue95818(self):
|
|
# See GH-95818 for details
|
|
code = textwrap.dedent(f"""
|
|
import gc
|
|
|
|
gc.set_threshold(1,1,1)
|
|
class GCHello:
|
|
def __del__(self):
|
|
print("Destroyed from gc")
|
|
|
|
def gen():
|
|
yield
|
|
|
|
fd = open({__file__!r})
|
|
l = [fd, GCHello()]
|
|
l.append(l)
|
|
del fd
|
|
del l
|
|
gen()
|
|
""")
|
|
assert_python_ok("-c", code)
|
|
|
|
@support.cpython_only
|
|
@threading_helper.requires_working_threading()
|
|
def test_sneaky_frame_object_teardown(self):
|
|
|
|
class SneakyDel:
|
|
def __del__(self):
|
|
"""
|
|
Stash a reference to the entire stack for walking later.
|
|
|
|
It may look crazy, but you'd be surprised how common this is
|
|
when using a test runner (like pytest). The typical recipe is:
|
|
ResourceWarning + -Werror + a custom sys.unraisablehook.
|
|
"""
|
|
nonlocal sneaky_frame_object
|
|
sneaky_frame_object = sys._getframe()
|
|
|
|
class SneakyThread(threading.Thread):
|
|
"""
|
|
A separate thread isn't needed to make this code crash, but it does
|
|
make crashes more consistent, since it means sneaky_frame_object is
|
|
backed by freed memory after the thread completes!
|
|
"""
|
|
|
|
def run(self):
|
|
"""Run SneakyDel.__del__ as this frame is popped."""
|
|
ref = SneakyDel()
|
|
|
|
sneaky_frame_object = None
|
|
t = SneakyThread()
|
|
t.start()
|
|
t.join()
|
|
# sneaky_frame_object can be anything, really, but it's crucial that
|
|
# SneakyThread.run's frame isn't anywhere on the stack while it's being
|
|
# torn down:
|
|
self.assertIsNotNone(sneaky_frame_object)
|
|
while sneaky_frame_object is not None:
|
|
self.assertIsNot(
|
|
sneaky_frame_object.f_code, SneakyThread.run.__code__
|
|
)
|
|
sneaky_frame_object = sneaky_frame_object.f_back
|
|
|
|
def test_entry_frames_are_invisible_during_teardown(self):
|
|
class C:
|
|
"""A weakref'able class."""
|
|
|
|
def f():
|
|
"""Try to find globals and locals as this frame is being cleared."""
|
|
ref = C()
|
|
# Ignore the fact that exec(C()) is a nonsense callback. We're only
|
|
# using exec here because it tries to access the current frame's
|
|
# globals and locals. If it's trying to get those from a shim frame,
|
|
# we'll crash before raising:
|
|
return weakref.ref(ref, exec)
|
|
|
|
with support.catch_unraisable_exception() as catcher:
|
|
# Call from C, so there is a shim frame directly above f:
|
|
weak = operator.call(f) # BOOM!
|
|
# Cool, we didn't crash. Check that the callback actually happened:
|
|
self.assertIs(catcher.unraisable.exc_type, TypeError)
|
|
self.assertIsNone(weak())
|
|
|
|
@unittest.skipIf(_testcapi is None, 'need _testcapi')
|
|
class TestCAPI(unittest.TestCase):
|
|
def getframe(self):
|
|
return sys._getframe()
|
|
|
|
def test_frame_getters(self):
|
|
frame = self.getframe()
|
|
self.assertEqual(frame.f_locals, _testcapi.frame_getlocals(frame))
|
|
self.assertIs(frame.f_globals, _testcapi.frame_getglobals(frame))
|
|
self.assertIs(frame.f_builtins, _testcapi.frame_getbuiltins(frame))
|
|
self.assertEqual(frame.f_lasti, _testcapi.frame_getlasti(frame))
|
|
|
|
def test_getvar(self):
|
|
current_frame = sys._getframe()
|
|
x = 1
|
|
self.assertEqual(_testcapi.frame_getvar(current_frame, "x"), 1)
|
|
self.assertEqual(_testcapi.frame_getvarstring(current_frame, b"x"), 1)
|
|
with self.assertRaises(NameError):
|
|
_testcapi.frame_getvar(current_frame, "y")
|
|
with self.assertRaises(NameError):
|
|
_testcapi.frame_getvarstring(current_frame, b"y")
|
|
|
|
# wrong name type
|
|
with self.assertRaises(TypeError):
|
|
_testcapi.frame_getvar(current_frame, b'x')
|
|
with self.assertRaises(TypeError):
|
|
_testcapi.frame_getvar(current_frame, 123)
|
|
|
|
def getgenframe(self):
|
|
yield sys._getframe()
|
|
|
|
def test_frame_get_generator(self):
|
|
gen = self.getgenframe()
|
|
frame = next(gen)
|
|
self.assertIs(gen, _testcapi.frame_getgenerator(frame))
|
|
|
|
def test_frame_fback_api(self):
|
|
"""Test that accessing `f_back` does not cause a segmentation fault on
|
|
a frame created with `PyFrame_New` (GH-99110)."""
|
|
def dummy():
|
|
pass
|
|
|
|
frame = _testcapi.frame_new(dummy.__code__, globals(), locals())
|
|
# The following line should not cause a segmentation fault.
|
|
self.assertIsNone(frame.f_back)
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|