1from __future__ import absolute_import 2 3import os 4import platform 5import sys 6from dataclasses import dataclass, field 7from traceback import walk_tb 8from types import TracebackType 9from typing import Any, Callable, Dict, Iterable, List, Optional, Type 10 11from pygments.lexers import guess_lexer_for_filename 12from pygments.token import Comment, Keyword, Name, Number, Operator, String 13from pygments.token import Text as TextToken 14from pygments.token import Token 15 16from . import pretty 17from ._loop import loop_first, loop_last 18from .columns import Columns 19from .console import ( 20 Console, 21 ConsoleOptions, 22 ConsoleRenderable, 23 RenderResult, 24 render_group, 25) 26from .constrain import Constrain 27from .highlighter import RegexHighlighter, ReprHighlighter 28from .panel import Panel 29from .scope import render_scope 30from .style import Style 31from .syntax import Syntax 32from .text import Text 33from .theme import Theme 34 35WINDOWS = platform.system() == "Windows" 36 37LOCALS_MAX_LENGTH = 10 38LOCALS_MAX_STRING = 80 39 40 41def install( 42 *, 43 console: Console = None, 44 width: Optional[int] = 100, 45 extra_lines: int = 3, 46 theme: Optional[str] = None, 47 word_wrap: bool = False, 48 show_locals: bool = False, 49 indent_guides: bool = True, 50) -> Callable: 51 """Install a rich traceback handler. 52 53 Once installed, any tracebacks will be printed with syntax highlighting and rich formatting. 54 55 56 Args: 57 console (Optional[Console], optional): Console to write exception to. Default uses internal Console instance. 58 width (Optional[int], optional): Width (in characters) of traceback. Defaults to 100. 59 extra_lines (int, optional): Extra lines of code. Defaults to 3. 60 theme (Optional[str], optional): Pygments theme to use in traceback. Defaults to ``None`` which will pick 61 a theme appropriate for the platform. 62 word_wrap (bool, optional): Enable word wrapping of long lines. Defaults to False. 63 show_locals (bool, optional): Enable display of local variables. Defaults to False. 64 indent_guides (bool, optional): Enable indent guides in code and locals. Defaults to True. 65 66 Returns: 67 Callable: The previous exception handler that was replaced. 68 69 """ 70 traceback_console = Console(file=sys.stderr) if console is None else console 71 72 def excepthook( 73 type_: Type[BaseException], 74 value: BaseException, 75 traceback: Optional[TracebackType], 76 ) -> None: 77 traceback_console.print( 78 Traceback.from_exception( 79 type_, 80 value, 81 traceback, 82 width=width, 83 extra_lines=extra_lines, 84 theme=theme, 85 word_wrap=word_wrap, 86 show_locals=show_locals, 87 indent_guides=indent_guides, 88 ) 89 ) 90 91 def ipy_excepthook_closure(ip) -> None: # pragma: no cover 92 tb_data = {} # store information about showtraceback call 93 default_showtraceback = ip.showtraceback # keep reference of default traceback 94 95 def ipy_show_traceback(*args, **kwargs) -> None: 96 """wrap the default ip.showtraceback to store info for ip._showtraceback""" 97 nonlocal tb_data 98 tb_data = kwargs 99 default_showtraceback(*args, **kwargs) 100 101 def ipy_display_traceback(*args, is_syntax: bool = False, **kwargs) -> None: 102 """Internally called traceback from ip._showtraceback""" 103 nonlocal tb_data 104 exc_tuple = ip._get_exc_info() 105 106 # do not display trace on syntax error 107 tb: Optional[TracebackType] = None if is_syntax else exc_tuple[2] 108 109 # determine correct tb_offset 110 compiled = tb_data.get("running_compiled_code", False) 111 tb_offset = tb_data.get("tb_offset", 1 if compiled else 0) 112 # remove ipython internal frames from trace with tb_offset 113 for _ in range(tb_offset): 114 if tb is None: 115 break 116 tb = tb.tb_next 117 118 excepthook(exc_tuple[0], exc_tuple[1], tb) 119 tb_data = {} # clear data upon usage 120 121 # replace _showtraceback instead of showtraceback to allow ipython features such as debugging to work 122 # this is also what the ipython docs recommends to modify when subclassing InteractiveShell 123 ip._showtraceback = ipy_display_traceback 124 # add wrapper to capture tb_data 125 ip.showtraceback = ipy_show_traceback 126 ip.showsyntaxerror = lambda *args, **kwargs: ipy_display_traceback( 127 *args, is_syntax=True, **kwargs 128 ) 129 130 try: # pragma: no cover 131 # if wihin ipython, use customized traceback 132 ip = get_ipython() # type: ignore 133 ipy_excepthook_closure(ip) 134 return sys.excepthook 135 except Exception: 136 # otherwise use default system hook 137 old_excepthook = sys.excepthook 138 sys.excepthook = excepthook 139 return old_excepthook 140 141 142@dataclass 143class Frame: 144 filename: str 145 lineno: int 146 name: str 147 line: str = "" 148 locals: Optional[Dict[str, pretty.Node]] = None 149 150 151@dataclass 152class _SyntaxError: 153 offset: int 154 filename: str 155 line: str 156 lineno: int 157 msg: str 158 159 160@dataclass 161class Stack: 162 exc_type: str 163 exc_value: str 164 syntax_error: Optional[_SyntaxError] = None 165 is_cause: bool = False 166 frames: List[Frame] = field(default_factory=list) 167 168 169@dataclass 170class Trace: 171 stacks: List[Stack] 172 173 174class PathHighlighter(RegexHighlighter): 175 highlights = [r"(?P<dim>.*/)(?P<bold>.+)"] 176 177 178class Traceback: 179 """A Console renderable that renders a traceback. 180 181 Args: 182 trace (Trace, optional): A `Trace` object produced from `extract`. Defaults to None, which uses 183 the last exception. 184 width (Optional[int], optional): Number of characters used to traceback. Defaults to 100. 185 extra_lines (int, optional): Additional lines of code to render. Defaults to 3. 186 theme (str, optional): Override pygments theme used in traceback. 187 word_wrap (bool, optional): Enable word wrapping of long lines. Defaults to False. 188 show_locals (bool, optional): Enable display of local variables. Defaults to False. 189 indent_guides (bool, optional): Enable indent guides in code and locals. Defaults to True. 190 locals_max_length (int, optional): Maximum length of containers before abbreviating, or None for no abbreviation. 191 Defaults to 10. 192 locals_max_string (int, optional): Maximum length of string before truncating, or None to disable. Defaults to 80. 193 """ 194 195 LEXERS = { 196 "": "text", 197 ".py": "python", 198 ".pxd": "cython", 199 ".pyx": "cython", 200 ".pxi": "pyrex", 201 } 202 203 def __init__( 204 self, 205 trace: Trace = None, 206 width: Optional[int] = 100, 207 extra_lines: int = 3, 208 theme: Optional[str] = None, 209 word_wrap: bool = False, 210 show_locals: bool = False, 211 indent_guides: bool = True, 212 locals_max_length: int = LOCALS_MAX_LENGTH, 213 locals_max_string: int = LOCALS_MAX_STRING, 214 ): 215 if trace is None: 216 exc_type, exc_value, traceback = sys.exc_info() 217 if exc_type is None or exc_value is None or traceback is None: 218 raise ValueError( 219 "Value for 'trace' required if not called in except: block" 220 ) 221 trace = self.extract( 222 exc_type, exc_value, traceback, show_locals=show_locals 223 ) 224 self.trace = trace 225 self.width = width 226 self.extra_lines = extra_lines 227 self.theme = Syntax.get_theme(theme or "ansi_dark") 228 self.word_wrap = word_wrap 229 self.show_locals = show_locals 230 self.indent_guides = indent_guides 231 self.locals_max_length = locals_max_length 232 self.locals_max_string = locals_max_string 233 234 @classmethod 235 def from_exception( 236 cls, 237 exc_type: Type, 238 exc_value: BaseException, 239 traceback: Optional[TracebackType], 240 width: Optional[int] = 100, 241 extra_lines: int = 3, 242 theme: Optional[str] = None, 243 word_wrap: bool = False, 244 show_locals: bool = False, 245 indent_guides: bool = True, 246 locals_max_length: int = LOCALS_MAX_LENGTH, 247 locals_max_string: int = LOCALS_MAX_STRING, 248 ) -> "Traceback": 249 """Create a traceback from exception info 250 251 Args: 252 exc_type (Type[BaseException]): Exception type. 253 exc_value (BaseException): Exception value. 254 traceback (TracebackType): Python Traceback object. 255 width (Optional[int], optional): Number of characters used to traceback. Defaults to 100. 256 extra_lines (int, optional): Additional lines of code to render. Defaults to 3. 257 theme (str, optional): Override pygments theme used in traceback. 258 word_wrap (bool, optional): Enable word wrapping of long lines. Defaults to False. 259 show_locals (bool, optional): Enable display of local variables. Defaults to False. 260 indent_guides (bool, optional): Enable indent guides in code and locals. Defaults to True. 261 locals_max_length (int, optional): Maximum length of containers before abbreviating, or None for no abbreviation. 262 Defaults to 10. 263 locals_max_string (int, optional): Maximum length of string before truncating, or None to disable. Defaults to 80. 264 265 Returns: 266 Traceback: A Traceback instance that may be printed. 267 """ 268 rich_traceback = cls.extract( 269 exc_type, exc_value, traceback, show_locals=show_locals 270 ) 271 return cls( 272 rich_traceback, 273 width=width, 274 extra_lines=extra_lines, 275 theme=theme, 276 word_wrap=word_wrap, 277 show_locals=show_locals, 278 indent_guides=indent_guides, 279 locals_max_length=locals_max_length, 280 locals_max_string=locals_max_string, 281 ) 282 283 @classmethod 284 def extract( 285 cls, 286 exc_type: Type[BaseException], 287 exc_value: BaseException, 288 traceback: Optional[TracebackType], 289 show_locals: bool = False, 290 locals_max_length: int = LOCALS_MAX_LENGTH, 291 locals_max_string: int = LOCALS_MAX_STRING, 292 ) -> Trace: 293 """Extract traceback information. 294 295 Args: 296 exc_type (Type[BaseException]): Exception type. 297 exc_value (BaseException): Exception value. 298 traceback (TracebackType): Python Traceback object. 299 show_locals (bool, optional): Enable display of local variables. Defaults to False. 300 locals_max_length (int, optional): Maximum length of containers before abbreviating, or None for no abbreviation. 301 Defaults to 10. 302 locals_max_string (int, optional): Maximum length of string before truncating, or None to disable. Defaults to 80. 303 304 Returns: 305 Trace: A Trace instance which you can use to construct a `Traceback`. 306 """ 307 308 stacks: List[Stack] = [] 309 is_cause = False 310 311 from rich import _IMPORT_CWD 312 313 def safe_str(_object: Any) -> str: 314 """Don't allow exceptions from __str__ to propegate.""" 315 try: 316 return str(_object) 317 except Exception: 318 return "<exception str() failed>" 319 320 while True: 321 stack = Stack( 322 exc_type=safe_str(exc_type.__name__), 323 exc_value=safe_str(exc_value), 324 is_cause=is_cause, 325 ) 326 327 if isinstance(exc_value, SyntaxError): 328 stack.syntax_error = _SyntaxError( 329 offset=exc_value.offset or 0, 330 filename=exc_value.filename or "?", 331 lineno=exc_value.lineno or 0, 332 line=exc_value.text or "", 333 msg=exc_value.msg, 334 ) 335 336 stacks.append(stack) 337 append = stack.frames.append 338 339 for frame_summary, line_no in walk_tb(traceback): 340 filename = frame_summary.f_code.co_filename 341 if filename and not filename.startswith("<"): 342 if not os.path.isabs(filename): 343 filename = os.path.join(_IMPORT_CWD, filename) 344 frame = Frame( 345 filename=filename or "?", 346 lineno=line_no, 347 name=frame_summary.f_code.co_name, 348 locals={ 349 key: pretty.traverse( 350 value, 351 max_length=locals_max_length, 352 max_string=locals_max_string, 353 ) 354 for key, value in frame_summary.f_locals.items() 355 } 356 if show_locals 357 else None, 358 ) 359 append(frame) 360 361 cause = getattr(exc_value, "__cause__", None) 362 if cause and cause.__traceback__: 363 exc_type = cause.__class__ 364 exc_value = cause 365 traceback = cause.__traceback__ 366 if traceback: 367 is_cause = True 368 continue 369 370 cause = exc_value.__context__ 371 if ( 372 cause 373 and cause.__traceback__ 374 and not getattr(exc_value, "__suppress_context__", False) 375 ): 376 exc_type = cause.__class__ 377 exc_value = cause 378 traceback = cause.__traceback__ 379 if traceback: 380 is_cause = False 381 continue 382 # No cover, code is reached but coverage doesn't recognize it. 383 break # pragma: no cover 384 385 trace = Trace(stacks=stacks) 386 return trace 387 388 def __rich_console__( 389 self, console: Console, options: ConsoleOptions 390 ) -> RenderResult: 391 theme = self.theme 392 background_style = theme.get_background_style() 393 token_style = theme.get_style_for_token 394 395 traceback_theme = Theme( 396 { 397 "pretty": token_style(TextToken), 398 "pygments.text": token_style(Token), 399 "pygments.string": token_style(String), 400 "pygments.function": token_style(Name.Function), 401 "pygments.number": token_style(Number), 402 "repr.indent": token_style(Comment) + Style(dim=True), 403 "repr.str": token_style(String), 404 "repr.brace": token_style(TextToken) + Style(bold=True), 405 "repr.number": token_style(Number), 406 "repr.bool_true": token_style(Keyword.Constant), 407 "repr.bool_false": token_style(Keyword.Constant), 408 "repr.none": token_style(Keyword.Constant), 409 "scope.border": token_style(String.Delimiter), 410 "scope.equals": token_style(Operator), 411 "scope.key": token_style(Name), 412 "scope.key.special": token_style(Name.Constant) + Style(dim=True), 413 } 414 ) 415 416 highlighter = ReprHighlighter() 417 for last, stack in loop_last(reversed(self.trace.stacks)): 418 if stack.frames: 419 stack_renderable: ConsoleRenderable = Panel( 420 self._render_stack(stack), 421 title="[traceback.title]Traceback [dim](most recent call last)", 422 style=background_style, 423 border_style="traceback.border.syntax_error", 424 expand=True, 425 padding=(0, 1), 426 ) 427 stack_renderable = Constrain(stack_renderable, self.width) 428 with console.use_theme(traceback_theme): 429 yield stack_renderable 430 if stack.syntax_error is not None: 431 with console.use_theme(traceback_theme): 432 yield Constrain( 433 Panel( 434 self._render_syntax_error(stack.syntax_error), 435 style=background_style, 436 border_style="traceback.border", 437 expand=True, 438 padding=(0, 1), 439 width=self.width, 440 ), 441 self.width, 442 ) 443 yield Text.assemble( 444 (f"{stack.exc_type}: ", "traceback.exc_type"), 445 highlighter(stack.syntax_error.msg), 446 ) 447 else: 448 yield Text.assemble( 449 (f"{stack.exc_type}: ", "traceback.exc_type"), 450 highlighter(stack.exc_value), 451 ) 452 453 if not last: 454 if stack.is_cause: 455 yield Text.from_markup( 456 "\n[i]The above exception was the direct cause of the following exception:\n", 457 ) 458 else: 459 yield Text.from_markup( 460 "\n[i]During handling of the above exception, another exception occurred:\n", 461 ) 462 463 @render_group() 464 def _render_syntax_error(self, syntax_error: _SyntaxError) -> RenderResult: 465 highlighter = ReprHighlighter() 466 path_highlighter = PathHighlighter() 467 if syntax_error.filename != "<stdin>": 468 text = Text.assemble( 469 (f" {syntax_error.filename}", "pygments.string"), 470 (":", "pygments.text"), 471 (str(syntax_error.lineno), "pygments.number"), 472 style="pygments.text", 473 ) 474 yield path_highlighter(text) 475 syntax_error_text = highlighter(syntax_error.line.rstrip()) 476 syntax_error_text.no_wrap = True 477 offset = min(syntax_error.offset - 1, len(syntax_error_text)) 478 syntax_error_text.stylize("bold underline", offset, offset + 1) 479 syntax_error_text += Text.from_markup( 480 "\n" + " " * offset + "[traceback.offset]▲[/]", 481 style="pygments.text", 482 ) 483 yield syntax_error_text 484 485 @classmethod 486 def _guess_lexer(cls, filename: str, code: str) -> str: 487 ext = os.path.splitext(filename)[-1] 488 if not ext: 489 # No extension, look at first line to see if it is a hashbang 490 # Note, this is an educated guess and not a guarantee 491 # If it fails, the only downside is that the code is highlighted strangely 492 new_line_index = code.index("\n") 493 first_line = code[:new_line_index] if new_line_index != -1 else code 494 if first_line.startswith("#!") and "python" in first_line.lower(): 495 return "python" 496 lexer_name = ( 497 cls.LEXERS.get(ext) or guess_lexer_for_filename(filename, code).name 498 ) 499 return lexer_name 500 501 @render_group() 502 def _render_stack(self, stack: Stack) -> RenderResult: 503 path_highlighter = PathHighlighter() 504 theme = self.theme 505 code_cache: Dict[str, str] = {} 506 507 def read_code(filename: str) -> str: 508 """Read files, and cache results on filename. 509 510 Args: 511 filename (str): Filename to read 512 513 Returns: 514 str: Contents of file 515 """ 516 code = code_cache.get(filename) 517 if code is None: 518 with open( 519 filename, "rt", encoding="utf-8", errors="replace" 520 ) as code_file: 521 code = code_file.read() 522 code_cache[filename] = code 523 return code 524 525 def render_locals(frame: Frame) -> Iterable[ConsoleRenderable]: 526 if frame.locals: 527 yield render_scope( 528 frame.locals, 529 title="locals", 530 indent_guides=self.indent_guides, 531 max_length=self.locals_max_length, 532 max_string=self.locals_max_string, 533 ) 534 535 for first, frame in loop_first(stack.frames): 536 text = Text.assemble( 537 path_highlighter(Text(frame.filename, style="pygments.string")), 538 (":", "pygments.text"), 539 (str(frame.lineno), "pygments.number"), 540 " in ", 541 (frame.name, "pygments.function"), 542 style="pygments.text", 543 ) 544 if not frame.filename.startswith("<") and not first: 545 yield "" 546 yield text 547 if frame.filename.startswith("<"): 548 yield from render_locals(frame) 549 continue 550 try: 551 code = read_code(frame.filename) 552 lexer_name = self._guess_lexer(frame.filename, code) 553 syntax = Syntax( 554 code, 555 lexer_name, 556 theme=theme, 557 line_numbers=True, 558 line_range=( 559 frame.lineno - self.extra_lines, 560 frame.lineno + self.extra_lines, 561 ), 562 highlight_lines={frame.lineno}, 563 word_wrap=self.word_wrap, 564 code_width=88, 565 indent_guides=self.indent_guides, 566 dedent=False, 567 ) 568 yield "" 569 except Exception as error: 570 yield Text.assemble( 571 (f"\n{error}", "traceback.error"), 572 ) 573 else: 574 yield ( 575 Columns( 576 [ 577 syntax, 578 *render_locals(frame), 579 ], 580 padding=1, 581 ) 582 if frame.locals 583 else syntax 584 ) 585 586 587if __name__ == "__main__": # pragma: no cover 588 589 from .console import Console 590 591 console = Console() 592 import sys 593 594 def bar(a): # 这是对亚洲语言支持的测试。面对模棱两可的想法,拒绝猜测的诱惑 595 one = 1 596 print(one / a) 597 598 def foo(a): 599 600 zed = { 601 "characters": { 602 "Paul Atreides", 603 "Vladimir Harkonnen", 604 "Thufir Hawat", 605 "Duncan Idaho", 606 }, 607 "atomic_types": (None, False, True), 608 } 609 bar(a) 610 611 def error(): 612 613 try: 614 try: 615 foo(0) 616 except: 617 slfkjsldkfj # type: ignore 618 except: 619 console.print_exception(show_locals=True) 620 621 error() 622