From 65c34c49df75dce643a87f2ecee39c3a5f9aa31a 2021-10-05 01:42:09 From: Matthias Bussonnier Date: 2021-10-05 01:42:09 Subject: [PATCH] Expand and Fix PDB skip. This expand and fix the logic arround PBD skip. 1) it should support nested decorators, as long as they are all marked. 2) it will stop into breakpoints if those are set even in debuggerskip. 3) Documentation mentioned those facts. --- diff --git a/IPython/core/debugger.py b/IPython/core/debugger.py index 11f1bec..0083f53 100644 --- a/IPython/core/debugger.py +++ b/IPython/core/debugger.py @@ -24,8 +24,12 @@ By default, frames from readonly files will be hidden, frames containing Frames containing ``__debuggerskip__`` will be stepped over, frames who's parent frames value of ``__debuggerskip__`` is ``True`` will be skipped. - >>> def helper_1(): + >>> def helpers_helper(): + ... pass + ... + ... def helper_1(): ... print("don't step in me") + ... helpers_helpers() # will be stepped over unless breakpoint set. ... ... ... def helper_2(): @@ -44,6 +48,7 @@ One can define a decorator that wraps a function between the two helpers: ... result = function(*args, **kwargs) ... __debuggerskip__ = True ... helper_2() + ... # setting __debuggerskip__ to False again is not necessary ... return result ... ... return wrapped_fn @@ -902,12 +907,16 @@ class Pdb(OldPdb): stop at any point inside the function """ + + sup = super().break_anywhere(frame) + if sup: + return sup 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) + return False @skip_doctest def _is_in_decorator_internal_and_should_skip(self, frame): @@ -926,9 +935,13 @@ class Pdb(OldPdb): 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 + # if one of the parent frame value set to True skip as well. + + cframe = frame + while getattr(cframe, "f_back", None): + cframe = cframe.f_back + if self._get_frame_locals(cframe).get(DEBUGGERSKIP): + return True return False diff --git a/IPython/core/tests/test_debugger.py b/IPython/core/tests/test_debugger.py index 217eb6b..7e1a07d 100644 --- a/IPython/core/tests/test_debugger.py +++ b/IPython/core/tests/test_debugger.py @@ -12,6 +12,7 @@ import subprocess import sys import time import warnings + from subprocess import PIPE, CalledProcessError, check_output from tempfile import NamedTemporaryFile from textwrap import dedent @@ -325,14 +326,30 @@ def test_xmode_skip(): skip_decorators_blocks = ( """ + def helpers_helper(): + pass # should not stop here except breakpoint + """, + """ def helper_1(): - pass # should not stop here + helpers_helper() # should not stop here """, """ def helper_2(): pass # should not stop here """, """ + def pdb_skipped_decorator2(function): + def wrapped_fn(*args, **kwargs): + __debuggerskip__ = True + helper_2() + __debuggerskip__ = False + result = function(*args, **kwargs) + __debuggerskip__ = True + helper_2() + return result + return wrapped_fn + """, + """ def pdb_skipped_decorator(function): def wrapped_fn(*args, **kwargs): __debuggerskip__ = True @@ -346,6 +363,7 @@ skip_decorators_blocks = ( """, """ @pdb_skipped_decorator + @pdb_skipped_decorator2 def bar(x, y): return x * y """, @@ -423,7 +441,7 @@ def test_decorator_skip_disabled(): ("step", "----> 3 __debuggerskip__"), ("step", "----> 4 helper_1()"), ("step", "---> 1 def helper_1():"), - ("next", "----> 2 pass"), + ("next", "----> 2 helpers_helper()"), ("next", "--Return--"), ("next", "----> 5 __debuggerskip__ = False"), ]: @@ -436,6 +454,62 @@ def test_decorator_skip_disabled(): @skip_win32 +def test_decorator_skip_with_breakpoint(): + """test that decorator frame skipping can be disabled""" + + 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") + + ### we need a filename, so we need to exec the full block with a filename + with NamedTemporaryFile(suffix=".py", dir=".", delete=True) as tf: + + name = tf.name[:-3].split("/")[-1] + tf.write("\n".join([dedent(x) for x in skip_decorators_blocks[:-1]]).encode()) + tf.flush() + codeblock = f"from {name} import f" + + dedented_blocks = [ + codeblock, + "f()", + ] + + 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("") + + # as the filename does not exists, we'll rely on the filename prompt + child.expect_exact("47 bar(3, 4)") + + for input_, expected in [ + (f"b {name}.py:3", ""), + ("step", "1---> 3 pass # should not stop here except"), + ("step", "---> 38 @pdb_skipped_decorator"), + ("continue", ""), + ]: + 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.""" import pexpect