-
-
Notifications
You must be signed in to change notification settings - Fork 32k
gh-83151: Make closure work on pdb #111094
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? S 8000 ign in to your account
Conversation
Great work! I'm not sure I have the time (or even, at this point, the context) to review this. Maybe someone else can do this more justice? |
Oh I invited you because you reopened #65360 (comment) and this fix involves s 8000 ome black magic of Python. But no worries I'm sure I can find great reviewers for feedbacks of the PR (a couple invited already)! |
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.
Been playing around with this locally for a while, and it seems like it works really well! Nice job.
Here's a first round of review. This is a tricky task, but I think the current solution may be overcomplicating it in places:
Misc/NEWS.d/next/Library/2023-10-20-03-50-17.gh-issue-83151.bcsD40.rst
Outdated
Show resolved
Hide resolved
I just realized this might have some conflict with PEP 667 which will probably be merged today. I will rework the code. Also taking the notes from your reviews. I will also hopefully finish this tomorrow. |
I had to make quite a bit of changes to make this work with PEP 667 -
This relies on the fix from #118583 so please do not merge it yet. I applied the change from that issue just to make the tests pass. I'll merge it in this PR when that one is merged in main. |
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 like how this is starting to look. Some more thoughts:
Co-authored-by: Brandt Bucher <brandtbucher@gmail.com>
We need to put the other
We should be able to get the value. But compiling this with our instrumentation would be:
and returns an error that's confusing to users - That's why we need to fall back to normal. |
Co-authored-by: Brandt Bucher <brandtbucher@gmail.com>
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.
Okay, this seems correct! Thanks for tackling such a longstanding and hairy issue, and for taking the time to walk me through the fix. :)
I'm glad this finally got fixed :) |
Closure on pdb has been an issue for years, and is often considered "to difficult to fix". PEP709 alleviated the issue by inlining all list, dict and set comprehensions, but the fundamental issue is still there - you can easily reproduce the issue with lambda functions (
(lambda: x)()
) or generators (any(x for x in lst if x != z)
). Now thatpdb
supports multi-line statement, you can even create your own function scope. Neither of them work in currentpdb
.The related (open) issues I can find are #83151, #80052, #76987, #70260 and #65360.
This PR fixes all the issues mentioned above, while keep (almost?) all the current correct behaviors.
Boiler alert, black magic involved.
The fundamental reason for the issue above, is that when python compiles code that generates nested functions, the inner function has no idea about the context. For example:
The code above works fine because python compiles
any([x for x in range(2) if x != z])
with the awareness thatz
is a local variable off
, so closure should be used. However, inpdb
, if you execute/compileany([x for x in range(2) if x != z])
, there's no way for Python to know thatz
is an outer local variable and should be passed as a cell variable.So, the solution is - trick the compiler.
What we can do, is to force compiler to compile the code we need in a closure-awareness environment. Take the code above as an example, instead of compiling
any([x for x in range(2) if x != z])
, we compile the following code:Then we get the code object of
outer.__pdb_scope
, and that is the code object we actually want - it considersz
as a free variable. Then, we pass the value ofz
withclosure
argument ofexec
:The solution is not done, as we still need to write the local variables back (
locals
won't be changed as all the variables are considered freevar). Also, it's possible for the code itself to create new variables and we want that in locals as well (k=1
for example). So, we just write all the local variables in the scope back by adding some new code at the end of the code.To keep the original behavior as close as possible, if any exception is raised in the new method, we fallback to the original simple
exec
solution (it might be a valid exception, but we will always have the same exception as before).Why not ...
globals()
andlocals()
as the global namespace?globals()
would be wrongglobal x; print(x)
would be wrong ifx
appears in both global and local namespace (if local is preferred, same issue if global is preferred).UnboundLocalError
vsNameError
)global x; print(x)
asx
would be defined as bothglobal
andnonlocal
(it will fallback to the original method and work)This is not the perfect solution, but it's pretty close. It passed all the existing tests and fixed all the cases in the issues mentioned above. I have not came up with an example that does not work with this solution (I would guess there will be some dark corners).
Yes, I know this is complicated, and it kind of depends on the implementation of CPython, but I still believe we should do this, because this issue has been repeatedly raised by users of
pdb
for probably > 10 years.