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:: 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
4 changes: 3 additions & 1 deletion Doc/whatsnew/3.16.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
----------------------
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(PyObject *) PyUnstable_InterpreterFrame_GetLocal(
struct _PyInterpreterFrame *frame, Py_ssize_t index);
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.
39 changes: 39 additions & 0 deletions Modules/_testinternalcapi.c
Original file line number Diff line number Diff line change
Expand Up @@ -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_GetLocals and returns them as a name -> value

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.

Suggested change
// PyUnstable_InterpreterFrame_GetLocals and returns them as a name -> value
// 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)
{
Expand Down Expand Up @@ -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},
Expand Down
36 changes: 36 additions & 0 deletions Objects/frameobject.c
Original file line number Diff line number Diff line change
Expand Up @@ -2247,6 +2247,42 @@ 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)) {

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.

Suggested change
&& PyStackRef_FunctionCheck(frame->f_funcobj)) {
&& PyStackRef_FunctionCheck(frame->f_funcobj))
{

See PEP7:

When you break a long expression at a binary operator, braces should be formatted as shown:

PyFunctionObject *func = _PyFrame_GetFunction(frame);
PyObject *cell = PyTuple_GET_ITEM(func->func_closure, index - offset);
return Py_XNewRef(PyCell_GET(cell));

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.

This may have concurrency issue under free-thread build, we can use PyCell_GetRef here instead, see it's defination:

// Gets the cell contents. Returns a new reference.
static inline PyObject *
PyCell_GetRef(PyCellObject *cell)
{
PyObject *res;
Py_BEGIN_CRITICAL_SECTION(cell);
#ifdef Py_GIL_DISABLED
res = _Py_XNewRefWithLock(cell->ob_ref);
#else
res = Py_XNewRef(cell->ob_ref);
#endif
Py_END_CRITICAL_SECTION();
return res;
}

Suggested change
return Py_XNewRef(PyCell_GET(cell));
return PyCell_GetRef((PyCellObject *)cell);

}
return NULL;
}


bool
_PyFrame_HasHiddenLocals(_PyInterpreterFrame *frame)
{
Expand Down
Loading