https://github.com/python/cpython/commit/caee16f05229de5bc5ed2743c531f1696641888a commit: caee16f05229de5bc5ed2743c531f1696641888a branch: main author: Tian Gao <gaogaotiant...@hotmail.com> committer: gaogaotiantian <gaogaotiant...@hotmail.com> date: 2025-04-29T12:28:24-04:00 summary:
gh-121468: Support async breakpoint in pdb (#132576) files: A Misc/NEWS.d/next/Library/2025-04-16-01-41-34.gh-issue-121468.rxgE1z.rst M Doc/library/pdb.rst M Doc/whatsnew/3.14.rst M Lib/pdb.py M Lib/test/test_pdb.py diff --git a/Doc/library/pdb.rst b/Doc/library/pdb.rst index 8601f390aeb9c9..3c8c07074993f2 100644 --- a/Doc/library/pdb.rst +++ b/Doc/library/pdb.rst @@ -188,6 +188,21 @@ slightly different way: .. versionadded:: 3.14 The *commands* argument. + +.. awaitablefunction:: set_trace_async(*, header=None, commands=None) + + async version of :func:`set_trace`. This function should be used inside an + async function with :keyword:`await`. + + .. code-block:: python + + async def f(): + await pdb.set_trace_async() + + :keyword:`await` statements are supported if the debugger is invoked by this function. + + .. versionadded:: 3.14 + .. function:: post_mortem(t=None) Enter post-mortem debugging of the given exception or diff --git a/Doc/whatsnew/3.14.rst b/Doc/whatsnew/3.14.rst index 646a0b4007fc05..128ada4284330b 100644 --- a/Doc/whatsnew/3.14.rst +++ b/Doc/whatsnew/3.14.rst @@ -1168,6 +1168,11 @@ pdb backend by default, which is configurable. (Contributed by Tian Gao in :gh:`124533`.) +* :func:`pdb.set_trace_async` is added to support debugging asyncio + coroutines. :keyword:`await` statements are supported with this + function. + (Contributed by Tian Gao in :gh:`132576`.) + pickle ------ diff --git a/Lib/pdb.py b/Lib/pdb.py index 5ade628e2d5d23..e38621d4533e14 100644 --- a/Lib/pdb.py +++ b/Lib/pdb.py @@ -385,6 +385,9 @@ def __init__(self, completekey='tab', stdin=None, stdout=None, skip=None, self.commands_bnum = None # The breakpoint number for which we are # defining a list + self.async_shim_frame = None + self.async_awaitable = None + self._chained_exceptions = tuple() self._chained_exception_index = 0 @@ -400,6 +403,57 @@ def set_trace(self, frame=None, *, commands=None): super().set_trace(frame) + async def set_trace_async(self, frame=None, *, commands=None): + if self.async_awaitable is not None: + # We are already in a set_trace_async call, do not mess with it + return + + if frame is None: + frame = sys._getframe().f_back + + # We need set_trace to set up the basics, however, this will call + # set_stepinstr() will we need to compensate for, because we don't + # want to trigger on calls + self.set_trace(frame, commands=commands) + # Changing the stopframe will disable trace dispatch on calls + self.stopframe = frame + # We need to stop tracing because we don't have the privilege to avoid + # triggering tracing functions as normal, as we are not already in + # tracing functions + self.stop_trace() + + self.async_shim_frame = sys._getframe() + self.async_awaitable = None + + while True: + self.async_awaitable = None + # Simulate a trace event + # This should bring up pdb and make pdb believe it's debugging the + # caller frame + self.trace_dispatch(frame, "opcode", None) + if self.async_awaitable is not None: + try: + if self.breaks: + with self.set_enterframe(frame): + # set_continue requires enterframe to work + self.set_continue() + self.start_trace() + await self.async_awaitable + except Exception: + self._error_exc() + else: + break + + self.async_shim_frame = None + + # start the trace (the actual command is already set by set_* calls) + if self.returnframe is None and self.stoplineno == -1 and not self.breaks: + # This means we did a continue without any breakpoints, we should not + # start the trace + return + + self.start_trace() + def sigint_handler(self, signum, frame): if self.allow_kbdint: raise KeyboardInterrupt @@ -782,12 +836,25 @@ def _exec_in_closure(self, source, globals, locals): return True - def default(self, line): - if line[:1] == '!': line = line[1:].strip() - locals = self.curframe.f_locals - globals = self.curframe.f_globals + def _exec_await(self, source, globals, locals): + """ Run source code that contains await by playing with async shim frame""" + # Put the source in an async function + source_async = ( + "async def __pdb_await():\n" + + textwrap.indent(source, " ") + '\n' + + " __pdb_locals.update(locals())" + ) + ns = globals | locals + # We use __pdb_locals to do write back + ns["__pdb_locals"] = locals + exec(source_async, ns) + self.async_awaitable = ns["__pdb_await"]() + + def _read_code(self, line): + buffer = line + is_await_code = False + code = None try: - buffer = line if (code := codeop.compile_command(line + '\n', '<stdin>', 'single')) is None: # Multi-line mode with self._enable_multiline_completion(): @@ -800,7 +867,7 @@ def default(self, line): except (EOFError, KeyboardInterrupt): self.lastcmd = "" print('\n') - return + return None, None, False else: self.stdout.write(continue_prompt) self.stdout.flush() @@ -809,11 +876,31 @@ def default(self, line): self.lastcmd = "" self.stdout.write('\n') self.stdout.flush() - return + return None, None, False else: line = line.rstrip('\r\n') buffer += '\n' + line self.lastcmd = buffer + except SyntaxError as e: + # Maybe it's an await expression/statement + if ( + self.async_shim_frame is not None + and e.msg == "'await' outside function" + ): + is_await_code = True + else: + raise + + return code, buffer, is_await_code + + def default(self, line): + if line[:1] == '!': line = line[1:].strip() + locals = self.curframe.f_locals + globals = self.curframe.f_globals + try: + code, buffer, is_await_code = self._read_code(line) + if buffer is None: + return save_stdout = sys.stdout save_stdin = sys.stdin save_displayhook = sys.displayhook @@ -821,8 +908,12 @@ def default(self, line): sys.stdin = self.stdin sys.stdout = self.stdout sys.displayhook = self.displayhook - if not self._exec_in_closure(buffer, globals, locals): - exec(code, globals, locals) + if is_await_code: + self._exec_await(buffer, globals, locals) + return True + else: + if not self._exec_in_closure(buffer, globals, locals): + exec(code, globals, locals) finally: sys.stdout = save_stdout sys.stdin = save_stdin @@ -2501,6 +2592,21 @@ def set_trace(*, header=None, commands=None): pdb.message(header) pdb.set_trace(sys._getframe().f_back, commands=commands) +async def set_trace_async(*, header=None, commands=None): + """Enter the debugger at the calling stack frame, but in async mode. + + This should be used as await pdb.set_trace_async(). Users can do await + if they enter the debugger with this function. Otherwise it's the same + as set_trace(). + """ + if Pdb._last_pdb_instance is not None: + pdb = Pdb._last_pdb_instance + else: + pdb = Pdb(mode='inline', backend='monitoring') + if header is not None: + pdb.message(header) + await pdb.set_trace_async(sys._getframe().f_back, commands=commands) + # Remote PDB class _PdbServer(Pdb): diff --git a/Lib/test/test_pdb.py b/Lib/test/test_pdb.py index 741b5ab92856b7..ae84fe3ce7d65a 100644 --- a/Lib/test/test_pdb.py +++ b/Lib/test/test_pdb.py @@ -1,6 +1,7 @@ # A test suite for pdb; not very comprehensive at the moment. import doctest +import gc import os import pdb import sys @@ -2142,6 +2143,179 @@ def test_pdb_asynctask(): (Pdb) continue """ + def test_pdb_await_support(): + """Testing await support in pdb + + >>> import asyncio + + >>> async def test(): + ... print("hello") + ... await asyncio.sleep(0) + ... print("world") + ... return 42 + + >>> async def main(): + ... import pdb + ... task = asyncio.create_task(test()) + ... await pdb.Pdb(nosigint=True, readrc=False).set_trace_async() + ... pass + + >>> def test_function(): + ... asyncio.run(main(), loop_factory=asyncio.EventLoop) + + >>> with PdbTestInput([ # doctest: +ELLIPSIS + ... 'x = await task', + ... 'p x', + ... 'x = await test()', + ... 'p x', + ... 'new_task = asyncio.create_task(test())', + ... 'await new_task', + ... 'await non_exist()', + ... 's', + ... 'continue', + ... ]): + ... test_function() + > <doctest test.test_pdb.test_pdb_await_support[2]>(4)main() + -> await pdb.Pdb(nosigint=True, readrc=False).set_trace_async() + (Pdb) x = await task + hello + world + > <doctest test.test_pdb.test_pdb_await_support[2]>(4)main() + -> await pdb.Pdb(nosigint=True, readrc=False).set_trace_async() + (Pdb) p x + 42 + (Pdb) x = await test() + hello + world + > <doctest test.test_pdb.test_pdb_await_support[2]>(4)main() + -> await pdb.Pdb(nosigint=True, readrc=False).set_trace_async() + (Pdb) p x + 42 + (Pdb) new_task = asyncio.create_task(test()) + (Pdb) await new_task + hello + world + > <doctest test.test_pdb.test_pdb_await_support[2]>(4)main() + -> await pdb.Pdb(nosigint=True, readrc=False).set_trace_async() + (Pdb) await non_exist() + *** NameError: name 'non_exist' is not defined + > <doctest test.test_pdb.test_pdb_await_support[2]>(4)main() + -> await pdb.Pdb(nosigint=True, readrc=False).set_trace_async() + (Pdb) s + > <doctest test.test_pdb.test_pdb_await_support[2]>(5)main() + -> pass + (Pdb) continue + """ + + def test_pdb_await_with_breakpoint(): + """Testing await support with breakpoints set in tasks + + >>> import asyncio + + >>> async def test(): + ... x = 2 + ... await asyncio.sleep(0) + ... return 42 + + >>> async def main(): + ... import pdb + ... task = asyncio.create_task(test()) + ... await pdb.Pdb(nosigint=True, readrc=False).set_trace_async() + + >>> def test_function(): + ... asyncio.run(main(), loop_factory=asyncio.EventLoop) + + >>> with PdbTestInput([ # doctest: +ELLIPSIS + ... 'b test', + ... 'k = await task', + ... 'n', + ... 'p x', + ... 'continue', + ... 'p k', + ... 'continue', + ... ]): + ... test_function() + > <doctest test.test_pdb.test_pdb_await_with_breakpoint[2]>(4)main() + -> await pdb.Pdb(nosigint=True, readrc=False).set_trace_async() + (Pdb) b test + Breakpoint 1 at <doctest test.test_pdb.test_pdb_await_with_breakpoint[1]>:2 + (Pdb) k = await task + > <doctest test.test_pdb.test_pdb_await_with_breakpoint[1]>(2)test() + -> x = 2 + (Pdb) n + > <doctest test.test_pdb.test_pdb_await_with_breakpoint[1]>(3)test() + -> await asyncio.sleep(0) + (Pdb) p x + 2 + (Pdb) continue + > <doctest test.test_pdb.test_pdb_await_with_breakpoint[2]>(4)main() + -> await pdb.Pdb(nosigint=True, readrc=False).set_trace_async() + (Pdb) p k + 42 + (Pdb) continue + """ + + def test_pdb_await_contextvar(): + """Testing await support context vars + + >>> import asyncio + >>> import contextvars + + >>> var = contextvars.ContextVar('var') + + >>> async def get_var(): + ... return var.get() + + >>> async def set_var(val): + ... var.set(val) + ... return var.get() + + >>> async def main(): + ... var.set(42) + ... import pdb + ... await pdb.Pdb(nosigint=True, readrc=False).set_trace_async() + + >>> def test_function(): + ... asyncio.run(main(), loop_factory=asyncio.EventLoop) + + >>> with PdbTestInput([ + ... 'p var.get()', + ... 'print(await get_var())', + ... 'print(await asyncio.create_task(set_var(100)))', + ... 'p var.get()', + ... 'print(await set_var(99))', + ... 'p var.get()', + ... 'print(await get_var())', + ... 'continue', + ... ]): + ... test_function() + > <doctest test.test_pdb.test_pdb_await_contextvar[5]>(4)main() + -> await pdb.Pdb(nosigint=True, readrc=False).set_trace_async() + (Pdb) p var.get() + 42 + (Pdb) print(await get_var()) + 42 + > <doctest test.test_pdb.test_pdb_await_contextvar[5]>(4)main() + -> await pdb.Pdb(nosigint=True, readrc=False).set_trace_async() + (Pdb) print(await asyncio.create_task(set_var(100))) + 100 + > <doctest test.test_pdb.test_pdb_await_contextvar[5]>(4)main() + -> await pdb.Pdb(nosigint=True, readrc=False).set_trace_async() + (Pdb) p var.get() + 42 + (Pdb) print(await set_var(99)) + 99 + > <doctest test.test_pdb.test_pdb_await_contextvar[5]>(4)main() + -> await pdb.Pdb(nosigint=True, readrc=False).set_trace_async() + (Pdb) p var.get() + 99 + (Pdb) print(await get_var()) + 99 + > <doctest test.test_pdb.test_pdb_await_contextvar[5]>(4)main() + -> await pdb.Pdb(nosigint=True, readrc=False).set_trace_async() + (Pdb) continue + """ + def test_pdb_next_command_for_coroutine(): """Testing skip unwinding stack on yield for coroutines for "next" command @@ -4712,6 +4886,10 @@ def tearDown(test): pdb.Pdb._last_pdb_instance.stop_trace() pdb.Pdb._last_pdb_instance = None + # If garbage objects are collected right after we start tracing, we + # could stop at __del__ of the object which would fail the test. + gc.collect() + tests.addTest( doctest.DocTestSuite( test_pdb, diff --git a/Misc/NEWS.d/next/Library/2025-04-16-01-41-34.gh-issue-121468.rxgE1z.rst b/Misc/NEWS.d/next/Library/2025-04-16-01-41-34.gh-issue-121468.rxgE1z.rst new file mode 100644 index 00000000000000..a46db6b73b7945 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2025-04-16-01-41-34.gh-issue-121468.rxgE1z.rst @@ -0,0 +1 @@ +Add :func:`pdb.set_trace_async` function to support :keyword:`await` statements in :mod:`pdb`. _______________________________________________ Python-checkins mailing list -- python-checkins@python.org To unsubscribe send an email to python-checkins-le...@python.org https://mail.python.org/mailman3/lists/python-checkins.python.org/ Member address: arch...@mail-archive.com