diff --git a/IPython/core/debugger.py b/IPython/core/debugger.py index 5c2887f..89eae38 100644 --- a/IPython/core/debugger.py +++ b/IPython/core/debugger.py @@ -634,6 +634,34 @@ class Pdb(OldPdb): do_w = do_where +class InterruptiblePdb(Pdb): + """Version of debugger where KeyboardInterrupt exits the debugger altogether.""" + + def cmdloop(self): + """Wrap cmdloop() such that KeyboardInterrupt stops the debugger.""" + try: + return OldPdb.cmdloop(self) + except KeyboardInterrupt: + self.stop_here = lambda frame: False + self.do_quit("") + sys.settrace(None) + self.quitting = False + raise + + def _cmdloop(self): + while True: + try: + # keyboard interrupts allow for an easy way to cancel + # the current command, so allow them during interactive input + self.allow_kbdint = True + self.cmdloop() + self.allow_kbdint = False + break + except KeyboardInterrupt: + self.message('--KeyboardInterrupt--') + raise + + def set_trace(frame=None): """ Start debugging from `frame`. diff --git a/IPython/core/tests/test_debugger.py b/IPython/core/tests/test_debugger.py index dcfd9a4..7f3720a 100644 --- a/IPython/core/tests/test_debugger.py +++ b/IPython/core/tests/test_debugger.py @@ -4,8 +4,16 @@ # Copyright (c) IPython Development Team. # Distributed under the terms of the Modified BSD License. +import signal import sys +import time import warnings +from tempfile import NamedTemporaryFile +from subprocess import check_output, CalledProcessError, PIPE +import subprocess +from unittest.mock import patch +import builtins +import bdb import nose.tools as nt @@ -223,3 +231,26 @@ def can_exit(): >>> sys.settrace(old_trace) ''' + + +def test_interruptible_core_debugger(): + """The debugger can be interrupted. + + The presumption is there is some mechanism that causes a KeyboardInterrupt + (this is implemented in ipykernel). We want to ensure the + KeyboardInterrupt cause debugging to cease. + """ + def raising_input(msg="", called=[0]): + called[0] += 1 + if called[0] == 1: + raise KeyboardInterrupt() + else: + raise AssertionError("input() should only be called once!") + + with patch.object(builtins, "input", raising_input): + debugger.InterruptiblePdb().set_trace() + # The way this test will fail is by set_trace() never exiting, + # resulting in a timeout by the test runner. The alternative + # implementation would involve a subprocess, but that adds issues with + # interrupting subprocesses that are rather complex, so it's simpler + # just to do it this way. diff --git a/docs/source/whatsnew/pr/interruptible-debugger.rst b/docs/source/whatsnew/pr/interruptible-debugger.rst new file mode 100644 index 0000000..f501b34 --- /dev/null +++ b/docs/source/whatsnew/pr/interruptible-debugger.rst @@ -0,0 +1,4 @@ +IPython.core.debugger.Pdb is now interruptible +============================================== + +A ``KeyboardInterrupt`` will now interrupt IPython's extended debugger, in order to make Jupyter able to interrupt it.