| """ |
| 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__` |
| |
| |
| Global Configuration |
| -------------------- |
| |
| The IPython debugger will by read the global ``~/.pdbrc`` file. |
| That is to say you can list all commands supported by ipdb in your `~/.pdbrc` |
| configuration file, to globally configure pdb. |
| |
| Example:: |
| |
| # ~/.pdbrc |
| skip_predicates debuggerskip false |
| skip_hidden false |
| context 25 |
| |
| Features |
| -------- |
| |
| The IPython debugger can hide and skip frames when printing or moving through |
| the stack. This can have a performance impact, so can be configures. |
| |
| 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 whose parent |
| frames value of ``__debuggerskip__`` is ``True`` will also be skipped. |
| |
| >>> 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(): |
| ... 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() |
| ... # setting __debuggerskip__ to False again is not necessary |
| ... 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`` |
| |
| |
| |
| License |
| ------- |
| |
| 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. |
| |
| In the future, this class will be expanded with improvements over the standard |
| pdb. |
| |
| 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. |
| |
| """ |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
|
|
| from __future__ import annotations |
|
|
| import inspect |
| import linecache |
| import os |
| import re |
| import sys |
| import warnings |
| from contextlib import contextmanager |
| from functools import lru_cache |
|
|
| from IPython import get_ipython |
| from IPython.core.debugger_backport import PdbClosureBackport |
| from IPython.utils import PyColorize |
| from IPython.utils.PyColorize import TokenStream |
|
|
| from typing import TYPE_CHECKING |
| from types import FrameType |
|
|
| |
| from pdb import Pdb as _OldPdb |
| from pygments.token import Token |
|
|
|
|
| if sys.version_info < (3, 13): |
|
|
| class OldPdb(PdbClosureBackport, _OldPdb): |
| pass |
|
|
| else: |
| OldPdb = _OldPdb |
|
|
| if TYPE_CHECKING: |
| |
| from IPython.core.interactiveshell import InteractiveShell |
|
|
| |
| __skip_doctest__ = True |
|
|
| prompt = "ipdb> " |
|
|
|
|
| |
| |
| |
|
|
| DEBUGGERSKIP = "__debuggerskip__" |
|
|
|
|
| |
| |
| CHAIN_EXCEPTIONS = sys.version_info < (3, 13) |
|
|
|
|
| def BdbQuit_excepthook(et, ev, tb, excepthook=None): |
| """Exception hook which handles `BdbQuit` exceptions. |
| |
| All other exceptions are processed using the `excepthook` |
| parameter. |
| """ |
| raise ValueError( |
| "`BdbQuit_excepthook` is deprecated since version 5.1. It is still around only because it is still imported by ipdb.", |
| ) |
|
|
|
|
| RGX_EXTRA_INDENT = re.compile(r"(?<=\n)\s+") |
|
|
|
|
| def strip_indentation(multiline_string): |
| return RGX_EXTRA_INDENT.sub("", multiline_string) |
|
|
|
|
| def decorate_fn_with_doc(new_fn, old_fn, additional_text=""): |
| """Make new_fn have old_fn's doc string. This is particularly useful |
| for the ``do_...`` commands that hook into the help system. |
| Adapted from from a comp.lang.python posting |
| by Duncan Booth.""" |
|
|
| def wrapper(*args, **kw): |
| return new_fn(*args, **kw) |
|
|
| if old_fn.__doc__: |
| wrapper.__doc__ = strip_indentation(old_fn.__doc__) + additional_text |
| return wrapper |
|
|
|
|
| class Pdb(OldPdb): |
| """Modified Pdb class, does not load readline. |
| |
| for a standalone version that uses prompt_toolkit, see |
| `IPython.terminal.debugger.TerminalPdb` and |
| `IPython.terminal.debugger.set_trace()` |
| |
| |
| This debugger can hide and skip frames that are tagged according to some predicates. |
| See the `skip_predicates` commands. |
| |
| """ |
|
|
| shell: InteractiveShell |
| _theme_name: str |
| _context: int |
|
|
| _chained_exceptions: tuple[Exception, ...] |
| _chained_exception_index: int |
|
|
| if CHAIN_EXCEPTIONS: |
| MAX_CHAINED_EXCEPTION_DEPTH = 999 |
|
|
| default_predicates = { |
| "tbhide": True, |
| "readonly": False, |
| "ipython_internal": True, |
| "debuggerskip": True, |
| } |
|
|
| def __init__( |
| self, |
| completekey=None, |
| stdin=None, |
| stdout=None, |
| context: int | None | str = 5, |
| **kwargs, |
| ): |
| """Create a new IPython debugger. |
| |
| Parameters |
| ---------- |
| completekey : default None |
| Passed to pdb.Pdb. |
| stdin : default None |
| Passed to pdb.Pdb. |
| stdout : default None |
| Passed to pdb.Pdb. |
| context : int |
| Number of lines of source code context to show when |
| displaying stacktrace information. |
| **kwargs |
| Passed to pdb.Pdb. |
| |
| Notes |
| ----- |
| The possibilities are python version dependent, see the python |
| docs for more info. |
| """ |
| |
| if context is None: |
| context = 5 |
| if isinstance(context, str): |
| context = int(context) |
| self.context = context |
|
|
| |
| OldPdb.__init__(self, completekey, stdin, stdout, **kwargs) |
| |
| |
| if sys.version_info < (3, 15): |
| self.curframe = None |
|
|
| |
| self.shell = get_ipython() |
|
|
| if self.shell is None: |
| save_main = sys.modules["__main__"] |
| |
| from IPython.terminal.interactiveshell import TerminalInteractiveShell |
|
|
| self.shell = TerminalInteractiveShell.instance() |
| |
| |
| sys.modules["__main__"] = save_main |
|
|
| self.aliases = {} |
|
|
| theme_name = self.shell.colors |
| assert isinstance(theme_name, str) |
| assert theme_name.lower() == theme_name |
|
|
| |
| |
| self.parser = PyColorize.Parser(theme_name=theme_name) |
| self.set_theme_name(theme_name) |
|
|
| |
| self.prompt = prompt |
| self.skip_hidden = True |
| self.report_skipped = True |
|
|
| |
| self._predicates = self.default_predicates |
|
|
| if CHAIN_EXCEPTIONS: |
| self._chained_exceptions = tuple() |
| self._chained_exception_index = 0 |
|
|
| @property |
| def context(self) -> int: |
| return self._context |
|
|
| @context.setter |
| def context(self, value: int | str) -> None: |
| |
| if not isinstance(value, int): |
| value = int(value) |
| assert isinstance(value, int) |
| assert value >= 0 |
| self._context = value |
|
|
| def set_theme_name(self, name): |
| assert name.lower() == name |
| assert isinstance(name, str) |
| self._theme_name = name |
| self.parser.theme_name = name |
|
|
| @property |
| def theme(self): |
| return PyColorize.theme_table[self._theme_name] |
|
|
| |
| def set_colors(self, scheme): |
| """Shorthand access to the color table scheme selector method.""" |
| warnings.warn( |
| "set_colors is deprecated since IPython 9.0, use set_theme_name instead", |
| DeprecationWarning, |
| stacklevel=2, |
| ) |
| assert scheme == scheme.lower() |
| self._theme_name = scheme.lower() |
| self.parser.theme_name = scheme.lower() |
|
|
| def set_trace(self, frame=None): |
| if frame is None: |
| frame = sys._getframe().f_back |
| self.initial_frame = frame |
| return super().set_trace(frame) |
|
|
| def get_stack(self, *args, **kwargs): |
| stack, pos = super().get_stack(*args, **kwargs) |
| if len(stack) >= 0 and self._is_internal_frame(stack[0][0]): |
| stack.pop(0) |
| pos -= 1 |
| return stack, pos |
|
|
| def _is_internal_frame(self, frame): |
| """Determine if this frame should be skipped as internal""" |
| filename = frame.f_code.co_filename |
|
|
| |
| if filename.endswith("bdb.py"): |
| func_name = frame.f_code.co_name |
| |
| if func_name in ("runcall", "run", "runeval"): |
| return True |
|
|
| return False |
|
|
| def _hidden_predicate(self, frame): |
| """ |
| Given a frame return whether it it should be hidden or not by IPython. |
| """ |
|
|
| if self._predicates["readonly"]: |
| fname = frame.f_code.co_filename |
| |
| |
| if os.path.isfile(fname) and not os.access(fname, os.W_OK): |
| return True |
|
|
| if self._predicates["tbhide"]: |
| if frame in (self.curframe, getattr(self, "initial_frame", None)): |
| return False |
| frame_locals = self._get_frame_locals(frame) |
| if "__tracebackhide__" not in frame_locals: |
| return False |
| return frame_locals["__tracebackhide__"] |
| return False |
|
|
| def hidden_frames(self, stack): |
| """ |
| Given an index in the stack return whether it should be skipped. |
| |
| This is used in up/down and where to skip frames. |
| """ |
| |
| |
| |
| |
| ip_hide = [self._hidden_predicate(s[0]) for s in stack] |
| ip_start = [i for i, s in enumerate(ip_hide) if s == "__ipython_bottom__"] |
| if ip_start and self._predicates["ipython_internal"]: |
| ip_hide = [h if i > ip_start[0] else True for (i, h) in enumerate(ip_hide)] |
| return ip_hide |
|
|
| if CHAIN_EXCEPTIONS: |
|
|
| def _get_tb_and_exceptions(self, tb_or_exc): |
| """ |
| Given a tracecack or an exception, return a tuple of chained exceptions |
| and current traceback to inspect. |
| This will deal with selecting the right ``__cause__`` or ``__context__`` |
| as well as handling cycles, and return a flattened list of exceptions we |
| can jump to with do_exceptions. |
| """ |
| _exceptions = [] |
| if isinstance(tb_or_exc, BaseException): |
| traceback, current = tb_or_exc.__traceback__, tb_or_exc |
|
|
| while current is not None: |
| if current in _exceptions: |
| break |
| _exceptions.append(current) |
| if current.__cause__ is not None: |
| current = current.__cause__ |
| elif ( |
| current.__context__ is not None |
| and not current.__suppress_context__ |
| ): |
| current = current.__context__ |
|
|
| if len(_exceptions) >= self.MAX_CHAINED_EXCEPTION_DEPTH: |
| self.message( |
| f"More than {self.MAX_CHAINED_EXCEPTION_DEPTH}" |
| " chained exceptions found, not all exceptions" |
| "will be browsable with `exceptions`." |
| ) |
| break |
| else: |
| traceback = tb_or_exc |
| return tuple(reversed(_exceptions)), traceback |
|
|
| @contextmanager |
| def _hold_exceptions(self, exceptions): |
| """ |
| Context manager to ensure proper cleaning of exceptions references |
| When given a chained exception instead of a traceback, |
| pdb may hold references to many objects which may leak memory. |
| We use this context manager to make sure everything is properly cleaned |
| """ |
| try: |
| self._chained_exceptions = exceptions |
| self._chained_exception_index = len(exceptions) - 1 |
| yield |
| finally: |
| |
| |
| self._chained_exceptions = tuple() |
| self._chained_exception_index = 0 |
|
|
| def do_exceptions(self, arg): |
| """exceptions [number] |
| List or change current exception in an exception chain. |
| Without arguments, list all the current exception in the exception |
| chain. Exceptions will be numbered, with the current exception indicated |
| with an arrow. |
| If given an integer as argument, switch to the exception at that index. |
| """ |
| if not self._chained_exceptions: |
| self.message( |
| "Did not find chained exceptions. To move between" |
| " exceptions, pdb/post_mortem must be given an exception" |
| " object rather than a traceback." |
| ) |
| return |
| if not arg: |
| for ix, exc in enumerate(self._chained_exceptions): |
| prompt = ">" if ix == self._chained_exception_index else " " |
| rep = repr(exc) |
| if len(rep) > 80: |
| rep = rep[:77] + "..." |
| indicator = ( |
| " -" |
| if self._chained_exceptions[ix].__traceback__ is None |
| else f"{ix:>3}" |
| ) |
| self.message(f"{prompt} {indicator} {rep}") |
| else: |
| try: |
| number = int(arg) |
| except ValueError: |
| self.error("Argument must be an integer") |
| return |
| if 0 <= number < len(self._chained_exceptions): |
| if self._chained_exceptions[number].__traceback__ is None: |
| self.error( |
| "This exception does not have a traceback, cannot jump to it" |
| ) |
| return |
|
|
| self._chained_exception_index = number |
| self.setup(None, self._chained_exceptions[number].__traceback__) |
| self.print_stack_entry(self.stack[self.curindex]) |
| else: |
| self.error("No exception with that number") |
|
|
| def interaction(self, frame, tb_or_exc): |
| try: |
| if CHAIN_EXCEPTIONS: |
| |
| _chained_exceptions, tb = self._get_tb_and_exceptions(tb_or_exc) |
| if isinstance(tb_or_exc, BaseException): |
| assert tb is not None, "main exception must have a traceback" |
| with self._hold_exceptions(_chained_exceptions): |
| OldPdb.interaction(self, frame, tb) |
| else: |
| OldPdb.interaction(self, frame, tb_or_exc) |
|
|
| except KeyboardInterrupt: |
| self.stdout.write("\n" + self.shell.get_exception_only()) |
|
|
| def precmd(self, line): |
| """Perform useful escapes on the command before it is executed.""" |
|
|
| if line.endswith("??"): |
| line = "pinfo2 " + line[:-2] |
| elif line.endswith("?"): |
| line = "pinfo " + line[:-1] |
|
|
| line = super().precmd(line) |
|
|
| return line |
|
|
| def new_do_quit(self, arg): |
| return OldPdb.do_quit(self, arg) |
|
|
| do_q = do_quit = decorate_fn_with_doc(new_do_quit, OldPdb.do_quit) |
|
|
| def print_stack_trace(self, context: int | None = None): |
| if context is None: |
| context = self.context |
| try: |
| skipped = 0 |
| to_print = "" |
| for hidden, frame_lineno in zip(self.hidden_frames(self.stack), self.stack): |
| if hidden and self.skip_hidden: |
| skipped += 1 |
| continue |
| if skipped: |
| to_print += self.theme.format( |
| [ |
| ( |
| Token.ExcName, |
| f" [... skipping {skipped} hidden frame(s)]", |
| ), |
| (Token, "\n"), |
| ] |
| ) |
|
|
| skipped = 0 |
| to_print += self.format_stack_entry(frame_lineno) |
| if skipped: |
| to_print += self.theme.format( |
| [ |
| ( |
| Token.ExcName, |
| f" [... skipping {skipped} hidden frame(s)]", |
| ), |
| (Token, "\n"), |
| ] |
| ) |
| print(to_print, file=self.stdout) |
| except KeyboardInterrupt: |
| pass |
|
|
| def print_stack_entry( |
| self, frame_lineno: tuple[FrameType, int], prompt_prefix: str = "\n-> " |
| ) -> None: |
| """ |
| Overwrite print_stack_entry from superclass (PDB) |
| """ |
| print(self.format_stack_entry(frame_lineno, ""), file=self.stdout) |
|
|
| frame, lineno = frame_lineno |
| filename = frame.f_code.co_filename |
| self.shell.hooks.synchronize_with_editor(filename, lineno, 0) |
|
|
| def _get_frame_locals(self, frame): |
| """ " |
| Accessing f_local of current frame reset the namespace, so we want to avoid |
| that or the following can happen |
| |
| ipdb> foo |
| "old" |
| ipdb> foo = "new" |
| ipdb> foo |
| "new" |
| ipdb> where |
| ipdb> foo |
| "old" |
| |
| So if frame is self.current_frame we instead return self.curframe_locals |
| |
| """ |
| if frame is self.curframe: |
| return self.curframe_locals |
| else: |
| return frame.f_locals |
|
|
| def format_stack_entry( |
| self, |
| frame_lineno: tuple[FrameType, int], |
| lprefix: str = ": ", |
| ) -> str: |
| """ |
| overwrite from super class so must -> str |
| """ |
| context = self.context |
| try: |
| context = int(context) |
| if context <= 0: |
| print("Context must be a positive integer", file=self.stdout) |
| except (TypeError, ValueError): |
| print("Context must be a positive integer", file=self.stdout) |
|
|
| import reprlib |
|
|
| ret_tok = [] |
|
|
| frame, lineno = frame_lineno |
|
|
| return_value = "" |
| loc_frame = self._get_frame_locals(frame) |
| if "__return__" in loc_frame: |
| rv = loc_frame["__return__"] |
| |
| return_value += reprlib.repr(rv) + "\n" |
| ret_tok.extend([(Token, return_value)]) |
|
|
| |
| filename = self.canonic(frame.f_code.co_filename) |
| link_tok = (Token.FilenameEm, filename) |
|
|
| if frame.f_code.co_name: |
| func = frame.f_code.co_name |
| else: |
| func = "<lambda>" |
|
|
| call_toks = [] |
| if func != "?": |
| if "__args__" in loc_frame: |
| args = reprlib.repr(loc_frame["__args__"]) |
| else: |
| args = "()" |
| call_toks = [(Token.VName, func), (Token.ValEm, args)] |
|
|
| |
| |
| if frame is self.curframe: |
| ret_tok.append((Token.CurrentFrame, self.theme.make_arrow(2))) |
| else: |
| ret_tok.append((Token, " ")) |
|
|
| ret_tok.extend( |
| [ |
| link_tok, |
| (Token, "("), |
| (Token.Lineno, str(lineno)), |
| (Token, ")"), |
| *call_toks, |
| (Token, "\n"), |
| ] |
| ) |
|
|
| start = lineno - 1 - context // 2 |
| lines = linecache.getlines(filename) |
| start = min(start, len(lines) - context) |
| start = max(start, 0) |
| lines = lines[start : start + context] |
|
|
| for i, line in enumerate(lines): |
| show_arrow = start + 1 + i == lineno |
|
|
| bp, num, colored_line = self.__line_content( |
| filename, |
| start + 1 + i, |
| line, |
| arrow=show_arrow, |
| ) |
| if frame is self.curframe or show_arrow: |
| rlt = [ |
| bp, |
| (Token.LinenoEm, num), |
| (Token, " "), |
| |
| |
| |
| |
| (Token, colored_line), |
| ] |
| else: |
| rlt = [ |
| bp, |
| (Token.Lineno, num), |
| (Token, " "), |
| |
| |
| |
| |
| (Token.Line, colored_line), |
| ] |
| ret_tok.extend(rlt) |
|
|
| return self.theme.format(ret_tok) |
|
|
| def __line_content( |
| self, filename: str, lineno: int, line: str, arrow: bool = False |
| ): |
| bp_mark = "" |
| BreakpointToken = Token.Breakpoint |
|
|
| new_line, err = self.parser.format2(line, "str") |
| if not err: |
| assert new_line is not None |
| line = new_line |
|
|
| bp = None |
| if lineno in self.get_file_breaks(filename): |
| bps = self.get_breaks(filename, lineno) |
| bp = bps[-1] |
|
|
| if bp: |
| bp_mark = str(bp.number) |
| BreakpointToken = Token.Breakpoint.Enabled |
| if not bp.enabled: |
| BreakpointToken = Token.Breakpoint.Disabled |
| numbers_width = 7 |
| if arrow: |
| |
| pad = numbers_width - len(str(lineno)) - len(bp_mark) |
| num = "%s%s" % (self.theme.make_arrow(pad), str(lineno)) |
| else: |
| num = "%*s" % (numbers_width - len(bp_mark), str(lineno)) |
| bp_str = (BreakpointToken, bp_mark) |
| return (bp_str, num, line) |
|
|
| def print_list_lines(self, filename: str, first: int, last: int) -> None: |
| """The printing (as opposed to the parsing part of a 'list' |
| command.""" |
| toks: TokenStream = [] |
| try: |
| if filename == "<string>" and hasattr(self, "_exec_filename"): |
| filename = self._exec_filename |
|
|
| for lineno in range(first, last + 1): |
| line = linecache.getline(filename, lineno) |
| if not line: |
| break |
|
|
| assert self.curframe is not None |
|
|
| if lineno == self.curframe.f_lineno: |
| bp, num, colored_line = self.__line_content( |
| filename, lineno, line, arrow=True |
| ) |
| toks.extend( |
| [ |
| bp, |
| (Token.LinenoEm, num), |
| (Token, " "), |
| |
| (Token, colored_line), |
| ] |
| ) |
| else: |
| bp, num, colored_line = self.__line_content( |
| filename, lineno, line, arrow=False |
| ) |
| toks.extend( |
| [ |
| bp, |
| (Token.Lineno, num), |
| (Token, " "), |
| (Token, colored_line), |
| ] |
| ) |
|
|
| self.lineno = lineno |
|
|
| print(self.theme.format(toks), file=self.stdout) |
|
|
| except KeyboardInterrupt: |
| pass |
|
|
| def do_skip_predicates(self, args): |
| """ |
| Turn on/off individual predicates as to whether a frame should be hidden/skip. |
| |
| The global option to skip (or not) hidden frames is set with skip_hidden |
| |
| To change the value of a predicate |
| |
| skip_predicates key [true|false] |
| |
| Call without arguments to see the current values. |
| |
| To permanently change the value of an option add the corresponding |
| command to your ``~/.pdbrc`` file. If you are programmatically using the |
| Pdb instance you can also change the ``default_predicates`` class |
| attribute. |
| """ |
| if not args.strip(): |
| print("current predicates:") |
| for p, v in self._predicates.items(): |
| print(" ", p, ":", v) |
| return |
| type_value = args.strip().split(" ") |
| if len(type_value) != 2: |
| print( |
| f"Usage: skip_predicates <type> <value>, with <type> one of {set(self._predicates.keys())}" |
| ) |
| return |
|
|
| type_, value = type_value |
| if type_ not in self._predicates: |
| print(f"{type_!r} not in {set(self._predicates.keys())}") |
| return |
| if value.lower() not in ("true", "yes", "1", "no", "false", "0"): |
| print( |
| f"{value!r} is invalid - use one of ('true', 'yes', '1', 'no', 'false', '0')" |
| ) |
| return |
|
|
| self._predicates[type_] = value.lower() in ("true", "yes", "1") |
| if not any(self._predicates.values()): |
| print( |
| "Warning, all predicates set to False, skip_hidden may not have any effects." |
| ) |
|
|
| def do_skip_hidden(self, arg): |
| """ |
| Change whether or not we should skip frames with the |
| __tracebackhide__ attribute. |
| """ |
| if not arg.strip(): |
| print( |
| f"skip_hidden = {self.skip_hidden}, use 'yes','no', 'true', or 'false' to change." |
| ) |
| elif arg.strip().lower() in ("true", "yes"): |
| self.skip_hidden = True |
| elif arg.strip().lower() in ("false", "no"): |
| self.skip_hidden = False |
| if not any(self._predicates.values()): |
| print( |
| "Warning, all predicates set to False, skip_hidden may not have any effects." |
| ) |
|
|
| def do_list(self, arg): |
| """Print lines of code from the current stack frame""" |
| self.lastcmd = "list" |
| last = None |
| if arg and arg != ".": |
| try: |
| x = eval(arg, {}, {}) |
| if type(x) == type(()): |
| first, last = x |
| first = int(first) |
| last = int(last) |
| if last < first: |
| |
| last = first + last |
| else: |
| first = max(1, int(x) - 5) |
| except: |
| print("*** Error in argument:", repr(arg), file=self.stdout) |
| return |
| elif self.lineno is None or arg == ".": |
| assert self.curframe is not None |
| first = max(1, self.curframe.f_lineno - 5) |
| else: |
| first = self.lineno + 1 |
| if last is None: |
| last = first + 10 |
| assert self.curframe is not None |
| self.print_list_lines(self.curframe.f_code.co_filename, first, last) |
|
|
| lineno = first |
| filename = self.curframe.f_code.co_filename |
| self.shell.hooks.synchronize_with_editor(filename, lineno, 0) |
|
|
| do_l = do_list |
|
|
| def getsourcelines(self, obj): |
| lines, lineno = inspect.findsource(obj) |
| if inspect.isframe(obj) and obj.f_globals is self._get_frame_locals(obj): |
| |
| return lines, 1 |
| elif inspect.ismodule(obj): |
| return lines, 1 |
| return inspect.getblock(lines[lineno:]), lineno + 1 |
|
|
| def do_longlist(self, arg): |
| """Print lines of code from the current stack frame. |
| |
| Shows more lines than 'list' does. |
| """ |
| self.lastcmd = "longlist" |
| try: |
| lines, lineno = self.getsourcelines(self.curframe) |
| except OSError as err: |
| self.error(str(err)) |
| return |
| last = lineno + len(lines) |
| assert self.curframe is not None |
| self.print_list_lines(self.curframe.f_code.co_filename, lineno, last) |
|
|
| do_ll = do_longlist |
|
|
| def do_debug(self, arg): |
| """debug code |
| Enter a recursive debugger that steps through the code |
| argument (which is an arbitrary expression or statement to be |
| executed in the current environment). |
| """ |
| trace_function = sys.gettrace() |
| sys.settrace(None) |
| assert self.curframe is not None |
| globals = self.curframe.f_globals |
| locals = self.curframe_locals |
| p = self.__class__( |
| completekey=self.completekey, stdin=self.stdin, stdout=self.stdout |
| ) |
| p.use_rawinput = self.use_rawinput |
| p.prompt = "(%s) " % self.prompt.strip() |
| self.message("ENTERING RECURSIVE DEBUGGER") |
| sys.call_tracing(p.run, (arg, globals, locals)) |
| self.message("LEAVING RECURSIVE DEBUGGER") |
| sys.settrace(trace_function) |
| self.lastcmd = p.lastcmd |
|
|
| def do_pdef(self, arg): |
| """Print the call signature for any callable object. |
| |
| The debugger interface to %pdef""" |
| assert self.curframe is not None |
| namespaces = [ |
| ("Locals", self.curframe_locals), |
| ("Globals", self.curframe.f_globals), |
| ] |
| self.shell.find_line_magic("pdef")(arg, namespaces=namespaces) |
|
|
| def do_pdoc(self, arg): |
| """Print the docstring for an object. |
| |
| The debugger interface to %pdoc.""" |
| assert self.curframe is not None |
| namespaces = [ |
| ("Locals", self.curframe_locals), |
| ("Globals", self.curframe.f_globals), |
| ] |
| self.shell.find_line_magic("pdoc")(arg, namespaces=namespaces) |
|
|
| def do_pfile(self, arg): |
| """Print (or run through pager) the file where an object is defined. |
| |
| The debugger interface to %pfile. |
| """ |
| assert self.curframe is not None |
| namespaces = [ |
| ("Locals", self.curframe_locals), |
| ("Globals", self.curframe.f_globals), |
| ] |
| self.shell.find_line_magic("pfile")(arg, namespaces=namespaces) |
|
|
| def do_pinfo(self, arg): |
| """Provide detailed information about an object. |
| |
| The debugger interface to %pinfo, i.e., obj?.""" |
| assert self.curframe is not None |
| namespaces = [ |
| ("Locals", self.curframe_locals), |
| ("Globals", self.curframe.f_globals), |
| ] |
| self.shell.find_line_magic("pinfo")(arg, namespaces=namespaces) |
|
|
| def do_pinfo2(self, arg): |
| """Provide extra detailed information about an object. |
| |
| The debugger interface to %pinfo2, i.e., obj??.""" |
| assert self.curframe is not None |
| namespaces = [ |
| ("Locals", self.curframe_locals), |
| ("Globals", self.curframe.f_globals), |
| ] |
| self.shell.find_line_magic("pinfo2")(arg, namespaces=namespaces) |
|
|
| def do_psource(self, arg): |
| """Print (or run through pager) the source code for an object.""" |
| assert self.curframe is not None |
| namespaces = [ |
| ("Locals", self.curframe_locals), |
| ("Globals", self.curframe.f_globals), |
| ] |
| self.shell.find_line_magic("psource")(arg, namespaces=namespaces) |
|
|
| def do_where(self, arg: str): |
| """w(here) |
| Print a stack trace, with the most recent frame at the bottom. |
| An arrow indicates the "current frame", which determines the |
| context of most commands. 'bt' is an alias for this command. |
| |
| Take a number as argument as an (optional) number of context line to |
| print""" |
| if arg: |
| try: |
| context = int(arg) |
| except ValueError as err: |
| self.error(str(err)) |
| return |
| self.print_stack_trace(context) |
| else: |
| self.print_stack_trace() |
|
|
| 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 |
| |
| """ |
|
|
| 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 False |
|
|
| 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 not self._predicates["debuggerskip"]: |
| return False |
|
|
| return self._cachable_skip(frame) |
|
|
| @lru_cache(1024) |
| def _cached_one_parent_frame_debuggerskip(self, frame): |
| """ |
| Cache looking up for DEBUGGERSKIP on parent frame. |
| |
| This should speedup walking through deep frame when one of the highest |
| one does have a debugger skip. |
| |
| This is likely to introduce fake positive though. |
| """ |
| while getattr(frame, "f_back", None): |
| frame = frame.f_back |
| if self._get_frame_locals(frame).get(DEBUGGERSKIP): |
| return True |
| return None |
|
|
| @lru_cache(1024) |
| def _cachable_skip(self, frame): |
| |
| if DEBUGGERSKIP in frame.f_code.co_varnames: |
| return True |
|
|
| |
| if self._cached_one_parent_frame_debuggerskip(frame): |
| 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) |
| if hidden: |
| if self.report_skipped: |
| print( |
| self.theme.format( |
| [ |
| ( |
| Token.ExcName, |
| " [... skipped 1 hidden frame(s)]", |
| ), |
| (Token, "\n"), |
| ] |
| ) |
| ) |
| if self.skip and self.is_skipped_module(frame.f_globals.get("__name__", "")): |
| print( |
| self.theme.format( |
| [ |
| ( |
| Token.ExcName, |
| " [... skipped 1 ignored module(s)]", |
| ), |
| (Token, "\n"), |
| ] |
| ) |
| ) |
|
|
| return False |
|
|
| return super().stop_here(frame) |
|
|
| def do_up(self, arg): |
| """u(p) [count] |
| Move the current frame count (default one) levels up in the |
| stack trace (to an older frame). |
| |
| Will skip hidden frames and ignored modules. |
| """ |
| |
| |
| if self.curindex == 0: |
| self.error("Oldest frame") |
| return |
| try: |
| count = int(arg or 1) |
| except ValueError: |
| self.error("Invalid frame count (%s)" % arg) |
| return |
|
|
| hidden_skipped = 0 |
| module_skipped = 0 |
|
|
| if count < 0: |
| _newframe = 0 |
| else: |
| counter = 0 |
| hidden_frames = self.hidden_frames(self.stack) |
|
|
| for i in range(self.curindex - 1, -1, -1): |
| should_skip_hidden = hidden_frames[i] and self.skip_hidden |
| should_skip_module = self.skip and self.is_skipped_module( |
| self.stack[i][0].f_globals.get("__name__", "") |
| ) |
|
|
| if should_skip_hidden or should_skip_module: |
| if should_skip_hidden: |
| hidden_skipped += 1 |
| if should_skip_module: |
| module_skipped += 1 |
| continue |
| counter += 1 |
| if counter >= count: |
| break |
| else: |
| |
| self.error( |
| "all frames above skipped (hidden frames and ignored modules). Use `skip_hidden False` for hidden frames or unignore_module for ignored modules." |
| ) |
| return |
|
|
| _newframe = i |
| self._select_frame(_newframe) |
|
|
| total_skipped = hidden_skipped + module_skipped |
| if total_skipped: |
| print( |
| self.theme.format( |
| [ |
| ( |
| Token.ExcName, |
| f" [... skipped {total_skipped} frame(s): {hidden_skipped} hidden frames + {module_skipped} ignored modules]", |
| ), |
| (Token, "\n"), |
| ] |
| ) |
| ) |
|
|
| def do_down(self, arg): |
| """d(own) [count] |
| Move the current frame count (default one) levels down in the |
| stack trace (to a newer frame). |
| |
| Will skip hidden frames and ignored modules. |
| """ |
| if self.curindex + 1 == len(self.stack): |
| self.error("Newest frame") |
| return |
| try: |
| count = int(arg or 1) |
| except ValueError: |
| self.error("Invalid frame count (%s)" % arg) |
| return |
| if count < 0: |
| _newframe = len(self.stack) - 1 |
| else: |
| counter = 0 |
| hidden_skipped = 0 |
| module_skipped = 0 |
| hidden_frames = self.hidden_frames(self.stack) |
|
|
| for i in range(self.curindex + 1, len(self.stack)): |
| should_skip_hidden = hidden_frames[i] and self.skip_hidden |
| should_skip_module = self.skip and self.is_skipped_module( |
| self.stack[i][0].f_globals.get("__name__", "") |
| ) |
|
|
| if should_skip_hidden or should_skip_module: |
| if should_skip_hidden: |
| hidden_skipped += 1 |
| if should_skip_module: |
| module_skipped += 1 |
| continue |
| counter += 1 |
| if counter >= count: |
| break |
| else: |
| self.error( |
| "all frames below skipped (hidden frames and ignored modules). Use `skip_hidden False` for hidden frames or unignore_module for ignored modules." |
| ) |
| return |
|
|
| total_skipped = hidden_skipped + module_skipped |
| if total_skipped: |
| print( |
| self.theme.format( |
| [ |
| ( |
| Token.ExcName, |
| f" [... skipped {total_skipped} frame(s): {hidden_skipped} hidden frames + {module_skipped} ignored modules]", |
| ), |
| (Token, "\n"), |
| ] |
| ) |
| ) |
| _newframe = i |
|
|
| self._select_frame(_newframe) |
|
|
| do_d = do_down |
| do_u = do_up |
|
|
| def _show_ignored_modules(self): |
| """Display currently ignored modules.""" |
| if self.skip: |
| print(f"Currently ignored modules: {sorted(self.skip)}") |
| else: |
| print("No modules are currently ignored.") |
|
|
| def do_ignore_module(self, arg): |
| """ignore_module <module_name> |
| |
| Add a module to the list of modules to skip when navigating frames. |
| When a module is ignored, the debugger will automatically skip over |
| frames from that module. |
| |
| Supports wildcard patterns using fnmatch syntax: |
| |
| Usage: |
| ignore_module threading # Skip threading module frames |
| ignore_module asyncio.\\* # Skip all asyncio submodules |
| ignore_module \\*.tests # Skip all test modules |
| ignore_module # List currently ignored modules |
| """ |
|
|
| if self.skip is None: |
| self.skip = set() |
|
|
| module_name = arg.strip() |
|
|
| if not module_name: |
| self._show_ignored_modules() |
| return |
|
|
| self.skip.add(module_name) |
|
|
| def do_unignore_module(self, arg): |
| """unignore_module <module_name> |
| |
| Remove a module from the list of modules to skip when navigating frames. |
| This will allow the debugger to step into frames from the specified module. |
| |
| Usage: |
| unignore_module threading # Stop ignoring threading module frames |
| unignore_module asyncio.\\* # Remove asyncio.* pattern |
| unignore_module # List currently ignored modules |
| """ |
|
|
| if self.skip is None: |
| self.skip = set() |
|
|
| module_name = arg.strip() |
|
|
| if not module_name: |
| self._show_ignored_modules() |
| return |
|
|
| try: |
| self.skip.remove(module_name) |
| except KeyError: |
| print(f"Module {module_name} is not currently ignored") |
| self._show_ignored_modules() |
|
|
| def do_context(self, context: str): |
| """context number_of_lines |
| Set the number of lines of source code to show when displaying |
| stacktrace information. |
| """ |
| try: |
| new_context = int(context) |
| if new_context <= 0: |
| raise ValueError() |
| self.context = new_context |
| except ValueError: |
| self.error( |
| f"The 'context' command requires a positive integer argument (current value {self.context})." |
| ) |
|
|
|
|
| class InterruptiblePdb(Pdb): |
| """Version of debugger where KeyboardInterrupt exits the debugger altogether.""" |
|
|
| def cmdloop(self, intro=None): |
| """Wrap cmdloop() such that KeyboardInterrupt stops the debugger.""" |
| try: |
| return OldPdb.cmdloop(self, intro=intro) |
| except KeyboardInterrupt: |
| self.stop_here = lambda frame: False |
| self.do_quit("") |
| sys.settrace(None) |
| self.quitting = False |
| raise |
|
|
| def _cmdloop(self): |
| while True: |
| try: |
| |
| |
| self.allow_kbdint = True |
| self.cmdloop() |
| self.allow_kbdint = False |
| break |
| except KeyboardInterrupt: |
| self.message("--KeyboardInterrupt--") |
| raise |
|
|
|
|
| def set_trace(frame=None, header=None): |
| """ |
| Start debugging from `frame`. |
| |
| If frame is not specified, debugging starts from caller's frame. |
| """ |
| pdb = Pdb() |
| if header is not None: |
| pdb.message(header) |
| pdb.set_trace(frame or sys._getframe().f_back) |
|
|