Skip to content

Commit bfec9d8

Browse files
committed
gh-66333: Handle Ctrl-C during pdb step, next, and until
1 parent c71bf4d commit bfec9d8

4 files changed

Lines changed: 149 additions & 12 deletions

File tree

‎Doc/library/pdb.rst‎

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -278,7 +278,8 @@ access further features, you have to do this yourself:
278278
that matches one of these patterns. [1]_
279279

280280
By default, Pdb sets a handler for the SIGINT signal (which is sent when the
281-
user presses :kbd:`Ctrl-C` on the console) when you give a :pdbcmd:`continue` command.
281+
user presses :kbd:`Ctrl-C` on the console) when you resume execution with
282+
:pdbcmd:`continue`, :pdbcmd:`step`, :pdbcmd:`next`, or :pdbcmd:`until`.
282283
This allows you to break into the debugger again by pressing :kbd:`Ctrl-C`. If you
283284
want Pdb not to touch the SIGINT handler, set *nosigint* to true.
284285

@@ -328,6 +329,10 @@ access further features, you have to do this yourself:
328329
Inline breakpoints like :func:`breakpoint` or :func:`pdb.set_trace` will
329330
always stop the program at calling frame, ignoring the *skip* pattern (if any).
330331

332+
.. versionchanged:: 3.16
333+
SIGINT handling also applies to :pdbcmd:`step`, :pdbcmd:`next`, and
334+
:pdbcmd:`until`.
335+
331336
.. method:: run(statement, globals=None, locals=None)
332337
runeval(expression, globals=None, locals=None)
333338
runcall(function, *args, **kwds)

‎Lib/pdb.py‎

Lines changed: 21 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -628,11 +628,20 @@ async def set_trace_async(self, frame=None, *, commands=None):
628628

629629
self.start_trace()
630630

631+
def _install_sigint_handler(self):
632+
if self.nosigint:
633+
return
634+
try:
635+
Pdb._previous_sigint_handler = \
636+
signal.signal(signal.SIGINT, self.sigint_handler)
637+
except ValueError:
638+
# signal only works in the main thread.
639+
pass
640+
631641
def sigint_handler(self, signum, frame):
632642
if self.allow_kbdint:
633643
raise KeyboardInterrupt
634644
self.message("\nProgram interrupted. (Use 'cont' to resume).")
635-
self.set_step()
636645
self.set_trace(frame)
637646

638647
def reset(self):
@@ -690,6 +699,13 @@ def curframe_locals(self, value):
690699

691700
# Override Bdb methods
692701

702+
def dispatch_call(self, frame, arg):
703+
# Do not step into the debugger's own signal handler.
704+
if frame.f_code is Pdb.sigint_handler.__code__:
705+
self.stop_trace()
706+
return
707+
return super().dispatch_call(frame, arg)
708+
693709
def user_call(self, frame, argument_list):
694710
"""This method is called when there is the remote possibility
695711
that we ever need to stop in this function."""
@@ -2045,6 +2061,7 @@ def do_until(self, arg):
20452061
else:
20462062
lineno = None
20472063
self.set_until(self.curframe, lineno)
2064+
self._install_sigint_handler()
20482065
return 1
20492066
do_unt = do_until
20502067

@@ -2059,6 +2076,7 @@ def do_step(self, arg):
20592076
self._print_invalid_arg(arg)
20602077
return
20612078
self.set_step()
2079+
self._install_sigint_handler()
20622080
return 1
20632081
do_s = do_step
20642082

@@ -2072,6 +2090,7 @@ def do_next(self, arg):
20722090
self._print_invalid_arg(arg)
20732091
return
20742092
self.set_next(self.curframe)
2093+
self._install_sigint_handler()
20752094
return 1
20762095
do_n = do_next
20772096

@@ -2121,16 +2140,7 @@ def do_continue(self, arg):
21212140
if arg:
21222141
self._print_invalid_arg(arg)
21232142
return
2124-
if not self.nosigint:
2125-
try:
2126-
Pdb._previous_sigint_handler = \
2127-
signal.signal(signal.SIGINT, self.sigint_handler)
2128-
except ValueError:
2129-
# ValueError happens when do_continue() is invoked from
2130-
# a non-main thread in which case we just continue without
2131-
# SIGINT set. Would printing a message here (once) make
2132-
# sense?
2133-
pass
2143+
self._install_sigint_handler()
21342144
self.set_continue()
21352145
return 1
21362146
do_c = do_cont = do_continue

‎Lib/test/test_pdb.py‎

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import os
88
import pdb
99
import re
10+
import signal
1011
import sys
1112
import types
1213
import codecs
@@ -2973,6 +2974,118 @@ def test_pdb_with_inline_breakpoint():
29732974
(Pdb) c
29742975
"""
29752976

2977+
def test_pdb_sigint_continue():
2978+
"""SIGINT interrupts continue with a breakpoint set.
2979+
2980+
>>> def callee():
2981+
... signal.raise_signal(signal.SIGINT)
2982+
... return 42
2983+
>>> def caller():
2984+
... result = callee()
2985+
... return result
2986+
2987+
With a breakpoint after the call, tracing remains active. The signal
2988+
stops in the callee, and continuing then reaches the breakpoint.
2989+
2990+
>>> with PdbTestInput(['break 3', 'continue', 'continue', 'continue']):
2991+
... try:
2992+
... pdb.Pdb(readrc=False).runcall(caller)
2993+
... except KeyboardInterrupt:
2994+
... raise AssertionError('SIGINT unwound the debuggee')
2995+
> <doctest test.test_pdb.test_pdb_sigint_continue[1]>(2)caller()
2996+
-> result = callee()
2997+
(Pdb) break 3
2998+
Breakpoint 1 at <doctest test.test_pdb.test_pdb_sigint_continue[1]>:3
2999+
(Pdb) continue
3000+
<BLANKLINE>
3001+
Program interrupted. (Use 'cont' to resume).
3002+
> <doctest test.test_pdb.test_pdb_sigint_continue[0]>(2)callee()
3003+
-> signal.raise_signal(signal.SIGINT)
3004+
(Pdb) continue
3005+
> <doctest test.test_pdb.test_pdb_sigint_continue[1]>(3)caller()
3006+
-> return result
3007+
(Pdb) continue
3008+
42
3009+
"""
3010+
3011+
3012+
def test_pdb_sigint_step():
3013+
"""SIGINT during step stops in the debuggee, not in Pdb's handler.
3014+
3015+
>>> def test_function():
3016+
... signal.raise_signal(signal.SIGINT)
3017+
... return 42
3018+
3019+
>>> with PdbTestInput(['step', 'continue']):
3020+
... try:
3021+
... pdb.Pdb(readrc=False).runcall(test_function)
3022+
... except KeyboardInterrupt:
3023+
... raise AssertionError('SIGINT unwound the debuggee')
3024+
> <doctest test.test_pdb.test_pdb_sigint_step[0]>(2)test_function()
3025+
-> signal.raise_signal(signal.SIGINT)
3026+
(Pdb) step
3027+
<BLANKLINE>
3028+
Program interrupted. (Use 'cont' to resume).
3029+
> <doctest test.test_pdb.test_pdb_sigint_step[0]>(2)test_function()
3030+
-> signal.raise_signal(signal.SIGINT)
3031+
(Pdb) continue
3032+
42
3033+
"""
3034+
3035+
3036+
def test_pdb_sigint_next():
3037+
"""SIGINT during next stops inside a previously untraced callee.
3038+
3039+
>>> def callee():
3040+
... signal.raise_signal(signal.SIGINT)
3041+
... return 42
3042+
>>> def caller():
3043+
... result = callee()
3044+
... return result
3045+
3046+
>>> with PdbTestInput(['next', 'continue']):
3047+
... try:
3048+
... pdb.Pdb(readrc=False).runcall(caller)
3049+
... except KeyboardInterrupt:
3050+
... raise AssertionError('SIGINT unwound the debuggee')
3051+
> <doctest test.test_pdb.test_pdb_sigint_next[1]>(2)caller()
3052+
-> result = callee()
3053+
(Pdb) next
3054+
<BLANKLINE>
3055+
Program interrupted. (Use 'cont' to resume).
3056+
> <doctest test.test_pdb.test_pdb_sigint_next[0]>(2)callee()
3057+
-> signal.raise_signal(signal.SIGINT)
3058+
(Pdb) continue
3059+
42
3060+
"""
3061+
3062+
3063+
def test_pdb_sigint_until():
3064+
"""SIGINT during until stops inside a previously untraced callee.
3065+
3066+
>>> def callee():
3067+
... signal.raise_signal(signal.SIGINT)
3068+
... return 42
3069+
>>> def caller():
3070+
... result = callee()
3071+
... return result
3072+
3073+
>>> with PdbTestInput(['until 3', 'continue']):
3074+
... try:
3075+
... pdb.Pdb(readrc=False).runcall(caller)
3076+
... except KeyboardInterrupt:
3077+
... raise AssertionError('SIGINT unwound the debuggee')
3078+
> <doctest test.test_pdb.test_pdb_sigint_until[1]>(2)caller()
3079+
-> result = callee()
3080+
(Pdb) until 3
3081+
<BLANKLINE>
3082+
Program interrupted. (Use 'cont' to resume).
3083+
> <doctest test.test_pdb.test_pdb_sigint_until[0]>(2)callee()
3084+
-> signal.raise_signal(signal.SIGINT)
3085+
(Pdb) continue
3086+
42
3087+
"""
3088+
29763089
def test_pdb_issue_20766():
29773090
"""Test for reference leaks when the SIGINT handler is set.
29783091
@@ -5432,9 +5545,15 @@ def setUpPdbBackend(backend):
54325545
def setUp(test):
54335546
import pdb
54345547
pdb.set_default_backend(backend)
5548+
test.original_sigint_handler = signal.getsignal(signal.SIGINT)
5549+
test.previous_sigint_handler = pdb.Pdb._previous_sigint_handler
54355550
return setUp
54365551

54375552
def tearDown(test):
5553+
# Restore signal state even if a SIGINT doctest fails.
5554+
signal.signal(signal.SIGINT, test.original_sigint_handler)
5555+
pdb.Pdb._previous_sigint_handler = test.previous_sigint_handler
5556+
54385557
# Ensure that asyncio state has been cleared at the end of the test.
54395558
# This prevents a "test altered the execution environment" warning if
54405559
# asyncio features are used.
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
Allow Ctrl-C to interrupt execution resumed with the :mod:`pdb`
2+
``step``, ``next``, and ``until`` commands without unwinding the debugged
3+
program, as with ``continue``.

0 commit comments

Comments
 (0)