Skip to content

gh-105340: include hidden fast-locals in locals() #105715

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

Merged
merged 11 commits into from
Jul 5, 2023
2 changes: 2 additions & 0 deletions Include/cpython/ceval.h
Original file line number Diff line number Diff line change
Expand Up @@ -33,3 +33,5 @@ _PyEval_RequestCodeExtraIndex(freefunc f) {

PyAPI_FUNC(int) _PyEval_SliceIndex(PyObject *, Py_ssize_t *);
PyAPI_FUNC(int) _PyEval_SliceIndexNotNone(PyObject *, Py_ssize_t *);

PyAPI_FUNC(PyObject *) PyEval_GetFrameLocals(void);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

At least in 3.12, should we name this with a leading underscore and keep it in the internal API? It's a bit late to add a public API function, and it doesn't seem necessary to fix the bug.

Or do you think this is needed for C extensions that call PyEval_GetLocals()?

Copy link
Member Author

@carljm carljm Jun 21, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If there are C extensions calling PyEval_GetLocals() while within a class or module-scoped comprehension and running into the equivalent of the OP case, then they would want to switch to PyEval_GetFrameLocals() to fix that. I'm skeptical that any such cases will exist, but that's why I left it public. Open to whatever @Yhg1s prefers here.

3 changes: 3 additions & 0 deletions Include/internal/pycore_frame.h
Original file line number Diff line number Diff line change
Expand Up @@ -234,6 +234,9 @@ _PyFrame_ClearExceptCode(_PyInterpreterFrame * frame);
int
_PyFrame_Traverse(_PyInterpreterFrame *frame, visitproc visit, void *arg);

PyObject *
_PyFrame_GetLocals(_PyInterpreterFrame *frame, int include_hidden);

int
_PyFrame_FastToLocalsWithError(_PyInterpreterFrame *frame);

Expand Down
22 changes: 22 additions & 0 deletions Lib/test/test_listcomps.py
Original file line number Diff line number Diff line change
Expand Up @@ -539,6 +539,28 @@ def b():
self._check_in_scopes(code, {"x": True, "y": ["b"]}, scopes=["function"])
self._check_in_scopes(code, raises=NameError, scopes=["class"])

def test_iter_var_available_in_locals(self):
code = """
l = [1, 2]
y = 0
items = [locals()["x"] for x in l]
items2 = [vars()["x"] for x in l]
items3 = [("x" in dir()) for x in l]
items4 = [eval("x") for x in l]
# x is available, and does not overwrite y
[exec("y = x") for x in l]
"""
self._check_in_scopes(
code,
{
"items": [1, 2],
"items2": [1, 2],
"items3": [True, True],
"items4": [1, 2],
"y": 0
}
)


__test__ = {'doctests' : doctests}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Include the comprehension iteration variable in ``locals()`` inside a
module- or class-scope comprehension.
65 changes: 60 additions & 5 deletions Objects/frameobject.c
Original file line number Diff line number Diff line change
Expand Up @@ -1200,15 +1200,28 @@ frame_get_var(_PyInterpreterFrame *frame, PyCodeObject *co, int i,
return 1;
}

int
_PyFrame_FastToLocalsWithError(_PyInterpreterFrame *frame)

PyObject *
_PyFrame_GetLocals(_PyInterpreterFrame *frame, int include_hidden)
{
/* Merge fast locals into f->f_locals */
PyObject *locals = frame->f_locals;
if (locals == NULL) {
locals = frame->f_locals = PyDict_New();
if (locals == NULL) {
return -1;
return NULL;
}
}
PyObject *hidden = NULL;

/* If include_hidden, "hidden" fast locals (from inlined comprehensions in
module/class scopes) will be included in the returned dict, but not in
frame->f_locals; the returned dict will be a modified copy. Non-hidden
locals will still be updated in frame->f_locals. */
if (include_hidden) {
hidden = PyDict_New();
if (hidden == NULL) {
return NULL;
}
}

Expand All @@ -1224,6 +1237,11 @@ _PyFrame_FastToLocalsWithError(_PyInterpreterFrame *frame)
PyObject *name = PyTuple_GET_ITEM(co->co_localsplusnames, i);
_PyLocals_Kind kind = _PyLocals_GetKind(co->co_localspluskinds, i);
if (kind & CO_FAST_HIDDEN) {
if (include_hidden && value != NULL) {
if (PyObject_SetItem(hidden, name, value) != 0) {
goto error;
}
}
continue;
}
if (value == NULL) {
Expand All @@ -1232,16 +1250,53 @@ _PyFrame_FastToLocalsWithError(_PyInterpreterFrame *frame)
PyErr_Clear();
}
else {
return -1;
goto error;
}
}
}
else {
if (PyObject_SetItem(locals, name, value) != 0) {
return -1;
goto error;
}
}
}

if (include_hidden && PyDict_Size(hidden)) {
PyObject *innerlocals = PyDict_New();
if (innerlocals == NULL) {
goto error;
}
if (PyDict_Merge(innerlocals, locals, 1) != 0) {
Py_DECREF(innerlocals);
goto error;
}
if (PyDict_Merge(innerlocals, hidden, 1) != 0) {
Py_DECREF(innerlocals);
goto error;
}
locals = innerlocals;
}
else {
Py_INCREF(locals);
}
Py_CLEAR(hidden);

return locals;

error:
Py_XDECREF(hidden);
return NULL;
}


int
_PyFrame_FastToLocalsWithError(_PyInterpreterFrame *frame)
{
PyObject *locals = _PyFrame_GetLocals(frame, 0);
if (locals == NULL) {
return -1;
}
Py_DECREF(locals);
return 0;
}

Expand Down
7 changes: 4 additions & 3 deletions Objects/object.c
Original file line number Diff line number Diff line change
Expand Up @@ -1690,13 +1690,15 @@ _dir_locals(void)
PyObject *names;
PyObject *locals;

locals = PyEval_GetLocals();
locals = PyEval_GetFrameLocals();
if (locals == NULL)
return NULL;

names = PyMapping_Keys(locals);
if (!names)
Py_DECREF(locals);
if (!names) {
return NULL;
}
if (!PyList_Check(names)) {
PyErr_Format(PyExc_TypeError,
"dir(): expected keys() of locals to be a list, "
Expand All @@ -1708,7 +1710,6 @@ _dir_locals(void)
Py_DECREF(names);
return NULL;
}
/* the locals don't need to be DECREF'd */
return names;
}

Expand Down
87 changes: 53 additions & 34 deletions Python/bltinmodule.c
Original file line number Diff line number Diff line change
Expand Up @@ -908,7 +908,7 @@ builtin_eval_impl(PyObject *module, PyObject *source, PyObject *globals,
PyObject *locals)
/*[clinic end generated code: output=0a0824aa70093116 input=11ee718a8640e527]*/
{
PyObject *result, *source_copy;
PyObject *result = NULL, *source_copy;
const char *str;

if (locals != Py_None && !PyMapping_Check(locals)) {
Expand All @@ -924,54 +924,64 @@ builtin_eval_impl(PyObject *module, PyObject *source, PyObject *globals,
if (globals == Py_None) {
globals = PyEval_GetGlobals();
if (locals == Py_None) {
locals = PyEval_GetLocals();
locals = PyEval_GetFrameLocals();
if (locals == NULL)
return NULL;
}
else {
Py_INCREF(locals);
}
}
else if (locals == Py_None)
locals = globals;
locals = Py_NewRef(globals);
else {
Py_INCREF(locals);
}

if (globals == NULL || locals == NULL) {
PyErr_SetString(PyExc_TypeError,
"eval must be given globals and locals "
"when called without a frame");
return NULL;
goto error;
}

int r = PyDict_Contains(globals, &_Py_ID(__builtins__));
if (r == 0) {
r = PyDict_SetItem(globals, &_Py_ID(__builtins__), PyEval_GetBuiltins());
}
if (r < 0) {
return NULL;
goto error;
}

if (PyCode_Check(source)) {
if (PySys_Audit("exec", "O", source) < 0) {
return NULL;
goto error;
}

if (PyCode_GetNumFree((PyCodeObject *)source) > 0) {
PyErr_SetString(PyExc_TypeError,
"code object passed to eval() may not contain free variables");
return NULL;
goto error;
}
return PyEval_EvalCode(source, globals, locals);
result = PyEval_EvalCode(source, globals, locals);
}
else {
PyCompilerFlags cf = _PyCompilerFlags_INIT;
cf.cf_flags = PyCF_SOURCE_IS_UTF8;
str = _Py_SourceAsString(source, "eval", "string, bytes or code", &cf, &source_copy);
if (str == NULL)
goto error;

PyCompilerFlags cf = _PyCompilerFlags_INIT;
cf.cf_flags = PyCF_SOURCE_IS_UTF8;
str = _Py_SourceAsString(source, "eval", "string, bytes or code", &cf, &source_copy);
if (str == NULL)
return NULL;
while (*str == ' ' || *str == '\t')
str++;

while (*str == ' ' || *str == '\t')
str++;
(void)PyEval_MergeCompilerFlags(&cf);
result = PyRun_StringFlags(str, Py_eval_input, globals, locals, &cf);
Py_XDECREF(source_copy);
}

(void)PyEval_MergeCompilerFlags(&cf);
result = PyRun_StringFlags(str, Py_eval_input, globals, locals, &cf);
Py_XDECREF(source_copy);
error:
Py_XDECREF(locals);
return result;
}

Expand Down Expand Up @@ -1006,36 +1016,43 @@ builtin_exec_impl(PyObject *module, PyObject *source, PyObject *globals,
if (globals == Py_None) {
globals = PyEval_GetGlobals();
if (locals == Py_None) {
locals = PyEval_GetLocals();
locals = PyEval_GetFrameLocals();
if (locals == NULL)
return NULL;
}
else {
Py_INCREF(locals);
}
if (!globals || !locals) {
PyErr_SetString(PyExc_SystemError,
"globals and locals cannot be NULL");
return NULL;
}
}
else if (locals == Py_None)
locals = globals;
else if (locals == Py_None) {
locals = Py_NewRef(globals);
}
else {
Py_INCREF(locals);
}

if (!PyDict_Check(globals)) {
PyErr_Format(PyExc_TypeError, "exec() globals must be a dict, not %.100s",
Py_TYPE(globals)->tp_name);
return NULL;
goto error;
}
if (!PyMapping_Check(locals)) {
PyErr_Format(PyExc_TypeError,
"locals must be a mapping or None, not %.100s",
Py_TYPE(locals)->tp_name);
return NULL;
goto error;
}
int r = PyDict_Contains(globals, &_Py_ID(__builtins__));
if (r == 0) {
r = PyDict_SetItem(globals, &_Py_ID(__builtins__), PyEval_GetBuiltins());
}
if (r < 0) {
return NULL;
goto error;
}

if (closure == Py_None) {
Expand All @@ -1048,7 +1065,7 @@ builtin_exec_impl(PyObject *module, PyObject *source, PyObject *globals,
if (closure) {
PyErr_SetString(PyExc_TypeError,
"cannot use a closure with this code object");
return NULL;
goto error;
}
} else {
int closure_is_ok =
Expand All @@ -1068,12 +1085,12 @@ builtin_exec_impl(PyObject *module, PyObject *source, PyObject *globals,
PyErr_Format(PyExc_TypeError,
"code object requires a closure of exactly length %zd",
num_free);
return NULL;
goto error;
}
}

if (PySys_Audit("exec", "O", source) < 0) {
return NULL;
goto error;
}

if (!closure) {
Expand All @@ -1100,7 +1117,7 @@ builtin_exec_impl(PyObject *module, PyObject *source, PyObject *globals,
"string, bytes or code", &cf,
&source_copy);
if (str == NULL)
return NULL;
goto error;
if (PyEval_MergeCompilerFlags(&cf))
v = PyRun_StringFlags(str, Py_file_input, globals,
locals, &cf);
Expand All @@ -1109,9 +1126,14 @@ builtin_exec_impl(PyObject *module, PyObject *source, PyObject *globals,
Py_XDECREF(source_copy);
}
if (v == NULL)
return NULL;
goto error;
Py_DECREF(locals);
Py_DECREF(v);
Py_RETURN_NONE;

error:
Py_XDECREF(locals);
return NULL;
}


Expand Down Expand Up @@ -1721,10 +1743,7 @@ static PyObject *
builtin_locals_impl(PyObject *module)
/*[clinic end generated code: output=b46c94015ce11448 input=7874018d478d5c4b]*/
{
PyObject *d;

d = PyEval_GetLocals();
return Py_XNewRef(d);
return PyEval_GetFrameLocals();
}


Expand Down Expand Up @@ -2442,7 +2461,7 @@ builtin_vars_impl(PyObject *module, PyObject *object)
PyObject *d;

if (object == NULL) {
d = Py_XNewRef(PyEval_GetLocals());
d = PyEval_GetFrameLocals();
}
else {
if (_PyObject_LookupAttr(object, &_Py_ID(__dict__), &d) == 0) {
Expand Down
Loading