diff --git a/Doc/library/pdb.rst b/Doc/library/pdb.rst index c48f2319b43c5b9..e6a3599116b523f 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:`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. @@ -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:`next`, :pdbcmd:`until`, and + :pdbcmd:`return`. + .. 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..13f629ae8180591 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): @@ -2045,6 +2054,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 @@ -2072,6 +2082,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 @@ -2110,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 @@ -2121,16 +2133,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..2addc9e97d7b5b4 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,121 @@ 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_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_sigint_return(): + """SIGINT during return stops inside a previously untraced callee. + + >>> def callee(): + ... signal.raise_signal(signal.SIGINT) + ... return 42 + >>> def caller(): + ... result = callee() + ... return result + + >>> with PdbTestInput(['return', 'continue']): + ... try: + ... pdb.Pdb(readrc=False).runcall(caller) + ... except KeyboardInterrupt: + ... raise AssertionError('SIGINT unwound the debuggee') + > (2)caller() + -> result = callee() + (Pdb) return + + 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 +5548,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..057caa487e9631b --- /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` +``next``, ``until``, and ``return`` commands without unwinding the debugged +program, as with ``continue``.