diff --git a/Doc/c-api/frame.rst b/Doc/c-api/frame.rst index 4159ff6e5965fbd..7b524b8f0510b55 100644 --- a/Doc/c-api/frame.rst +++ b/Doc/c-api/frame.rst @@ -243,3 +243,17 @@ Unless using :pep:`523`, you will not need this. Return the currently executing line number, or -1 if there is no line number. .. versionadded:: 3.12 + + +.. c:function:: PyObject* PyUnstable_InterpreterFrame_GetLocal(struct _PyInterpreterFrame *frame, Py_ssize_t index) + + Return a new :term:`strong reference` to the local variable at *index* in the + frame's localsplus array, with cell and free variables unboxed to their + contents. Free variables are resolved from the function closure, so this + also works on a frame that has not started executing. + + *index* must be in range ``[0, co_nlocalsplus)``. Return ``NULL`` with an + :exc:`IndexError` set if it is out of range, or ``NULL`` without an exception + set if the slot is unset or hidden. + + .. versionadded:: 3.16 diff --git a/Doc/whatsnew/3.16.rst b/Doc/whatsnew/3.16.rst index 063755e1eadcb53..5d645be8470b103 100644 --- a/Doc/whatsnew/3.16.rst +++ b/Doc/whatsnew/3.16.rst @@ -896,7 +896,9 @@ C API changes New features ------------ -* TODO +* Add :c:func:`PyUnstable_InterpreterFrame_GetLocal` to read a local variable + of an internal interpreter frame by its localsplus index. + (Contributed by Guilherme Leobas in :gh:`156133`.) Porting to Python 3.16 ---------------------- diff --git a/Include/cpython/pyframe.h b/Include/cpython/pyframe.h index 24a947de1ede1e1..a8d187862213165 100644 --- a/Include/cpython/pyframe.h +++ b/Include/cpython/pyframe.h @@ -35,3 +35,8 @@ PyAPI_FUNC(int) PyUnstable_InterpreterFrame_GetLasti(struct _PyInterpreterFrame /* Returns the currently executing line number, or -1 if there is no line number. * Does not raise an exception. */ PyAPI_FUNC(int) PyUnstable_InterpreterFrame_GetLine(struct _PyInterpreterFrame *frame); + +/* Returns a new (strong) reference to the local variable at `index` in the + * frame's localsplus array. */ +PyAPI_FUNC(PyObject *) PyUnstable_InterpreterFrame_GetLocal( + struct _PyInterpreterFrame *frame, Py_ssize_t index); diff --git a/Lib/test/test_capi/test_misc.py b/Lib/test/test_capi/test_misc.py index 7d668843d07debc..691a6c25125b2ff 100644 --- a/Lib/test/test_capi/test_misc.py +++ b/Lib/test/test_capi/test_misc.py @@ -2800,6 +2800,42 @@ def test_line(self): firstline = self.func.__code__.co_firstlineno self.assertEqual(line, firstline + 2) + # get_frame_locals() returns the caller frame's locals as a name -> value + # dict via PyUnstable_InterpreterFrame_GetLocal (one strong reference per + # localsplus index). + def helper_plain(self, a, b): + c = a + b + return _testinternalcapi.get_frame_locals() + + def test_get_local_plain(self): + d = self.helper_plain(3, 4) + self.assertEqual(d['a'], 3) + self.assertEqual(d['b'], 4) + self.assertEqual(d['c'], 7) + self.assertIs(d['self'], self) + + def test_get_local_cell(self): + # y is a cell variable of this frame because inner closes over it. + y = 100 + + def inner(): + return y + + d = _testinternalcapi.get_frame_locals() + self.assertEqual(d['y'], 100) + self.assertIs(d['inner'], inner) + + def test_get_local_free(self): + # z is a free variable of inner, read from the closure. + z = 7 + + def inner(): + _ = z + return _testinternalcapi.get_frame_locals() + + d = inner() + self.assertEqual(d['z'], 7) + SUFFICIENT_TO_DEOPT_AND_SPECIALIZE = 100 diff --git a/Misc/NEWS.d/next/C_API/2026-08-20-12-00-00.gh-issue-156133.GetLoc.rst b/Misc/NEWS.d/next/C_API/2026-08-20-12-00-00.gh-issue-156133.GetLoc.rst new file mode 100644 index 000000000000000..4a02830d97ecb06 --- /dev/null +++ b/Misc/NEWS.d/next/C_API/2026-08-20-12-00-00.gh-issue-156133.GetLoc.rst @@ -0,0 +1,2 @@ +Add :c:func:`PyUnstable_InterpreterFrame_GetLocal` to read a local variable of +an internal interpreter frame by its localsplus index. diff --git a/Modules/_testinternalcapi.c b/Modules/_testinternalcapi.c index e9950bb232431c6..2127f2b7fd41368 100644 --- a/Modules/_testinternalcapi.c +++ b/Modules/_testinternalcapi.c @@ -1514,6 +1514,44 @@ iframe_getlasti(PyObject *self, PyObject *frame) return PyLong_FromLong(PyUnstable_InterpreterFrame_GetLasti(f)); } +// Reads the locals of the Python frame that called this C function using +// PyUnstable_InterpreterFrame_GetLocal and returns them as a name -> value +// dict, skipping NULL (unset or hidden) slots. +static PyObject * +get_frame_locals(PyObject *self, PyObject *Py_UNUSED(ignored)) +{ + PyThreadState *tstate = _PyThreadState_GET(); + _PyInterpreterFrame *frame = _PyThreadState_GetFrame(tstate); + if (frame == NULL) { + PyErr_SetString(PyExc_RuntimeError, "no caller frame"); + return NULL; + } + PyCodeObject *co = _PyFrame_GetCode(frame); + Py_ssize_t n = co->co_nlocalsplus; + PyObject *dict = PyDict_New(); + if (dict == NULL) { + return NULL; + } + for (Py_ssize_t i = 0; i < n; i++) { + PyObject *value = PyUnstable_InterpreterFrame_GetLocal(frame, i); + if (value == NULL) { + if (PyErr_Occurred()) { + Py_DECREF(dict); + return NULL; + } + continue; // unset or hidden slot + } + PyObject *name = PyTuple_GET_ITEM(co->co_localsplusnames, i); + int err = PyDict_SetItem(dict, name, value); + Py_DECREF(value); + if (err < 0) { + Py_DECREF(dict); + return NULL; + } + } + return dict; +} + static PyObject * code_returns_only_none(PyObject *self, PyObject *arg) { @@ -3305,6 +3343,7 @@ static PyMethodDef module_functions[] = { {"iframe_getcode", iframe_getcode, METH_O, NULL}, {"iframe_getline", iframe_getline, METH_O, NULL}, {"iframe_getlasti", iframe_getlasti, METH_O, NULL}, + {"get_frame_locals", get_frame_locals, METH_NOARGS, NULL}, {"code_returns_only_none", code_returns_only_none, METH_O, NULL}, {"get_co_framesize", get_co_framesize, METH_O, NULL}, {"get_co_localskinds", get_co_localskinds, METH_O, NULL}, diff --git a/Objects/frameobject.c b/Objects/frameobject.c index 5889cdaf2aa1652..80306e13e94a5d4 100644 --- a/Objects/frameobject.c +++ b/Objects/frameobject.c @@ -2247,6 +2247,43 @@ frame_get_var(_PyInterpreterFrame *frame, PyCodeObject *co, int i, } +PyObject * +PyUnstable_InterpreterFrame_GetLocal(_PyInterpreterFrame *frame, + Py_ssize_t index) +{ + PyCodeObject *co = _PyFrame_GetCode(frame); + if (index < 0 || index >= co->co_nlocalsplus) { + PyErr_Format( + PyExc_IndexError, + "PyUnstable_InterpreterFrame_GetLocal: index %zd out of range [0, %d)", + index, co->co_nlocalsplus); + return NULL; + } + + int offset = PyUnstable_Code_GetFirstFree(co); // co_nlocalsplus - co_nfreevars + if (index < offset) { + // Local or cell variable. frame_get_var unboxes cells and copes with + // not-yet-started frames and arguments not yet promoted by MAKE_CELL. + if (_PyLocals_GetKind(co->co_localspluskinds, (int)index) & CO_FAST_HIDDEN) { + return NULL; + } + PyObject *value = NULL; + frame_get_var(frame, co, (int)index, &value); + return value; // strong reference, or NULL if unset + } + + // Free variable: read from the function closure rather than localsplus. + if ((co->co_flags & CO_OPTIMIZED) + && PyStackRef_FunctionCheck(frame->f_funcobj)) + { + PyFunctionObject *func = _PyFrame_GetFunction(frame); + PyObject *cell = PyTuple_GET_ITEM(func->func_closure, index - offset); + return PyCell_GetRef((PyCellObject *)cell); + } + return NULL; +} + + bool _PyFrame_HasHiddenLocals(_PyInterpreterFrame *frame) {