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