gh-92203: Add closure support to exec(). (#92204)

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.
This commit is contained in:
larryhastings 2022-05-06 10:09:35 -07:00 committed by GitHub
parent 973a5203c1
commit 5021064390
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 171 additions and 21 deletions

View File

@ -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
@ -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
@ -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)

View File

@ -24,7 +24,7 @@ from functools import partial
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)
@ -772,6 +772,84 @@ class BuiltinTest(unittest.TestCase):
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])

View File

@ -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.

View File

@ -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.
@ -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;
@ -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;

View File

@ -408,7 +408,7 @@ exit:
}
PyDoc_STRVAR(builtin_exec__doc__,
"exec($module, source, globals=None, locals=None, /)\n"
"exec($module, source, globals=None, locals=None, /, *, closure=None)\n"
"--\n"
"\n"
"Execute the given source in the context of globals and locals.\n"
@ -417,37 +417,52 @@ PyDoc_STRVAR(builtin_exec__doc__,
"or a code object as returned by compile().\n"
"The globals must be a dictionary and locals can be any mapping,\n"
"defaulting to the current globals and locals.\n"
"If only globals is given, locals defaults to it.");
"If only globals is given, locals defaults to it.\n"
"The closure must be a tuple of cellvars, and can only be used\n"
"when source is a code object requiring exactly that many cellvars.");
#define BUILTIN_EXEC_METHODDEF \
{"exec", _PyCFunction_CAST(builtin_exec), METH_FASTCALL, builtin_exec__doc__},
{"exec", _PyCFunction_CAST(builtin_exec), METH_FASTCALL|METH_KEYWORDS, builtin_exec__doc__},
static PyObject *
builtin_exec_impl(PyObject *module, PyObject *source, PyObject *globals,
PyObject *locals);
PyObject *locals, PyObject *closure);
static PyObject *
builtin_exec(PyObject *module, PyObject *const *args, Py_ssize_t nargs)
builtin_exec(PyObject *module, PyObject *const *args, Py_ssize_t nargs, PyObject *kwnames)
{
PyObject *return_value = NULL;
static const char * const _keywords[] = {"", "", "", "closure", NULL};
static _PyArg_Parser _parser = {NULL, _keywords, "exec", 0};
PyObject *argsbuf[4];
Py_ssize_t noptargs = nargs + (kwnames ? PyTuple_GET_SIZE(kwnames) : 0) - 1;
PyObject *source;
PyObject *globals = Py_None;
PyObject *locals = Py_None;
PyObject *closure = NULL;
if (!_PyArg_CheckPositional("exec", nargs, 1, 3)) {
args = _PyArg_UnpackKeywords(args, nargs, NULL, kwnames, &_parser, 1, 3, 0, argsbuf);
if (!args) {
goto exit;
}
source = args[0];
if (nargs < 2) {
goto skip_optional;
goto skip_optional_posonly;
}
noptargs--;
globals = args[1];
if (nargs < 3) {
goto skip_optional;
goto skip_optional_posonly;
}
noptargs--;
locals = args[2];
skip_optional:
return_value = builtin_exec_impl(module, source, globals, locals);
skip_optional_posonly:
if (!noptargs) {
goto skip_optional_kwonly;
}
closure = args[3];
skip_optional_kwonly:
return_value = builtin_exec_impl(module, source, globals, locals, closure);
exit:
return return_value;
@ -1030,4 +1045,4 @@ builtin_issubclass(PyObject *module, PyObject *const *args, Py_ssize_t nargs)
exit:
return return_value;
}
/*[clinic end generated code: output=6a2b78ef82bc5155 input=a9049054013a1b77]*/
/*[clinic end generated code: output=a2c5c53e8aead7c3 input=a9049054013a1b77]*/