Skip to content
9 changes: 9 additions & 0 deletions Doc/c-api/code.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
14 changes: 14 additions & 0 deletions Doc/c-api/frame.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Comment on lines +250 to +254

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The localplus array is undocumented. Should we add PyUnstable_Code_GetLocalPlusNames as well?

* 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
6 changes: 6 additions & 0 deletions Doc/whatsnew/3.16.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
----------------------

Expand Down
5 changes: 5 additions & 0 deletions Include/cpython/code.h
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand Down
5 changes: 5 additions & 0 deletions Include/cpython/pyframe.h
Original file line number Diff line number Diff line change
Expand Up @@ -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);
86 changes: 86 additions & 0 deletions Lib/test/test_capi/test_misc.py

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you also test errors with -1 and co->co_nlocalsplus indices, and an unset variable?

Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
Original file line number Diff line number Diff line change
@@ -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.
84 changes: 84 additions & 0 deletions Modules/_testinternalcapi.c
Original file line number Diff line number Diff line change
Expand Up @@ -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)
{
Expand Down Expand Up @@ -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},
Expand Down
47 changes: 47 additions & 0 deletions Objects/frameobject.c
Original file line number Diff line number Diff line change
Expand Up @@ -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)
{
Expand Down
Loading