codekingpro/portable-devtools
114k
1from __future__ import absolute_import2 3import linecache4import os5import platform6import sys7from dataclasses import dataclass, field8from traceback import walk_tb9from types import ModuleType, TracebackType10from typing import (11 Any,12 Callable,13 Dict,14 Iterable,15 List,16 Optional,17 Sequence,18 Tuple,19 Type,20 Union,21)22 23from pygments.lexers import guess_lexer_for_filename24from pygments.token import Comment, Keyword, Name, Number, Operator, String25from pygments.token import Text as TextToken26from pygments.token import Token27from pygments.util import ClassNotFound28 29from . import pretty30from ._loop import loop_last31from .columns import Columns32from .console import Console, ConsoleOptions, ConsoleRenderable, RenderResult, group33from .constrain import Constrain34from .highlighter import RegexHighlighter, ReprHighlighter35from .panel import Panel36from .scope import render_scope37from .style import Style38from .syntax import Syntax39from .text import Text40from .theme import Theme41 42WINDOWS = platform.system() == "Windows"43 44LOCALS_MAX_LENGTH = 1045LOCALS_MAX_STRING = 8046 47 48def install(49 *,50 console: Optional[Console] = None,51 width: Optional[int] = 100,52 extra_lines: int = 3,53 theme: Optional[str] = None,54 word_wrap: bool = False,55 show_locals: bool = False,56 locals_max_length: int = LOCALS_MAX_LENGTH,57 locals_max_string: int = LOCALS_MAX_STRING,58 locals_hide_dunder: bool = True,59 locals_hide_sunder: Optional[bool] = None,60 indent_guides: bool = True,61 suppress: Iterable[Union[str, ModuleType]] = (),62 max_frames: int = 100,63) -> Callable[[Type[BaseException], BaseException, Optional[TracebackType]], Any]:64 """Install a rich traceback handler.65 66 Once installed, any tracebacks will be printed with syntax highlighting and rich formatting.67 68 69 Args:70 console (Optional[Console], optional): Console to write exception to. Default uses internal Console instance.71 width (Optional[int], optional): Width (in characters) of traceback. Defaults to 100.72 extra_lines (int, optional): Extra lines of code. Defaults to 3.73 theme (Optional[str], optional): Pygments theme to use in traceback. Defaults to ``None`` which will pick74 a theme appropriate for the platform.75 word_wrap (bool, optional): Enable word wrapping of long lines. Defaults to False.76 show_locals (bool, optional): Enable display of local variables. Defaults to False.77 locals_max_length (int, optional): Maximum length of containers before abbreviating, or None for no abbreviation.78 Defaults to 10.79 locals_max_string (int, optional): Maximum length of string before truncating, or None to disable. Defaults to 80.80 locals_hide_dunder (bool, optional): Hide locals prefixed with double underscore. Defaults to True.81 locals_hide_sunder (bool, optional): Hide locals prefixed with single underscore. Defaults to False.82 indent_guides (bool, optional): Enable indent guides in code and locals. Defaults to True.83 suppress (Sequence[Union[str, ModuleType]]): Optional sequence of modules or paths to exclude from traceback.84 85 Returns:86 Callable: The previous exception handler that was replaced.87 88 """89 traceback_console = Console(stderr=True) if console is None else console90 91 locals_hide_sunder = (92 True93 if (traceback_console.is_jupyter and locals_hide_sunder is None)94 else locals_hide_sunder95 )96 97 def excepthook(98 type_: Type[BaseException],99 value: BaseException,100 traceback: Optional[TracebackType],101 ) -> None:102 traceback_console.print(103 Traceback.from_exception(104 type_,105 value,106 traceback,107 width=width,108 extra_lines=extra_lines,109 theme=theme,110 word_wrap=word_wrap,111 show_locals=show_locals,112 locals_max_length=locals_max_length,113 locals_max_string=locals_max_string,114 locals_hide_dunder=locals_hide_dunder,115 locals_hide_sunder=bool(locals_hide_sunder),116 indent_guides=indent_guides,117 suppress=suppress,118 max_frames=max_frames,119 )120 )121 122 def ipy_excepthook_closure(ip: Any) -> None: # pragma: no cover123 tb_data = {} # store information about showtraceback call124 default_showtraceback = ip.showtraceback # keep reference of default traceback125 126 def ipy_show_traceback(*args: Any, **kwargs: Any) -> None:127 """wrap the default ip.showtraceback to store info for ip._showtraceback"""128 nonlocal tb_data129 tb_data = kwargs130 default_showtraceback(*args, **kwargs)131 132 def ipy_display_traceback(133 *args: Any, is_syntax: bool = False, **kwargs: Any134 ) -> None:135 """Internally called traceback from ip._showtraceback"""136 nonlocal tb_data137 exc_tuple = ip._get_exc_info()138 139 # do not display trace on syntax error140 tb: Optional[TracebackType] = None if is_syntax else exc_tuple[2]141 142 # determine correct tb_offset143 compiled = tb_data.get("running_compiled_code", False)144 tb_offset = tb_data.get("tb_offset", 1 if compiled else 0)145 # remove ipython internal frames from trace with tb_offset146 for _ in range(tb_offset):147 if tb is None:148 break149 tb = tb.tb_next150 151 excepthook(exc_tuple[0], exc_tuple[1], tb)152 tb_data = {} # clear data upon usage153 154 # replace _showtraceback instead of showtraceback to allow ipython features such as debugging to work155 # this is also what the ipython docs recommends to modify when subclassing InteractiveShell156 ip._showtraceback = ipy_display_traceback157 # add wrapper to capture tb_data158 ip.showtraceback = ipy_show_traceback159 ip.showsyntaxerror = lambda *args, **kwargs: ipy_display_traceback(160 *args, is_syntax=True, **kwargs161 )162 163 try: # pragma: no cover164 # if within ipython, use customized traceback165 ip = get_ipython() # type: ignore[name-defined]166 ipy_excepthook_closure(ip)167 return sys.excepthook168 except Exception:169 # otherwise use default system hook170 old_excepthook = sys.excepthook171 sys.excepthook = excepthook172 return old_excepthook173 174 175@dataclass176class Frame:177 filename: str178 lineno: int179 name: str180 line: str = ""181 locals: Optional[Dict[str, pretty.Node]] = None182 183 184@dataclass185class _SyntaxError:186 offset: int187 filename: str188 line: str189 lineno: int190 msg: str191 192 193@dataclass194class Stack:195 exc_type: str196 exc_value: str197 syntax_error: Optional[_SyntaxError] = None198 is_cause: bool = False199 frames: List[Frame] = field(default_factory=list)200 201 202@dataclass203class Trace:204 stacks: List[Stack]205 206 207class PathHighlighter(RegexHighlighter):208 highlights = [r"(?P<dim>.*/)(?P<bold>.+)"]209 210 211class Traceback:212 """A Console renderable that renders a traceback.213 214 Args:215 trace (Trace, optional): A `Trace` object produced from `extract`. Defaults to None, which uses216 the last exception.217 width (Optional[int], optional): Number of characters used to traceback. Defaults to 100.218 extra_lines (int, optional): Additional lines of code to render. Defaults to 3.219 theme (str, optional): Override pygments theme used in traceback.220 word_wrap (bool, optional): Enable word wrapping of long lines. Defaults to False.221 show_locals (bool, optional): Enable display of local variables. Defaults to False.222 indent_guides (bool, optional): Enable indent guides in code and locals. Defaults to True.223 locals_max_length (int, optional): Maximum length of containers before abbreviating, or None for no abbreviation.224 Defaults to 10.225 locals_max_string (int, optional): Maximum length of string before truncating, or None to disable. Defaults to 80.226 locals_hide_dunder (bool, optional): Hide locals prefixed with double underscore. Defaults to True.227 locals_hide_sunder (bool, optional): Hide locals prefixed with single underscore. Defaults to False.228 suppress (Sequence[Union[str, ModuleType]]): Optional sequence of modules or paths to exclude from traceback.229 max_frames (int): Maximum number of frames to show in a traceback, 0 for no maximum. Defaults to 100.230 231 """232 233 LEXERS = {234 "": "text",235 ".py": "python",236 ".pxd": "cython",237 ".pyx": "cython",238 ".pxi": "pyrex",239 }240 241 def __init__(242 self,243 trace: Optional[Trace] = None,244 *,245 width: Optional[int] = 100,246 extra_lines: int = 3,247 theme: Optional[str] = None,248 word_wrap: bool = False,249 show_locals: bool = False,250 locals_max_length: int = LOCALS_MAX_LENGTH,251 locals_max_string: int = LOCALS_MAX_STRING,252 locals_hide_dunder: bool = True,253 locals_hide_sunder: bool = False,254 indent_guides: bool = True,255 suppress: Iterable[Union[str, ModuleType]] = (),256 max_frames: int = 100,257 ):258 if trace is None:259 exc_type, exc_value, traceback = sys.exc_info()260 if exc_type is None or exc_value is None or traceback is None:261 raise ValueError(262 "Value for 'trace' required if not called in except: block"263 )264 trace = self.extract(265 exc_type, exc_value, traceback, show_locals=show_locals266 )267 self.trace = trace268 self.width = width269 self.extra_lines = extra_lines270 self.theme = Syntax.get_theme(theme or "ansi_dark")271 self.word_wrap = word_wrap272 self.show_locals = show_locals273 self.indent_guides = indent_guides274 self.locals_max_length = locals_max_length275 self.locals_max_string = locals_max_string276 self.locals_hide_dunder = locals_hide_dunder277 self.locals_hide_sunder = locals_hide_sunder278 279 self.suppress: Sequence[str] = []280 for suppress_entity in suppress:281 if not isinstance(suppress_entity, str):282 assert (283 suppress_entity.__file__ is not None284 ), f"{suppress_entity!r} must be a module with '__file__' attribute"285 path = os.path.dirname(suppress_entity.__file__)286 else:287 path = suppress_entity288 path = os.path.normpath(os.path.abspath(path))289 self.suppress.append(path)290 self.max_frames = max(4, max_frames) if max_frames > 0 else 0291 292 @classmethod293 def from_exception(294 cls,295 exc_type: Type[Any],296 exc_value: BaseException,297 traceback: Optional[TracebackType],298 *,299 width: Optional[int] = 100,300 extra_lines: int = 3,301 theme: Optional[str] = None,302 word_wrap: bool = False,303 show_locals: bool = False,304 locals_max_length: int = LOCALS_MAX_LENGTH,305 locals_max_string: int = LOCALS_MAX_STRING,306 locals_hide_dunder: bool = True,307 locals_hide_sunder: bool = False,308 indent_guides: bool = True,309 suppress: Iterable[Union[str, ModuleType]] = (),310 max_frames: int = 100,311 ) -> "Traceback":312 """Create a traceback from exception info313 314 Args:315 exc_type (Type[BaseException]): Exception type.316 exc_value (BaseException): Exception value.317 traceback (TracebackType): Python Traceback object.318 width (Optional[int], optional): Number of characters used to traceback. Defaults to 100.319 extra_lines (int, optional): Additional lines of code to render. Defaults to 3.320 theme (str, optional): Override pygments theme used in traceback.321 word_wrap (bool, optional): Enable word wrapping of long lines. Defaults to False.322 show_locals (bool, optional): Enable display of local variables. Defaults to False.323 indent_guides (bool, optional): Enable indent guides in code and locals. Defaults to True.324 locals_max_length (int, optional): Maximum length of containers before abbreviating, or None for no abbreviation.325 Defaults to 10.326 locals_max_string (int, optional): Maximum length of string before truncating, or None to disable. Defaults to 80.327 locals_hide_dunder (bool, optional): Hide locals prefixed with double underscore. Defaults to True.328 locals_hide_sunder (bool, optional): Hide locals prefixed with single underscore. Defaults to False.329 suppress (Iterable[Union[str, ModuleType]]): Optional sequence of modules or paths to exclude from traceback.330 max_frames (int): Maximum number of frames to show in a traceback, 0 for no maximum. Defaults to 100.331 332 Returns:333 Traceback: A Traceback instance that may be printed.334 """335 rich_traceback = cls.extract(336 exc_type,337 exc_value,338 traceback,339 show_locals=show_locals,340 locals_max_length=locals_max_length,341 locals_max_string=locals_max_string,342 locals_hide_dunder=locals_hide_dunder,343 locals_hide_sunder=locals_hide_sunder,344 )345 346 return cls(347 rich_traceback,348 width=width,349 extra_lines=extra_lines,350 theme=theme,351 word_wrap=word_wrap,352 show_locals=show_locals,353 indent_guides=indent_guides,354 locals_max_length=locals_max_length,355 locals_max_string=locals_max_string,356 locals_hide_dunder=locals_hide_dunder,357 locals_hide_sunder=locals_hide_sunder,358 suppress=suppress,359 max_frames=max_frames,360 )361 362 @classmethod363 def extract(364 cls,365 exc_type: Type[BaseException],366 exc_value: BaseException,367 traceback: Optional[TracebackType],368 *,369 show_locals: bool = False,370 locals_max_length: int = LOCALS_MAX_LENGTH,371 locals_max_string: int = LOCALS_MAX_STRING,372 locals_hide_dunder: bool = True,373 locals_hide_sunder: bool = False,374 ) -> Trace:375 """Extract traceback information.376 377 Args:378 exc_type (Type[BaseException]): Exception type.379 exc_value (BaseException): Exception value.380 traceback (TracebackType): Python Traceback object.381 show_locals (bool, optional): Enable display of local variables. Defaults to False.382 locals_max_length (int, optional): Maximum length of containers before abbreviating, or None for no abbreviation.383 Defaults to 10.384 locals_max_string (int, optional): Maximum length of string before truncating, or None to disable. Defaults to 80.385 locals_hide_dunder (bool, optional): Hide locals prefixed with double underscore. Defaults to True.386 locals_hide_sunder (bool, optional): Hide locals prefixed with single underscore. Defaults to False.387 388 Returns:389 Trace: A Trace instance which you can use to construct a `Traceback`.390 """391 392 stacks: List[Stack] = []393 is_cause = False394 395 from rich import _IMPORT_CWD396 397 def safe_str(_object: Any) -> str:398 """Don't allow exceptions from __str__ to propagate."""399 try:400 return str(_object)401 except Exception:402 return "<exception str() failed>"403 404 while True:405 stack = Stack(406 exc_type=safe_str(exc_type.__name__),407 exc_value=safe_str(exc_value),408 is_cause=is_cause,409 )410 411 if isinstance(exc_value, SyntaxError):412 stack.syntax_error = _SyntaxError(413 offset=exc_value.offset or 0,414 filename=exc_value.filename or "?",415 lineno=exc_value.lineno or 0,416 line=exc_value.text or "",417 msg=exc_value.msg,418 )419 420 stacks.append(stack)421 append = stack.frames.append422 423 def get_locals(424 iter_locals: Iterable[Tuple[str, object]]425 ) -> Iterable[Tuple[str, object]]:426 """Extract locals from an iterator of key pairs."""427 if not (locals_hide_dunder or locals_hide_sunder):428 yield from iter_locals429 return430 for key, value in iter_locals:431 if locals_hide_dunder and key.startswith("__"):432 continue433 if locals_hide_sunder and key.startswith("_"):434 continue435 yield key, value436 437 for frame_summary, line_no in walk_tb(traceback):438 filename = frame_summary.f_code.co_filename439 if filename and not filename.startswith("<"):440 if not os.path.isabs(filename):441 filename = os.path.join(_IMPORT_CWD, filename)442 if frame_summary.f_locals.get("_rich_traceback_omit", False):443 continue444 445 frame = Frame(446 filename=filename or "?",447 lineno=line_no,448 name=frame_summary.f_code.co_name,449 locals={450 key: pretty.traverse(451 value,452 max_length=locals_max_length,453 max_string=locals_max_string,454 )455 for key, value in get_locals(frame_summary.f_locals.items())456 }457 if show_locals458 else None,459 )460 append(frame)461 if frame_summary.f_locals.get("_rich_traceback_guard", False):462 del stack.frames[:]463 464 cause = getattr(exc_value, "__cause__", None)465 if cause:466 exc_type = cause.__class__467 exc_value = cause468 # __traceback__ can be None, e.g. for exceptions raised by the469 # 'multiprocessing' module470 traceback = cause.__traceback__471 is_cause = True472 continue473 474 cause = exc_value.__context__475 if cause and not getattr(exc_value, "__suppress_context__", False):476 exc_type = cause.__class__477 exc_value = cause478 traceback = cause.__traceback__479 is_cause = False480 continue481 # No cover, code is reached but coverage doesn't recognize it.482 break # pragma: no cover483 484 trace = Trace(stacks=stacks)485 return trace486 487 def __rich_console__(488 self, console: Console, options: ConsoleOptions489 ) -> RenderResult:490 theme = self.theme491 background_style = theme.get_background_style()492 token_style = theme.get_style_for_token493 494 traceback_theme = Theme(495 {496 "pretty": token_style(TextToken),497 "pygments.text": token_style(Token),498 "pygments.string": token_style(String),499 "pygments.function": token_style(Name.Function),500 "pygments.number": token_style(Number),501 "repr.indent": token_style(Comment) + Style(dim=True),502 "repr.str": token_style(String),503 "repr.brace": token_style(TextToken) + Style(bold=True),504 "repr.number": token_style(Number),505 "repr.bool_true": token_style(Keyword.Constant),506 "repr.bool_false": token_style(Keyword.Constant),507 "repr.none": token_style(Keyword.Constant),508 "scope.border": token_style(String.Delimiter),509 "scope.equals": token_style(Operator),510 "scope.key": token_style(Name),511 "scope.key.special": token_style(Name.Constant) + Style(dim=True),512 },513 inherit=False,514 )515 516 highlighter = ReprHighlighter()517 for last, stack in loop_last(reversed(self.trace.stacks)):518 if stack.frames:519 stack_renderable: ConsoleRenderable = Panel(520 self._render_stack(stack),521 title="[traceback.title]Traceback [dim](most recent call last)",522 style=background_style,523 border_style="traceback.border",524 expand=True,525 padding=(0, 1),526 )527 stack_renderable = Constrain(stack_renderable, self.width)528 with console.use_theme(traceback_theme):529 yield stack_renderable530 if stack.syntax_error is not None:531 with console.use_theme(traceback_theme):532 yield Constrain(533 Panel(534 self._render_syntax_error(stack.syntax_error),535 style=background_style,536 border_style="traceback.border.syntax_error",537 expand=True,538 padding=(0, 1),539 width=self.width,540 ),541 self.width,542 )543 yield Text.assemble(544 (f"{stack.exc_type}: ", "traceback.exc_type"),545 highlighter(stack.syntax_error.msg),546 )547 elif stack.exc_value:548 yield Text.assemble(549 (f"{stack.exc_type}: ", "traceback.exc_type"),550 highlighter(stack.exc_value),551 )552 else:553 yield Text.assemble((f"{stack.exc_type}", "traceback.exc_type"))554 555 if not last:556 if stack.is_cause:557 yield Text.from_markup(558 "\n[i]The above exception was the direct cause of the following exception:\n",559 )560 else:561 yield Text.from_markup(562 "\n[i]During handling of the above exception, another exception occurred:\n",563 )564 565 @group()566 def _render_syntax_error(self, syntax_error: _SyntaxError) -> RenderResult:567 highlighter = ReprHighlighter()568 path_highlighter = PathHighlighter()569 if syntax_error.filename != "<stdin>":570 if os.path.exists(syntax_error.filename):571 text = Text.assemble(572 (f" {syntax_error.filename}", "pygments.string"),573 (":", "pygments.text"),574 (str(syntax_error.lineno), "pygments.number"),575 style="pygments.text",576 )577 yield path_highlighter(text)578 syntax_error_text = highlighter(syntax_error.line.rstrip())579 syntax_error_text.no_wrap = True580 offset = min(syntax_error.offset - 1, len(syntax_error_text))581 syntax_error_text.stylize("bold underline", offset, offset)582 syntax_error_text += Text.from_markup(583 "\n" + " " * offset + "[traceback.offset]▲[/]",584 style="pygments.text",585 )586 yield syntax_error_text587 588 @classmethod589 def _guess_lexer(cls, filename: str, code: str) -> str:590 ext = os.path.splitext(filename)[-1]591 if not ext:592 # No extension, look at first line to see if it is a hashbang593 # Note, this is an educated guess and not a guarantee594 # If it fails, the only downside is that the code is highlighted strangely595 new_line_index = code.index("\n")596 first_line = code[:new_line_index] if new_line_index != -1 else code597 if first_line.startswith("#!") and "python" in first_line.lower():598 return "python"599 try:600 return cls.LEXERS.get(ext) or guess_lexer_for_filename(filename, code).name601 except ClassNotFound:602 return "text"603 604 @group()605 def _render_stack(self, stack: Stack) -> RenderResult:606 path_highlighter = PathHighlighter()607 theme = self.theme608 609 def read_code(filename: str) -> str:610 """Read files, and cache results on filename.611 612 Args:613 filename (str): Filename to read614 615 Returns:616 str: Contents of file617 """618 return "".join(linecache.getlines(filename))619 620 def render_locals(frame: Frame) -> Iterable[ConsoleRenderable]:621 if frame.locals:622 yield render_scope(623 frame.locals,624 title="locals",625 indent_guides=self.indent_guides,626 max_length=self.locals_max_length,627 max_string=self.locals_max_string,628 )629 630 exclude_frames: Optional[range] = None631 if self.max_frames != 0:632 exclude_frames = range(633 self.max_frames // 2,634 len(stack.frames) - self.max_frames // 2,635 )636 637 excluded = False638 for frame_index, frame in enumerate(stack.frames):639 if exclude_frames and frame_index in exclude_frames:640 excluded = True641 continue642 643 if excluded:644 assert exclude_frames is not None645 yield Text(646 f"\n... {len(exclude_frames)} frames hidden ...",647 justify="center",648 style="traceback.error",649 )650 excluded = False651 652 first = frame_index == 0653 frame_filename = frame.filename654 suppressed = any(frame_filename.startswith(path) for path in self.suppress)655 656 if os.path.exists(frame.filename):657 text = Text.assemble(658 path_highlighter(Text(frame.filename, style="pygments.string")),659 (":", "pygments.text"),660 (str(frame.lineno), "pygments.number"),661 " in ",662 (frame.name, "pygments.function"),663 style="pygments.text",664 )665 else:666 text = Text.assemble(667 "in ",668 (frame.name, "pygments.function"),669 (":", "pygments.text"),670 (str(frame.lineno), "pygments.number"),671 style="pygments.text",672 )673 if not frame.filename.startswith("<") and not first:674 yield ""675 yield text676 if frame.filename.startswith("<"):677 yield from render_locals(frame)678 continue679 if not suppressed:680 try:681 code = read_code(frame.filename)682 if not code:683 # code may be an empty string if the file doesn't exist, OR684 # if the traceback filename is generated dynamically685 continue686 lexer_name = self._guess_lexer(frame.filename, code)687 syntax = Syntax(688 code,689 lexer_name,690 theme=theme,691 line_numbers=True,692 line_range=(693 frame.lineno - self.extra_lines,694 frame.lineno + self.extra_lines,695 ),696 highlight_lines={frame.lineno},697 word_wrap=self.word_wrap,698 code_width=88,699 indent_guides=self.indent_guides,700 dedent=False,701 )702 yield ""703 except Exception as error:704 yield Text.assemble(705 (f"\n{error}", "traceback.error"),706 )707 else:708 yield (709 Columns(710 [711 syntax,712 *render_locals(frame),713 ],714 padding=1,715 )716 if frame.locals717 else syntax718 )719 720 721if __name__ == "__main__": # pragma: no cover722 from .console import Console723 724 console = Console()725 import sys726 727 def bar(a: Any) -> None: # 这是对亚洲语言支持的测试。面对模棱两可的想法,拒绝猜测的诱惑728 one = 1729 print(one / a)730 731 def foo(a: Any) -> None:732 _rich_traceback_guard = True733 zed = {734 "characters": {735 "Paul Atreides",736 "Vladimir Harkonnen",737 "Thufir Hawat",738 "Duncan Idaho",739 },740 "atomic_types": (None, False, True),741 }742 bar(a)743 744 def error() -> None:745 try:746 try:747 foo(0)748 except:749 slfkjsldkfj # type: ignore[name-defined]750 except:751 console.print_exception(show_locals=True)752 753 error()754 