diff --git a/Doc/c-api/code.rst b/Doc/c-api/code.rst index 96104017da59754..0cada3a14675dd0 100644 --- a/Doc/c-api/code.rst +++ b/Doc/c-api/code.rst @@ -44,6 +44,14 @@ bound into a function. The old name is deprecated, but will remain available until the signature changes again. +.. c:function:: PyObject* PyUnstable_Code_GetLocalPlusNames(PyCodeObject *co) + + Return a :term:`strong reference` to a tuple of names of the local, cell and + free variables of a code object. The names are in the same order as the + values returned by :c:func:`PyUnstable_InterpreterFrame_GetLocal`. + + .. versionadded:: next + .. c:function:: PyCodeObject* PyUnstable_Code_New(int argcount, int kwonlyargcount, int nlocals, int stacksize, int flags, PyObject *code, PyObject *consts, PyObject *names, PyObject *varnames, PyObject *freevars, PyObject *cellvars, PyObject *filename, PyObject *name, PyObject *qualname, int firstlineno, PyObject *linetable, PyObject *exceptiontable) Return a new code object. If you need a dummy code object to create a frame, diff --git a/Doc/c-api/frame.rst b/Doc/c-api/frame.rst index 4159ff6e5965fbd..7967e586e65d3ce 100644 --- a/Doc/c-api/frame.rst +++ b/Doc/c-api/frame.rst @@ -243,3 +243,21 @@ 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:: int PyUnstable_InterpreterFrame_GetLocal(struct _PyInterpreterFrame *frame, Py_ssize_t index, PyObject **result) + + Retrieve the local variable at *index*. + To determine the index of a particular variable, use + :c:func:`PyUnstable_InterpreterFrame_GetCode` and + :c:func:`PyUnstable_Code_GetLocalPlusNames`. + + Cell and free variables are unboxed to their contents. Free variables are + resolved from the function closure, so this also works on a frame that has + not started executing. + + * On success, store a new :term:`strong reference` in *result* and return ``1``. + * If the slot is unset or hidden, store ``NULL`` in *result* and return ``0``. + * On error, raise an exception, store ``NULL`` in *result* and return ``-1``. + + .. versionadded:: next diff --git a/Doc/whatsnew/3.16.rst b/Doc/whatsnew/3.16.rst index e342aa71c7924ea..069cda039abe302 100644 --- a/Doc/whatsnew/3.16.rst +++ b/Doc/whatsnew/3.16.rst @@ -1354,6 +1354,12 @@ New features API. (Contributed by Nathan Goldbaum and Victor Stinner in :gh:`158842`.) +* Add :c:func:`PyUnstable_InterpreterFrame_GetLocal` to read a local variable + of an internal interpreter frame by its localsplus index, and + :c:func:`PyUnstable_Code_GetLocalPlusNames` to get the matching variable + names. + (Contributed by Guilherme Leobas in :gh:`156133`.) + Porting to Python 3.16 ---------------------- diff --git a/Include/cpython/code.h b/Include/cpython/code.h index 5238447070ee548..5ab7e6586ec8a88 100644 --- a/Include/cpython/code.h +++ b/Include/cpython/code.h @@ -177,6 +177,11 @@ static inline int PyUnstable_Code_GetFirstFree(PyCodeObject *op) { return op->co_nlocalsplus - op->co_nfreevars; } +static inline PyObject *PyUnstable_Code_GetLocalPlusNames(PyCodeObject *op) { + assert(PyCode_Check(op)); + return Py_NewRef(op->co_localsplusnames); +} + Py_DEPRECATED(3.13) static inline int PyCode_GetFirstFree(PyCodeObject *op) { return PyUnstable_Code_GetFirstFree(op); } diff --git a/Include/cpython/pyframe.h b/Include/cpython/pyframe.h index 24a947de1ede1e1..88f074a9ac8d86c 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(int) PyUnstable_InterpreterFrame_GetLocal( + struct _PyInterpreterFrame *frame, Py_ssize_t index, PyObject **result); diff --git a/Lib/test/test_capi/test_misc.py b/Lib/test/test_capi/test_misc.py index b9c6882444ab767..b8f78144a380daf 100644 --- a/Lib/test/test_capi/test_misc.py +++ b/Lib/test/test_capi/test_misc.py @@ -2913,6 +2913,80 @@ 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, 'b': 4, 'c': 7, '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) + + def test_get_local_index_out_of_range(self): + def f(index): + return _testinternalcapi.get_frame_local(index) + + n = len(_testinternalcapi.code_get_localsplus_names(f.__code__)) + for index in (-1, n, n + 1): + with self.subTest(index=index): + with self.assertRaises(IndexError): + f(index) + + def test_get_local_unset(self): + def f(): + if False: + unset = 1 + names = _testinternalcapi.code_get_localsplus_names(f.__code__) + return _testinternalcapi.get_frame_local(names.index('unset')) + + self.assertEqual(f(), (0, None)) + + def test_get_local_set(self): + x = 5 + code = self.test_get_local_set.__code__ + names = _testinternalcapi.code_get_localsplus_names(code) + index = names.index('x') + self.assertEqual(_testinternalcapi.get_frame_local(index), (1, 5)) + + def test_code_get_localsplus_names(self): + def outer(a, b): + c = a + def inner(): + return a, d + d = b + return inner + + names = _testinternalcapi.code_get_localsplus_names(outer.__code__) + self.assertIsInstance(names, tuple) + self.assertCountEqual(names, ('a', 'b', 'c', 'd', 'inner')) + inner = outer(1, 2) + names = _testinternalcapi.code_get_localsplus_names(inner.__code__) + self.assertCountEqual(names, ('a', 'd')) + 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..be780cba1214d30 --- /dev/null +++ b/Misc/NEWS.d/next/C_API/2026-08-20-12-00-00.gh-issue-156133.GetLoc.rst @@ -0,0 +1,3 @@ +Add :c:func:`PyUnstable_InterpreterFrame_GetLocal` to read a local variable of +an internal interpreter frame by its localsplus index, and +:c:func:`PyUnstable_Code_GetLocalPlusNames` to get the matching variable names. diff --git a/Modules/_testinternalcapi.c b/Modules/_testinternalcapi.c index 16be593763bf667..4453cfcb013abed 100644 --- a/Modules/_testinternalcapi.c +++ b/Modules/_testinternalcapi.c @@ -1564,6 +1564,80 @@ 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); + assert(frame != NULL); // there must be a caller frame + PyCodeObject *co = _PyFrame_GetCode(frame); + Py_ssize_t n = co->co_nlocalsplus; + PyObject *dict = PyDict_New(); + if (dict == NULL) { + return NULL; + } + PyObject *names = PyUnstable_Code_GetLocalPlusNames(co); + for (Py_ssize_t i = 0; i < n; i++) { + PyObject *value; + int rc = PyUnstable_InterpreterFrame_GetLocal(frame, i, &value); + if (rc < 0) { + Py_DECREF(names); + Py_DECREF(dict); + assert(value == NULL); + return NULL; + } + if (rc == 0) { + assert(value == NULL); + continue; // unset or hidden slot + } + PyObject *name = PyTuple_GET_ITEM(names, i); + int err = PyDict_SetItem(dict, name, value); + Py_DECREF(value); + if (err < 0) { + Py_DECREF(names); + Py_DECREF(dict); + return NULL; + } + } + Py_DECREF(names); + return dict; +} + +// Calls PyUnstable_InterpreterFrame_GetLocal on the caller's frame for a single +// index and returns (rc, value), or raises if rc is -1. +static PyObject * +get_frame_local(PyObject *self, PyObject *arg) +{ + Py_ssize_t index = PyLong_AsSsize_t(arg); + if (index == -1 && PyErr_Occurred()) { + return NULL; + } + PyThreadState *tstate = _PyThreadState_GET(); + _PyInterpreterFrame *frame = _PyThreadState_GetFrame(tstate); + assert(frame != NULL); // there must be a caller frame + PyObject *value; + int rc = PyUnstable_InterpreterFrame_GetLocal(frame, index, &value); + if (rc < 0) { + assert(value == NULL); + return NULL; + } + if (rc == 0) { + assert(value == NULL); + return Py_BuildValue("iO", rc, Py_None); + } + return Py_BuildValue("iN", rc, value); +} + +static PyObject * +code_get_localsplus_names(PyObject *self, PyObject *arg) +{ + assert(PyCode_Check(arg)); + return PyUnstable_Code_GetLocalPlusNames((PyCodeObject *)arg); +} + static PyObject * code_returns_only_none(PyObject *self, PyObject *arg) { @@ -3381,6 +3455,9 @@ 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}, + {"get_frame_local", get_frame_local, METH_O, NULL}, + {"code_get_localsplus_names", code_get_localsplus_names, METH_O, 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 61be2beca22d15f..475be76dc3da21b 100644 --- a/Objects/frameobject.c +++ b/Objects/frameobject.c @@ -2327,6 +2327,53 @@ frame_get_var(_PyInterpreterFrame *frame, PyCodeObject *co, int i, } +int +PyUnstable_InterpreterFrame_GetLocal(_PyInterpreterFrame *frame, + Py_ssize_t index, PyObject **result) +{ + PyCodeObject *co = _PyFrame_GetCode(frame); + *result = NULL; + 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 -1; + } + + 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 0; + } + PyObject *value = NULL; + frame_get_var(frame, co, (int)index, &value); + if (value == NULL) { + return 0; + } + *result = value; // strong reference + return 1; + } + + // 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); + PyObject *value = PyCell_GetRef((PyCellObject *)cell); + if (value == NULL) { + return 0; + } + *result = value; + return 1; + } + return 0; +} + + bool _PyFrame_HasHiddenLocals(_PyInterpreterFrame *frame) {