From bfec9d886bcc6584102e2ecee14be55d1468e075 Mon Sep 17 00:00:00 2001 From: devjoe <1658389+devjoe@users.noreply.github.com> Date: Wed, 28 Oct 2026 23:23:26 +0800 Subject: [PATCH 1/2] gh-66333: Handle Ctrl-C during pdb step, next, and until --- Doc/library/pdb.rst | 7 +- Lib/pdb.py | 32 +++-- Lib/test/test_pdb.py | 119 ++++++++++++++++++ ...-28-16-40-00.gh-issue-66333.pdb-sigint.rst | 3 + 4 files changed, 149 insertions(+), 12 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2026-10-28-16-40-00.gh-issue-66333.pdb-sigint.rst diff --git a/Doc/library/pdb.rst b/Doc/library/pdb.rst index c48f2319b43c5b9..b126063328f09eb 100644 --- a/Doc/library/pdb.rst +++ b/Doc/library/pdb.rst @@ -278,7 +278,8 @@ access further features, you have to do this yourself: that matches one of these patterns. [1]_ By default, Pdb sets a handler for the SIGINT signal (which is sent when the - user presses :kbd:`Ctrl-C` on the console) when you give a :pdbcmd:`continue` command. + user presses :kbd:`Ctrl-C` on the console) when you resume execution with + :pdbcmd:`continue`, :pdbcmd:`step`, :pdbcmd:`next`, or :pdbcmd:`until`. This allows you to break into the debugger again by pressing :kbd:`Ctrl-C`. If you want Pdb not to touch the SIGINT handler, set *nosigint* to true. @@ -328,6 +329,10 @@ access further features, you have to do this yourself: Inline breakpoints like :func:`breakpoint` or :func:`pdb.set_trace` will always stop the program at calling frame, ignoring the *skip* pattern (if any). + .. versionchanged:: 3.16 + SIGINT handling also applies to :pdbcmd:`step`, :pdbcmd:`next`, and + :pdbcmd:`until`. + .. method:: run(statement, globals=None, locals=None) runeval(expression, globals=None, locals=None) runcall(function, *args, **kwds) diff --git a/Lib/pdb.py b/Lib/pdb.py index 114634a4469c131..de2a42542345d9b 100644 --- a/Lib/pdb.py +++ b/Lib/pdb.py @@ -628,11 +628,20 @@ async def set_trace_async(self, frame=None, *, commands=None): self.start_trace() + def _install_sigint_handler(self): + if self.nosigint: + return + try: + Pdb._previous_sigint_handler = \ + signal.signal(signal.SIGINT, self.sigint_handler) + except ValueError: + # signal only works in the main thread. + pass + def sigint_handler(self, signum, frame): if self.allow_kbdint: raise KeyboardInterrupt self.message("\nProgram interrupted. (Use 'cont' to resume).") - self.set_step() self.set_trace(frame) def reset(self): @@ -690,6 +699,13 @@ def curframe_locals(self, value): # Override Bdb methods + def dispatch_call(self, frame, arg): + # Do not step into the debugger's own signal handler. + if frame.f_code is Pdb.sigint_handler.__code__: + self.stop_trace() + return + return super().dispatch_call(frame, arg) + def user_call(self, frame, argument_list): """This method is called when there is the remote possibility that we ever need to stop in this function.""" @@ -2045,6 +2061,7 @@ def do_until(self, arg): else: lineno = None self.set_until(self.curframe, lineno) + self._install_sigint_handler() return 1 do_unt = do_until @@ -2059,6 +2076,7 @@ def do_step(self, arg): self._print_invalid_arg(arg) return self.set_step() + self._install_sigint_handler() return 1 do_s = do_step @@ -2072,6 +2090,7 @@ def do_next(self, arg): self._print_invalid_arg(arg) return self.set_next(self.curframe) + self._install_sigint_handler() return 1 do_n = do_next @@ -2121,16 +2140,7 @@ def do_continue(self, arg): if arg: self._print_invalid_arg(arg) return - if not self.nosigint: - try: - Pdb._previous_sigint_handler = \ - signal.signal(signal.SIGINT, self.sigint_handler) - except ValueError: - # ValueError happens when do_continue() is invoked from - # a non-main thread in which case we just continue without - # SIGINT set. Would printing a message here (once) make - # sense? - pass + self._install_sigint_handler() self.set_continue() return 1 do_c = do_cont = do_continue diff --git a/Lib/test/test_pdb.py b/Lib/test/test_pdb.py index cfcc8c6df57b948..13f2d108cfcec8c 100644 --- a/Lib/test/test_pdb.py +++ b/Lib/test/test_pdb.py @@ -7,6 +7,7 @@ import os import pdb import re +import signal import sys import types import codecs @@ -2973,6 +2974,118 @@ def test_pdb_with_inline_breakpoint(): (Pdb) c """ +def test_pdb_sigint_continue(): + """SIGINT interrupts continue with a breakpoint set. + + >>> def callee(): + ... signal.raise_signal(signal.SIGINT) + ... return 42 + >>> def caller(): + ... result = callee() + ... return result + + With a breakpoint after the call, tracing remains active. The signal + stops in the callee, and continuing then reaches the breakpoint. + + >>> with PdbTestInput(['break 3', 'continue', 'continue', 'continue']): + ... try: + ... pdb.Pdb(readrc=False).runcall(caller) + ... except KeyboardInterrupt: + ... raise AssertionError('SIGINT unwound the debuggee') + > (2)caller() + -> result = callee() + (Pdb) break 3 + Breakpoint 1 at :3 + (Pdb) continue + + Program interrupted. (Use 'cont' to resume). + > (2)callee() + -> signal.raise_signal(signal.SIGINT) + (Pdb) continue + > (3)caller() + -> return result + (Pdb) continue + 42 + """ + + +def test_pdb_sigint_step(): + """SIGINT during step stops in the debuggee, not in Pdb's handler. + + >>> def test_function(): + ... signal.raise_signal(signal.SIGINT) + ... return 42 + + >>> with PdbTestInput(['step', 'continue']): + ... try: + ... pdb.Pdb(readrc=False).runcall(test_function) + ... except KeyboardInterrupt: + ... raise AssertionError('SIGINT unwound the debuggee') + > (2)test_function() + -> signal.raise_signal(signal.SIGINT) + (Pdb) step + + Program interrupted. (Use 'cont' to resume). + > (2)test_function() + -> signal.raise_signal(signal.SIGINT) + (Pdb) continue + 42 + """ + + +def test_pdb_sigint_next(): + """SIGINT during next stops inside a previously untraced callee. + + >>> def callee(): + ... signal.raise_signal(signal.SIGINT) + ... return 42 + >>> def caller(): + ... result = callee() + ... return result + + >>> with PdbTestInput(['next', 'continue']): + ... try: + ... pdb.Pdb(readrc=False).runcall(caller) + ... except KeyboardInterrupt: + ... raise AssertionError('SIGINT unwound the debuggee') + > (2)caller() + -> result = callee() + (Pdb) next + + Program interrupted. (Use 'cont' to resume). + > (2)callee() + -> signal.raise_signal(signal.SIGINT) + (Pdb) continue + 42 + """ + + +def test_pdb_sigint_until(): + """SIGINT during until stops inside a previously untraced callee. + + >>> def callee(): + ... signal.raise_signal(signal.SIGINT) + ... return 42 + >>> def caller(): + ... result = callee() + ... return result + + >>> with PdbTestInput(['until 3', 'continue']): + ... try: + ... pdb.Pdb(readrc=False).runcall(caller) + ... except KeyboardInterrupt: + ... raise AssertionError('SIGINT unwound the debuggee') + > (2)caller() + -> result = callee() + (Pdb) until 3 + + Program interrupted. (Use 'cont' to resume). + > (2)callee() + -> signal.raise_signal(signal.SIGINT) + (Pdb) continue + 42 + """ + def test_pdb_issue_20766(): """Test for reference leaks when the SIGINT handler is set. @@ -5432,9 +5545,15 @@ def setUpPdbBackend(backend): def setUp(test): import pdb pdb.set_default_backend(backend) + test.original_sigint_handler = signal.getsignal(signal.SIGINT) + test.previous_sigint_handler = pdb.Pdb._previous_sigint_handler return setUp def tearDown(test): + # Restore signal state even if a SIGINT doctest fails. + signal.signal(signal.SIGINT, test.original_sigint_handler) + pdb.Pdb._previous_sigint_handler = test.previous_sigint_handler + # Ensure that asyncio state has been cleared at the end of the test. # This prevents a "test altered the execution environment" warning if # asyncio features are used. diff --git a/Misc/NEWS.d/next/Library/2026-10-28-16-40-00.gh-issue-66333.pdb-sigint.rst b/Misc/NEWS.d/next/Library/2026-10-28-16-40-00.gh-issue-66333.pdb-sigint.rst new file mode 100644 index 000000000000000..f09e343519cddce --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-10-28-16-40-00.gh-issue-66333.pdb-sigint.rst @@ -0,0 +1,3 @@ +Allow Ctrl-C to interrupt execution resumed with the :mod:`pdb` +``step``, ``next``, and ``until`` commands without unwinding the debugged +program, as with ``continue``. From 6e8ab894db5e3c6c2ec6be744bbcb8d422e4ac72 Mon Sep 17 00:00:00 2001 From: devjoe <1658389+devjoe@users.noreply.github.com> Date: Fri, 6 Nov 2026 15:04:28 +0800 Subject: [PATCH 2/2] gh-66333: Handle Ctrl-C during pdb return instead of step --- Doc/library/pdb.rst | 6 +-- Lib/pdb.py | 9 +--- Lib/test/test_pdb.py | 45 ++++++++++--------- ...-28-16-40-00.gh-issue-66333.pdb-sigint.rst | 2 +- 4 files changed, 29 insertions(+), 33 deletions(-) diff --git a/Doc/library/pdb.rst b/Doc/library/pdb.rst index b126063328f09eb..e6a3599116b523f 100644 --- a/Doc/library/pdb.rst +++ b/Doc/library/pdb.rst @@ -279,7 +279,7 @@ access further features, you have to do this yourself: By default, Pdb sets a handler for the SIGINT signal (which is sent when the user presses :kbd:`Ctrl-C` on the console) when you resume execution with - :pdbcmd:`continue`, :pdbcmd:`step`, :pdbcmd:`next`, or :pdbcmd:`until`. + :pdbcmd:`continue`, :pdbcmd:`next`, :pdbcmd:`until`, or :pdbcmd:`return`. This allows you to break into the debugger again by pressing :kbd:`Ctrl-C`. If you want Pdb not to touch the SIGINT handler, set *nosigint* to true. @@ -330,8 +330,8 @@ access further features, you have to do this yourself: always stop the program at calling frame, ignoring the *skip* pattern (if any). .. versionchanged:: 3.16 - SIGINT handling also applies to :pdbcmd:`step`, :pdbcmd:`next`, and - :pdbcmd:`until`. + SIGINT handling also applies to :pdbcmd:`next`, :pdbcmd:`until`, and + :pdbcmd:`return`. .. method:: run(statement, globals=None, locals=None) runeval(expression, globals=None, locals=None) diff --git a/Lib/pdb.py b/Lib/pdb.py index de2a42542345d9b..13f629ae8180591 100644 --- a/Lib/pdb.py +++ b/Lib/pdb.py @@ -699,13 +699,6 @@ def curframe_locals(self, value): # Override Bdb methods - def dispatch_call(self, frame, arg): - # Do not step into the debugger's own signal handler. - if frame.f_code is Pdb.sigint_handler.__code__: - self.stop_trace() - return - return super().dispatch_call(frame, arg) - def user_call(self, frame, argument_list): """This method is called when there is the remote possibility that we ever need to stop in this function.""" @@ -2076,7 +2069,6 @@ def do_step(self, arg): self._print_invalid_arg(arg) return self.set_step() - self._install_sigint_handler() return 1 do_s = do_step @@ -2129,6 +2121,7 @@ def do_return(self, arg): self._print_invalid_arg(arg) return self.set_return(self.curframe) + self._install_sigint_handler() return 1 do_r = do_return diff --git a/Lib/test/test_pdb.py b/Lib/test/test_pdb.py index 13f2d108cfcec8c..2addc9e97d7b5b4 100644 --- a/Lib/test/test_pdb.py +++ b/Lib/test/test_pdb.py @@ -3009,32 +3009,35 @@ def test_pdb_sigint_continue(): """ -def test_pdb_sigint_step(): - """SIGINT during step stops in the debuggee, not in Pdb's handler. +def test_pdb_sigint_next(): + """SIGINT during next stops inside a previously untraced callee. - >>> def test_function(): + >>> def callee(): ... signal.raise_signal(signal.SIGINT) ... return 42 + >>> def caller(): + ... result = callee() + ... return result - >>> with PdbTestInput(['step', 'continue']): + >>> with PdbTestInput(['next', 'continue']): ... try: - ... pdb.Pdb(readrc=False).runcall(test_function) + ... pdb.Pdb(readrc=False).runcall(caller) ... except KeyboardInterrupt: ... raise AssertionError('SIGINT unwound the debuggee') - > (2)test_function() - -> signal.raise_signal(signal.SIGINT) - (Pdb) step + > (2)caller() + -> result = callee() + (Pdb) next Program interrupted. (Use 'cont' to resume). - > (2)test_function() + > (2)callee() -> signal.raise_signal(signal.SIGINT) (Pdb) continue 42 """ -def test_pdb_sigint_next(): - """SIGINT during next stops inside a previously untraced callee. +def test_pdb_sigint_until(): + """SIGINT during until stops inside a previously untraced callee. >>> def callee(): ... signal.raise_signal(signal.SIGINT) @@ -3043,25 +3046,25 @@ def test_pdb_sigint_next(): ... result = callee() ... return result - >>> with PdbTestInput(['next', 'continue']): + >>> with PdbTestInput(['until 3', 'continue']): ... try: ... pdb.Pdb(readrc=False).runcall(caller) ... except KeyboardInterrupt: ... raise AssertionError('SIGINT unwound the debuggee') - > (2)caller() + > (2)caller() -> result = callee() - (Pdb) next + (Pdb) until 3 Program interrupted. (Use 'cont' to resume). - > (2)callee() + > (2)callee() -> signal.raise_signal(signal.SIGINT) (Pdb) continue 42 """ -def test_pdb_sigint_until(): - """SIGINT during until stops inside a previously untraced callee. +def test_pdb_sigint_return(): + """SIGINT during return stops inside a previously untraced callee. >>> def callee(): ... signal.raise_signal(signal.SIGINT) @@ -3070,17 +3073,17 @@ def test_pdb_sigint_until(): ... result = callee() ... return result - >>> with PdbTestInput(['until 3', 'continue']): + >>> with PdbTestInput(['return', 'continue']): ... try: ... pdb.Pdb(readrc=False).runcall(caller) ... except KeyboardInterrupt: ... raise AssertionError('SIGINT unwound the debuggee') - > (2)caller() + > (2)caller() -> result = callee() - (Pdb) until 3 + (Pdb) return Program interrupted. (Use 'cont' to resume). - > (2)callee() + > (2)callee() -> signal.raise_signal(signal.SIGINT) (Pdb) continue 42 diff --git a/Misc/NEWS.d/next/Library/2026-10-28-16-40-00.gh-issue-66333.pdb-sigint.rst b/Misc/NEWS.d/next/Library/2026-10-28-16-40-00.gh-issue-66333.pdb-sigint.rst index f09e343519cddce..057caa487e9631b 100644 --- a/Misc/NEWS.d/next/Library/2026-10-28-16-40-00.gh-issue-66333.pdb-sigint.rst +++ b/Misc/NEWS.d/next/Library/2026-10-28-16-40-00.gh-issue-66333.pdb-sigint.rst @@ -1,3 +1,3 @@ Allow Ctrl-C to interrupt execution resumed with the :mod:`pdb` -``step``, ``next``, and ``until`` commands without unwinding the debugged +``next``, ``until``, and ``return`` commands without unwinding the debugged program, as with ``continue``.