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-92203: Add closure support to exec(). #92204

Merged
merged 8 commits into from
May 6, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion Doc/library/functions.rst
Original file line number Diff line number Diff line change
Expand Up @@ -547,7 +547,7 @@ are always available. They are listed here in alphabetical order.

.. index:: builtin: exec

.. function:: exec(object[, globals[, locals]])
.. function:: exec(object[, globals[, locals]], *, closure=None)

This function supports dynamic execution of Python code. *object* must be
either a string or a code object. If it is a string, the string is parsed as
Expand Down Expand Up @@ -576,6 +576,11 @@ are always available. They are listed here in alphabetical order.
builtins are available to the executed code by inserting your own
``__builtins__`` dictionary into *globals* before passing it to :func:`exec`.

The *closure* argument specifies a closure--a tuple of cellvars.
It's only valid when the *object* is a code object containing free variables.
The length of the tuple must exactly match the number of free variables
referenced by the code object.

.. audit-event:: exec code_object exec

Raises an :ref:`auditing event <auditing>` ``exec`` with the code object
Expand All @@ -594,6 +599,9 @@ are always available. They are listed here in alphabetical order.
Pass an explicit *locals* dictionary if you need to see effects of the
code on *locals* after function :func:`exec` returns.

.. versionchanged:: 3.11
Added the *closure* parameter.


.. function:: filter(function, iterable)

Expand Down
80 changes: 79 additions & 1 deletion Lib/test/test_builtin.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
from inspect import CO_COROUTINE
from itertools import product
from textwrap import dedent
from types import AsyncGeneratorType, FunctionType
from types import AsyncGeneratorType, FunctionType, CellType
from operator import neg
from test import support
from test.support import (swap_attr, maybe_get_event_loop_policy)
Expand Down Expand Up @@ -772,6 +772,84 @@ def test_exec_redirected(self):
finally:
sys.stdout = savestdout

def test_exec_closure(self):
def function_without_closures():
return 3 * 5

result = 0
def make_closure_functions():
a = 2
b = 3
c = 5
def three_freevars():
nonlocal result
nonlocal a
nonlocal b
result = a*b
def four_freevars():
nonlocal result
nonlocal a
nonlocal b
nonlocal c
result = a*b*c
return three_freevars, four_freevars
three_freevars, four_freevars = make_closure_functions()

# "smoke" test
result = 0
exec(three_freevars.__code__,
three_freevars.__globals__,
closure=three_freevars.__closure__)
self.assertEqual(result, 6)

# should also work with a manually created closure
result = 0
my_closure = (CellType(35), CellType(72), three_freevars.__closure__[2])
exec(three_freevars.__code__,
three_freevars.__globals__,
closure=my_closure)
self.assertEqual(result, 2520)

# should fail: closure isn't allowed
# for functions without free vars
self.assertRaises(TypeError,
exec,
function_without_closures.__code__,
function_without_closures.__globals__,
closure=my_closure)

# should fail: closure required but wasn't specified
self.assertRaises(TypeError,
exec,
three_freevars.__code__,
three_freevars.__globals__,
closure=None)

# should fail: closure of wrong length
self.assertRaises(TypeError,
exec,
three_freevars.__code__,
three_freevars.__globals__,
closure=four_freevars.__closure__)

# should fail: closure using a list instead of a tuple
my_closure = list(my_closure)
self.assertRaises(TypeError,
exec,
three_freevars.__code__,
three_freevars.__globals__,
closure=my_closure)

# should fail: closure tuple with one non-cell-var
my_closure[0] = int
my_closure = tuple(my_closure)
self.assertRaises(TypeError,
exec,
three_freevars.__code__,
three_freevars.__globals__,
closure=my_closure)


def test_filter(self):
self.assertEqual(list(filter(lambda c: 'a' <= c <= 'z', 'Hello World')), list('elloorld'))
self.assertEqual(list(filter(None, [1, 'hello', [], [3], '', None, 9, 0])), [1, 'hello', [3], 9])
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
Add a closure keyword-only parameter to exec(). It can only be specified
when exec-ing a code object that uses free variables. When specified, it
must be a tuple, with exactly the number of cell variables referenced by the
code object. closure has a default value of None, and it must be None if the
code object doesn't refer to any free variables.
60 changes: 52 additions & 8 deletions Python/bltinmodule.c
Original file line number Diff line number Diff line change
Expand Up @@ -977,6 +977,8 @@ exec as builtin_exec
globals: object = None
locals: object = None
/
*
closure: object(c_default="NULL") = None

Execute the given source in the context of globals and locals.

Expand All @@ -985,12 +987,14 @@ or a code object as returned by compile().
The globals must be a dictionary and locals can be any mapping,
defaulting to the current globals and locals.
If only globals is given, locals defaults to it.
The closure must be a tuple of cellvars, and can only be used
when source is a code object requiring exactly that many cellvars.
[clinic start generated code]*/

static PyObject *
builtin_exec_impl(PyObject *module, PyObject *source, PyObject *globals,
PyObject *locals)
/*[clinic end generated code: output=3c90efc6ab68ef5d input=01ca3e1c01692829]*/
PyObject *locals, PyObject *closure)
/*[clinic end generated code: output=7579eb4e7646743d input=f13a7e2b503d1d9a]*/
{
PyObject *v;

Expand Down Expand Up @@ -1029,20 +1033,60 @@ builtin_exec_impl(PyObject *module, PyObject *source, PyObject *globals,
return NULL;
}

if (closure == Py_None) {
closure = NULL;
}

if (PyCode_Check(source)) {
Py_ssize_t num_free = PyCode_GetNumFree((PyCodeObject *)source);
if (num_free == 0) {
if (closure) {
PyErr_SetString(PyExc_TypeError,
"cannot use a closure with this code object");
return NULL;
}
} else {
int closure_is_ok =
closure
&& PyTuple_CheckExact(closure)
Copy link
Member

Choose a reason for hiding this comment

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

Why not allow subclasses of tuple?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I erred on the side of caution. I don't know if all the code that deals with closure objects inside CPython accept subclasses of tuple, or iterables generally; it seemed to me that the safest route was to require what CPython itself uses, which is exactly a tuple object. If this proves too restrictive we can relax the restriction later, once we prove to ourselves that it's safe to do so.

Copy link
Member

Choose a reason for hiding this comment

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

PyFunction_SetClosure() accepts subclasses of tuple.

&& (PyTuple_GET_SIZE(closure) == num_free);
if (closure_is_ok) {
for (Py_ssize_t i = 0; i < num_free; i++) {
PyObject *cell = PyTuple_GET_ITEM(closure, i);
if (!PyCell_Check(cell)) {
closure_is_ok = 0;
break;
}
}
}
if (!closure_is_ok) {
PyErr_Format(PyExc_TypeError,
"code object requires a closure of exactly length %zd",
num_free);
return NULL;
}
}

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

if (PyCode_GetNumFree((PyCodeObject *)source) > 0) {
PyErr_SetString(PyExc_TypeError,
"code object passed to exec() may not "
"contain free variables");
return NULL;
if (!closure) {
v = PyEval_EvalCode(source, globals, locals);
} else {
v = PyEval_EvalCodeEx(source, globals, locals,
NULL, 0,
NULL, 0,
NULL, 0,
NULL,
closure);
}
v = PyEval_EvalCode(source, globals, locals);
}
else {
if (closure != NULL) {
PyErr_SetString(PyExc_TypeError,
"closure can only be used when source is a code object");
}
PyObject *source_copy;
const char *str;
PyCompilerFlags cf = _PyCompilerFlags_INIT;
Expand Down
37 changes: 26 additions & 11 deletions Python/clinic/bltinmodule.c.h

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.