Skip to content

Commit b034f14

Browse files
gh-74929: Implement PEP 667 (GH-115153)
1 parent 1ab6356 commit b034f14

19 files changed

+921
-257
lines changed

Doc/data/stable_abi.dat

Lines changed: 3 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Include/ceval.h

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,10 @@ PyAPI_FUNC(PyObject *) PyEval_GetGlobals(void);
2222
PyAPI_FUNC(PyObject *) PyEval_GetLocals(void);
2323
PyAPI_FUNC(PyFrameObject *) PyEval_GetFrame(void);
2424

25+
PyAPI_FUNC(PyObject *) PyEval_GetFrameBuiltins(void);
26+
PyAPI_FUNC(PyObject *) PyEval_GetFrameGlobals(void);
27+
PyAPI_FUNC(PyObject *) PyEval_GetFrameLocals(void);
28+
2529
PyAPI_FUNC(int) Py_AddPendingCall(int (*func)(void *), void *arg);
2630
PyAPI_FUNC(int) Py_MakePendingCalls(void);
2731

Include/cpython/frameobject.h

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,3 +27,9 @@ PyAPI_FUNC(int) _PyFrame_IsEntryFrame(PyFrameObject *frame);
2727

2828
PyAPI_FUNC(int) PyFrame_FastToLocalsWithError(PyFrameObject *f);
2929
PyAPI_FUNC(void) PyFrame_FastToLocals(PyFrameObject *);
30+
31+
32+
typedef struct {
33+
PyObject_HEAD
34+
PyFrameObject* frame;
35+
} PyFrameLocalsProxyObject;

Include/cpython/pyframe.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,10 @@
33
#endif
44

55
PyAPI_DATA(PyTypeObject) PyFrame_Type;
6+
PyAPI_DATA(PyTypeObject) PyFrameLocalsProxy_Type;
67

78
#define PyFrame_Check(op) Py_IS_TYPE((op), &PyFrame_Type)
9+
#define PyFrameLocalsProxy_Check(op) Py_IS_TYPE((op), &PyFrameLocalsProxy_Type)
810

911
PyAPI_FUNC(PyFrameObject *) PyFrame_GetBack(PyFrameObject *frame);
1012
PyAPI_FUNC(PyObject *) PyFrame_GetLocals(PyFrameObject *frame);

Include/internal/pycore_frame.h

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ struct _frame {
2525
int f_lineno; /* Current line number. Only valid if non-zero */
2626
char f_trace_lines; /* Emit per-line trace events? */
2727
char f_trace_opcodes; /* Emit per-opcode trace events? */
28-
char f_fast_as_locals; /* Have the fast locals of this frame been converted to a dict? */
28+
PyObject *f_extra_locals; /* Dict for locals set by users using f_locals, could be NULL */
2929
/* The frame data, if this frame object owns the frame */
3030
PyObject *_f_frame_data[1];
3131
};
@@ -245,14 +245,11 @@ _PyFrame_ClearExceptCode(_PyInterpreterFrame * frame);
245245
int
246246
_PyFrame_Traverse(_PyInterpreterFrame *frame, visitproc visit, void *arg);
247247

248-
PyObject *
249-
_PyFrame_GetLocals(_PyInterpreterFrame *frame, int include_hidden);
250-
251-
int
252-
_PyFrame_FastToLocalsWithError(_PyInterpreterFrame *frame);
248+
bool
249+
_PyFrame_HasHiddenLocals(_PyInterpreterFrame *frame);
253250

254-
void
255-
_PyFrame_LocalsToFast(_PyInterpreterFrame *frame, int clear);
251+
PyObject *
252+
_PyFrame_GetLocals(_PyInterpreterFrame *frame);
256253

257254
static inline bool
258255
_PyThreadState_HasStackSpace(PyThreadState *tstate, int size)

Lib/test/test_frame.py

Lines changed: 175 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import copy
12
import gc
23
import operator
34
import re
@@ -13,7 +14,7 @@
1314
_testcapi = None
1415

1516
from test import support
16-
from test.support import threading_helper, Py_GIL_DISABLED
17+
from test.support import import_helper, threading_helper, Py_GIL_DISABLED
1718
from test.support.script_helper import assert_python_ok
1819

1920

@@ -198,14 +199,6 @@ def inner():
198199
tb = tb.tb_next
199200
return frames
200201

201-
def test_locals(self):
202-
f, outer, inner = self.make_frames()
203-
outer_locals = outer.f_locals
204-
self.assertIsInstance(outer_locals.pop('inner'), types.FunctionType)
205-
self.assertEqual(outer_locals, {'x': 5, 'y': 6})
206-
inner_locals = inner.f_locals
207-
self.assertEqual(inner_locals, {'x': 5, 'z': 7})
208-
209202
def test_clear_locals(self):
210203
# Test f_locals after clear() (issue #21897)
211204
f, outer, inner = self.make_frames()
@@ -217,8 +210,8 @@ def test_clear_locals(self):
217210
def test_locals_clear_locals(self):
218211
# Test f_locals before and after clear() (to exercise caching)
219212
f, outer, inner = self.make_frames()
220-
outer.f_locals
221-
inner.f_locals
213+
self.assertNotEqual(outer.f_locals, {})
214+
self.assertNotEqual(inner.f_locals, {})
222215
outer.clear()
223216
inner.clear()
224217
self.assertEqual(outer.f_locals, {})
@@ -269,6 +262,177 @@ def inner():
269262
r"^<frame at 0x[0-9a-fA-F]+, file %s, line %d, code inner>$"
270263
% (file_repr, offset + 5))
271264

265+
class TestFrameLocals(unittest.TestCase):
266+
def test_scope(self):
267+
class A:
268+
x = 1
269+
sys._getframe().f_locals['x'] = 2
270+
sys._getframe().f_locals['y'] = 2
271+
272+
self.assertEqual(A.x, 2)
273+
self.assertEqual(A.y, 2)
274+
275+
def f():
276+
x = 1
277+
sys._getframe().f_locals['x'] = 2
278+
sys._getframe().f_locals['y'] = 2
279+
self.assertEqual(x, 2)
280+
self.assertEqual(locals()['y'], 2)
281+
f()
282+
283+
def test_closure(self):
284+
x = 1
285+
y = 2
286+
287+
def f():
288+
z = x + y
289+
d = sys._getframe().f_locals
290+
self.assertEqual(d['x'], 1)
291+
self.assertEqual(d['y'], 2)
292+
d['x'] = 2
293+
d['y'] = 3
294+
295+
f()
296+
self.assertEqual(x, 2)
297+
self.assertEqual(y, 3)
298+
299+
def test_as_dict(self):
300+
x = 1
301+
y = 2
302+
d = sys._getframe().f_locals
303+
# self, x, y, d
304+
self.assertEqual(len(d), 4)
305+
self.assertIs(d['d'], d)
306+
self.assertEqual(set(d.keys()), set(['x', 'y', 'd', 'self']))
307+
self.assertEqual(len(d.values()), 4)
308+
self.assertIn(1, d.values())
309+
self.assertEqual(len(d.items()), 4)
310+
self.assertIn(('x', 1), d.items())
311+
self.assertEqual(d.__getitem__('x'), 1)
312+
d.__setitem__('x', 2)
313+
self.assertEqual(d['x'], 2)
314+
self.assertEqual(d.get('x'), 2)
315+
self.assertIs(d.get('non_exist', None), None)
316+
self.assertEqual(d.__len__(), 4)
317+
self.assertEqual(set([key for key in d]), set(['x', 'y', 'd', 'self']))
318+
self.assertIn('x', d)
319+
self.assertTrue(d.__contains__('x'))
320+
321+
self.assertEqual(reversed(d), list(reversed(d.keys())))
322+
323+
d.update({'x': 3, 'z': 4})
324+
self.assertEqual(d['x'], 3)
325+
self.assertEqual(d['z'], 4)
326+
327+
with self.assertRaises(TypeError):
328+
d.update([1, 2])
329+
330+
self.assertEqual(d.setdefault('x', 5), 3)
331+
self.assertEqual(d.setdefault('new', 5), 5)
332+
self.assertEqual(d['new'], 5)
333+
334+
with self.assertRaises(KeyError):
335+
d['non_exist']
336+
337+
def test_as_number(self):
338+
x = 1
339+
y = 2
340+
d = sys._getframe().f_locals
341+
self.assertIn('z', d | {'z': 3})
342+
d |= {'z': 3}
343+
self.assertEqual(d['z'], 3)
344+
d |= {'y': 3}
345+
self.assertEqual(d['y'], 3)
346+
with self.assertRaises(TypeError):
347+
d |= 3
348+
with self.assertRaises(TypeError):
349+
_ = d | [3]
350+
351+
def test_non_string_key(self):
352+
d = sys._getframe().f_locals
353+
d[1] = 2
354+
self.assertEqual(d[1], 2)
355+
356+
def test_write_with_hidden(self):
357+
def f():
358+
f_locals = [sys._getframe().f_locals for b in [0]][0]
359+
f_locals['b'] = 2
360+
f_locals['c'] = 3
361+
self.assertEqual(b, 2)
362+
self.assertEqual(c, 3)
363+
b = 0
364+
c = 0
365+
f()
366+
367+
def test_repr(self):
368+
x = 1
369+
# Introduce a reference cycle
370+
frame = sys._getframe()
371+
self.assertEqual(repr(frame.f_locals), repr(dict(frame.f_locals)))
372+
373+
def test_delete(self):
374+
x = 1
375+
d = sys._getframe().f_locals
376+
with self.assertRaises(TypeError):
377+
del d['x']
378+
379+
with self.assertRaises(AttributeError):
380+
d.clear()
381+
382+
with self.assertRaises(AttributeError):
383+
d.pop('x')
384+
385+
@support.cpython_only
386+
def test_sizeof(self):
387+
proxy = sys._getframe().f_locals
388+
support.check_sizeof(self, proxy, support.calcobjsize("P"))
389+
390+
def test_unsupport(self):
391+
x = 1
392+
d = sys._getframe().f_locals
393+
with self.assertRaises(AttributeError):
394+
d.copy()
395+
396+
with self.assertRaises(TypeError):
397+
copy.copy(d)
398+
399+
with self.assertRaises(TypeError):
400+
copy.deepcopy(d)
401+
402+
403+
class TestFrameCApi(unittest.TestCase):
404+
def test_basic(self):
405+
x = 1
406+
ctypes = import_helper.import_module('ctypes')
407+
PyEval_GetFrameLocals = ctypes.pythonapi.PyEval_GetFrameLocals
408+
PyEval_GetFrameLocals.restype = ctypes.py_object
409+
frame_locals = PyEval_GetFrameLocals()
410+
self.assertTrue(type(frame_locals), dict)
411+
self.assertEqual(frame_locals['x'], 1)
412+
frame_locals['x'] = 2
413+
self.assertEqual(x, 1)
414+
415+
PyEval_GetFrameGlobals = ctypes.pythonapi.PyEval_GetFrameGlobals
416+
PyEval_GetFrameGlobals.restype = ctypes.py_object
417+
frame_globals = PyEval_GetFrameGlobals()
418+
self.assertTrue(type(frame_globals), dict)
419+
self.assertIs(frame_globals, globals())
420+
421+
PyEval_GetFrameBuiltins = ctypes.pythonapi.PyEval_GetFrameBuiltins
422+
PyEval_GetFrameBuiltins.restype = ctypes.py_object
423+
frame_builtins = PyEval_GetFrameBuiltins()
424+
self.assertEqual(frame_builtins, __builtins__)
425+
426+
PyFrame_GetLocals = ctypes.pythonapi.PyFrame_GetLocals
427+
PyFrame_GetLocals.argtypes = [ctypes.py_object]
428+
PyFrame_GetLocals.restype = ctypes.py_object
429+
frame = sys._getframe()
430+
f_locals = PyFrame_GetLocals(frame)
431+
self.assertTrue(f_locals['x'], 1)
432+
f_locals['x'] = 2
433+
self.assertEqual(x, 2)
434+
435+
272436
class TestIncompleteFrameAreInvisible(unittest.TestCase):
273437

274438
def test_issue95818(self):

Lib/test/test_listcomps.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -622,9 +622,14 @@ def test_exception_in_post_comp_call(self):
622622

623623
def test_frame_locals(self):
624624
code = """
625-
val = [sys._getframe().f_locals for a in [0]][0]["a"]
625+
val = "a" in [sys._getframe().f_locals for a in [0]][0]
626626
"""
627627
import sys
628+
self._check_in_scopes(code, {"val": False}, ns={"sys": sys})
629+
630+
code = """
631+
val = [sys._getframe().f_locals["a"] for a in [0]][0]
632+
"""
628633
self._check_in_scopes(code, {"val": 0}, ns={"sys": sys})
629634

630635
def _recursive_replace(self, maybe_code):

Lib/test/test_peepholer.py

Lines changed: 0 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -933,23 +933,6 @@ def f():
933933
self.assertNotInBytecode(f, "LOAD_FAST_CHECK")
934934
return f
935935

936-
def test_deleting_local_warns_and_assigns_none(self):
937-
f = self.make_function_with_no_checks()
938-
co_code = f.__code__.co_code
939-
def trace(frame, event, arg):
940-
if event == 'line' and frame.f_lineno == 4:
941-
del frame.f_locals["x"]
942-
sys.settrace(None)
943-
return None
944-
return trace
945-
e = r"assigning None to unbound local 'x'"
946-
with self.assertWarnsRegex(RuntimeWarning, e):
947-
sys.settrace(trace)
948-
f()
949-
self.assertInBytecode(f, "LOAD_FAST")
950-
self.assertNotInBytecode(f, "LOAD_FAST_CHECK")
951-
self.assertEqual(f.__code__.co_code, co_code)
952-
953936
def test_modifying_local_does_not_add_check(self):
954937
f = self.make_function_with_no_checks()
955938
def trace(frame, event, arg):

Lib/test/test_stable_abi_ctypes.py

Lines changed: 3 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Lib/test/test_sys.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1561,7 +1561,7 @@ class C(object): pass
15611561
def func():
15621562
return sys._getframe()
15631563
x = func()
1564-
check(x, size('3Pi3c7P2ic??2P'))
1564+
check(x, size('3Pi2cP7P2ic??2P'))
15651565
# function
15661566
def func(): pass
15671567
check(func, size('15Pi'))
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Implement PEP 667 - converted ``frame.f_locals`` to a write through proxy

Misc/stable_abi.toml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2501,3 +2501,9 @@
25012501
added = '3.13'
25022502
[function.PyType_GetModuleByDef]
25032503
added = '3.13'
2504+
[function.PyEval_GetFrameBuiltins]
2505+
added = '3.13'
2506+
[function.PyEval_GetFrameGlobals]
2507+
added = '3.13'
2508+
[function.PyEval_GetFrameLocals]
2509+
added = '3.13'

0 commit comments

Comments
 (0)