Skip to content

Commit caee16f

Browse files
pythongh-121468: Support async breakpoint in pdb (python#132576)
1 parent 4265854 commit caee16f

File tree

5 files changed

+314
-9
lines changed

5 files changed

+314
-9
lines changed

Doc/library/pdb.rst

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,21 @@ slightly different way:
188188
.. versionadded:: 3.14
189189
The *commands* argument.
190190

191+
192+
.. awaitablefunction:: set_trace_async(*, header=None, commands=None)
193+
194+
async version of :func:`set_trace`. This function should be used inside an
195+
async function with :keyword:`await`.
196+
197+
.. code-block:: python
198+
199+
async def f():
200+
await pdb.set_trace_async()
201+
202+
:keyword:`await` statements are supported if the debugger is invoked by this function.
203+
204+
.. versionadded:: 3.14
205+
191206
.. function:: post_mortem(t=None)
192207

193208
Enter post-mortem debugging of the given exception or

Doc/whatsnew/3.14.rst

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1168,6 +1168,11 @@ pdb
11681168
backend by default, which is configurable.
11691169
(Contributed by Tian Gao in :gh:`124533`.)
11701170

1171+
* :func:`pdb.set_trace_async` is added to support debugging asyncio
1172+
coroutines. :keyword:`await` statements are supported with this
1173+
function.
1174+
(Contributed by Tian Gao in :gh:`132576`.)
1175+
11711176

11721177
pickle
11731178
------

Lib/pdb.py

Lines changed: 115 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -385,6 +385,9 @@ def __init__(self, completekey='tab', stdin=None, stdout=None, skip=None,
385385
self.commands_bnum = None # The breakpoint number for which we are
386386
# defining a list
387387

388+
self.async_shim_frame = None
389+
self.async_awaitable = None
390+
388391
self._chained_exceptions = tuple()
389392
self._chained_exception_index = 0
390393

@@ -400,6 +403,57 @@ def set_trace(self, frame=None, *, commands=None):
400403

401404
super().set_trace(frame)
402405

406+
async def set_trace_async(self, frame=None, *, commands=None):
407+
if self.async_awaitable is not None:
408+
# We are already in a set_trace_async call, do not mess with it
409+
return
410+
411+
if frame is None:
412+
frame = sys._getframe().f_back
413+
414+
# We need set_trace to set up the basics, however, this will call
415+
# set_stepinstr() will we need to compensate for, because we don't
416+
# want to trigger on calls
417+
self.set_trace(frame, commands=commands)
418+
# Changing the stopframe will disable trace dispatch on calls
419+
self.stopframe = frame
420+
# We need to stop tracing because we don't have the privilege to avoid
421+
# triggering tracing functions as normal, as we are not already in
422+
# tracing functions
423+
self.stop_trace()
424+
425+
self.async_shim_frame = sys._getframe()
426+
self.async_awaitable = None
427+
428+
while True:
429+
self.async_awaitable = None
430+
# Simulate a trace event
431+
# This should bring up pdb and make pdb believe it's debugging the
432+
# caller frame
433+
self.trace_dispatch(frame, "opcode", None)
434+
if self.async_awaitable is not None:
435+
try:
436+
if self.breaks:
437+
with self.set_enterframe(frame):
438+
# set_continue requires enterframe to work
439+
self.set_continue()
440+
self.start_trace()
441+
await self.async_awaitable
442+
except Exception:
443+
self._error_exc()
444+
else:
445+
break
446+
447+
self.async_shim_frame = None
448+
449+
# start the trace (the actual command is already set by set_* calls)
450+
if self.returnframe is None and self.stoplineno == -1 and not self.breaks:
451+
# This means we did a continue without any breakpoints, we should not
452+
# start the trace
453+
return
454+
455+
self.start_trace()
456+
403457
def sigint_handler(self, signum, frame):
404458
if self.allow_kbdint:
405459
raise KeyboardInterrupt
@@ -782,12 +836,25 @@ def _exec_in_closure(self, source, globals, locals):
782836

783837
return True
784838

785-
def default(self, line):
786-
if line[:1] == '!': line = line[1:].strip()
787-
locals = self.curframe.f_locals
788-
globals = self.curframe.f_globals
839+
def _exec_await(self, source, globals, locals):
840+
""" Run source code that contains await by playing with async shim frame"""
841+
# Put the source in an async function
842+
source_async = (
843+
"async def __pdb_await():\n" +
844+
textwrap.indent(source, " ") + '\n' +
845+
" __pdb_locals.update(locals())"
846+
)
847+
ns = globals | locals
848+
# We use __pdb_locals to do write back
849+
ns["__pdb_locals"] = locals
850+
exec(source_async, ns)
851+
self.async_awaitable = ns["__pdb_await"]()
852+
853+
def _read_code(self, line):
854+
buffer = line
855+
is_await_code = False
856+
code = None
789857
try:
790-
buffer = line
791858
if (code := codeop.compile_command(line + '\n', '<stdin>', 'single')) is None:
792859
# Multi-line mode
793860
with self._enable_multiline_completion():
@@ -800,7 +867,7 @@ def default(self, line):
800867
except (EOFError, KeyboardInterrupt):
801868
self.lastcmd = ""
802869
print('\n')
803-
return
870+
return None, None, False
804871
else:
805872
self.stdout.write(continue_prompt)
806873
self.stdout.flush()
@@ -809,20 +876,44 @@ def default(self, line):
809876
self.lastcmd = ""
810877
self.stdout.write('\n')
811878
self.stdout.flush()
812-
return
879+
return None, None, False
813880
else:
814881
line = line.rstrip('\r\n')
815882
buffer += '\n' + line
816883
self.lastcmd = buffer
884+
except SyntaxError as e:
885+
# Maybe it's an await expression/statement
886+
if (
887+
self.async_shim_frame is not None
888+
and e.msg == "'await' outside function"
889+
):
890+
is_await_code = True
891+
else:
892+
raise
893+
894+
return code, buffer, is_await_code
895+
896+
def default(self, line):
897+
if line[:1] == '!': line = line[1:].strip()
898+
locals = self.curframe.f_locals
899+
globals = self.curframe.f_globals
900+
try:
901+
code, buffer, is_await_code = self._read_code(line)
902+
if buffer is None:
903+
return
817904
save_stdout = sys.stdout
818905
save_stdin = sys.stdin
819906
save_displayhook = sys.displayhook
820907
try:
821908
sys.stdin = self.stdin
822909
sys.stdout = self.stdout
823910
sys.displayhook = self.displayhook
824-
if not self._exec_in_closure(buffer, globals, locals):
825-
exec(code, globals, locals)
911+
if is_await_code:
912+
self._exec_await(buffer, globals, locals)
913+
return True
914+
else:
915+
if not self._exec_in_closure(buffer, globals, locals):
916+
exec(code, globals, locals)
826917
finally:
827918
sys.stdout = save_stdout
828919
sys.stdin = save_stdin
@@ -2501,6 +2592,21 @@ def set_trace(*, header=None, commands=None):
25012592
pdb.message(header)
25022593
pdb.set_trace(sys._getframe().f_back, commands=commands)
25032594

2595+
async def set_trace_async(*, header=None, commands=None):
2596+
"""Enter the debugger at the calling stack frame, but in async mode.
2597+
2598+
This should be used as await pdb.set_trace_async(). Users can do await
2599+
if they enter the debugger with this function. Otherwise it's the same
2600+
as set_trace().
2601+
"""
2602+
if Pdb._last_pdb_instance is not None:
2603+
pdb = Pdb._last_pdb_instance
2604+
else:
2605+
pdb = Pdb(mode='inline', backend='monitoring')
2606+
if header is not None:
2607+
pdb.message(header)
2608+
await pdb.set_trace_async(sys._getframe().f_back, commands=commands)
2609+
25042610
# Remote PDB
25052611

25062612
class _PdbServer(Pdb):

0 commit comments

Comments
 (0)