Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

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 ourterms of service andprivacy statement. We’ll occasionally send you account related emails.

Already on GitHub?Sign in to your account

Merged
brandtbucher merged 13 commits intopython:mainfromgaogaotiantian:pdb-closure
May 6, 2024

Conversation

@gaogaotiantian
Copy link
Member

@gaogaotiantiangaogaotiantian commentedOct 20, 2023
edited by bedevere-appbot
Loading

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:

deff():z=1any([xforxinrange(2)ifx!=z])

The code above works fine because python compilesany([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 compilingany([x for x in range(2) if x != z]), we compile the following code:

defouter():z=Nonedef__pdb_scope():nonlocalzany([xforxinrange(2)ifx!=z])

Then we get the code object ofouter.__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:

exec(code,globals,locals,closure=(types.CellType(1),))

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 simpleexec solution (it might be a valid exception, but we will always have the same exception as before).

Why not ...

  • just combineglobals() andlocals() as the global namespace?
    • result ofglobals() would be wrong
    • result ofglobal x; print(x) would be wrong ifx appears in both global and local namespace (if local is preferred, same issue if global is preferred).
    • impossible to do writeback correctly
  • use the new method only?
    • the new method will give different exception on some cases (UnboundLocalError vsNameError)
    • the new method does not work well withglobal 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 ofpdb for probably > 10 years.

@gvanrossum
Copy link
Member

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?

@gaogaotiantian
Copy link
MemberAuthor

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 some black magic of Python. But no worries I'm sure I can find great reviewers for feedbacks of the PR (a couple invited already)!

Copy link
Member

@brandtbucherbrandtbucher left a 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:

@brandtbucherbrandtbucher self-assigned thisMay 4, 2024
@gaogaotiantian
Copy link
MemberAuthor

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.

brandtbucher reacted with thumbs up emoji

@gaogaotiantian
Copy link
MemberAuthor

I had to make quite a bit of changes to make this work with PEP 667 -locals() is not write-through anymore for function scope. Neither can it dopop(). So a couple of things were changed:

  1. locals is copied first and the writeback only happens at the end of the function.
  2. Whether there is nested function is checked first, if not, use the normalexec. This actually solves plenty of interesting issues likelocals() (which will print the extra helper variables without the protection).
  3. Some of the reviews are applied.
  4. We can't uselocals() to pass around data anymore, so everything we need is now stored in a dict__pdb_eval__.

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.

Copy link
Member

@brandtbucherbrandtbucher left a 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:

@gaogaotiantian
Copy link
MemberAuthor

We need to put the otherexec in the try .. except block as well. The newly added test case is for that. Basically if we have a global variablex and we do

global x; (lambda: x)()

We should be able to get the value. But compiling this with our instrumentation would be:

nonlocal xglobal x; (lambda: x)()

and returns an error that's confusing to users -*** SyntaxError: name 'g' is nonlocal and global

That's why we need to fall back to normal.

brandtbucher reacted with thumbs up emoji

Co-authored-by: Brandt Bucher <brandtbucher@gmail.com>
Copy link
Member

@brandtbucherbrandtbucher left a 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. :)

@brandtbucherbrandtbucher merged commite5353d4 intopython:mainMay 6, 2024
@gaogaotiantiangaogaotiantian deleted the pdb-closure branchMay 6, 2024 19:41
@gaogaotiantian
Copy link
MemberAuthor

I'm glad this finally got fixed :)

SonicField pushed a commit to SonicField/cpython that referenced this pull requestMay 8, 2024
krassowski added a commit to ipython/ipython that referenced this pull requestJul 1, 2025
…from Python 3.13 (#14933)- [x] add a failing test case- [x] backportpython/cpython#111094 intoIPython codebase-fixesgotcha/ipdb#256- solves the pdb-related comments on#62![image](https://github.com/user-attachments/assets/c1a33ad0-d8a5-44f2-84d3-73f708918747)### ContextIPython already includes bits of Pdb code from Python itself, asdocumented in:https://github.com/ipython/ipython/blob/1e9a2e8b5a584ffe1973318fdabf2e39395ec367/IPython/core/debugger.py#L92-L110I am taking a more cautious approach and separating the backport to anew file to make it easy to delete in the (near) future (1 year fromnow).
Sign up for freeto join this conversation on GitHub. Already have an account?Sign in to comment

Reviewers

@brandtbucherbrandtbucherbrandtbucher approved these changes

Assignees

@brandtbucherbrandtbucher

Labels

None yet

Projects

None yet

Milestone

No milestone

Development

Successfully merging this pull request may close these issues.

3 participants

@gaogaotiantian@gvanrossum@brandtbucher

[8]ページ先頭

©2009-2025 Movatter.jp