| from __future__ import absolute_import |
| |
| 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, 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, |
| 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. |
| 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(file=sys.stderr) if console is None else console |
| |
| 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, |
| 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. |
| 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, |
| indent_guides: bool = True, |
| locals_max_length: int = LOCALS_MAX_LENGTH, |
| locals_max_string: int = LOCALS_MAX_STRING, |
| 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.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, |
| indent_guides: bool = True, |
| locals_max_length: int = LOCALS_MAX_LENGTH, |
| locals_max_string: int = LOCALS_MAX_STRING, |
| 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. |
| 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 |
| ) |
| 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, |
| 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, |
| ) -> 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. |
| |
| 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 propegate.""" |
| 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 |
| |
| 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 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 and cause.__traceback__: |
| exc_type = cause.__class__ |
| exc_value = cause |
| traceback = cause.__traceback__ |
| is_cause = True |
| continue |
| |
| cause = exc_value.__context__ |
| if ( |
| cause |
| and cause.__traceback__ |
| 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>": |
| 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 |
| code_cache: Dict[str, str] = {} |
| |
| def read_code(filename: str) -> str: |
| """Read files, and cache results on filename. |
| |
| Args: |
| filename (str): Filename to read |
| |
| Returns: |
| str: Contents of file |
| """ |
| code = code_cache.get(filename) |
| if code is None: |
| with open( |
| filename, "rt", encoding="utf-8", errors="replace" |
| ) as code_file: |
| code = code_file.read() |
| code_cache[filename] = code |
| return code |
| |
| 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) |
| |
| 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", |
| ) |
| 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) |
| 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() |