Repository navigation
gh-156133: Add PyUnstable_InterpreterFrame_GetLocal
#156134
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
608356e
d29acd6
892795b
f7f33ad
5075201
442d8cd
cd8b975
a271e36
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -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. | ||||||||||||||||||||||||||
|
Comment on lines
+250
to
+253
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
|
encukou marked this conversation as resolved.
|
||||||||||||||||||||||||||
| * 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 | ||||||||||||||||||||||||||
|
encukou marked this conversation as resolved.
|
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -2800,6 +2800,92 @@ 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) | ||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+2812
to
+2815
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We can test the whole dict at once.
Suggested change
|
||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| 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) | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| # f has no cell or free variables, so co_nlocalsplus == co_nlocals. | ||||||||||||||||||||||||||||||||||||||
| code = f.__code__ | ||||||||||||||||||||||||||||||||||||||
| self.assertFalse(code.co_cellvars or code.co_freevars) | ||||||||||||||||||||||||||||||||||||||
| nlocalsplus = code.co_nlocals | ||||||||||||||||||||||||||||||||||||||
| for index in (-1, nlocalsplus, nlocalsplus + 1): | ||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+2843
to
+2847
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This can be simpler now.
Suggested change
|
||||||||||||||||||||||||||||||||||||||
| with self.subTest(index=index): | ||||||||||||||||||||||||||||||||||||||
| with self.assertRaises(IndexError): | ||||||||||||||||||||||||||||||||||||||
| f(index) | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| def test_get_local_unset(self): | ||||||||||||||||||||||||||||||||||||||
| def f(): | ||||||||||||||||||||||||||||||||||||||
| if False: | ||||||||||||||||||||||||||||||||||||||
| unset = 1 | ||||||||||||||||||||||||||||||||||||||
| names = f.__code__.co_varnames | ||||||||||||||||||||||||||||||||||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'd prefer using the matching API
Suggested change
|
||||||||||||||||||||||||||||||||||||||
| return _testinternalcapi.get_frame_local(names.index('unset')) | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| self.assertEqual(f(), (0, None)) | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| def test_get_local_set(self): | ||||||||||||||||||||||||||||||||||||||
| x = 5 | ||||||||||||||||||||||||||||||||||||||
| index = self.test_get_local_set.__code__.co_varnames.index('x') | ||||||||||||||||||||||||||||||||||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Same here:
Suggested change
|
||||||||||||||||||||||||||||||||||||||
| 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) | ||||||||||||||||||||||||||||||||||||||
| # Arguments and locals come first, followed by cells that are not | ||||||||||||||||||||||||||||||||||||||
| # arguments; here a and d are cells. | ||||||||||||||||||||||||||||||||||||||
| self.assertEqual(sorted(names), sorted(['a', 'b', 'c', 'd', 'inner'])) | ||||||||||||||||||||||||||||||||||||||
| self.assertEqual(names[:2], ('a', 'b')) | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| inner = outer(1, 2) | ||||||||||||||||||||||||||||||||||||||
| names = _testinternalcapi.code_get_localsplus_names(inner.__code__) | ||||||||||||||||||||||||||||||||||||||
| self.assertEqual(names, ('a', 'd')) | ||||||||||||||||||||||||||||||||||||||
| self.assertEqual(names[inner.__code__.co_nlocals:], ('a', 'd')) | ||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+2874
to
+2884
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This API doesn't guarantee that arguments/locals are first -- that's a different CPython implementation detail.
Suggested change
|
||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| with self.assertRaises(TypeError): | ||||||||||||||||||||||||||||||||||||||
| _testinternalcapi.code_get_localsplus_names(None) | ||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+2885
to
+2887
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This only exercises the test helper.
Suggested change
|
||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| SUFFICIENT_TO_DEOPT_AND_SPECIALIZE = 100 | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| 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. |
| Original file line number | Diff line number | Diff line change | ||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -1535,6 +1535,87 @@ 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; | ||||||||||||
| } | ||||||||||||
|
Comment on lines
+1546
to
+1549
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Test functions can assert when they're used wrong.
Suggested change
|
||||||||||||
| 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); | ||||||||||||
| return NULL; | ||||||||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||||||
| } | ||||||||||||
| if (rc == 0) { | ||||||||||||
| continue; // unset or hidden slot | ||||||||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||||||
| } | ||||||||||||
| 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); | ||||||||||||
| if (frame == NULL) { | ||||||||||||
| PyErr_SetString(PyExc_RuntimeError, "no caller frame"); | ||||||||||||
| return NULL; | ||||||||||||
| } | ||||||||||||
| 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) | ||||||||||||
| { | ||||||||||||
| if (!PyCode_Check(arg)) { | ||||||||||||
| PyErr_SetString(PyExc_TypeError, "argument must be a code object"); | ||||||||||||
| return NULL; | ||||||||||||
| } | ||||||||||||
|
Comment on lines
+1612
to
+1615
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||||||
| return PyUnstable_Code_GetLocalPlusNames((PyCodeObject *)arg); | ||||||||||||
| } | ||||||||||||
|
|
||||||||||||
| static PyObject * | ||||||||||||
| code_returns_only_none(PyObject *self, PyObject *arg) | ||||||||||||
| { | ||||||||||||
|
|
@@ -3349,6 +3430,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}, | ||||||||||||
|
|
||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.