Skip to content

Commit

Permalink
pythongh-92203: Add closure support to exec(). (python#92204)
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
larryhastings authored May 6, 2022
1 parent 973a520 commit 5021064
Show file tree
Hide file tree
Showing 5 changed files with 171 additions and 21 deletions.
10 changes: 9 additions & 1 deletion Doc/library/functions.rst
Original file line number Diff line number Diff line change
Expand Up @@ -552,7 +552,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 @@ -581,6 +581,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 @@ -599,6 +604,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)
&& (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.

0 comments on commit 5021064

Please sign in to comment.