Skip to content

Commit 608356e

Browse files
Add PyUnstable_InterpreterFrame_GetLocal
Add an unstable C API that returns a strong reference to a single local variable of an internal interpreter frame, addressed by its localsplus index, with cell and free variables unboxed to their contents. Free variables are resolved from the function closure, so the API also works on a frame that has not started executing (before COPY_FREE_VARS runs) -- the case that motivated it -- and it does not modify the frame. Includes the PEP 689 deliverables: reference documentation in Doc/c-api/frame.rst, a What's New entry for 3.16, a Misc/NEWS.d blurb, and tests in Lib/test/test_capi/test_misc.py (TestInternalFrameApi) covering plain locals, a cell variable, and a free variable. Authored with the assistance of an AI coding agent (Claude Opus)
1 parent 9721f8f commit 608356e

7 files changed

Lines changed: 135 additions & 1 deletion

File tree

‎Doc/c-api/frame.rst‎

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -243,3 +243,17 @@ Unless using :pep:`523`, you will not need this.
243243
Return the currently executing line number, or -1 if there is no line number.
244244
245245
.. versionadded:: 3.12
246+
247+
248+
.. c:function:: PyObject* PyUnstable_InterpreterFrame_GetLocal(struct _PyInterpreterFrame *frame, Py_ssize_t index)
249+
250+
Return a new :term:`strong reference` to the local variable at *index* in the
251+
frame's localsplus array, with cell and free variables unboxed to their
252+
contents. Free variables are resolved from the function closure, so this
253+
also works on a frame that has not started executing.
254+
255+
*index* must be in range ``[0, co_nlocalsplus)``. Return ``NULL`` with an
256+
:exc:`IndexError` set if it is out of range, or ``NULL`` without an exception
257+
set if the slot is unset or hidden.
258+
259+
.. versionadded:: 3.16

‎Doc/whatsnew/3.16.rst‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -896,7 +896,9 @@ C API changes
896896
New features
897897
------------
898898

899-
* TODO
899+
* Add :c:func:`PyUnstable_InterpreterFrame_GetLocal` to read a local variable
900+
of an internal interpreter frame by its localsplus index.
901+
(Contributed by Guilherme Leobas in :gh:`156133`.)
900902

901903
Porting to Python 3.16
902904
----------------------

‎Include/cpython/pyframe.h‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,3 +35,8 @@ PyAPI_FUNC(int) PyUnstable_InterpreterFrame_GetLasti(struct _PyInterpreterFrame
3535
/* Returns the currently executing line number, or -1 if there is no line number.
3636
* Does not raise an exception. */
3737
PyAPI_FUNC(int) PyUnstable_InterpreterFrame_GetLine(struct _PyInterpreterFrame *frame);
38+
39+
/* Returns a new (strong) reference to the local variable at `index` in the
40+
* frame's localsplus array. */
41+
PyAPI_FUNC(PyObject *) PyUnstable_InterpreterFrame_GetLocal(
42+
struct _PyInterpreterFrame *frame, Py_ssize_t index);

‎Lib/test/test_capi/test_misc.py‎

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2800,6 +2800,42 @@ def test_line(self):
28002800
firstline = self.func.__code__.co_firstlineno
28012801
self.assertEqual(line, firstline + 2)
28022802

2803+
# get_frame_locals() returns the caller frame's locals as a name -> value
2804+
# dict via PyUnstable_InterpreterFrame_GetLocal (one strong reference per
2805+
# localsplus index).
2806+
def helper_plain(self, a, b):
2807+
c = a + b
2808+
return _testinternalcapi.get_frame_locals()
2809+
2810+
def test_get_local_plain(self):
2811+
d = self.helper_plain(3, 4)
2812+
self.assertEqual(d['a'], 3)
2813+
self.assertEqual(d['b'], 4)
2814+
self.assertEqual(d['c'], 7)
2815+
self.assertIs(d['self'], self)
2816+
2817+
def test_get_local_cell(self):
2818+
# y is a cell variable of this frame because inner closes over it.
2819+
y = 100
2820+
2821+
def inner():
2822+
return y
2823+
2824+
d = _testinternalcapi.get_frame_locals()
2825+
self.assertEqual(d['y'], 100)
2826+
self.assertIs(d['inner'], inner)
2827+
2828+
def test_get_local_free(self):
2829+
# z is a free variable of inner, read from the closure.
2830+
z = 7
2831+
2832+
def inner():
2833+
_ = z
2834+
return _testinternalcapi.get_frame_locals()
2835+
2836+
d = inner()
2837+
self.assertEqual(d['z'], 7)
2838+
28032839

28042840
SUFFICIENT_TO_DEOPT_AND_SPECIALIZE = 100
28052841

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Add :c:func:`PyUnstable_InterpreterFrame_GetLocal` to read a local variable of
2+
an internal interpreter frame by its localsplus index.

‎Modules/_testinternalcapi.c‎

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1514,6 +1514,44 @@ iframe_getlasti(PyObject *self, PyObject *frame)
15141514
return PyLong_FromLong(PyUnstable_InterpreterFrame_GetLasti(f));
15151515
}
15161516

1517+
// Reads the locals of the Python frame that called this C function using
1518+
// PyUnstable_InterpreterFrame_GetLocals and returns them as a name -> value
1519+
// dict, skipping NULL (unset or hidden) slots.
1520+
static PyObject *
1521+
get_frame_locals(PyObject *self, PyObject *Py_UNUSED(ignored))
1522+
{
1523+
PyThreadState *tstate = _PyThreadState_GET();
1524+
_PyInterpreterFrame *frame = _PyThreadState_GetFrame(tstate);
1525+
if (frame == NULL) {
1526+
PyErr_SetString(PyExc_RuntimeError, "no caller frame");
1527+
return NULL;
1528+
}
1529+
PyCodeObject *co = _PyFrame_GetCode(frame);
1530+
Py_ssize_t n = co->co_nlocalsplus;
1531+
PyObject *dict = PyDict_New();
1532+
if (dict == NULL) {
1533+
return NULL;
1534+
}
1535+
for (Py_ssize_t i = 0; i < n; i++) {
1536+
PyObject *value = PyUnstable_InterpreterFrame_GetLocal(frame, i);
1537+
if (value == NULL) {
1538+
if (PyErr_Occurred()) {
1539+
Py_DECREF(dict);
1540+
return NULL;
1541+
}
1542+
continue; // unset or hidden slot
1543+
}
1544+
PyObject *name = PyTuple_GET_ITEM(co->co_localsplusnames, i);
1545+
int err = PyDict_SetItem(dict, name, value);
1546+
Py_DECREF(value);
1547+
if (err < 0) {
1548+
Py_DECREF(dict);
1549+
return NULL;
1550+
}
1551+
}
1552+
return dict;
1553+
}
1554+
15171555
static PyObject *
15181556
code_returns_only_none(PyObject *self, PyObject *arg)
15191557
{
@@ -3305,6 +3343,7 @@ static PyMethodDef module_functions[] = {
33053343
{"iframe_getcode", iframe_getcode, METH_O, NULL},
33063344
{"iframe_getline", iframe_getline, METH_O, NULL},
33073345
{"iframe_getlasti", iframe_getlasti, METH_O, NULL},
3346+
{"get_frame_locals", get_frame_locals, METH_NOARGS, NULL},
33083347
{"code_returns_only_none", code_returns_only_none, METH_O, NULL},
33093348
{"get_co_framesize", get_co_framesize, METH_O, NULL},
33103349
{"get_co_localskinds", get_co_localskinds, METH_O, NULL},

‎Objects/frameobject.c‎

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2247,6 +2247,42 @@ frame_get_var(_PyInterpreterFrame *frame, PyCodeObject *co, int i,
22472247
}
22482248

22492249

2250+
PyObject *
2251+
PyUnstable_InterpreterFrame_GetLocal(_PyInterpreterFrame *frame,
2252+
Py_ssize_t index)
2253+
{
2254+
PyCodeObject *co = _PyFrame_GetCode(frame);
2255+
if (index < 0 || index >= co->co_nlocalsplus) {
2256+
PyErr_Format(
2257+
PyExc_IndexError,
2258+
"PyUnstable_InterpreterFrame_GetLocal: index %zd out of range [0, %d)",
2259+
index, co->co_nlocalsplus);
2260+
return NULL;
2261+
}
2262+
2263+
int offset = PyUnstable_Code_GetFirstFree(co); // co_nlocalsplus - co_nfreevars
2264+
if (index < offset) {
2265+
// Local or cell variable. frame_get_var unboxes cells and copes with
2266+
// not-yet-started frames and arguments not yet promoted by MAKE_CELL.
2267+
if (_PyLocals_GetKind(co->co_localspluskinds, (int)index) & CO_FAST_HIDDEN) {
2268+
return NULL;
2269+
}
2270+
PyObject *value = NULL;
2271+
frame_get_var(frame, co, (int)index, &value);
2272+
return value; // strong reference, or NULL if unset
2273+
}
2274+
2275+
// Free variable: read from the function closure rather than localsplus.
2276+
if ((co->co_flags & CO_OPTIMIZED)
2277+
&& PyStackRef_FunctionCheck(frame->f_funcobj)) {
2278+
PyFunctionObject *func = _PyFrame_GetFunction(frame);
2279+
PyObject *cell = PyTuple_GET_ITEM(func->func_closure, index - offset);
2280+
return Py_XNewRef(PyCell_GET(cell));
2281+
}
2282+
return NULL;
2283+
}
2284+
2285+
22502286
bool
22512287
_PyFrame_HasHiddenLocals(_PyInterpreterFrame *frame)
22522288
{

0 commit comments

Comments
 (0)