diff --git a/IPython/core/debugger.py b/IPython/core/debugger.py index e2676a6..11f1bec 100644 --- a/IPython/core/debugger.py +++ b/IPython/core/debugger.py @@ -2,6 +2,71 @@ """ Pdb debugger class. + +This is an extension to PDB which adds a number of new features. +Note that there is also the `IPython.terminal.debugger` class which provides UI +improvements. + +We also strongly recommend to use this via the `ipdb` package, which provides +extra configuration options. + +Among other things, this subclass of PDB: + - supports many IPython magics like pdef/psource + - hide frames in tracebacks based on `__tracebackhide__` + - allows to skip frames based on `__debuggerskip__` + +The skipping and hiding frames are configurable via the `skip_predicates` +command. + +By default, frames from readonly files will be hidden, frames containing +``__tracebackhide__=True`` will be hidden. + +Frames containing ``__debuggerskip__`` will be stepped over, frames who's parent +frames value of ``__debuggerskip__`` is ``True`` will be skipped. + + >>> def helper_1(): + ... print("don't step in me") + ... + ... + ... def helper_2(): + ... print("in me neither") + ... + +One can define a decorator that wraps a function between the two helpers: + + >>> def pdb_skipped_decorator(function): + ... + ... + ... def wrapped_fn(*args, **kwargs): + ... __debuggerskip__ = True + ... helper_1() + ... __debuggerskip__ = False + ... result = function(*args, **kwargs) + ... __debuggerskip__ = True + ... helper_2() + ... return result + ... + ... return wrapped_fn + +When decorating a function, ipdb will directly step into ``bar()`` by +default: + + >>> @foo_decorator + ... def bar(x, y): + ... return x * y + + +You can toggle the behavior with + + ipdb> skip_predicates debuggerskip false + +or configure it in your ``.pdbrc`` + + + +Licencse +-------- + Modified from the standard pdb.Pdb class to avoid including readline, so that the command line completion of other programs which include this isn't damaged. @@ -9,11 +74,16 @@ damaged. In the future, this class will be expanded with improvements over the standard pdb. -The code in this file is mainly lifted out of cmd.py in Python 2.2, with minor -changes. Licensing should therefore be under the standard Python terms. For -details on the PSF (Python Software Foundation) standard license, see: +The original code in this file is mainly lifted out of cmd.py in Python 2.2, +with minor changes. Licensing should therefore be under the standard Python +terms. For details on the PSF (Python Software Foundation) standard license, +see: https://docs.python.org/2/license.html + + +All the changes since then are under the same license as IPython. + """ #***************************************************************************** @@ -51,6 +121,8 @@ from pdb import Pdb as OldPdb # it does so with some limitations. The rest of this support is implemented in # the Tracer constructor. +DEBUGGERSKIP = "__debuggerskip__" + def make_arrow(pad): """generate the leading arrow in front of traceback or debugger""" @@ -206,7 +278,12 @@ class Pdb(OldPdb): """ - default_predicates = {"tbhide": True, "readonly": False, "ipython_internal": True} + default_predicates = { + "tbhide": True, + "readonly": False, + "ipython_internal": True, + "debuggerskip": True, + } def __init__(self, color_scheme=None, completekey=None, stdin=None, stdout=None, context=5, **kwargs): @@ -304,6 +381,7 @@ class Pdb(OldPdb): # list of predicates we use to skip frames self._predicates = self.default_predicates + # def set_colors(self, scheme): """Shorthand access to the color table scheme selector method.""" self.color_scheme_table.set_active_scheme(scheme) @@ -815,7 +893,50 @@ class Pdb(OldPdb): do_w = do_where + def break_anywhere(self, frame): + """ + + _stop_in_decorator_internals is overly restrictive, as we may still want + to trace function calls, so we need to also update break_anywhere so + that is we don't `stop_here`, because of debugger skip, we may still + stop at any point inside the function + + """ + if self._predicates["debuggerskip"]: + if DEBUGGERSKIP in frame.f_code.co_varnames: + return True + if frame.f_back and self._get_frame_locals(frame.f_back).get(DEBUGGERSKIP): + return True + return super().break_anywhere(frame) + + @skip_doctest + def _is_in_decorator_internal_and_should_skip(self, frame): + """ + Utility to tell us whether we are in a decorator internal and should stop. + + + + """ + + # if we are disabled don't skip + if not self._predicates["debuggerskip"]: + return False + + # if frame is tagged, skip by default. + if DEBUGGERSKIP in frame.f_code.co_varnames: + return True + + # if parent frame value set to True skip as well. + if frame.f_back and self._get_frame_locals(frame.f_back).get(DEBUGGERSKIP): + return True + + return False + def stop_here(self, frame): + + if self._is_in_decorator_internal_and_should_skip(frame) is True: + return False + hidden = False if self.skip_hidden: hidden = self._hidden_predicate(frame) @@ -938,10 +1059,10 @@ class Pdb(OldPdb): class InterruptiblePdb(Pdb): """Version of debugger where KeyboardInterrupt exits the debugger altogether.""" - def cmdloop(self): + def cmdloop(self, intro=None): """Wrap cmdloop() such that KeyboardInterrupt stops the debugger.""" try: - return OldPdb.cmdloop(self) + return OldPdb.cmdloop(self, intro=intro) except KeyboardInterrupt: self.stop_here = lambda frame: False self.do_quit("") diff --git a/IPython/core/tests/test_debugger.py b/IPython/core/tests/test_debugger.py index 35e77e4..217eb6b 100644 --- a/IPython/core/tests/test_debugger.py +++ b/IPython/core/tests/test_debugger.py @@ -323,6 +323,118 @@ def test_xmode_skip(): child.close() +skip_decorators_blocks = ( + """ + def helper_1(): + pass # should not stop here + """, + """ + def helper_2(): + pass # should not stop here + """, + """ + def pdb_skipped_decorator(function): + def wrapped_fn(*args, **kwargs): + __debuggerskip__ = True + helper_1() + __debuggerskip__ = False + result = function(*args, **kwargs) + __debuggerskip__ = True + helper_2() + return result + return wrapped_fn + """, + """ + @pdb_skipped_decorator + def bar(x, y): + return x * y + """, + """import IPython.terminal.debugger as ipdb""", + """ + def f(): + ipdb.set_trace() + bar(3, 4) + """, + """ + f() + """, +) + + +def _decorator_skip_setup(): + import pexpect + + env = os.environ.copy() + env["IPY_TEST_SIMPLE_PROMPT"] = "1" + + child = pexpect.spawn( + sys.executable, ["-m", "IPython", "--colors=nocolor"], env=env + ) + child.timeout = 5 * IPYTHON_TESTING_TIMEOUT_SCALE + + child.expect("IPython") + child.expect("\n") + + dedented_blocks = [dedent(b).strip() for b in skip_decorators_blocks] + in_prompt_number = 1 + for cblock in dedented_blocks: + child.expect_exact(f"In [{in_prompt_number}]:") + in_prompt_number += 1 + for line in cblock.splitlines(): + child.sendline(line) + child.expect_exact(line) + child.sendline("") + return child + + +@skip_win32 +def test_decorator_skip(): + """test that decorator frames can be skipped.""" + + child = _decorator_skip_setup() + + child.expect_exact("3 bar(3, 4)") + child.expect("ipdb>") + + child.expect("ipdb>") + child.sendline("step") + child.expect_exact("step") + + child.expect_exact("1 @pdb_skipped_decorator") + + child.sendline("s") + child.expect_exact("return x * y") + + child.close() + + +@skip_win32 +def test_decorator_skip_disabled(): + """test that decorator frame skipping can be disabled""" + + child = _decorator_skip_setup() + + child.expect_exact("3 bar(3, 4)") + + for input_, expected in [ + ("skip_predicates debuggerskip False", ""), + ("skip_predicates", "debuggerskip : False"), + ("step", "---> 2 def wrapped_fn"), + ("step", "----> 3 __debuggerskip__"), + ("step", "----> 4 helper_1()"), + ("step", "---> 1 def helper_1():"), + ("next", "----> 2 pass"), + ("next", "--Return--"), + ("next", "----> 5 __debuggerskip__ = False"), + ]: + child.expect("ipdb>") + child.sendline(input_) + child.expect_exact(input_) + child.expect_exact(expected) + + child.close() + + @skip_win32 def test_where_erase_value(): """Test that `where` does not access f_locals and erase values.""" diff --git a/IPython/terminal/debugger.py b/IPython/terminal/debugger.py index 5c9c361..6ba73c7 100644 --- a/IPython/terminal/debugger.py +++ b/IPython/terminal/debugger.py @@ -79,7 +79,7 @@ class TerminalPdb(Pdb): enable_history_search=True, mouse_support=self.shell.mouse_support, complete_style=self.shell.pt_complete_style, - style=self.shell.style, + style=getattr(self.shell, "style", None), color_depth=self.shell.color_depth, ) @@ -104,7 +104,6 @@ class TerminalPdb(Pdb): # prompt itself in a different thread (we can't start an event loop # within an event loop). This new thread won't have any event loop # running, and here we run our prompt-loop. - self.preloop() try: @@ -139,7 +138,6 @@ class TerminalPdb(Pdb): if keyboard_interrupt: raise KeyboardInterrupt - line = self.precmd(line) stop = self.onecmd(line) stop = self.postcmd(stop, line) diff --git a/docs/source/whatsnew/version7.rst b/docs/source/whatsnew/version7.rst index 84fccd2..990e0eb 100644 --- a/docs/source/whatsnew/version7.rst +++ b/docs/source/whatsnew/version7.rst @@ -2,6 +2,7 @@ 7.x Series ============ + .. _version 7.28: IPython 7.28 @@ -543,7 +544,7 @@ Change of API and exposed objects automatically detected using `frappuccino `_ (still in beta): -The following items are new and mostly related to understanding ``__tracebackbide__``:: +The following items are new and mostly related to understanding ``__tracebackhide__``:: + IPython.core.debugger.Pdb.do_down(self, arg) + IPython.core.debugger.Pdb.do_skip_hidden(self, arg)