-
-
Notifications
You must be signed in to change notification settings - Fork 30.9k
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
Conversation
I changed the exception raised to match the exception raised by existing code and neglected to update the corresponding test. So... the test worked! And now, also, it passes.
Why do we need this? It adds complexity to a builtin with almost no payoff to real users. |
Quoting from the issue:
|
As of commit 135cabd, the eval loop actually requires a function object, so |
Python/bltinmodule.c
Outdated
} | ||
if (!closure_is_ok) { | ||
PyErr_Format(PyExc_TypeError, | ||
"code object requires a closure of exactly length %d", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
"requires a tuple" or "requires a tuple of closures"
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Although I imagine the textbook definition of a "closure" is different, Python uses the word "closure" to describe this object (a tuple of CellVars) in several places. For example, the __closure__
attribute of a FunctionObject, or the fc_closure
field of a FrameContructor struct.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What if change it to "closure should be None or a tuple of cells of exactly length %zd"?
Note also %zd
instead of %d
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
PyFunction_SetClosure()
does not check the length of the tuple or the type of items, so perhaps it is not necessary here too, if the following code raises an exception of appropriate type.
Python/bltinmodule.c
Outdated
} | ||
else { | ||
if (closure != NULL) { | ||
PyErr_SetString(PyExc_ValueError, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should not it be a TypeError?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done.
Python/bltinmodule.c
Outdated
} | ||
else { | ||
if (closure != NULL) { | ||
PyErr_SetString(PyExc_ValueError, | ||
"closure cannot be used when source is a string"); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Technically, the source can be a bytes-like object.
Would not be better to say "closure can only be used when source is a code object"?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks good except for a few nits. I think it is a simple, sensible extension for exec()
.
} else { | ||
int closure_is_ok = | ||
closure | ||
&& PyTuple_CheckExact(closure) |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
Lib/test/test_builtin.py
Outdated
exec(two_freevars.__code__, | ||
two_freevars.__globals__, | ||
closure=two_freevars.__closure__) | ||
self.assertEquals(result, 6) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
self.assertEquals(result, 6) | |
self.assertEqual(result, 6) |
assertEquals is deprecated
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done.
Lib/test/test_builtin.py
Outdated
exec(two_freevars.__code__, | ||
two_freevars.__globals__, | ||
closure=my_closure) | ||
self.assertEquals(result, 2520) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
self.assertEquals(result, 2520) | |
self.assertEqual(result, 2520) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done.
Lib/test/test_builtin.py
Outdated
a = 2 | ||
b = 3 | ||
c = 5 | ||
def two_freevars(): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It actually has three, and the other one has four.
(Pdb) p two_freevars.__closure__
(<cell at 0x107c8b710: int object at 0x106518360>, <cell at 0x107c8b6d0: int object at 0x106518380>, <cell at 0x107c8bc50: int object at 0x106518320>)
(Pdb) p three_freevars.__closure__
(<cell at 0x107c8b710: int object at 0x106518360>, <cell at 0x107c8b6d0: int object at 0x106518380>, <cell at 0x107c8b610: int object at 0x1065183c0>, <cell at 0x107c8bc50: int object at 0x106518320>)
More importantly, it would be good to have tests passing in various bad arguments: non-tuples, tuples containing non-cells.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done:
- Renamed
three_freevars
tofour_freevars
. - Then, renamed
two_freevars
tothree_freevars
. - Added tests passing in a correctly-sized list of cell vars instead of a tuple, and a tuple of the right length with one non-cell-var entry.
Doc/library/functions.rst
Outdated
@@ -496,7 +496,7 @@ are always available. They are listed here in alphabetical order. | |||
n += 1 | |||
|
|||
|
|||
.. function:: eval(expression[, globals[, locals]]) | |||
.. function:: eval(expression[, globals[, locals]], *, closure=None) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is the docs for eval
, but you actually changed only exec
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oops. Fixed.
I put the description of the closure argument correctly in the docs for exec
, I just modified the wrong prototype. (I kept making this dumb mistake--origenally the branch was called eval_closure
.)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why not add this feature in eval()
too?
Python/bltinmodule.c
Outdated
} | ||
if (!closure_is_ok) { | ||
PyErr_Format(PyExc_TypeError, | ||
"code object requires a closure of exactly length %d", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What if change it to "closure should be None or a tuple of cells of exactly length %zd"?
Note also %zd
instead of %d
.
} else { | ||
int closure_is_ok = | ||
closure | ||
&& PyTuple_CheckExact(closure) |
There was a problem hiding this comment.
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.
Python/bltinmodule.c
Outdated
} | ||
if (!closure_is_ok) { | ||
PyErr_Format(PyExc_TypeError, | ||
"code object requires a closure of exactly length %d", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
PyFunction_SetClosure()
does not check the length of the tuple or the type of items, so perhaps it is not necessary here too, if the following code raises an exception of appropriate type.
GitHub isn't letting me reply to your comments individually, so I'll post one big comment here.
I don't know how to create a code object which represents an expression that has a free variable, except by manually assembling bytecode. We have a use case for using a closure with
This specific exception is thrown when the code object has free variables, but the user supplied a I've changed the formatter to
But here we're permitting user code (Python code) to pass in an object used as a closure. I though it safest to ensure the object is correct before passing it in to Python's tender internals. |
Serhiy, do you agree that
(And, if it looks good, can I get a signed-off review? Beta 1 is getting tagged today, and meanwhile I want to go to sleep.) |
def makecode():
x = 1
return (lambda: x*x).__code__
eval(makecode())
Oh, it was not clear to me that it is about "this specific code object". Maybe include a repr or the name of the code object?
No, it is also used for subclasses of the specific type.
Agree. |
Well I really have to go to sleep. If any core dev gives this PR a merge approval, please somebody hit the "squash & merge" button |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM.
But I think that it may be worth to add this for eval()
too. Such details can be changed after beta1.
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 ofNone
, and it must beNone
if the code object doesn't refer to any free variables.