diff --git a/Doc/c-api/code.rst b/Doc/c-api/code.rst index 57b77f92a7d2e6a..055de030dc36fc1 100644 --- a/Doc/c-api/code.rst +++ b/Doc/c-api/code.rst @@ -44,6 +44,15 @@ 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 new :term:`strong reference` to the tuple of names of the local, + cell and free variables of a code object. The tuple is indexed like the + *localsplus* array of a frame, so it can be used together with + :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..b4136c0b101a5c2 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:: int PyUnstable_InterpreterFrame_GetLocal(struct _PyInterpreterFrame *frame, Py_ssize_t index, PyObject **result) + + Retrieve 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. + + * 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 29bd7dcd749ebc2..df9bbcc9b55e96c 100644 --- a/Doc/whatsnew/3.16.rst +++ b/Doc/whatsnew/3.16.rst @@ -1177,6 +1177,12 @@ New features * Add :c:func:`Py_HashBuffer` to the limited C API. (Contributed by Victor Stinner in :gh:`148233`.) +* 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 1be47e42ed62bfb..5ed4ad0f6f83fcd 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 9a5a1d938e2869f..4547a654727e2ae 100644 --- a/Lib/test/test_capi/test_misc.py +++ b/Lib/test/test_capi/test_misc.py @@ -2800,6 +2800,92 @@ 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) + + def test_get_local_index_out_of_range(self): + def f(index): + return _testinternalcapi.get_frame_local(index) + + # f has no cell or free variables, so co_nlocalsplus == co_nlocals. + code = f.__code__ + self.assertFalse(code.co_cellvars or code.co_freevars) + nlocalsplus = code.co_nlocals + for index in (-1, nlocalsplus, nlocalsplus + 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 = f.__code__.co_varnames + return _testinternalcapi.get_frame_local(names.index('unset')) + + self.assertEqual(f(), (0, None)) + + def test_get_local_set(self): + x = 5 + index = self.test_get_local_set.__code__.co_varnames.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) + # Arguments and locals come first, followed by cells that are not + # arguments; here a and d are cells. + self.assertEqual(sorted(names), sorted(['a', 'b', 'c', 'd', 'inner'])) + self.assertEqual(names[:2], ('a', 'b')) + + inner = outer(1, 2) + names = _testinternalcapi.code_get_localsplus_names(inner.__code__) + self.assertEqual(names, ('a', 'd')) + self.assertEqual(names[inner.__code__.co_nlocals:], ('a', 'd')) + + with self.assertRaises(TypeError): + _testinternalcapi.code_get_localsplus_names(None) + 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 a2ce266eb35a655..bdb7fb4ab99a115 100644 --- a/Modules/_testinternalcapi.c +++ b/Modules/_testinternalcapi.c @@ -1535,6 +1535,87 @@ 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; + } + 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); + return NULL; + } + if (rc == 0) { + 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); + if (frame == NULL) { + PyErr_SetString(PyExc_RuntimeError, "no caller frame"); + return NULL; + } + 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) +{ + if (!PyCode_Check(arg)) { + PyErr_SetString(PyExc_TypeError, "argument must be a code object"); + return NULL; + } + return PyUnstable_Code_GetLocalPlusNames((PyCodeObject *)arg); +} + static PyObject * code_returns_only_none(PyObject *self, PyObject *arg) { @@ -3349,6 +3430,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 a4cc14a6eaad457..d9d0759530ef892 100644 --- a/Objects/frameobject.c +++ b/Objects/frameobject.c @@ -2326,6 +2326,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) {