Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion Doc/library/pdb.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down Expand Up @@ -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)
Expand Down
25 changes: 14 additions & 11 deletions Lib/pdb.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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

Expand All @@ -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
Expand Down
122 changes: 122 additions & 0 deletions Lib/test/test_pdb.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import os
import pdb
import re
import signal
import sys
import types
import codecs
Expand Down Expand Up @@ -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')
> <doctest test.test_pdb.test_pdb_sigint_continue[1]>(2)caller()
-> result = callee()
(Pdb) break 3
Breakpoint 1 at <doctest test.test_pdb.test_pdb_sigint_continue[1]>:3
(Pdb) continue
<BLANKLINE>
Program interrupted. (Use 'cont' to resume).
> <doctest test.test_pdb.test_pdb_sigint_continue[0]>(2)callee()
-> signal.raise_signal(signal.SIGINT)
(Pdb) continue
> <doctest test.test_pdb.test_pdb_sigint_continue[1]>(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')
> <doctest test.test_pdb.test_pdb_sigint_next[1]>(2)caller()
-> result = callee()
(Pdb) next
<BLANKLINE>
Program interrupted. (Use 'cont' to resume).
> <doctest test.test_pdb.test_pdb_sigint_next[0]>(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')
> <doctest test.test_pdb.test_pdb_sigint_until[1]>(2)caller()
-> result = callee()
(Pdb) until 3
<BLANKLINE>
Program interrupted. (Use 'cont' to resume).
> <doctest test.test_pdb.test_pdb_sigint_until[0]>(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')
> <doctest test.test_pdb.test_pdb_sigint_return[1]>(2)caller()
-> result = callee()
(Pdb) return
<BLANKLINE>
Program interrupted. (Use 'cont' to resume).
> <doctest test.test_pdb.test_pdb_sigint_return[0]>(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.

Expand Down Expand Up @@ -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.
Expand Down
Original file line number Diff line number Diff line change
@@ -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``.
Loading