Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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.

* 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
4 changes: 3 additions & 1 deletion Doc/whatsnew/3.16.rst
Original file line number Diff line number Diff line change
Expand Up @@ -1077,7 +1077,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
----------------------
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);
36 changes: 36 additions & 0 deletions Lib/test/test_capi/test_misc.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Add :c:func:`PyUnstable_InterpreterFrame_GetLocal` to read a local variable of
an internal interpreter frame by its localsplus index.
40 changes: 40 additions & 0 deletions Modules/_testinternalcapi.c
Original file line number Diff line number Diff line change
Expand Up @@ -1514,6 +1514,45 @@ 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;
int rc = PyUnstable_InterpreterFrame_GetLocal(frame, i, &value);
if (rc < 0) {
Py_DECREF(dict);
return NULL;
}
if (rc == 0) {
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)
{
Expand Down Expand Up @@ -3305,6 +3344,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},
Expand Down
47 changes: 47 additions & 0 deletions Objects/frameobject.c
Original file line number Diff line number Diff line change
Expand Up @@ -2247,6 +2247,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