Repository navigation
gh-156133: Add PyUnstable_InterpreterFrame_GetLocal - #156134
guilhermeleobas wants to merge 8 commits into
Conversation
049e1b3 to
9145c2f
Compare
Documentation build overview
400 files changed ·
|
PyUnstable_InterpreterFrame_GetLocalsPyUnstable_InterpreterFrame_GetLocal
Add an unstable C API that returns a strong reference to a single local variable of an internal interpreter frame, addressed by its localsplus index, with cell and free variables unboxed to their contents. Free variables are resolved from the function closure, so the API also works on a frame that has not started executing (before COPY_FREE_VARS runs) -- the case that motivated it -- and it does not modify the frame. Includes the PEP 689 deliverables: reference documentation in Doc/c-api/frame.rst, a What's New entry for 3.16, a Misc/NEWS.d blurb, and tests in Lib/test/test_capi/test_misc.py (TestInternalFrameApi) covering plain locals, a cell variable, and a free variable. Authored with the assistance of an AI coding agent (Claude Opus)
cbb346a to
d29acd6
Compare
|
Thanks @aisk. I've addressed your comments. |
|
Hi, thank you for the contribution, but please avoid using force push in the future, see: https://devguide.python.org/getting-started/pull-request-lifecycle/#don-t-force-push |
Sorry, it won't happen again. I did because the e-mail used in the commit was wrong and the cla-bot was failing. |
|
@aisk could you take a look at this PR again once you have some cycles to spare? |
| .. versionadded:: 3.12 | ||
|
|
||
|
|
||
| .. c:function:: PyObject* PyUnstable_InterpreterFrame_GetLocal(struct _PyInterpreterFrame *frame, Py_ssize_t index) |
There was a problem hiding this comment.
Ideally we should add an unstable API for the _PyInterpreterFrame struct too, right?
There was a problem hiding this comment.
From the discuss topic, Petr said this can be done in a follow-up PR. I can add it here if you think it is necessary.
There was a problem hiding this comment.
@encukou could you take a look at this PR once you have a chance?
There was a problem hiding this comment.
I will, after the 3.15.0 release.
…emRef` convention
…t_of_range`) and unset variable (`get_get_local_unset`)
encukou
left a comment
There was a problem hiding this comment.
Thank you! I have a bunch of small nitpicks, hopehully the last batch.
|
|
||
| with self.assertRaises(TypeError): | ||
| _testinternalcapi.code_get_localsplus_names(None) |
There was a problem hiding this comment.
This only exercises the test helper.
| with self.assertRaises(TypeError): | |
| _testinternalcapi.code_get_localsplus_names(None) |
| if (!PyCode_Check(arg)) { | ||
| PyErr_SetString(PyExc_TypeError, "argument must be a code object"); | ||
| return NULL; | ||
| } |
There was a problem hiding this comment.
| if (!PyCode_Check(arg)) { | |
| PyErr_SetString(PyExc_TypeError, "argument must be a code object"); | |
| return NULL; | |
| } | |
| assert(PyCode_Check(arg)); |
| self.assertEqual(d['a'], 3) | ||
| self.assertEqual(d['b'], 4) | ||
| self.assertEqual(d['c'], 7) | ||
| self.assertIs(d['self'], self) |
There was a problem hiding this comment.
We can test the whole dict at once.
| self.assertEqual(d['a'], 3) | |
| self.assertEqual(d['b'], 4) | |
| self.assertEqual(d['c'], 7) | |
| self.assertIs(d['self'], self) | |
| self.assertEqual(d, {'a': 3, 'b': 4, 'c': 7, 'self': self}) |
| # 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): |
There was a problem hiding this comment.
This can be simpler now.
| # 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): | |
| n = len(_testinternalcapi.code_get_localsplus_names(f.__code__)) | |
| for index in (-1, n, n + 1): |
| def f(): | ||
| if False: | ||
| unset = 1 | ||
| names = f.__code__.co_varnames |
There was a problem hiding this comment.
I'd prefer using the matching API
| names = f.__code__.co_varnames | |
| names = _testinternalcapi.code_get_localsplus_names(f.__code__) |
| if (frame == NULL) { | ||
| PyErr_SetString(PyExc_RuntimeError, "no caller frame"); | ||
| return NULL; | ||
| } |
There was a problem hiding this comment.
Test functions can assert when they're used wrong.
| if (frame == NULL) { | |
| PyErr_SetString(PyExc_RuntimeError, "no caller frame"); | |
| return NULL; | |
| } | |
| assert(frame != NULL); // there must be a caller frame |
| return NULL; | ||
| } | ||
| if (rc == 0) { | ||
| continue; // unset or hidden slot |
There was a problem hiding this comment.
| continue; // unset or hidden slot | |
| assert(value == NULL); | |
| continue; // unset or hidden slot |
| if (rc < 0) { | ||
| Py_DECREF(names); | ||
| Py_DECREF(dict); | ||
| return NULL; |
There was a problem hiding this comment.
| return NULL; | |
| assert(value == NULL); | |
| return NULL; |
| Return a new :term:`strong reference` to the tuple of names of the local, | ||
| cell and free variables of a code object. The tuple is indexed like the | ||
| *localsplus* array of a frame, so it can be used together with | ||
| :c:func:`PyUnstable_InterpreterFrame_GetLocal`. |
There was a problem hiding this comment.
| Return a new :term:`strong reference` to the tuple of names of the local, | |
| cell and free variables of a code object. The tuple is indexed like the | |
| *localsplus* array of a frame, so it can be used together with | |
| :c:func:`PyUnstable_InterpreterFrame_GetLocal`. | |
| 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`. |
| 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. |
There was a problem hiding this comment.
| 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. | |
| 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. |
Add an unstable C API that reads a frame's local variables into a caller-provided array indexed by localsplus index, with cell and free variables unboxed to their contents. Free variables are resolved from the function closure, so the API also works on a frame that has not started executing (before COPY_FREE_VARS runs), which is the case that motivated it.
Includes the three PEP 689 deliverables:
Modules/_testinternalcapi.cdriven byLib/test/test_capi/test_frame_getlocals.pycovering plain locals, a cell variable, and a free variable).Authored with the assistance of an AI coding agent (Claude Opus)
PyUnstable_InterpreterFrame_GetLocalto read a local var from an interp. frame #156133