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
8 changes: 8 additions & 0 deletions Doc/c-api/code.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
18 changes: 18 additions & 0 deletions Doc/c-api/frame.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
6 changes: 6 additions & 0 deletions Doc/whatsnew/3.16.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
----------------------

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);
74 changes: 74 additions & 0 deletions Lib/test/test_capi/test_misc.py
Comment thread
encukou marked this conversation as resolved.
Resolved
Original file line number Diff line number Diff line change
Expand Up @@ -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

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.
77 changes: 77 additions & 0 deletions Modules/_testinternalcapi.c
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Comment thread
guilhermeleobas marked this conversation as resolved.
Resolved
}
if (rc == 0) {
assert(value == NULL);
continue; // unset or hidden slot
Comment thread
guilhermeleobas marked this conversation as resolved.
Resolved
}
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)
{
Expand Down Expand Up @@ -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},
Expand Down
47 changes: 47 additions & 0 deletions Objects/frameobject.c
Original file line number Diff line number Diff line change
Expand Up @@ -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)
{
Expand Down
Loading