From 71ee8fe8abe6ca2d11c7cfcc1fc55937b1dcdea2 Mon Sep 17 00:00:00 2001 From: krassowski <5832902+krassowski@users.noreply.github.com> Date: Thu, 24 Sep 2026 13:45:17 +0100 Subject: [PATCH 1/6] Add a test for incorrect debugger frame --- tests/test_interactive.py | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/tests/test_interactive.py b/tests/test_interactive.py index 4203c705..b92cf05f 100644 --- a/tests/test_interactive.py +++ b/tests/test_interactive.py @@ -3703,6 +3703,32 @@ def test_breakpoint_auto_import_p_1(enter_debugger, frontend): """, frontend=frontend) +# "%debug pass" is excluded: it runs the given statement under the debugger, +# so it legitimately stops elsewhere. +caller_frame_debugger_activations = [ + "breakpoint()", + "import ipdb; ipdb.set_trace()", +] + + +@pytest.mark.parametrize("enter_debugger", caller_frame_debugger_activations) +def test_breakpoint_stops_in_caller_frame_1(enter_debugger, frontend): + # Verify that the debugger stops in the frame that entered it + ipython(f""" + In [1]: import pyflyby; pyflyby.enable_auto_importer() + In [2]: def f(): + ...: wallaby53023190 = 1 + ...: {enter_debugger} + ...: + In [3]: f() + > (3)f() + .... + ipdb> p wallaby53023190 + 1 + ipdb> q + """, frontend=frontend) + + @pytest.mark.skipif( (3, 14, 1) <= sys.version_info[:3] <= (3, 14, 5), reason="linux + >=3.14.1 show some extra stacks.", From d71511d5cc42e50c694660052eb9c96ed8fc1319 Mon Sep 17 00:00:00 2001 From: krassowski <5832902+krassowski@users.noreply.github.com> Date: Fri, 16 Oct 2026 09:37:43 +0100 Subject: [PATCH 2/6] Rewrite the frame via `_skip_frames` and pin `stopframe` --- lib/python/pyflyby/_interactive.py | 36 ++++++++++++++++++++++++------ 1 file changed, 29 insertions(+), 7 deletions(-) diff --git a/lib/python/pyflyby/_interactive.py b/lib/python/pyflyby/_interactive.py index 4112a432..e4bdaa2c 100644 --- a/lib/python/pyflyby/_interactive.py +++ b/lib/python/pyflyby/_interactive.py @@ -6,7 +6,7 @@ import ast import builtins -from contextlib import contextmanager +from contextlib import ExitStack, contextmanager import errno import inspect import logging @@ -1570,6 +1570,7 @@ def safe_execfile_with_autoimport(filename, return False def _enable_debugger_hook(self, ip): + import pdb try: Pdb = _get_IPdb_class() except Exception as e: @@ -1590,12 +1591,33 @@ def Pdb_with_autoimport(self_pdb, *args): def TerminalPdb_with_autoimport(self_pdb, *args): __original__(self_pdb, *args) _enable_terminal_pdb_hooks(self_pdb, self) - with AdviceCtx(Pdb.__init__, Pdb_with_autoimport): - if TerminalPdb is None: - yield - else: - with AdviceCtx(TerminalPdb.__init__, TerminalPdb_with_autoimport): - yield + def set_trace_in_caller_frame(self_pdb, frame=None, **kwargs): + # Skip the frames that our own advice adds, so that the + # debugger opens where the user entered it. + if frame is None: + frame = sys._getframe().f_back + frame = _skip_frames(frame, {"pyflyby"})[0] or frame + result = __original__(self_pdb, frame, **kwargs) + # Equivalent to Bdb.set_next(frame): stop in the user's + # frame, rather than in "step" mode, which would stop at the + # next event in the advice we are about to return through. + # Assigned rather than called, because tracing is enabled by + # now, so the call itself would be traced (and stopped in). + self_pdb.stopframe = frame + self_pdb.returnframe = None + self_pdb.stoplineno = 0 + return result + with ExitStack() as stack: + enter = stack.enter_context + enter(AdviceCtx(Pdb.__init__, Pdb_with_autoimport)) + # IPython's Pdb overrides set_trace() to record + # `initial_frame`; plain pdb.Pdb inherits Bdb's. + enter(AdviceCtx((Pdb, "set_trace"), set_trace_in_caller_frame)) + enter(AdviceCtx((pdb.Pdb, "set_trace"), set_trace_in_caller_frame)) + if TerminalPdb is not None: + enter(AdviceCtx(TerminalPdb.__init__, + TerminalPdb_with_autoimport)) + yield ok = True @self._advise(sys.breakpointhook) From cdf4aba81f1c7afe1935c767cc6f2b3e5e02f50f Mon Sep 17 00:00:00 2001 From: krassowski <5832902+krassowski@users.noreply.github.com> Date: Fri, 16 Oct 2026 10:44:07 +0100 Subject: [PATCH 3/6] Improve tests, add version check --- lib/python/pyflyby/_interactive.py | 48 +++++++++++++++++------------- tests/test_interactive.py | 28 ++++++++++++----- 2 files changed, 48 insertions(+), 28 deletions(-) diff --git a/lib/python/pyflyby/_interactive.py b/lib/python/pyflyby/_interactive.py index e4bdaa2c..d045e023 100644 --- a/lib/python/pyflyby/_interactive.py +++ b/lib/python/pyflyby/_interactive.py @@ -6,7 +6,7 @@ import ast import builtins -from contextlib import ExitStack, contextmanager +from contextlib import contextmanager import errno import inspect import logging @@ -34,7 +34,8 @@ from pyflyby._modules import ModuleHandle from pyflyby._parse import PythonBlock from pyflyby._util import (AdviceCtx, Aspect, CwdCtx, - FunctionWithGlobals, advise) + FunctionWithGlobals, advise, + nested) if TYPE_CHECKING: from IPython.core.application import BaseIPythonApplication @@ -1596,27 +1597,34 @@ def set_trace_in_caller_frame(self_pdb, frame=None, **kwargs): # debugger opens where the user entered it. if frame is None: frame = sys._getframe().f_back - frame = _skip_frames(frame, {"pyflyby"})[0] or frame - result = __original__(self_pdb, frame, **kwargs) - # Equivalent to Bdb.set_next(frame): stop in the user's - # frame, rather than in "step" mode, which would stop at the - # next event in the advice we are about to return through. - # Assigned rather than called, because tracing is enabled by - # now, so the call itself would be traced (and stopped in). - self_pdb.stopframe = frame - self_pdb.returnframe = None - self_pdb.stoplineno = 0 + target = _skip_frames(frame, {"pyflyby"})[0] or frame + result = __original__(self_pdb, target, **kwargs) + if target is not frame: + # set_trace() leaves the debugger stepping, so it would + # stop in the first call made while unwinding our advice. + # Pin the stop to the user's frame instead. Not with + # Bdb.set_next(): calling it would itself be traced, and + # it turns off the opcode tracing this relies on (below). + self_pdb.stopframe = target + self_pdb.returnframe = None + self_pdb.stoplineno = 0 return result - with ExitStack() as stack: - enter = stack.enter_context - enter(AdviceCtx(Pdb.__init__, Pdb_with_autoimport)) + mgrs = [AdviceCtx(Pdb.__init__, Pdb_with_autoimport)] + if TerminalPdb is not None: + mgrs.append(AdviceCtx(TerminalPdb.__init__, + TerminalPdb_with_autoimport)) + # Pinning the stop relies on set_trace() turning on opcode + # tracing, which it does since 3.13. Without that, bdb ignores + # return events in generators and coroutines unless stepping, so + # a breakpoint() at the end of one would never stop. + if sys.version_info >= (3, 13): # IPython's Pdb overrides set_trace() to record # `initial_frame`; plain pdb.Pdb inherits Bdb's. - enter(AdviceCtx((Pdb, "set_trace"), set_trace_in_caller_frame)) - enter(AdviceCtx((pdb.Pdb, "set_trace"), set_trace_in_caller_frame)) - if TerminalPdb is not None: - enter(AdviceCtx(TerminalPdb.__init__, - TerminalPdb_with_autoimport)) + mgrs.append(AdviceCtx((Pdb, "set_trace"), + set_trace_in_caller_frame)) + mgrs.append(AdviceCtx((pdb.Pdb, "set_trace"), + set_trace_in_caller_frame)) + with nested(*mgrs): yield ok = True diff --git a/tests/test_interactive.py b/tests/test_interactive.py index b92cf05f..5b2711de 100644 --- a/tests/test_interactive.py +++ b/tests/test_interactive.py @@ -3706,22 +3706,34 @@ def test_breakpoint_auto_import_p_1(enter_debugger, frontend): # "%debug pass" is excluded: it runs the given statement under the debugger, # so it legitimately stops elsewhere. caller_frame_debugger_activations = [ - "breakpoint()", - "import ipdb; ipdb.set_trace()", + a for a in debugger_activations if not a.startswith("%debug") ] +# The debugger is entered on the last line of each: with no next line to stop +# on, this is where generators and coroutines behave differently. +caller_frame_functions = { + "function": ("def f():", [], "f()"), + "generator": ("def f():", ["yield 1"], "list(f())"), + "coroutine": ("async def f():", [], "await f()"), +} + +@pytest.mark.skipif(sys.version_info < (3, 13), + reason="The debugger frame is only corrected on 3.13+") +@pytest.mark.parametrize("kind", caller_frame_functions) @pytest.mark.parametrize("enter_debugger", caller_frame_debugger_activations) -def test_breakpoint_stops_in_caller_frame_1(enter_debugger, frontend): +def test_breakpoint_stops_in_caller_frame_1(enter_debugger, kind, frontend): # Verify that the debugger stops in the frame that entered it + header, body, call = caller_frame_functions[kind] + lines = ["wallaby53023190 = 1", *body, enter_debugger] + cell = ("\n" + " " * 8).join(" ...: " + line for line in lines) ipython(f""" In [1]: import pyflyby; pyflyby.enable_auto_importer() - In [2]: def f(): - ...: wallaby53023190 = 1 - ...: {enter_debugger} + In [2]: {header} + {cell} ...: - In [3]: f() - > (3)f() + In [3]: {call} + > (...)f() .... ipdb> p wallaby53023190 1 From dcff6655e3aeb0482c1faa483b5eb404a7e74f67 Mon Sep 17 00:00:00 2001 From: krassowski <5832902+krassowski@users.noreply.github.com> Date: Fri, 16 Oct 2026 10:47:11 +0100 Subject: [PATCH 4/6] Lint --- lib/python/pyflyby/_interactive.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/lib/python/pyflyby/_interactive.py b/lib/python/pyflyby/_interactive.py index d045e023..855fdda1 100644 --- a/lib/python/pyflyby/_interactive.py +++ b/lib/python/pyflyby/_interactive.py @@ -34,8 +34,7 @@ from pyflyby._modules import ModuleHandle from pyflyby._parse import PythonBlock from pyflyby._util import (AdviceCtx, Aspect, CwdCtx, - FunctionWithGlobals, advise, - nested) + FunctionWithGlobals, advise, nested) if TYPE_CHECKING: from IPython.core.application import BaseIPythonApplication From ab360b335334642d851eb680f78870df5825bf47 Mon Sep 17 00:00:00 2001 From: krassowski <5832902+krassowski@users.noreply.github.com> Date: Fri, 16 Oct 2026 11:21:41 +0100 Subject: [PATCH 5/6] Rewrite to use global advice for debugger --- lib/python/pyflyby/_interactive.py | 114 +++++++---------------------- tests/test_interactive.py | 57 +++++++++++++-- 2 files changed, 79 insertions(+), 92 deletions(-) diff --git a/lib/python/pyflyby/_interactive.py b/lib/python/pyflyby/_interactive.py index 855fdda1..5d28cf92 100644 --- a/lib/python/pyflyby/_interactive.py +++ b/lib/python/pyflyby/_interactive.py @@ -33,8 +33,8 @@ from pyflyby._log import logger from pyflyby._modules import ModuleHandle from pyflyby._parse import PythonBlock -from pyflyby._util import (AdviceCtx, Aspect, CwdCtx, - FunctionWithGlobals, advise, nested) +from pyflyby._util import (Aspect, CwdCtx, FunctionWithGlobals, + advise) if TYPE_CHECKING: from IPython.core.application import BaseIPythonApplication @@ -646,6 +646,10 @@ def _enable_pdb_hooks(pdb_instance): # Enable hooks in pdb.Pdb. # Should be called after pdb.Pdb.__init__(). logger.debug("_enable_pdb_hooks(%r)", pdb_instance) + # Already done by our advice on pdb.Pdb.__init__, if it's installed. + if getattr(pdb_instance, "_pyflyby_hooks_enabled", False): + return + pdb_instance._pyflyby_hooks_enabled = True # Patch Pdb._getval() to use auto_eval. # This supports 'ipdb> p foo'. @advise(pdb_instance._getval) @@ -1571,92 +1575,32 @@ def safe_execfile_with_autoimport(filename, def _enable_debugger_hook(self, ip): import pdb - try: - Pdb = _get_IPdb_class() - except Exception as e: - logger.debug("Couldn't locate Pdb class: %s: %s", - type(e).__name__, e) - return False + # Advise the debugger classes rather than the ways of entering a + # debugger: advice around e.g. breakpoint() adds pyflyby frames below + # the user's, and the debugger would open in those. IPython's Pdb and + # TerminalPdb reach pdb.Pdb.__init__ via super(). + @self._advise(pdb.Pdb.__init__) + def Pdb_with_autoimport(self_pdb, *args, **kwargs): + __original__(self_pdb, *args, **kwargs) + # This runs for every debugger: never stop one from starting. + try: + _enable_pdb_hooks(self_pdb) + except Exception as e: + logger.error("%s: %s", type(e).__name__, e) try: TerminalPdb = _get_TerminalPdb_class() except Exception as e: logger.debug("Couldn't locate TerminalPdb class: %s: %s", type(e).__name__, e) - TerminalPdb = None - @contextmanager - def HookPdbCtx(): - def Pdb_with_autoimport(self_pdb, *args): - __original__(self_pdb, *args) - _enable_pdb_hooks(self_pdb) - def TerminalPdb_with_autoimport(self_pdb, *args): - __original__(self_pdb, *args) - _enable_terminal_pdb_hooks(self_pdb, self) - def set_trace_in_caller_frame(self_pdb, frame=None, **kwargs): - # Skip the frames that our own advice adds, so that the - # debugger opens where the user entered it. - if frame is None: - frame = sys._getframe().f_back - target = _skip_frames(frame, {"pyflyby"})[0] or frame - result = __original__(self_pdb, target, **kwargs) - if target is not frame: - # set_trace() leaves the debugger stepping, so it would - # stop in the first call made while unwinding our advice. - # Pin the stop to the user's frame instead. Not with - # Bdb.set_next(): calling it would itself be traced, and - # it turns off the opcode tracing this relies on (below). - self_pdb.stopframe = target - self_pdb.returnframe = None - self_pdb.stoplineno = 0 - return result - mgrs = [AdviceCtx(Pdb.__init__, Pdb_with_autoimport)] - if TerminalPdb is not None: - mgrs.append(AdviceCtx(TerminalPdb.__init__, - TerminalPdb_with_autoimport)) - # Pinning the stop relies on set_trace() turning on opcode - # tracing, which it does since 3.13. Without that, bdb ignores - # return events in generators and coroutines unless stepping, so - # a breakpoint() at the end of one would never stop. - if sys.version_info >= (3, 13): - # IPython's Pdb overrides set_trace() to record - # `initial_frame`; plain pdb.Pdb inherits Bdb's. - mgrs.append(AdviceCtx((Pdb, "set_trace"), - set_trace_in_caller_frame)) - mgrs.append(AdviceCtx((pdb.Pdb, "set_trace"), - set_trace_in_caller_frame)) - with nested(*mgrs): - yield - ok = True - - @self._advise(sys.breakpointhook) - def breakpoint_debugger_with_autoimport(*args, **kwargs): - with HookPdbCtx(): - return __original__(*args, **kwargs) - - try: - import ipdb - - if hasattr(ipdb, "set_trace"): - @self._advise((ipdb, "set_trace")) - def ipdb_debugger_with_autoimport(*args, **kwargs): - with HookPdbCtx(): - return __original__(*args, **kwargs) - else: - ok = False - except ModuleNotFoundError: - pass - - iptb = getattr(ip, "InteractiveTB", None) - if hasattr(iptb, "debugger"): - # Hook ip.InteractiveTB.debugger(). This implements auto - # importing for "%debug" (postmortem mode). - # Tested with IPython 0.10, 0.11, 0.12, 0.13, 1.0, 1.1, 1.2, 2.0, - # 2.1, 2.2, 2.3, 2.4, 3.0, 3.1, 3.2, 4.0. - @self._advise(iptb.debugger) - def debugger_with_autoimport(*args, **kwargs): - with HookPdbCtx(): - return __original__(*args, **kwargs) else: - ok = False + @self._advise(TerminalPdb.__init__) + def TerminalPdb_with_autoimport(self_pdb, *args, **kwargs): + __original__(self_pdb, *args, **kwargs) + try: + _enable_terminal_pdb_hooks(self_pdb, self) + except Exception as e: + logger.error("%s: %s", type(e).__name__, e) + ok = True if hasattr(ip, 'magics_manager'): # Hook ExecutionMagics._run_with_debugger(). This implements auto # importing for "%debug ". @@ -1671,10 +1615,8 @@ def run_with_debugger_with_autoimport(code, code_ns, *args, **kwargs): db = ImportDB.get_default(filename or ".") auto_import(code, namespaces=[code_ns], db=db) - with HookPdbCtx(): - return __original__(code, code_ns, filename, - *args, **kwargs - ) + return __original__(code, code_ns, filename, + *args, **kwargs) else: ok = False return ok diff --git a/tests/test_interactive.py b/tests/test_interactive.py index 5b2711de..2c0902af 100644 --- a/tests/test_interactive.py +++ b/tests/test_interactive.py @@ -218,7 +218,7 @@ def parse_template(template, clear_tab_completions=False): template = dedent(template).strip().encode('utf-8') input = [] expected = b"" - pattern = re.compile(br"^(?:In \[[0-9]+\]:| [.][.][.]+:|ipdb>|>>>)(?: |$)", re.M) + pattern = re.compile(br"^(?:In \[[0-9]+\]:| [.][.][.]+:|ipdb>|\(Pdb\)|>>>)(?: |$)", re.M) while template: m = pattern.search(template) if not m: @@ -483,10 +483,12 @@ def _parse_version(version_string): _IPYTHON_PROMPT2 = br"\n\r? [.][.][.]+: " _PYTHON_PROMPT = br">>> " _IPDB_PROMPT = br"\nipdb> " +_PDB_PROMPT = br"\n\(Pdb\) " _IPYTHON_PROMPTS = [_IPYTHON_PROMPT1, _IPYTHON_PROMPT2, _PYTHON_PROMPT, - _IPDB_PROMPT] + _IPDB_PROMPT, + _PDB_PROMPT] # Currently _interact_ipython() assumes the first is "In [nn]:" assert b"In " in _IPYTHON_PROMPTS[0] @@ -3718,22 +3720,26 @@ def test_breakpoint_auto_import_p_1(enter_debugger, frontend): } -@pytest.mark.skipif(sys.version_info < (3, 13), - reason="The debugger frame is only corrected on 3.13+") @pytest.mark.parametrize("kind", caller_frame_functions) @pytest.mark.parametrize("enter_debugger", caller_frame_debugger_activations) def test_breakpoint_stops_in_caller_frame_1(enter_debugger, kind, frontend): # Verify that the debugger stops in the frame that entered it header, body, call = caller_frame_functions[kind] + newline = "\n" + " " * 8 lines = ["wallaby53023190 = 1", *body, enter_debugger] - cell = ("\n" + " " * 8).join(" ...: " + line for line in lines) + cell = newline.join(" ...: " + line for line in lines) + stop = ["> (...)f()"] + if sys.version_info < (3, 13): + # With no next line to step to, older versions stop on the return. + stop = ["--Return--", "None", *stop] + stop = newline.join(stop) ipython(f""" In [1]: import pyflyby; pyflyby.enable_auto_importer() In [2]: {header} {cell} ...: In [3]: {call} - > (...)f() + {stop} .... ipdb> p wallaby53023190 1 @@ -3741,6 +3747,45 @@ def test_breakpoint_stops_in_caller_frame_1(enter_debugger, kind, frontend): """, frontend=frontend) +def test_breakpoint_stdlib_pdb_1(frontend): + # Verify auto importing, and the stop frame, in plain pdb: what + # breakpoint() uses when PYTHONBREAKPOINT is unset. + ipython(""" + In [1]: import pyflyby; pyflyby.enable_auto_importer() + In [2]: import os; del os.environ["PYTHONBREAKPOINT"] + In [3]: def f(): + ...: wallaby53023190 = 1 + ...: breakpoint() + ...: wallaby53023190 = 2 + ...: + In [4]: f() + > (...)f() + -> ... + (Pdb) p wallaby53023190 + 1 + (Pdb) p b64decode("UGFzY2Fs") + [PYFLYBY] from base64 import b64decode + b'Pascal' + (Pdb) c + """, frontend=frontend) + + +def test_ipdb_set_trace_context_1(frontend): + # Verify that the context ipdb asks for reaches its debugger. ipdb passes + # it to the constructor, and falls back to the defaults on TypeError. + ipython(""" + In [1]: import pyflyby; pyflyby.enable_auto_importer() + In [2]: def f(): + ...: wallaby53023190 = 1 + ...: wallaby53023190 = 2 + ...: import ipdb; ipdb.set_trace(context=1) + ...: wallaby53023190 = 3 + ...: + In [3]: f() + > (...)f() + ipdb> q + """, frontend=frontend) + @pytest.mark.skipif( (3, 14, 1) <= sys.version_info[:3] <= (3, 14, 5), reason="linux + >=3.14.1 show some extra stacks.", From 9d76879f3a59cfb921f0a6265ee89a343b9a3899 Mon Sep 17 00:00:00 2001 From: krassowski <5832902+krassowski@users.noreply.github.com> Date: Fri, 16 Oct 2026 17:22:22 +0100 Subject: [PATCH 6/6] Add another test, better advise wrapping, drop try-except that has little use --- lib/python/pyflyby/_interactive.py | 18 +++++---------- tests/test_interactive.py | 35 ++++++++++++++++++++++-------- 2 files changed, 32 insertions(+), 21 deletions(-) diff --git a/lib/python/pyflyby/_interactive.py b/lib/python/pyflyby/_interactive.py index 5d28cf92..f0943880 100644 --- a/lib/python/pyflyby/_interactive.py +++ b/lib/python/pyflyby/_interactive.py @@ -1578,28 +1578,22 @@ def _enable_debugger_hook(self, ip): # Advise the debugger classes rather than the ways of entering a # debugger: advice around e.g. breakpoint() adds pyflyby frames below # the user's, and the debugger would open in those. IPython's Pdb and - # TerminalPdb reach pdb.Pdb.__init__ via super(). - @self._advise(pdb.Pdb.__init__) + # TerminalPdb call pdb.Pdb.__init__ from theirs; in a kernel, pdb.Pdb + # is already IPython's InterruptiblePdb. + @self._advise((pdb.Pdb, "__init__")) def Pdb_with_autoimport(self_pdb, *args, **kwargs): __original__(self_pdb, *args, **kwargs) - # This runs for every debugger: never stop one from starting. - try: - _enable_pdb_hooks(self_pdb) - except Exception as e: - logger.error("%s: %s", type(e).__name__, e) + _enable_pdb_hooks(self_pdb) try: TerminalPdb = _get_TerminalPdb_class() except Exception as e: logger.debug("Couldn't locate TerminalPdb class: %s: %s", type(e).__name__, e) else: - @self._advise(TerminalPdb.__init__) + @self._advise((TerminalPdb, "__init__")) def TerminalPdb_with_autoimport(self_pdb, *args, **kwargs): __original__(self_pdb, *args, **kwargs) - try: - _enable_terminal_pdb_hooks(self_pdb, self) - except Exception as e: - logger.error("%s: %s", type(e).__name__, e) + _enable_terminal_pdb_hooks(self_pdb, self) ok = True if hasattr(ip, 'magics_manager'): # Hook ExecutionMagics._run_with_debugger(). This implements auto diff --git a/tests/test_interactive.py b/tests/test_interactive.py index 2c0902af..6c4c100d 100644 --- a/tests/test_interactive.py +++ b/tests/test_interactive.py @@ -3723,23 +3723,19 @@ def test_breakpoint_auto_import_p_1(enter_debugger, frontend): @pytest.mark.parametrize("kind", caller_frame_functions) @pytest.mark.parametrize("enter_debugger", caller_frame_debugger_activations) def test_breakpoint_stops_in_caller_frame_1(enter_debugger, kind, frontend): - # Verify that the debugger stops in the frame that entered it + # Verify that the debugger stops in the frame that entered it, the only + # place the local is visible. What's printed before the prompt varies: + # Python 3.12 and older, or another sys.monitoring tool such as + # coverage's, print "--Return--" first. header, body, call = caller_frame_functions[kind] - newline = "\n" + " " * 8 lines = ["wallaby53023190 = 1", *body, enter_debugger] - cell = newline.join(" ...: " + line for line in lines) - stop = ["> (...)f()"] - if sys.version_info < (3, 13): - # With no next line to step to, older versions stop on the return. - stop = ["--Return--", "None", *stop] - stop = newline.join(stop) + cell = ("\n" + " " * 8).join(" ...: " + line for line in lines) ipython(f""" In [1]: import pyflyby; pyflyby.enable_auto_importer() In [2]: {header} {cell} ...: In [3]: {call} - {stop} .... ipdb> p wallaby53023190 1 @@ -3770,6 +3766,27 @@ def test_breakpoint_stdlib_pdb_1(frontend): """, frontend=frontend) +def test_debugger_hook_with_replaced_pdb_init_1(frontend): + # Verify that the auto importer still enables, and hooks the debugger, + # when something else has already replaced pdb.Pdb.__init__. + ipython(""" + In [1]: import pdb + In [2]: def wrap(init): + ...: def wrapper(self, *args, **kwargs): + ...: init(self, *args, **kwargs) + ...: return wrapper + ...: + In [3]: pdb.Pdb.__init__ = wrap(pdb.Pdb.__init__) + In [4]: import pyflyby; pyflyby.enable_auto_importer() + In [5]: %debug pass + .... + ipdb> p b64decode("V29tYmF0") + [PYFLYBY] from base64 import b64decode + b'Wombat' + ipdb> q + """, frontend=frontend) + + def test_ipdb_set_trace_context_1(frontend): # Verify that the context ipdb asks for reaches its debugger. ipdb passes # it to the constructor, and falls back to the defaults on TypeError.