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

Appearance settings

Commit caee16f

Browse filesBrowse files
gh-121468: Support async breakpoint in pdb (#132576)
1 parent 4265854 commit caee16f
Copy full SHA for caee16f

File tree

Expand file treeCollapse file tree

5 files changed

+314
-9
lines changed
Filter options
Expand file treeCollapse file tree

5 files changed

+314
-9
lines changed

‎Doc/library/pdb.rst

Copy file name to clipboardExpand all lines: Doc/library/pdb.rst
+15Lines 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

Copy file name to clipboardExpand all lines: Doc/whatsnew/3.14.rst
+5Lines 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

Copy file name to clipboardExpand all lines: Lib/pdb.py
+115-9Lines 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 (0)
Morty Proxy This is a proxified and sanitized view of the page, visit original site.