Skip to content

Commit

Permalink
gh-91248: Add PyFrame_GetVar() function (#95712)
Browse files Browse the repository at this point in the history
Add PyFrame_GetVar() and PyFrame_GetVarString() functions to get a
frame variable by its name.

Move PyFrameObject C API tests from test_capi to test_frame.
  • Loading branch information
vstinner committed Nov 8, 2022
1 parent acf4d5d commit 4d5fcca
Show file tree
Hide file tree
Showing 8 changed files with 126 additions and 22 deletions.
19 changes: 19 additions & 0 deletions Doc/c-api/frame.rst
Expand Up @@ -79,6 +79,25 @@ See also :ref:`Reflection <reflection>`.
.. versionadded:: 3.11
.. c:function:: PyObject* PyFrame_GetVar(PyFrameObject *frame, PyObject *name)
Get the variable *name* of *frame*.
* Return a :term:`strong reference` to the variable value on success.
* Raise :exc:`NameError` and return ``NULL`` if the variable does not exist.
* Raise an exception and return ``NULL`` on error.
.. versionadded:: 3.12
.. c:function:: PyObject* PyFrame_GetVarString(PyFrameObject *frame, const char *name)
Similar to :c:func:`PyFrame_GetVar`, but the variable name is a C string
encoded in UTF-8.
.. versionadded:: 3.12
.. c:function:: PyObject* PyFrame_GetLocals(PyFrameObject *frame)
Get the *frame*'s ``f_locals`` attribute (:class:`dict`).
Expand Down
4 changes: 4 additions & 0 deletions Doc/whatsnew/3.12.rst
Expand Up @@ -735,6 +735,10 @@ New Features
(Contributed by Carl Meyer in :gh:`91051`.)


* Add :c:func:`PyFrame_GetVar` and :c:func:`PyFrame_GetVarString` functions to
get a frame variable by its name.
(Contributed by Victor Stinner in :gh:`91248`.)

Porting to Python 3.12
----------------------

Expand Down
3 changes: 2 additions & 1 deletion Include/cpython/pyframe.h
Expand Up @@ -14,4 +14,5 @@ PyAPI_FUNC(PyObject *) PyFrame_GetBuiltins(PyFrameObject *frame);

PyAPI_FUNC(PyObject *) PyFrame_GetGenerator(PyFrameObject *frame);
PyAPI_FUNC(int) PyFrame_GetLasti(PyFrameObject *frame);

PyAPI_FUNC(PyObject*) PyFrame_GetVar(PyFrameObject *frame, PyObject *name);
PyAPI_FUNC(PyObject*) PyFrame_GetVarString(PyFrameObject *frame, const char *name);
21 changes: 0 additions & 21 deletions Lib/test/test_capi.py
Expand Up @@ -1677,27 +1677,6 @@ class Subclass(BaseException, self.module.StateAccessType):
self.assertIs(Subclass().get_defining_module(), self.module)


class Test_FrameAPI(unittest.TestCase):

def getframe(self):
return sys._getframe()

def getgenframe(self):
yield sys._getframe()

def test_frame_getters(self):
frame = self.getframe()
self.assertEqual(frame.f_locals, _testcapi.frame_getlocals(frame))
self.assertIs(frame.f_globals, _testcapi.frame_getglobals(frame))
self.assertIs(frame.f_builtins, _testcapi.frame_getbuiltins(frame))
self.assertEqual(frame.f_lasti, _testcapi.frame_getlasti(frame))

def test_frame_get_generator(self):
gen = self.getgenframe()
frame = next(gen)
self.assertIs(gen, _testcapi.frame_getgenerator(frame))


SUFFICIENT_TO_DEOPT_AND_SPECIALIZE = 100

class Test_Pep523API(unittest.TestCase):
Expand Down
35 changes: 35 additions & 0 deletions Lib/test/test_frame.py
Expand Up @@ -5,6 +5,10 @@
import types
import unittest
import weakref
try:
import _testcapi
except ImportError:
_testcapi = None

from test import support
from test.support.script_helper import assert_python_ok
Expand Down Expand Up @@ -326,5 +330,36 @@ def f():
gc.enable()


@unittest.skipIf(_testcapi is None, 'need _testcapi')
class TestCAPI(unittest.TestCase):
def getframe(self):
return sys._getframe()

def test_frame_getters(self):
frame = self.getframe()
self.assertEqual(frame.f_locals, _testcapi.frame_getlocals(frame))
self.assertIs(frame.f_globals, _testcapi.frame_getglobals(frame))
self.assertIs(frame.f_builtins, _testcapi.frame_getbuiltins(frame))
self.assertEqual(frame.f_lasti, _testcapi.frame_getlasti(frame))

def test_getvar(self):
current_frame = sys._getframe()
x = 1
self.assertEqual(_testcapi.frame_getvar(current_frame, "x"), 1)
self.assertEqual(_testcapi.frame_getvarstring(current_frame, b"x"), 1)
with self.assertRaises(NameError):
_testcapi.frame_getvar(current_frame, "y")
with self.assertRaises(NameError):
_testcapi.frame_getvarstring(current_frame, b"y")

def getgenframe(self):
yield sys._getframe()

def test_frame_get_generator(self):
gen = self.getgenframe()
frame = next(gen)
self.assertIs(gen, _testcapi.frame_getgenerator(frame))


if __name__ == "__main__":
unittest.main()
@@ -0,0 +1,2 @@
Add :c:func:`PyFrame_GetVar` and :c:func:`PyFrame_GetVarString` functions to
get a frame variable by its name. Patch by Victor Stinner.
34 changes: 34 additions & 0 deletions Modules/_testcapimodule.c
Expand Up @@ -5607,6 +5607,38 @@ frame_getlasti(PyObject *self, PyObject *frame)
return PyLong_FromLong(lasti);
}

static PyObject *
test_frame_getvar(PyObject *self, PyObject *args)
{
PyObject *frame, *name;
if (!PyArg_ParseTuple(args, "OO", &frame, &name)) {
return NULL;
}
if (!PyFrame_Check(frame)) {
PyErr_SetString(PyExc_TypeError, "argument must be a frame");
return NULL;
}

return PyFrame_GetVar((PyFrameObject *)frame, name);
}

static PyObject *
test_frame_getvarstring(PyObject *self, PyObject *args)
{
PyObject *frame;
const char *name;
if (!PyArg_ParseTuple(args, "Oy", &frame, &name)) {
return NULL;
}
if (!PyFrame_Check(frame)) {
PyErr_SetString(PyExc_TypeError, "argument must be a frame");
return NULL;
}

return PyFrame_GetVarString((PyFrameObject *)frame, name);
}


static PyObject *
eval_get_func_name(PyObject *self, PyObject *func)
{
Expand Down Expand Up @@ -6294,6 +6326,8 @@ static PyMethodDef TestMethods[] = {
{"frame_getgenerator", frame_getgenerator, METH_O, NULL},
{"frame_getbuiltins", frame_getbuiltins, METH_O, NULL},
{"frame_getlasti", frame_getlasti, METH_O, NULL},
{"frame_getvar", test_frame_getvar, METH_VARARGS, NULL},
{"frame_getvarstring", test_frame_getvarstring, METH_VARARGS, NULL},
{"eval_get_func_name", eval_get_func_name, METH_O, NULL},
{"eval_get_func_desc", eval_get_func_desc, METH_O, NULL},
{"get_feature_macros", get_feature_macros, METH_NOARGS, NULL},
Expand Down
30 changes: 30 additions & 0 deletions Objects/frameobject.c
Expand Up @@ -1430,4 +1430,34 @@ _PyEval_BuiltinsFromGlobals(PyThreadState *tstate, PyObject *globals)
return _PyEval_GetBuiltins(tstate);
}

PyObject *
PyFrame_GetVar(PyFrameObject *frame, PyObject *name)
{
PyObject *locals = PyFrame_GetLocals(frame);
if (locals == NULL) {
return NULL;
}
PyObject *value = PyDict_GetItemWithError(locals, name);
Py_DECREF(locals);

if (value == NULL) {
if (PyErr_Occurred()) {
return NULL;
}
PyErr_Format(PyExc_NameError, "variable %R does not exist", name);
return NULL;
}
return Py_NewRef(value);
}

PyObject *
PyFrame_GetVarString(PyFrameObject *frame, const char *name)
{
PyObject *name_obj = PyUnicode_FromString(name);
if (name_obj == NULL) {
return NULL;
}
PyObject *value = PyFrame_GetVar(frame, name_obj);
Py_DECREF(name_obj);
return value;
}

0 comments on commit 4d5fcca

Please sign in to comment.