From ac35a5b3ad19735edf714c4f4faf23b42fd899f6 2021-10-07 16:23:40 From: Matthias Bussonnier Date: 2021-10-07 16:23:40 Subject: [PATCH] Backport PR #13175: Expand and Fix PDB skip. --- diff --git a/IPython/core/debugger.py b/IPython/core/debugger.py index 69cc55f..6d23336 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 @@ -892,12 +897,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): @@ -916,9 +925,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 08a2473..eb3b5f3 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 @@ -328,14 +329,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 @@ -349,6 +366,7 @@ skip_decorators_blocks = ( """, """ @pdb_skipped_decorator + @pdb_skipped_decorator2 def bar(x, y): return x * y """, @@ -426,7 +444,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"), ]: @@ -439,6 +457,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