Skip to content
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

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

Merged
merged 11 commits into from
Jul 5, 2023
1 change: 1 addition & 0 deletions Include/internal/pycore_ceval.h
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,7 @@ extern PyObject* _Py_MakeCoro(PyFunctionObject *func);

extern int _Py_HandlePending(PyThreadState *tstate);

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

Choose a reason for hiding this comment

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

Does this still need to be a PyAPI_FUNC? Since it's only used in the core, seems like we could leave it as simply extern like the function immediately above.

Copy link
Member Author

Choose a reason for hiding this comment

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

I wasn't sure what was preferred here; there are also lots of examples of functions declared in pycore_*.h files (including this header) that use PyAPI_FUNC. But the definition of PyAPI_FUNC says it is for "public API functions," and I think you're right that it's not needed, so I'll remove it.

Copy link
Member

Choose a reason for hiding this comment

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

Copy link
Member Author

Choose a reason for hiding this comment

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

I don't think extern does anything here, but some people prefer it for style reasons, and it seems like most (not all) non-static function declarations in this file use it, so I'll use it.

Copy link
Member

Choose a reason for hiding this comment

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

If the function is to be used by extension modules that are part of the standard library (or say they are, by defining Py_BUILD_CORE), which can be built as .so files, you need to use PyAPI_FUNC. Otherwise the symbol may not be exported on Windows (and I believe a few other platforms, like Android) or when using -fvisibility=hidden.

FWIW, I hadn't realised you're adding a _PyEval_GetLocals() when there's already a PyEval_GetLocals() that does a subtly different thing. I'm not fond of using the same name with just a _ difference here.

Copy link
Member Author

@carljm carljm Jul 3, 2023

Choose a reason for hiding this comment

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

I could make it _PyEval_GetLocalsRef (c.f. python/steering-council#201) since the one notable difference is that it returns a new reference.

The other notable difference is the behavior inside an inlined comprehension in module or class scope, but I don't think there is any way to summarize that difference in a name.

This function will go away in 3.13 if we do PEP 667, or something like it, to standardize the behavior of locals().

Copy link
Member Author

Choose a reason for hiding this comment

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

Currently there's no use of the function in any standard library extension modules (PyEval_GetLocals was used only once in object.c and several times in bltinmodule.c), and I don't expect it to be needed in any, so I don't think we need to use PyAPI_FUNC.

Copy link
Member Author

Choose a reason for hiding this comment

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

Or, I could go back to using _PyEval_GetFrameLocals like I did originally in this PR (but this time with a leading underscore). If we get PEP 667, it would introduce PyEval_GetFrameLocals as public API, and the private _PyEval_GetFrameLocals wouldn't be needed anymore and could be removed.



#ifdef __cplusplus
Expand Down
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_GetLocals();
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_GetLocals();
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_GetLocals();
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_GetLocals();
}


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_GetLocals();
}
else {
if (_PyObject_LookupAttr(object, &_Py_ID(__dict__), &d) == 0) {
Expand Down
Loading