8000 GH-83151: Add closure support to pdb (GH-111094) · python/cpython@e5353d4 · GitHub
[go: up one dir, main page]

Skip to content

Commit e5353d4

Browse files
GH-83151: Add closure support to pdb (GH-111094)
1 parent 5a1618a commit e5353d4

File tree

3 files changed

+156
-2
lines changed

3 files changed

+156
-2
lines changed

Lib/pdb.py

Lines changed: 89 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,10 +77,12 @@
7777
import code
7878
import glob
7979
import token
80+
import types
8081
import codeop
8182
import pprint
8283
import signal
8384
import inspect
85+
import textwrap
8486
import tokenize
8587
import traceback
8688
import linecache
@@ -624,11 +626,96 @@ def _disable_command_completion(self):
624626
self.completenames = completenames
625627
return
626628

629+
def _exec_in_closure(self, source, globals, locals):
630+
""" Run source code in closure so code object created within source
631+
can find variables in locals correctly
632+
633+
returns True if the source is executed, False otherwise
634+
"""
635+
636+
# Determine if the source should be executed in closure. Only when the
637+
# source compiled to multiple code objects, we should use this feature.
638+
# Otherwise, we can just raise an exception and normal exec will be used.
639+
640+
code = compile(source, "<string>", "exec")
641+
if not any(isinstance(const, CodeType) for const in code.co_consts):
642+
return False
643+
644+
# locals could be a proxy which does not support pop
645+
# copy it first to avoid modifying the original locals
646+
locals_copy = dict(locals)
647+
648+
locals_copy["__pdb_eval__"] = {
649+
"result": None,
650+
"write_back": {}
651+
}
652+
653+
# If the source is an expression, we need to print its value
654+
try:
655+
compile(source, "<string>", "eval")
656+
except SyntaxError:
657+
pass
658+
else:
659+
source = "__pdb_eval__['result'] = " + source
660+
661+
# Add write-back to update the locals
662+
source = ("try:\n" +
663+
textwrap.indent(source, " ") + "\n" +
664+
"finally:\n" +
665+
" __pdb_eval__['write_back'] = locals()")
666+
667+
# Build a closure source code with freevars from locals like:
668+
# def __pdb_outer():
669+
# var = None
670+
# def __pdb_scope(): # This is the code object we want to execute
671+
# nonlocal var
672+
# <source>
673+
# return __pdb_scope.__code__
674+
source_with_closure = ("def __pdb_outer():\n" +
675+
"\n".join(f" {var} = None" for var in locals_copy) + "\n" +
676+
" def __pdb_scope():\n" +
677+
"\n".join(f" nonlocal {var}" for var in locals_copy) + "\n" +
678+
textwrap.indent(source, " ") + "\n" +
679+
" return __pdb_scope.__code__"
680+
)
681+
682+
# Get the code object of __pdb_scope()
683+
# The exec fills locals_copy with the __pdb_outer() function and we can call
684+
# that to get the code object of __pdb_scope()
685+
ns = {}
686+
try:
687+
exec(source_with_closure, {}, ns)
688+
except Exception:
689+
return False
690+
code = ns["__pdb_outer"]()
691+
692+
cells = tuple(types.CellType(locals_copy.get(var)) for var in code.co_freevars)
693+
694+
try:
695+
exec(code, globals, locals_copy, closure=cells)
696+
except Exception:
697+
return False
698+
699+
# get the data we need from the statement
700+
pdb_eval = locals_copy["__pdb_eval__"]
701+
702+
# __pdb_eval__ should not be updated back to locals
703+
pdb_eval["write_back"].pop("__pdb_eval__")
704+
705+
# Write all local variables back to locals
706+
locals.update(pdb_eval["write_back"])
707+
eval_result = pdb_eval["result"]
708+
if eval_result is not None:
709+
print(repr(eval_result))
710+
711+
return True
712+
627713
def default(self, line):
628714
if line[:1] == '!': line = line[1:].strip()
629715
locals = self.curframe_locals
630716
globals = self.curframe.f_globals
631717
try:
718+
buffer = line
632719
if (code := codeop.compile_command(line + '\n', '<stdin>', 'single')) is None:
633720
# Multi-line mode
634721
with self._disable_command_completion():
@@ -661,7 +748,8 @@ def default(self, line):
661748
sys.stdin = self.stdin
662749
sys.stdout = self.stdout
663750
sys.displayhook = self.displayhook
664-
exec(code, globals, locals)
751+
if not self._exec_in_closure(buffer, globals, locals):
752+
exec(code, globals, locals)
665753
finally:
666754
sys.stdout = save_stdout
667755
sys.stdin = save_stdin

Lib/test/test_pdb.py

Lines changed: 64 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2224,8 +2224,71 @@ def test_pdb_multiline_statement():
22242224
(Pdb) c
22252225
"""
22262226

2227+
def test_pdb_closure():
2228+
"""Test for all expressions/statements that involve closure
2229+
2230+
>>> k = 0
2231+
>>> g = 1
2232+
>>> def test_function():
2233+
... x = 2
2234+
... g = 3
2235+
... import pdb; pdb.Pdb(nosigint=True, readrc=False).set_trace()
2236+
2237+
>>> with PdbTestInput([ # doctest: +NORMALIZE_WHITESPACE
2238+
... 'k',
2239+
... 'g',
2240+
... 'y = y',
2241+
... 'global g; g',
2242+
... 'global g; (lambda: g)()',
2243+
... '(lambda: x)()',
2244+
... '(lambda: g)()',
2245+
... 'lst = [n for n in range(10) if (n % x) == 0]',
2246+
... 'lst',
2247+
... 'sum(n for n in lst if n > x)',
2248+
... 'x = 1; raise Exception()',
2249+
... 'x',
2250+
... 'def f():',
2251+
... ' return x',
2252+
... '',
2253+
... 'f()',
2254+
... 'c'
2255+
... ]):
2256+
... test_function()
2257+
> <doctest test.test_pdb.test_pdb_closure[2]>(4)test_function()
2258+
-> import pdb; pdb.Pdb(nosigint=True, readrc=False).set_trace()
2259+
(Pdb) k
2260+
0
2261+
(Pdb) g
2262+
3
2263+
(Pdb) y = y
2264+
*** NameError: name 'y' is not defined
2265+
(Pdb) global g; g
2266+
1
2267+
(Pdb) global g; (lambda: g)()
2268+
1
2269+
(Pdb) (lambda: x)()
2270+
2
2271+
(Pdb) (lambda: g)()
2272+
3
2273+
(Pdb) lst = [n for n in range(10) if (n % x) == 0]
2274+
(Pdb) lst
2275+
[0, 2, 4, 6, 8]
2276+
(Pdb) sum(n for n in lst if n > x)
2277+
18
2278+
(Pdb) x = 1; raise Exception()
2279+
*** Exception
2280+
(Pdb) x
2281+
1
2282+
(Pdb) def f():
2283+
... return x
2284+
...
2285+
(Pdb) f()
2286+
1
2287+
(Pdb) c
2288+
"""
2289+
22272290
def test_pdb_show_attribute_and_item():
2228-
"""Test for multiline statement
2291+
"""Test for expressions with command prefix
22292292
22302293
>>> def test_function():
22312294
... n = lambda x: x
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
Enabled arbitrary statements and evaluations in :mod:`pdb` shell to access the
2+
local variables of the current frame, which made it possible for multi-scope
3+
code like generators or nested function to work.

0 commit comments

Comments
 (0)
0