You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

757 lines
29 KiB

6 months ago
from __future__ import absolute_import
import linecache
import os
import platform
import sys
from dataclasses import dataclass, field
from traceback import walk_tb
from types import ModuleType, TracebackType
from typing import (
Any,
Callable,
Dict,
Iterable,
List,
Optional,
Sequence,
Tuple,
Type,
Union,
)
from pip._vendor.pygments.lexers import guess_lexer_for_filename
from pip._vendor.pygments.token import Comment, Keyword, Name, Number, Operator, String
from pip._vendor.pygments.token import Text as TextToken
from pip._vendor.pygments.token import Token
from pip._vendor.pygments.util import ClassNotFound
from . import pretty
from ._loop import loop_last
from .columns import Columns
from .console import Console, ConsoleOptions, ConsoleRenderable, RenderResult, group
from .constrain import Constrain
from .highlighter import RegexHighlighter, ReprHighlighter
from .panel import Panel
from .scope import render_scope
from .style import Style
from .syntax import Syntax
from .text import Text
from .theme import Theme
WINDOWS = platform.system() == "Windows"
LOCALS_MAX_LENGTH = 10
LOCALS_MAX_STRING = 80
def install(
*,
console: Optional[Console] = None,
width: Optional[int] = 100,
extra_lines: int = 3,
theme: Optional[str] = None,
word_wrap: bool = False,
show_locals: bool = False,
locals_max_length: int = LOCALS_MAX_LENGTH,
locals_max_string: int = LOCALS_MAX_STRING,
locals_hide_dunder: bool = True,
locals_hide_sunder: Optional[bool] = None,
indent_guides: bool = True,
suppress: Iterable[Union[str, ModuleType]] = (),
max_frames: int = 100,
) -> Callable[[Type[BaseException], BaseException, Optional[TracebackType]], Any]:
"""Install a rich traceback handler.
Once installed, any tracebacks will be printed with syntax highlighting and rich formatting.
Args:
console (Optional[Console], optional): Console to write exception to. Default uses internal Console instance.
width (Optional[int], optional): Width (in characters) of traceback. Defaults to 100.
extra_lines (int, optional): Extra lines of code. Defaults to 3.
theme (Optional[str], optional): Pygments theme to use in traceback. Defaults to ``None`` which will pick
a theme appropriate for the platform.
word_wrap (bool, optional): Enable word wrapping of long lines. Defaults to False.
show_locals (bool, optional): Enable display of local variables. Defaults to False.
locals_max_length (int, optional): Maximum length of containers before abbreviating, or None for no abbreviation.
Defaults to 10.
locals_max_string (int, optional): Maximum length of string before truncating, or None to disable. Defaults to 80.
locals_hide_dunder (bool, optional): Hide locals prefixed with double underscore. Defaults to True.
locals_hide_sunder (bool, optional): Hide locals prefixed with single underscore. Defaults to False.
indent_guides (bool, optional): Enable indent guides in code and locals. Defaults to True.
suppress (Sequence[Union[str, ModuleType]]): Optional sequence of modules or paths to exclude from traceback.
Returns:
Callable: The previous exception handler that was replaced.
"""
traceback_console = Console(stderr=True) if console is None else console
locals_hide_sunder = (
True
if (traceback_console.is_jupyter and locals_hide_sunder is None)
else locals_hide_sunder
)
def excepthook(
type_: Type[BaseException],
value: BaseException,
traceback: Optional[TracebackType],
) -> None:
traceback_console.print(
Traceback.from_exception(
type_,
value,
traceback,
width=width,
extra_lines=extra_lines,
theme=theme,
word_wrap=word_wrap,
show_locals=show_locals,
locals_max_length=locals_max_length,
locals_max_string=locals_max_string,
locals_hide_dunder=locals_hide_dunder,
locals_hide_sunder=bool(locals_hide_sunder),
indent_guides=indent_guides,
suppress=suppress,
max_frames=max_frames,
)
)
def ipy_excepthook_closure(ip: Any) -> None: # pragma: no cover
tb_data = {} # store information about showtraceback call
default_showtraceback = ip.showtraceback # keep reference of default traceback
def ipy_show_traceback(*args: Any, **kwargs: Any) -> None:
"""wrap the default ip.showtraceback to store info for ip._showtraceback"""
nonlocal tb_data
tb_data = kwargs
default_showtraceback(*args, **kwargs)
def ipy_display_traceback(
*args: Any, is_syntax: bool = False, **kwargs: Any
) -> None:
"""Internally called traceback from ip._showtraceback"""
nonlocal tb_data
exc_tuple = ip._get_exc_info()
# do not display trace on syntax error
tb: Optional[TracebackType] = None if is_syntax else exc_tuple[2]
# determine correct tb_offset
compiled = tb_data.get("running_compiled_code", False)
tb_offset = tb_data.get("tb_offset", 1 if compiled else 0)
# remove ipython internal frames from trace with tb_offset
for _ in range(tb_offset):
if tb is None:
break
tb = tb.tb_next
excepthook(exc_tuple[0], exc_tuple[1], tb)
tb_data = {} # clear data upon usage
# replace _showtraceback instead of showtraceback to allow ipython features such as debugging to work
# this is also what the ipython docs recommends to modify when subclassing InteractiveShell
ip._showtraceback = ipy_display_traceback
# add wrapper to capture tb_data
ip.showtraceback = ipy_show_traceback
ip.showsyntaxerror = lambda *args, **kwargs: ipy_display_traceback(
*args, is_syntax=True, **kwargs
)
try: # pragma: no cover
# if within ipython, use customized traceback
ip = get_ipython() # type: ignore[name-defined]
ipy_excepthook_closure(ip)
return sys.excepthook
except Exception:
# otherwise use default system hook
old_excepthook = sys.excepthook
sys.excepthook = excepthook
return old_excepthook
@dataclass
class Frame:
filename: str
lineno: int
name: str
line: str = ""
locals: Optional[Dict[str, pretty.Node]] = None
@dataclass
class _SyntaxError:
offset: int
filename: str
line: str
lineno: int
msg: str
@dataclass
class Stack:
exc_type: str
exc_value: str
syntax_error: Optional[_SyntaxError] = None
is_cause: bool = False
frames: List[Frame] = field(default_factory=list)
@dataclass
class Trace:
stacks: List[Stack]
class PathHighlighter(RegexHighlighter):
highlights = [r"(?P<dim>.*/)(?P<bold>.+)"]
class Traceback:
"""A Console renderable that renders a traceback.
Args:
trace (Trace, optional): A `Trace` object produced from `extract`. Defaults to None, which uses
the last exception.
width (Optional[int], optional): Number of characters used to traceback. Defaults to 100.
extra_lines (int, optional): Additional lines of code to render. Defaults to 3.
theme (str, optional): Override pygments theme used in traceback.
word_wrap (bool, optional): Enable word wrapping of long lines. Defaults to False.
show_locals (bool, optional): Enable display of local variables. Defaults to False.
indent_guides (bool, optional): Enable indent guides in code and locals. Defaults to True.
locals_max_length (int, optional): Maximum length of containers before abbreviating, or None for no abbreviation.
Defaults to 10.
locals_max_string (int, optional): Maximum length of string before truncating, or None to disable. Defaults to 80.
locals_hide_dunder (bool, optional): Hide locals prefixed with double underscore. Defaults to True.
locals_hide_sunder (bool, optional): Hide locals prefixed with single underscore. Defaults to False.
suppress (Sequence[Union[str, ModuleType]]): Optional sequence of modules or paths to exclude from traceback.
max_frames (int): Maximum number of frames to show in a traceback, 0 for no maximum. Defaults to 100.
"""
LEXERS = {
"": "text",
".py": "python",
".pxd": "cython",
".pyx": "cython",
".pxi": "pyrex",
}
def __init__(
self,
trace: Optional[Trace] = None,
*,
width: Optional[int] = 100,
extra_lines: int = 3,
theme: Optional[str] = None,
word_wrap: bool = False,
show_locals: bool = False,
locals_max_length: int = LOCALS_MAX_LENGTH,
locals_max_string: int = LOCALS_MAX_STRING,
locals_hide_dunder: bool = True,
locals_hide_sunder: bool = False,
indent_guides: bool = True,
suppress: Iterable[Union[str, ModuleType]] = (),
max_frames: int = 100,
):
if trace is None:
exc_type, exc_value, traceback = sys.exc_info()
if exc_type is None or exc_value is None or traceback is None:
raise ValueError(
"Value for 'trace' required if not called in except: block"
)
trace = self.extract(
exc_type, exc_value, traceback, show_locals=show_locals
)
self.trace = trace
self.width = width
self.extra_lines = extra_lines
self.theme = Syntax.get_theme(theme or "ansi_dark")
self.word_wrap = word_wrap
self.show_locals = show_locals
self.indent_guides = indent_guides
self.locals_max_length = locals_max_length
self.locals_max_string = locals_max_string
self.locals_hide_dunder = locals_hide_dunder
self.locals_hide_sunder = locals_hide_sunder
self.suppress: Sequence[str] = []
for suppress_entity in suppress:
if not isinstance(suppress_entity, str):
assert (
suppress_entity.__file__ is not None
), f"{suppress_entity!r} must be a module with '__file__' attribute"
path = os.path.dirname(suppress_entity.__file__)
else:
path = suppress_entity
path = os.path.normpath(os.path.abspath(path))
self.suppress.append(path)
self.max_frames = max(4, max_frames) if max_frames > 0 else 0
@classmethod
def from_exception(
cls,
exc_type: Type[Any],
exc_value: BaseException,
traceback: Optional[TracebackType],
*,
width: Optional[int] = 100,
extra_lines: int = 3,
theme: Optional[str] = None,
word_wrap: bool = False,
show_locals: bool = False,
locals_max_length: int = LOCALS_MAX_LENGTH,
locals_max_string: int = LOCALS_MAX_STRING,
locals_hide_dunder: bool = True,
locals_hide_sunder: bool = False,
indent_guides: bool = True,
suppress: Iterable[Union[str, ModuleType]] = (),
max_frames: int = 100,
) -> "Traceback":
"""Create a traceback from exception info
Args:
exc_type (Type[BaseException]): Exception type.
exc_value (BaseException): Exception value.
traceback (TracebackType): Python Traceback object.
width (Optional[int], optional): Number of characters used to traceback. Defaults to 100.
extra_lines (int, optional): Additional lines of code to render. Defaults to 3.
theme (str, optional): Override pygments theme used in traceback.
word_wrap (bool, optional): Enable word wrapping of long lines. Defaults to False.
show_locals (bool, optional): Enable display of local variables. Defaults to False.
indent_guides (bool, optional): Enable indent guides in code and locals. Defaults to True.
locals_max_length (int, optional): Maximum length of containers before abbreviating, or None for no abbreviation.
Defaults to 10.
locals_max_string (int, optional): Maximum length of string before truncating, or None to disable. Defaults to 80.
locals_hide_dunder (bool, optional): Hide locals prefixed with double underscore. Defaults to True.
locals_hide_sunder (bool, optional): Hide locals prefixed with single underscore. Defaults to False.
suppress (Iterable[Union[str, ModuleType]]): Optional sequence of modules or paths to exclude from traceback.
max_frames (int): Maximum number of frames to show in a traceback, 0 for no maximum. Defaults to 100.
Returns:
Traceback: A Traceback instance that may be printed.
"""
rich_traceback = cls.extract(
exc_type,
exc_value,
traceback,
show_locals=show_locals,
locals_max_length=locals_max_length,
locals_max_string=locals_max_string,
locals_hide_dunder=locals_hide_dunder,
locals_hide_sunder=locals_hide_sunder,
)
return cls(
rich_traceback,
width=width,
extra_lines=extra_lines,
theme=theme,
word_wrap=word_wrap,
show_locals=show_locals,
indent_guides=indent_guides,
locals_max_length=locals_max_length,
locals_max_string=locals_max_string,
locals_hide_dunder=locals_hide_dunder,
locals_hide_sunder=locals_hide_sunder,
suppress=suppress,
max_frames=max_frames,
)
@classmethod
def extract(
cls,
exc_type: Type[BaseException],
exc_value: BaseException,
traceback: Optional[TracebackType],
*,
show_locals: bool = False,
locals_max_length: int = LOCALS_MAX_LENGTH,
locals_max_string: int = LOCALS_MAX_STRING,
locals_hide_dunder: bool = True,
locals_hide_sunder: bool = False,
) -> Trace:
"""Extract traceback information.
Args:
exc_type (Type[BaseException]): Exception type.
exc_value (BaseException): Exception value.
traceback (TracebackType): Python Traceback object.
show_locals (bool, optional): Enable display of local variables. Defaults to False.
locals_max_length (int, optional): Maximum length of containers before abbreviating, or None for no abbreviation.
Defaults to 10.
locals_max_string (int, optional): Maximum length of string before truncating, or None to disable. Defaults to 80.
locals_hide_dunder (bool, optional): Hide locals prefixed with double underscore. Defaults to True.
locals_hide_sunder (bool, optional): Hide locals prefixed with single underscore. Defaults to False.
Returns:
Trace: A Trace instance which you can use to construct a `Traceback`.
"""
stacks: List[Stack] = []
is_cause = False
from pip._vendor.rich import _IMPORT_CWD
def safe_str(_object: Any) -> str:
"""Don't allow exceptions from __str__ to propagate."""
try:
return str(_object)
except Exception:
return "<exception str() failed>"
while True:
stack = Stack(
exc_type=safe_str(exc_type.__name__),
exc_value=safe_str(exc_value),
is_cause=is_cause,
)
if isinstance(exc_value, SyntaxError):
stack.syntax_error = _SyntaxError(
offset=exc_value.offset or 0,
filename=exc_value.filename or "?",
lineno=exc_value.lineno or 0,
line=exc_value.text or "",
msg=exc_value.msg,
)
stacks.append(stack)
append = stack.frames.append
def get_locals(
iter_locals: Iterable[Tuple[str, object]]
) -> Iterable[Tuple[str, object]]:
"""Extract locals from an iterator of key pairs."""
if not (locals_hide_dunder or locals_hide_sunder):
yield from iter_locals
return
for key, value in iter_locals:
if locals_hide_dunder and key.startswith("__"):
continue
if locals_hide_sunder and key.startswith("_"):
continue
yield key, value
for frame_summary, line_no in walk_tb(traceback):
filename = frame_summary.f_code.co_filename
if filename and not filename.startswith("<"):
if not os.path.isabs(filename):
filename = os.path.join(_IMPORT_CWD, filename)
if frame_summary.f_locals.get("_rich_traceback_omit", False):
continue
frame = Frame(
filename=filename or "?",
lineno=line_no,
name=frame_summary.f_code.co_name,
locals={
key: pretty.traverse(
value,
max_length=locals_max_length,
max_string=locals_max_string,
)
for key, value in get_locals(frame_summary.f_locals.items())
}
if show_locals
else None,
)
append(frame)
if frame_summary.f_locals.get("_rich_traceback_guard", False):
del stack.frames[:]
cause = getattr(exc_value, "__cause__", None)
if cause:
exc_type = cause.__class__
exc_value = cause
# __traceback__ can be None, e.g. for exceptions raised by the
# 'multiprocessing' module
traceback = cause.__traceback__
is_cause = True
continue
cause = exc_value.__context__
if cause and not getattr(exc_value, "__suppress_context__", False):
exc_type = cause.__class__
exc_value = cause
traceback = cause.__traceback__
is_cause = False
continue
# No cover, code is reached but coverage doesn't recognize it.
break # pragma: no cover
trace = Trace(stacks=stacks)
return trace
def __rich_console__(
self, console: Console, options: ConsoleOptions
) -> RenderResult:
theme = self.theme
background_style = theme.get_background_style()
token_style = theme.get_style_for_token
traceback_theme = Theme(
{
"pretty": token_style(TextToken),
"pygments.text": token_style(Token),
"pygments.string": token_style(String),
"pygments.function": token_style(Name.Function),
"pygments.number": token_style(Number),
"repr.indent": token_style(Comment) + Style(dim=True),
"repr.str": token_style(String),
"repr.brace": token_style(TextToken) + Style(bold=True),
"repr.number": token_style(Number),
"repr.bool_true": token_style(Keyword.Constant),
"repr.bool_false": token_style(Keyword.Constant),
"repr.none": token_style(Keyword.Constant),
"scope.border": token_style(String.Delimiter),
"scope.equals": token_style(Operator),
"scope.key": token_style(Name),
"scope.key.special": token_style(Name.Constant) + Style(dim=True),
},
inherit=False,
)
highlighter = ReprHighlighter()
for last, stack in loop_last(reversed(self.trace.stacks)):
if stack.frames:
stack_renderable: ConsoleRenderable = Panel(
self._render_stack(stack),
title="[traceback.title]Traceback [dim](most recent call last)",
style=background_style,
border_style="traceback.border",
expand=True,
padding=(0, 1),
)
stack_renderable = Constrain(stack_renderable, self.width)
with console.use_theme(traceback_theme):
yield stack_renderable
if stack.syntax_error is not None:
with console.use_theme(traceback_theme):
yield Constrain(
Panel(
self._render_syntax_error(stack.syntax_error),
style=background_style,
border_style="traceback.border.syntax_error",
expand=True,
padding=(0, 1),
width=self.width,
),
self.width,
)
yield Text.assemble(
(f"{stack.exc_type}: ", "traceback.exc_type"),
highlighter(stack.syntax_error.msg),
)
elif stack.exc_value:
yield Text.assemble(
(f"{stack.exc_type}: ", "traceback.exc_type"),
highlighter(stack.exc_value),
)
else:
yield Text.assemble((f"{stack.exc_type}", "traceback.exc_type"))
if not last:
if stack.is_cause:
yield Text.from_markup(
"\n[i]The above exception was the direct cause of the following exception:\n",
)
else:
yield Text.from_markup(
"\n[i]During handling of the above exception, another exception occurred:\n",
)
@group()
def _render_syntax_error(self, syntax_error: _SyntaxError) -> RenderResult:
highlighter = ReprHighlighter()
path_highlighter = PathHighlighter()
if syntax_error.filename != "<stdin>":
if os.path.exists(syntax_error.filename):
text = Text.assemble(
(f" {syntax_error.filename}", "pygments.string"),
(":", "pygments.text"),
(str(syntax_error.lineno), "pygments.number"),
style="pygments.text",
)
yield path_highlighter(text)
syntax_error_text = highlighter(syntax_error.line.rstrip())
syntax_error_text.no_wrap = True
offset = min(syntax_error.offset - 1, len(syntax_error_text))
syntax_error_text.stylize("bold underline", offset, offset)
syntax_error_text += Text.from_markup(
"\n" + " " * offset + "[traceback.offset]▲[/]",
style="pygments.text",
)
yield syntax_error_text
@classmethod
def _guess_lexer(cls, filename: str, code: str) -> str:
ext = os.path.splitext(filename)[-1]
if not ext:
# No extension, look at first line to see if it is a hashbang
# Note, this is an educated guess and not a guarantee
# If it fails, the only downside is that the code is highlighted strangely
new_line_index = code.index("\n")
first_line = code[:new_line_index] if new_line_index != -1 else code
if first_line.startswith("#!") and "python" in first_line.lower():
return "python"
try:
return cls.LEXERS.get(ext) or guess_lexer_for_filename(filename, code).name
except ClassNotFound:
return "text"
@group()
def _render_stack(self, stack: Stack) -> RenderResult:
path_highlighter = PathHighlighter()
theme = self.theme
def read_code(filename: str) -> str:
"""Read files, and cache results on filename.
Args:
filename (str): Filename to read
Returns:
str: Contents of file
"""
return "".join(linecache.getlines(filename))
def render_locals(frame: Frame) -> Iterable[ConsoleRenderable]:
if frame.locals:
yield render_scope(
frame.locals,
title="locals",
indent_guides=self.indent_guides,
max_length=self.locals_max_length,
max_string=self.locals_max_string,
)
exclude_frames: Optional[range] = None
if self.max_frames != 0:
exclude_frames = range(
self.max_frames // 2,
len(stack.frames) - self.max_frames // 2,
)
excluded = False
for frame_index, frame in enumerate(stack.frames):
if exclude_frames and frame_index in exclude_frames:
excluded = True
continue
if excluded:
assert exclude_frames is not None
yield Text(
f"\n... {len(exclude_frames)} frames hidden ...",
justify="center",
style="traceback.error",
)
excluded = False
first = frame_index == 0
frame_filename = frame.filename
suppressed = any(frame_filename.startswith(path) for path in self.suppress)
if os.path.exists(frame.filename):
text = Text.assemble(
path_highlighter(Text(frame.filename, style="pygments.string")),
(":", "pygments.text"),
(str(frame.lineno), "pygments.number"),
" in ",
(frame.name, "pygments.function"),
style="pygments.text",
)
else:
text = Text.assemble(
"in ",
(frame.name, "pygments.function"),
(":", "pygments.text"),
(str(frame.lineno), "pygments.number"),
style="pygments.text",
)
if not frame.filename.startswith("<") and not first:
yield ""
yield text
if frame.filename.startswith("<"):
yield from render_locals(frame)
continue
if not suppressed:
try:
code = read_code(frame.filename)
if not code:
# code may be an empty string if the file doesn't exist, OR
# if the traceback filename is generated dynamically
continue
lexer_name = self._guess_lexer(frame.filename, code)
syntax = Syntax(
code,
lexer_name,
theme=theme,
line_numbers=True,
line_range=(
frame.lineno - self.extra_lines,
frame.lineno + self.extra_lines,
),
highlight_lines={frame.lineno},
word_wrap=self.word_wrap,
code_width=88,
indent_guides=self.indent_guides,
dedent=False,
)
yield ""
except Exception as error:
yield Text.assemble(
(f"\n{error}", "traceback.error"),
)
else:
yield (
Columns(
[
syntax,
*render_locals(frame),
],
padding=1,
)
if frame.locals
else syntax
)
if __name__ == "__main__": # pragma: no cover
from .console import Console
console = Console()
import sys
def bar(a: Any) -> None: # 这是对亚洲语言支持的测试。面对模棱两可的想法,拒绝猜测的诱惑
one = 1
print(one / a)
def foo(a: Any) -> None:
_rich_traceback_guard = True
zed = {
"characters": {
"Paul Atreides",
"Vladimir Harkonnen",
"Thufir Hawat",
"Duncan Idaho",
},
"atomic_types": (None, False, True),
}
bar(a)
def error() -> None:
try:
try:
foo(0)
except:
slfkjsldkfj # type: ignore[name-defined]
except:
console.print_exception(show_locals=True)
error()