debugger.py
1113 lines
| 37.5 KiB
| text/x-python
|
PythonLexer
|
r0 | """ | ||
Pdb debugger class. | ||||
|
r26810 | |||
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. | ||||
|
r26811 | 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__` | ||||
|
r26810 | |||
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. | ||||
|
r26824 | >>> def helpers_helper(): | ||
... pass | ||||
... | ||||
... def helper_1(): | ||||
|
r26810 | ... print("don't step in me") | ||
|
r26824 | ... helpers_helpers() # will be stepped over unless breakpoint set. | ||
|
r26810 | ... | ||
... | ||||
... def helper_2(): | ||||
... print("in me neither") | ||||
... | ||||
|
r26811 | One can define a decorator that wraps a function between the two helpers: | ||
|
r26810 | |||
>>> def pdb_skipped_decorator(function): | ||||
... | ||||
... | ||||
... def wrapped_fn(*args, **kwargs): | ||||
... __debuggerskip__ = True | ||||
... helper_1() | ||||
... __debuggerskip__ = False | ||||
... result = function(*args, **kwargs) | ||||
... __debuggerskip__ = True | ||||
... helper_2() | ||||
|
r26824 | ... # setting __debuggerskip__ to False again is not necessary | ||
|
r26810 | ... 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`` | ||||
|
r26875 | License | ||
------- | ||||
|
r26810 | |||
|
r0 | 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. | ||||
|
r26810 | 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: | ||||
|
r0 | |||
|
r23220 | https://docs.python.org/2/license.html | ||
|
r26810 | |||
All the changes since then are under the same license as IPython. | ||||
|
r23220 | """ | ||
|
r88 | |||
#***************************************************************************** | ||||
# | ||||
|
r911 | # This file is licensed under the PSF license. | ||
|
r88 | # | ||
# Copyright (C) 2001 Python Software Foundation, www.python.org | ||||
# Copyright (C) 2005-2006 Fernando Perez. <fperez@colorado.edu> | ||||
# | ||||
# | ||||
#***************************************************************************** | ||||
|
r28707 | from __future__ import annotations | ||
|
r21395 | import inspect | ||
|
r23116 | import linecache | ||
|
r26540 | import os | ||
|
r28706 | import re | ||
import sys | ||||
from contextlib import contextmanager | ||||
from functools import lru_cache | ||||
|
r52 | |||
|
r10580 | from IPython import get_ipython | ||
|
r2021 | from IPython.core.excolors import exception_colors | ||
|
r28706 | from IPython.utils import PyColorize, coloransi, py3compat | ||
|
r22390 | |||
|
r28707 | from typing import TYPE_CHECKING | ||
if TYPE_CHECKING: | ||||
# otherwise circular import | ||||
from IPython.core.interactiveshell import InteractiveShell | ||||
|
r26873 | # skip module docstests | ||
__skip_doctest__ = True | ||||
|
r22383 | |||
|
r552 | prompt = 'ipdb> ' | ||
|
r22359 | |||
|
r26325 | # We have to check this directly from sys.argv, config struct not yet available | ||
|
r22359 | from pdb import Pdb as OldPdb | ||
|
r393 | |||
|
r506 | # Allow the set_trace code to operate outside of an ipython instance, even if | ||
# it does so with some limitations. The rest of this support is implemented in | ||||
# the Tracer constructor. | ||||
|
r22083 | |||
|
r26810 | DEBUGGERSKIP = "__debuggerskip__" | ||
|
r26325 | |||
|
r28377 | # this has been implemented in Pdb in Python 3.13 (https://github.com/python/cpython/pull/106676 | ||
# on lower python versions, we backported the feature. | ||||
CHAIN_EXCEPTIONS = sys.version_info < (3, 13) | ||||
|
r22083 | def make_arrow(pad): | ||
"""generate the leading arrow in front of traceback or debugger""" | ||||
if pad >= 2: | ||||
return '-'*(pad-2) + '> ' | ||||
elif pad == 1: | ||||
return '>' | ||||
return '' | ||||
|
r28725 | 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 arround only because it is still imported by ipdb.", | ||||
) | ||||
|
r24451 | RGX_EXTRA_INDENT = re.compile(r'(?<=\n)\s+') | ||
|
r24093 | |||
|
r24092 | def strip_indentation(multiline_string): | ||
return RGX_EXTRA_INDENT.sub('', multiline_string) | ||||
|
r393 | def decorate_fn_with_doc(new_fn, old_fn, additional_text=""): | ||
"""Make new_fn have old_fn's doc string. This is particularly useful | ||||
|
r9244 | for the ``do_...`` commands that hook into the help system. | ||
|
r393 | Adapted from from a comp.lang.python posting | ||
by Duncan Booth.""" | ||||
def wrapper(*args, **kw): | ||||
return new_fn(*args, **kw) | ||||
if old_fn.__doc__: | ||||
|
r24092 | wrapper.__doc__ = strip_indentation(old_fn.__doc__) + additional_text | ||
|
r393 | return wrapper | ||
|
r2290 | |||
|
r23003 | class Pdb(OldPdb): | ||
|
r22724 | """Modified Pdb class, does not load readline. | ||
|
r22731 | for a standalone version that uses prompt_toolkit, see | ||
|
r22724 | `IPython.terminal.debugger.TerminalPdb` and | ||
`IPython.terminal.debugger.set_trace()` | ||||
|
r26566 | |||
This debugger can hide and skip frames that are tagged according to some predicates. | ||||
See the `skip_predicates` commands. | ||||
|
r22724 | """ | ||
|
r122 | |||
|
r28707 | shell: InteractiveShell | ||
|
r28377 | if CHAIN_EXCEPTIONS: | ||
MAX_CHAINED_EXCEPTION_DEPTH = 999 | ||||
|
r26810 | default_predicates = { | ||
"tbhide": True, | ||||
"readonly": False, | ||||
"ipython_internal": True, | ||||
"debuggerskip": True, | ||||
} | ||||
|
r26566 | |||
|
r27225 | def __init__(self, completekey=None, stdin=None, stdout=None, context=5, **kwargs): | ||
|
r25173 | """Create a new IPython debugger. | ||
|
r26325 | |||
|
r26566 | 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 | ||||
|
r25173 | displaying stacktrace information. | ||
|
r26566 | **kwargs | ||
Passed to pdb.Pdb. | ||||
Notes | ||||
----- | ||||
The possibilities are python version dependent, see the python | ||||
docs for more info. | ||||
|
r25173 | """ | ||
|
r367 | |||
|
r2290 | # Parent constructor: | ||
|
r21876 | try: | ||
|
r22687 | self.context = int(context) | ||
|
r21876 | if self.context <= 0: | ||
raise ValueError("Context must be a positive integer") | ||||
|
r25824 | except (TypeError, ValueError) as e: | ||
raise ValueError("Context must be a positive integer") from e | ||||
|
r25176 | |||
|
r25172 | # `kwargs` ensures full compatibility with stdlib's `pdb.Pdb`. | ||
OldPdb.__init__(self, completekey, stdin, stdout, **kwargs) | ||||
|
r4872 | |||
|
r2290 | # IPython changes... | ||
|
r10581 | self.shell = get_ipython() | ||
|
r367 | |||
|
r11114 | if self.shell is None: | ||
|
r22907 | save_main = sys.modules['__main__'] | ||
|
r11114 | # No IPython instance running, we must create one | ||
from IPython.terminal.interactiveshell import \ | ||||
TerminalInteractiveShell | ||||
self.shell = TerminalInteractiveShell.instance() | ||||
|
r22907 | # needed by any code which calls __import__("__main__") after | ||
# the debugger was entered. See also #9941. | ||||
|
r26328 | sys.modules["__main__"] = save_main | ||
|
r11114 | |||
|
r27225 | |||
color_scheme = self.shell.colors | ||||
|
r22687 | |||
|
r2290 | self.aliases = {} | ||
|
r553 | |||
|
r2290 | # Create color table: we copy the default one from the traceback | ||
# module and add a few attributes needed for debugging | ||||
self.color_scheme_table = exception_colors() | ||||
|
r367 | |||
|
r4872 | # shorthands | ||
|
r2290 | C = coloransi.TermColors | ||
cst = self.color_scheme_table | ||||
|
r367 | |||
|
r22609 | |||
|
r2290 | # Add a python parser so we can syntax highlight source while | ||
# debugging. | ||||
|
r22911 | self.parser = PyColorize.Parser(style=color_scheme) | ||
self.set_colors(color_scheme) | ||||
|
r553 | |||
|
r22094 | # Set the prompt - the default prompt is '(Pdb)' | ||
|
r22383 | self.prompt = prompt | ||
|
r25839 | self.skip_hidden = True | ||
|
r26573 | self.report_skipped = True | ||
|
r21692 | |||
|
r26540 | # list of predicates we use to skip frames | ||
|
r26566 | self._predicates = self.default_predicates | ||
|
r26540 | |||
|
r28377 | if CHAIN_EXCEPTIONS: | ||
self._chained_exceptions = tuple() | ||||
self._chained_exception_index = 0 | ||||
|
r26810 | # | ||
|
r46 | def set_colors(self, scheme): | ||
"""Shorthand access to the color table scheme selector method.""" | ||||
self.color_scheme_table.set_active_scheme(scheme) | ||||
|
r22911 | self.parser.style = scheme | ||
|
r46 | |||
|
r26135 | def set_trace(self, frame=None): | ||
if frame is None: | ||||
frame = sys._getframe().f_back | ||||
self.initial_frame = frame | ||||
return super().set_trace(frame) | ||||
|
r25839 | |||
|
r26540 | 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 | ||||
# we need to check for file existence and interactively define | ||||
# function would otherwise appear as RO. | ||||
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 | ||||
|
r27017 | frame_locals = self._get_frame_locals(frame) | ||
if "__tracebackhide__" not in frame_locals: | ||||
return False | ||||
return frame_locals["__tracebackhide__"] | ||||
|
r26540 | return False | ||
|
r25839 | def hidden_frames(self, stack): | ||
""" | ||||
|
r26053 | Given an index in the stack return whether it should be skipped. | ||
|
r25839 | |||
This is used in up/down and where to skip frames. | ||||
""" | ||||
|
r25895 | # The f_locals dictionary is updated from the actual frame | ||
# locals whenever the .f_locals accessor is called, so we | ||||
# avoid calling it here to preserve self.curframe_locals. | ||||
|
r26875 | # Furthermore, there is no good reason to hide the current frame. | ||
|
r26540 | ip_hide = [self._hidden_predicate(s[0]) for s in stack] | ||
|
r25839 | ip_start = [i for i, s in enumerate(ip_hide) if s == "__ipython_bottom__"] | ||
|
r26540 | if ip_start and self._predicates["ipython_internal"]: | ||
|
r25839 | ip_hide = [h if i > ip_start[0] else True for (i, h) in enumerate(ip_hide)] | ||
return ip_hide | ||||
|
r28377 | 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: | ||||
# we can't put those in forget as otherwise they would | ||||
# be cleared on exception change | ||||
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] + "..." | ||||
|
r28389 | indicator = ( | ||
" -" | ||||
if self._chained_exceptions[ix].__traceback__ is None | ||||
else f"{ix:>3}" | ||||
) | ||||
self.message(f"{prompt} {indicator} {rep}") | ||||
|
r28377 | else: | ||
try: | ||||
number = int(arg) | ||||
except ValueError: | ||||
self.error("Argument must be an integer") | ||||
return | ||||
if 0 <= number < len(self._chained_exceptions): | ||||
|
r28389 | if self._chained_exceptions[number].__traceback__ is None: | ||
self.error( | ||||
"This exception does not have a traceback, cannot jump to it" | ||||
) | ||||
return | ||||
|
r28377 | 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): | ||||
|
r22385 | try: | ||
|
r28377 | if CHAIN_EXCEPTIONS: | ||
# this context manager is part of interaction in 3.13 | ||||
_chained_exceptions, tb = self._get_tb_and_exceptions(tb_or_exc) | ||||
|
r28389 | if isinstance(tb_or_exc, BaseException): | ||
assert tb is not None, "main exception must have a traceback" | ||||
|
r28377 | with self._hold_exceptions(_chained_exceptions): | ||
OldPdb.interaction(self, frame, tb) | ||||
else: | ||||
|
r28558 | OldPdb.interaction(self, frame, tb_or_exc) | ||
|
r28377 | |||
|
r22385 | except KeyboardInterrupt: | ||
|
r25839 | self.stdout.write("\n" + self.shell.get_exception_only()) | ||
|
r393 | |||
|
r26327 | def precmd(self, line): | ||
"""Perform useful escapes on the command before it is executed.""" | ||||
|
r26328 | if line.endswith("??"): | ||
line = "pinfo2 " + line[:-2] | ||||
elif line.endswith("?"): | ||||
line = "pinfo " + line[:-1] | ||||
|
r26327 | |||
line = super().precmd(line) | ||||
return line | ||||
|
r393 | 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) | ||||
|
r21876 | def print_stack_trace(self, context=None): | ||
|
r25839 | Colors = self.color_scheme_table.active_colors | ||
ColorsNormal = Colors.Normal | ||||
|
r21876 | if context is None: | ||
context = self.context | ||||
try: | ||||
|
r26325 | context = int(context) | ||
|
r21876 | if context <= 0: | ||
raise ValueError("Context must be a positive integer") | ||||
|
r25824 | except (TypeError, ValueError) as e: | ||
raise ValueError("Context must be a positive integer") from e | ||||
|
r46 | try: | ||
|
r25839 | skipped = 0 | ||
for hidden, frame_lineno in zip(self.hidden_frames(self.stack), self.stack): | ||||
if hidden and self.skip_hidden: | ||||
skipped += 1 | ||||
continue | ||||
if skipped: | ||||
print( | ||||
f"{Colors.excName} [... skipping {skipped} hidden frame(s)]{ColorsNormal}\n" | ||||
) | ||||
skipped = 0 | ||||
|
r21876 | self.print_stack_entry(frame_lineno, context=context) | ||
|
r25839 | if skipped: | ||
print( | ||||
f"{Colors.excName} [... skipping {skipped} hidden frame(s)]{ColorsNormal}\n" | ||||
) | ||||
|
r46 | except KeyboardInterrupt: | ||
|
r0 | pass | ||
|
r46 | |||
|
r25083 | def print_stack_entry(self, frame_lineno, prompt_prefix='\n-> ', | ||
|
r21876 | context=None): | ||
if context is None: | ||||
context = self.context | ||||
try: | ||||
|
r26325 | context = int(context) | ||
|
r21876 | if context <= 0: | ||
raise ValueError("Context must be a positive integer") | ||||
|
r25824 | except (TypeError, ValueError) as e: | ||
raise ValueError("Context must be a positive integer") from e | ||||
|
r25083 | print(self.format_stack_entry(frame_lineno, '', context), file=self.stdout) | ||
|
r46 | |||
|
r1241 | # vds: >> | ||
frame, lineno = frame_lineno | ||||
filename = frame.f_code.co_filename | ||||
|
r2290 | self.shell.hooks.synchronize_with_editor(filename, lineno, 0) | ||
|
r1241 | # vds: << | ||
|
r26584 | def _get_frame_locals(self, frame): | ||
""" " | ||||
|
r26875 | Accessing f_local of current frame reset the namespace, so we want to avoid | ||
that or the following can happen | ||||
|
r26584 | |||
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 | ||||
|
r21876 | def format_stack_entry(self, frame_lineno, lprefix=': ', context=None): | ||
if context is None: | ||||
context = self.context | ||||
try: | ||||
|
r26325 | context = int(context) | ||
|
r21876 | if context <= 0: | ||
|
r25083 | print("Context must be a positive integer", file=self.stdout) | ||
|
r21876 | except (TypeError, ValueError): | ||
|
r25083 | print("Context must be a positive integer", file=self.stdout) | ||
|
r25516 | |||
import reprlib | ||||
|
r4872 | |||
|
r294 | ret = [] | ||
|
r4872 | |||
|
r46 | Colors = self.color_scheme_table.active_colors | ||
ColorsNormal = Colors.Normal | ||||
|
r26328 | tpl_link = "%s%%s%s" % (Colors.filenameEm, ColorsNormal) | ||
tpl_call = "%s%%s%s%%s%s" % (Colors.vName, Colors.valEm, ColorsNormal) | ||||
tpl_line = "%%s%s%%s %s%%s" % (Colors.lineno, ColorsNormal) | ||||
tpl_line_em = "%%s%s%%s %s%%s%s" % (Colors.linenoEm, Colors.line, ColorsNormal) | ||||
|
r4872 | |||
|
r46 | frame, lineno = frame_lineno | ||
|
r4872 | |||
|
r46 | return_value = '' | ||
|
r26584 | loc_frame = self._get_frame_locals(frame) | ||
if "__return__" in loc_frame: | ||||
rv = loc_frame["__return__"] | ||||
# return_value += '->' | ||||
return_value += reprlib.repr(rv) + "\n" | ||||
|
r294 | ret.append(return_value) | ||
|
r46 | |||
#s = filename + '(' + `lineno` + ')' | ||||
filename = self.canonic(frame.f_code.co_filename) | ||||
|
r8327 | link = tpl_link % py3compat.cast_unicode(filename) | ||
|
r4872 | |||
|
r46 | if frame.f_code.co_name: | ||
func = frame.f_code.co_name | ||||
else: | ||||
func = "<lambda>" | ||||
|
r4872 | |||
|
r26584 | call = "" | ||
if func != "?": | ||||
if "__args__" in loc_frame: | ||||
args = reprlib.repr(loc_frame["__args__"]) | ||||
|
r46 | else: | ||
args = '()' | ||||
call = tpl_call % (func, args) | ||||
|
r294 | |||
# The level info should be generated in the same format pdb uses, to | ||||
# avoid breaking the pdbtrack functionality of python-mode in *emacs. | ||||
|
r552 | if frame is self.curframe: | ||
ret.append('> ') | ||||
else: | ||||
|
r26328 | ret.append(" ") | ||
ret.append("%s(%s)%s\n" % (link, lineno, call)) | ||||
|
r4872 | |||
|
r46 | start = lineno - 1 - context//2 | ||
|
r23116 | lines = linecache.getlines(filename) | ||
|
r46 | start = min(start, len(lines) - context) | ||
|
r8895 | start = max(start, 0) | ||
|
r46 | lines = lines[start : start + context] | ||
|
r4872 | |||
|
r26325 | for i, line in enumerate(lines): | ||
|
r26328 | show_arrow = start + 1 + i == lineno | ||
linetpl = (frame is self.curframe or show_arrow) and tpl_line_em or tpl_line | ||||
ret.append( | ||||
self.__format_line( | ||||
linetpl, filename, start + 1 + i, line, arrow=show_arrow | ||||
) | ||||
) | ||||
return "".join(ret) | ||||
|
r46 | |||
|
r26325 | def __format_line(self, tpl_line, filename, lineno, line, arrow=False): | ||
|
r46 | bp_mark = "" | ||
bp_mark_color = "" | ||||
|
r22911 | new_line, err = self.parser.format2(line, 'str') | ||
if not err: | ||||
line = new_line | ||||
|
r553 | |||
|
r46 | bp = None | ||
if lineno in self.get_file_breaks(filename): | ||||
bps = self.get_breaks(filename, lineno) | ||||
bp = bps[-1] | ||||
|
r4872 | |||
|
r46 | if bp: | ||
Colors = self.color_scheme_table.active_colors | ||||
bp_mark = str(bp.number) | ||||
bp_mark_color = Colors.breakpoint_enabled | ||||
if not bp.enabled: | ||||
bp_mark_color = Colors.breakpoint_disabled | ||||
numbers_width = 7 | ||||
if arrow: | ||||
# This is the line with the error | ||||
pad = numbers_width - len(str(lineno)) - len(bp_mark) | ||||
|
r22083 | num = '%s%s' % (make_arrow(pad), str(lineno)) | ||
|
r46 | else: | ||
num = '%*s' % (numbers_width - len(bp_mark), str(lineno)) | ||||
|
r4872 | |||
|
r22083 | return tpl_line % (bp_mark_color + bp_mark, num, line) | ||
|
r393 | def print_list_lines(self, filename, first, last): | ||
"""The printing (as opposed to the parsing part of a 'list' | ||||
command.""" | ||||
try: | ||||
Colors = self.color_scheme_table.active_colors | ||||
ColorsNormal = Colors.Normal | ||||
tpl_line = '%%s%s%%s %s%%s' % (Colors.lineno, ColorsNormal) | ||||
tpl_line_em = '%%s%s%%s %s%%s%s' % (Colors.linenoEm, Colors.line, ColorsNormal) | ||||
src = [] | ||||
|
r8317 | if filename == "<string>" and hasattr(self, "_exec_filename"): | ||
|
r8324 | filename = self._exec_filename | ||
|
r8678 | |||
|
r393 | for lineno in range(first, last+1): | ||
|
r23116 | line = linecache.getline(filename, lineno) | ||
|
r393 | if not line: | ||
break | ||||
if lineno == self.curframe.f_lineno: | ||||
|
r26328 | line = self.__format_line( | ||
tpl_line_em, filename, lineno, line, arrow=True | ||||
) | ||||
|
r393 | else: | ||
|
r26328 | line = self.__format_line( | ||
tpl_line, filename, lineno, line, arrow=False | ||||
) | ||||
|
r393 | |||
src.append(line) | ||||
self.lineno = lineno | ||||
|
r25083 | print(''.join(src), file=self.stdout) | ||
|
r393 | |||
except KeyboardInterrupt: | ||||
pass | ||||
|
r26540 | 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. | ||||
|
r26566 | 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. | ||||
|
r26540 | """ | ||
if not args.strip(): | ||||
print("current predicates:") | ||||
|
r28124 | for p, v in self._predicates.items(): | ||
|
r26540 | print(" ", p, ":", v) | ||
return | ||||
type_value = args.strip().split(" ") | ||||
if len(type_value) != 2: | ||||
|
r26542 | print( | ||
f"Usage: skip_predicates <type> <value>, with <type> one of {set(self._predicates.keys())}" | ||||
) | ||||
|
r26540 | 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( | ||||
|
r26542 | f"{value!r} is invalid - use one of ('true', 'yes', '1', 'no', 'false', '0')" | ||
|
r26540 | ) | ||
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." | ||||
) | ||||
|
r25839 | def do_skip_hidden(self, arg): | ||
""" | ||||
Change whether or not we should skip frames with the | ||||
__tracebackhide__ attribute. | ||||
""" | ||||
|
r26540 | 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"): | ||||
|
r25839 | self.skip_hidden = True | ||
elif arg.strip().lower() in ("false", "no"): | ||||
self.skip_hidden = False | ||||
|
r26540 | if not any(self._predicates.values()): | ||
print( | ||||
"Warning, all predicates set to False, skip_hidden may not have any effects." | ||||
) | ||||
|
r25839 | |||
|
r46 | def do_list(self, arg): | ||
|
r23488 | """Print lines of code from the current stack frame | ||
""" | ||||
|
r46 | self.lastcmd = 'list' | ||
last = None | ||||
|
r28341 | if arg and arg != ".": | ||
|
r46 | try: | ||
x = eval(arg, {}, {}) | ||||
if type(x) == type(()): | ||||
first, last = x | ||||
first = int(first) | ||||
last = int(last) | ||||
if last < first: | ||||
# Assume it's a count | ||||
last = first + last | ||||
else: | ||||
first = max(1, int(x) - 5) | ||||
except: | ||||
|
r25083 | print('*** Error in argument:', repr(arg), file=self.stdout) | ||
|
r46 | return | ||
|
r28341 | elif self.lineno is None or arg == ".": | ||
|
r46 | first = max(1, self.curframe.f_lineno - 5) | ||
|
r0 | else: | ||
|
r46 | first = self.lineno + 1 | ||
if last is None: | ||||
last = first + 10 | ||||
|
r393 | self.print_list_lines(self.curframe.f_code.co_filename, first, last) | ||
|
r46 | |||
|
r1241 | # vds: >> | ||
lineno = first | ||||
filename = self.curframe.f_code.co_filename | ||||
|
r2290 | self.shell.hooks.synchronize_with_editor(filename, lineno, 0) | ||
|
r1241 | # vds: << | ||
|
r46 | do_l = do_list | ||
|
r393 | |||
|
r21395 | def getsourcelines(self, obj): | ||
lines, lineno = inspect.findsource(obj) | ||||
|
r26584 | if inspect.isframe(obj) and obj.f_globals is self._get_frame_locals(obj): | ||
|
r21395 | # must be a module frame: do not try to cut a block out of it | ||
return lines, 1 | ||||
elif inspect.ismodule(obj): | ||||
return lines, 1 | ||||
return inspect.getblock(lines[lineno:]), lineno+1 | ||||
def do_longlist(self, arg): | ||||
|
r23488 | """Print lines of code from the current stack frame. | ||
Shows more lines than 'list' does. | ||||
""" | ||||
|
r21395 | self.lastcmd = 'longlist' | ||
try: | ||||
lines, lineno = self.getsourcelines(self.curframe) | ||||
except OSError as err: | ||||
self.error(err) | ||||
return | ||||
last = lineno + len(lines) | ||||
self.print_list_lines(self.curframe.f_code.co_filename, lineno, last) | ||||
do_ll = do_longlist | ||||
|
r23823 | 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). | ||||
""" | ||||
|
r26149 | trace_function = sys.gettrace() | ||
|
r23823 | sys.settrace(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") | ||||
|
r26149 | sys.settrace(trace_function) | ||
|
r23823 | self.lastcmd = p.lastcmd | ||
|
r393 | def do_pdef(self, arg): | ||
|
r8707 | """Print the call signature for any callable object. | ||
|
r8698 | |||
The debugger interface to %pdef""" | ||||
|
r26170 | namespaces = [ | ||
("Locals", self.curframe_locals), | ||||
("Globals", self.curframe.f_globals), | ||||
] | ||||
self.shell.find_line_magic("pdef")(arg, namespaces=namespaces) | ||||
|
r393 | |||
def do_pdoc(self, arg): | ||||
|
r8698 | """Print the docstring for an object. | ||
The debugger interface to %pdoc.""" | ||||
|
r26170 | namespaces = [ | ||
("Locals", self.curframe_locals), | ||||
("Globals", self.curframe.f_globals), | ||||
] | ||||
self.shell.find_line_magic("pdoc")(arg, namespaces=namespaces) | ||||
|
r393 | |||
|
r8699 | def do_pfile(self, arg): | ||
"""Print (or run through pager) the file where an object is defined. | ||||
The debugger interface to %pfile. | ||||
""" | ||||
|
r26170 | namespaces = [ | ||
("Locals", self.curframe_locals), | ||||
("Globals", self.curframe.f_globals), | ||||
] | ||||
self.shell.find_line_magic("pfile")(arg, namespaces=namespaces) | ||||
|
r8699 | |||
|
r393 | def do_pinfo(self, arg): | ||
|
r8698 | """Provide detailed information about an object. | ||
The debugger interface to %pinfo, i.e., obj?.""" | ||||
|
r26170 | namespaces = [ | ||
("Locals", self.curframe_locals), | ||||
("Globals", self.curframe.f_globals), | ||||
] | ||||
self.shell.find_line_magic("pinfo")(arg, namespaces=namespaces) | ||||
|
r8701 | |||
def do_pinfo2(self, arg): | ||||
"""Provide extra detailed information about an object. | ||||
The debugger interface to %pinfo2, i.e., obj??.""" | ||||
|
r26170 | namespaces = [ | ||
("Locals", self.curframe_locals), | ||||
("Globals", self.curframe.f_globals), | ||||
] | ||||
self.shell.find_line_magic("pinfo2")(arg, namespaces=namespaces) | ||||
|
r2372 | |||
|
r8700 | def do_psource(self, arg): | ||
"""Print (or run through pager) the source code for an object.""" | ||||
|
r26170 | namespaces = [ | ||
("Locals", self.curframe_locals), | ||||
("Globals", self.curframe.f_globals), | ||||
] | ||||
self.shell.find_line_magic("psource")(arg, namespaces=namespaces) | ||||
|
r21876 | |||
|
r22959 | def do_where(self, arg): | ||
"""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: | ||||
|
r25620 | try: | ||
context = int(arg) | ||||
|
r25622 | except ValueError as err: | ||
self.error(err) | ||||
|
r25620 | return | ||
|
r22959 | self.print_stack_trace(context) | ||
else: | ||||
self.print_stack_trace() | ||||
|
r21876 | |||
|
r22959 | do_w = do_where | ||
|
r22871 | |||
|
r26810 | 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 | ||||
""" | ||||
|
r26824 | |||
sup = super().break_anywhere(frame) | ||||
if sup: | ||||
return sup | ||||
|
r26810 | 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 | ||||
|
r26824 | return False | ||
|
r26810 | |||
def _is_in_decorator_internal_and_should_skip(self, frame): | ||||
""" | ||||
Utility to tell us whether we are in a decorator internal and should stop. | ||||
""" | ||||
|
r26811 | # if we are disabled don't skip | ||
|
r26810 | if not self._predicates["debuggerskip"]: | ||
return False | ||||
|
r28706 | return self._cachable_skip(frame) | ||
|
r28735 | @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) | ||||
|
r28706 | def _cachable_skip(self, frame): | ||
|
r26810 | # if frame is tagged, skip by default. | ||
if DEBUGGERSKIP in frame.f_code.co_varnames: | ||||
return True | ||||
|
r26824 | # if one of the parent frame value set to True skip as well. | ||
|
r28735 | if self._cached_one_parent_frame_debuggerskip(frame): | ||
return True | ||||
|
r26810 | |||
return False | ||||
|
r25839 | def stop_here(self, frame): | ||
|
r26810 | if self._is_in_decorator_internal_and_should_skip(frame) is True: | ||
return False | ||||
|
r25839 | hidden = False | ||
if self.skip_hidden: | ||||
|
r26540 | hidden = self._hidden_predicate(frame) | ||
|
r25839 | if hidden: | ||
|
r26573 | if self.report_skipped: | ||
Colors = self.color_scheme_table.active_colors | ||||
ColorsNormal = Colors.Normal | ||||
print( | ||||
f"{Colors.excName} [... skipped 1 hidden frame]{ColorsNormal}\n" | ||||
) | ||||
|
r25839 | 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. | ||||
""" | ||||
|
r26325 | # modified version of upstream that skips | ||
|
r26540 | # frames with __tracebackhide__ | ||
|
r25839 | 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 | ||||
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): | ||||
if hidden_frames[i] and self.skip_hidden: | ||||
skipped += 1 | ||||
continue | ||||
counter += 1 | ||||
if counter >= count: | ||||
break | ||||
else: | ||||
|
r26875 | # if no break occurred. | ||
|
r26053 | self.error( | ||
"all frames above hidden, use `skip_hidden False` to get get into those." | ||||
) | ||||
|
r25839 | return | ||
Colors = self.color_scheme_table.active_colors | ||||
ColorsNormal = Colors.Normal | ||||
_newframe = i | ||||
self._select_frame(_newframe) | ||||
if skipped: | ||||
print( | ||||
f"{Colors.excName} [... skipped {skipped} hidden frame(s)]{ColorsNormal}\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. | ||||
""" | ||||
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 | ||||
skipped = 0 | ||||
hidden_frames = self.hidden_frames(self.stack) | ||||
for i in range(self.curindex + 1, len(self.stack)): | ||||
if hidden_frames[i] and self.skip_hidden: | ||||
skipped += 1 | ||||
continue | ||||
counter += 1 | ||||
if counter >= count: | ||||
break | ||||
else: | ||||
|
r26053 | self.error( | ||
|
r26875 | "all frames below hidden, use `skip_hidden False` to get get into those." | ||
|
r26053 | ) | ||
|
r25839 | return | ||
Colors = self.color_scheme_table.active_colors | ||||
ColorsNormal = Colors.Normal | ||||
if skipped: | ||||
print( | ||||
f"{Colors.excName} [... skipped {skipped} hidden frame(s)]{ColorsNormal}\n" | ||||
) | ||||
_newframe = i | ||||
self._select_frame(_newframe) | ||||
do_d = do_down | ||||
do_u = do_up | ||||
|
r22871 | |||
|
r26339 | def do_context(self, context): | ||
"""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() | ||||
|
r26357 | self.context = new_context | ||
|
r26339 | except ValueError: | ||
self.error("The 'context' command requires a positive integer argument.") | ||||
|
r25649 | class InterruptiblePdb(Pdb): | ||
"""Version of debugger where KeyboardInterrupt exits the debugger altogether.""" | ||||
|
r26810 | def cmdloop(self, intro=None): | ||
|
r25649 | """Wrap cmdloop() such that KeyboardInterrupt stops the debugger.""" | ||
try: | ||||
|
r26810 | return OldPdb.cmdloop(self, intro=intro) | ||
|
r25649 | except KeyboardInterrupt: | ||
self.stop_here = lambda frame: False | ||||
self.do_quit("") | ||||
sys.settrace(None) | ||||
self.quitting = False | ||||
raise | ||||
|
r25568 | def _cmdloop(self): | ||
while True: | ||||
try: | ||||
# keyboard interrupts allow for an easy way to cancel | ||||
# the current command, so allow them during interactive input | ||||
self.allow_kbdint = True | ||||
self.cmdloop() | ||||
self.allow_kbdint = False | ||||
break | ||||
except KeyboardInterrupt: | ||||
|
r25649 | self.message('--KeyboardInterrupt--') | ||
|
r25568 | raise | ||
|
r22871 | |||
|
r28583 | def set_trace(frame=None, header=None): | ||
|
r22872 | """ | ||
Start debugging from `frame`. | ||||
If frame is not specified, debugging starts from caller's frame. | ||||
""" | ||||
|
r28583 | pdb = Pdb() | ||
if header is not None: | ||||
pdb.message(header) | ||||
pdb.set_trace(frame or sys._getframe().f_back) | ||||