Skip to content

Commit 5021064

Browse files
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.
1 parent 973a520 commit 5021064

File tree

5 files changed

+171
-21
lines changed

5 files changed

+171
-21
lines changed

Doc/library/functions.rst

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -552,7 +552,7 @@ are always available. They are listed here in alphabetical order.
552552

553553
.. index:: builtin: exec
554554

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

557557
This function supports dynamic execution of Python code. *object* must be
558558
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.
581581
builtins are available to the executed code by inserting your own
582582
``__builtins__`` dictionary into *globals* before passing it to :func:`exec`.
583583

584+
The *closure* argument specifies a closure--a tuple of cellvars.
585+
It's only valid when the *object* is a code object containing free variables.
586+
The length of the tuple must exactly match the number of free variables
587+
referenced by the code object.
588+
584589
.. audit-event:: exec code_object exec
585590

586591
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.
599604
Pass an explicit *locals* dictionary if you need to see effects of the
600605
code on *locals* after function :func:`exec` returns.
601606

607+
.. versionchanged:: 3.11
608+
Added the *closure* parameter.
609+
602610

603611
.. function:: filter(function, iterable)
604612

Lib/test/test_builtin.py

Lines changed: 79 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@
2424
from inspect import CO_COROUTINE
2525
from itertools import product
2626
from textwrap import dedent
27-
from types import AsyncGeneratorType, FunctionType
27+
from types import AsyncGeneratorType, FunctionType, CellType
2828
from operator import neg
2929
from test import support
3030
from test.support import (swap_attr, maybe_get_event_loop_policy)
@@ -772,6 +772,84 @@ def test_exec_redirected(self):
772772
finally:
773773
sys.stdout = savestdout
774774

775+
def test_exec_closure(self):
776+
def function_without_closures():
777+
return 3 * 5
778+
779+
result = 0
780+
def make_closure_functions():
781+
a = 2
782+
b = 3
783+
c = 5
784+
def three_freevars():
785+
nonlocal result
786+
nonlocal a
787+
nonlocal b
788+
result = a*b
789+
def four_freevars():
790+
nonlocal result
791+
nonlocal a
792+
nonlocal b
793+
nonlocal c
794+
result = a*b*c
795+
return three_freevars, four_freevars
796+
three_freevars, four_freevars = make_closure_functions()
797+
798+
# "smoke" test
799+
result = 0
800+
exec(three_freevars.__code__,
801+
three_freevars.__globals__,
802+
closure=three_freevars.__closure__)
803+
self.assertEqual(result, 6)
804+
805+
# should also work with a manually created closure
806+
result = 0
807+
my_closure = (CellType(35), CellType(72), three_freevars.__closure__[2])
808+
exec(three_freevars.__code__,
809+
three_freevars.__globals__,
810+
closure=my_closure)
811+
self.assertEqual(result, 2520)
812+
813+
# should fail: closure isn't allowed
814+
# for functions without free vars
815+
self.assertRaises(TypeError,
816+
exec,
817+
function_without_closures.__code__,
818+
function_without_closures.__globals__,
819+
closure=my_closure)
820+
821+
# should fail: closure required but wasn't specified
822+
self.assertRaises(TypeError,
823+
exec,
824+
three_freevars.__code__,
825+
three_freevars.__globals__,
826+
closure=None)
827+
828+
# should fail: closure of wrong length
829+
self.assertRaises(TypeError,
830+
exec,
831+
three_freevars.__code__,
832+
three_freevars.__globals__,
833+
closure=four_freevars.__closure__)
834+
835+
# should fail: closure using a list instead of a tuple
836+
my_closure = list(my_closure)
837+
self.assertRaises(TypeError,
838+
exec,
839+
three_freevars.__code__,
840+
three_freevars.__globals__,
841+
closure=my_closure)
842+
843+
# should fail: closure tuple with one non-cell-var
844+
my_closure[0] = int
845+
my_closure = tuple(my_closure)
846+
self.assertRaises(TypeError,
847+
exec,
848+
three_freevars.__code__,
849+
three_freevars.__globals__,
850+
closure=my_closure)
851+
852+
775853
def test_filter(self):
776854
self.assertEqual(list(filter(lambda c: 'a' <= c <= 'z', 'Hello World')), list('elloorld'))
777855
self.assertEqual(list(filter(None, [1, 'hello', [], [3], '', None, 9, 0])), [1, 'hello', [3], 9])
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
Add a closure keyword-only parameter to exec(). It can only be specified
2+
when exec-ing a code object that uses free variables. When specified, it
3+
must be a tuple, with exactly the number of cell variables referenced by the
4+
code object. closure has a default value of None, and it must be None if the
5+
code object doesn't refer to any free variables.

Python/bltinmodule.c

Lines changed: 52 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -977,6 +977,8 @@ exec as builtin_exec
977977
globals: object = None
978978
locals: object = None
979979
/
980+
*
981+
closure: object(c_default="NULL") = None
980982
981983
Execute the given source in the context of globals and locals.
982984
@@ -985,12 +987,14 @@ or a code object as returned by compile().
985987
The globals must be a dictionary and locals can be any mapping,
986988
defaulting to the current globals and locals.
987989
If only globals is given, locals defaults to it.
990+
The closure must be a tuple of cellvars, and can only be used
991+
when source is a code object requiring exactly that many cellvars.
988992
[clinic start generated code]*/
989993

990994
static PyObject *
991995
builtin_exec_impl(PyObject *module, PyObject *source, PyObject *globals,
992-
PyObject *locals)
993-
/*[clinic end generated code: output=3c90efc6ab68ef5d input=01ca3e1c01692829]*/
996+
PyObject *locals, PyObject *closure)
997+
/*[clinic end generated code: output=7579eb4e7646743d input=f13a7e2b503d1d9a]*/
994998
{
995999
PyObject *v;
9961000

@@ -1029,20 +1033,60 @@ builtin_exec_impl(PyObject *module, PyObject *source, PyObject *globals,
10291033
return NULL;
10301034
}
10311035

1036+
if (closure == Py_None) {
1037+
closure = NULL;
1038+
}
1039+
10321040
if (PyCode_Check(source)) {
1041+
Py_ssize_t num_free = PyCode_GetNumFree((PyCodeObject *)source);
1042+
if (num_free == 0) {
1043+
if (closure) {
1044+
PyErr_SetString(PyExc_TypeError,
1045+
"cannot use a closure with this code object");
1046+
return NULL;
1047+
}
1048+
} else {
1049+
int closure_is_ok =
1050+
closure
1051+
&& PyTuple_CheckExact(closure)
1052+
&& (PyTuple_GET_SIZE(closure) == num_free);
1053+
if (closure_is_ok) {
1054+
for (Py_ssize_t i = 0; i < num_free; i++) {
1055+
PyObject *cell = PyTuple_GET_ITEM(closure, i);
1056+
if (!PyCell_Check(cell)) {
1057+
closure_is_ok = 0;
1058+
break;
1059+
}
1060+
}
1061+
}
1062+
if (!closure_is_ok) {
1063+
PyErr_Format(PyExc_TypeError,
1064+
"code object requires a closure of exactly length %zd",
1065+
num_free);
1066+
return NULL;
1067+
}
1068+
}
1069+
10331070
if (PySys_Audit("exec", "O", source) < 0) {
10341071
return NULL;
10351072
}
10361073

1037-
if (PyCode_GetNumFree((PyCodeObject *)source) > 0) {
1038-
PyErr_SetString(PyExc_TypeError,
1039-
"code object passed to exec() may not "
1040-
"contain free variables");
1041-
return NULL;
1074+
if (!closure) {
1075+
v = PyEval_EvalCode(source, globals, locals);
1076+
} else {
1077+
v = PyEval_EvalCodeEx(source, globals, locals,
1078+
NULL, 0,
1079+
NULL, 0,
1080+
NULL, 0,
1081+
NULL,
1082+
closure);
10421083
}
1043-
v = PyEval_EvalCode(source, globals, locals);
10441084
}
10451085
else {
1086+
if (closure != NULL) {
1087+
PyErr_SetString(PyExc_TypeError,
1088+
"closure can only be used when source is a code object");
1089+
}
10461090
PyObject *source_copy;
10471091
const char *str;
10481092
PyCompilerFlags cf = _PyCompilerFlags_INIT;

Python/clinic/bltinmodule.c.h

Lines changed: 26 additions & 11 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)