diff --git a/Doc/c-api/frame.rst b/Doc/c-api/frame.rst index 46ce700abf1..4a062dd8623 100644 --- a/Doc/c-api/frame.rst +++ b/Doc/c-api/frame.rst @@ -79,6 +79,25 @@ See also :ref:`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`). diff --git a/Doc/whatsnew/3.12.rst b/Doc/whatsnew/3.12.rst index cf71aab1d8a..7ebfc5537c7 100644 --- a/Doc/whatsnew/3.12.rst +++ b/Doc/whatsnew/3.12.rst @@ -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 ---------------------- diff --git a/Include/cpython/pyframe.h b/Include/cpython/pyframe.h index 1dc634ccee9..6ec292718af 100644 --- a/Include/cpython/pyframe.h +++ b/Include/cpython/pyframe.h @@ -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); diff --git a/Lib/test/test_capi.py b/Lib/test/test_capi.py index 213b6d4feb6..ea4c9de47d7 100644 --- a/Lib/test/test_capi.py +++ b/Lib/test/test_capi.py @@ -1677,27 +1677,6 @@ class Test_ModuleStateAccess(unittest.TestCase): 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): diff --git a/Lib/test/test_frame.py b/Lib/test/test_frame.py index 4b5bb7f94ac..ada96668392 100644 --- a/Lib/test/test_frame.py +++ b/Lib/test/test_frame.py @@ -5,6 +5,10 @@ import textwrap 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 @@ -326,5 +330,36 @@ class TestIncompleteFrameAreInvisible(unittest.TestCase): 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() diff --git a/Misc/NEWS.d/next/C API/2022-08-05-15-26-12.gh-issue-91248.ujirJJ.rst b/Misc/NEWS.d/next/C API/2022-08-05-15-26-12.gh-issue-91248.ujirJJ.rst new file mode 100644 index 00000000000..6521f57b8fe --- /dev/null +++ b/Misc/NEWS.d/next/C API/2022-08-05-15-26-12.gh-issue-91248.ujirJJ.rst @@ -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. diff --git a/Modules/_testcapimodule.c b/Modules/_testcapimodule.c index 1624a93ec3f..0615c7352ca 100644 --- a/Modules/_testcapimodule.c +++ b/Modules/_testcapimodule.c @@ -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) { @@ -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}, diff --git a/Objects/frameobject.c b/Objects/frameobject.c index 4b4be382d94..6337501cfca 100644 --- a/Objects/frameobject.c +++ b/Objects/frameobject.c @@ -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; +}