diff --git a/CHANGELOG.md b/CHANGELOG.md index d2da7e983b..b84fce7f0d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - An empty `FORCE_COLOR` env var is now considered disabled. https://github.com/Textualize/rich/pull/3675 - Rich tracebacks will now render notes on Python 3.11 onwards (added with `Exception.add_note`) https://github.com/Textualize/rich/pull/3676 - Indentation in exceptions won't be underlined https://github.com/Textualize/rich/pull/3678 +- Rich tracebacks will now render Exception Groups https://github.com/Textualize/rich/pull/3677 ## [13.9.4] - 2024-11-01 diff --git a/rich/default_styles.py b/rich/default_styles.py index 42020615f2..3a0ad83a29 100644 --- a/rich/default_styles.py +++ b/rich/default_styles.py @@ -122,6 +122,7 @@ "traceback.offset": Style(color="bright_red", bold=True), "traceback.error_range": Style(underline=True, bold=True), "traceback.note": Style(color="green", bold=True), + "traceback.group.border": Style(color="magenta"), "bar.back": Style(color="grey23"), "bar.complete": Style(color="rgb(249,38,114)"), "bar.finished": Style(color="rgb(114,156,31)"), diff --git a/rich/traceback.py b/rich/traceback.py index 86371d6f0f..b2cc630404 100644 --- a/rich/traceback.py +++ b/rich/traceback.py @@ -28,7 +28,14 @@ from . import pretty from ._loop import loop_first_last, loop_last from .columns import Columns -from .console import Console, ConsoleOptions, ConsoleRenderable, RenderResult, group +from .console import ( + Console, + ConsoleOptions, + ConsoleRenderable, + Group, + RenderResult, + group, +) from .constrain import Constrain from .highlighter import RegexHighlighter, ReprHighlighter from .panel import Panel @@ -128,26 +135,25 @@ def excepthook( value: BaseException, traceback: Optional[TracebackType], ) -> None: - traceback_console.print( - Traceback.from_exception( - type_, - value, - traceback, - width=width, - code_width=code_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, - ) + exception_traceback = Traceback.from_exception( + type_, + value, + traceback, + width=width, + code_width=code_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, ) + traceback_console.print(exception_traceback) def ipy_excepthook_closure(ip: Any) -> None: # pragma: no cover tb_data = {} # store information about showtraceback call @@ -230,6 +236,8 @@ class Stack: is_cause: bool = False frames: List[Frame] = field(default_factory=list) notes: List[str] = field(default_factory=list) + is_group: bool = False + exceptions: List["Trace"] = field(default_factory=list) @dataclass @@ -450,6 +458,22 @@ def safe_str(_object: Any) -> str: notes=notes, ) + if sys.version_info >= (3, 11): + if isinstance(exc_value, (BaseExceptionGroup, ExceptionGroup)): + stack.is_group = True + for exception in exc_value.exceptions: + stack.exceptions.append( + Traceback.extract( + type(exception), + exception, + exception.__traceback__, + show_locals=show_locals, + locals_max_length=locals_max_length, + locals_hide_dunder=locals_hide_dunder, + locals_hide_sunder=locals_hide_sunder, + ) + ) + if isinstance(exc_value, SyntaxError): stack.syntax_error = _SyntaxError( offset=exc_value.offset or 0, @@ -558,6 +582,7 @@ def get_locals( break # pragma: no cover trace = Trace(stacks=stacks) + return trace def __rich_console__( @@ -590,7 +615,9 @@ def __rich_console__( ) highlighter = ReprHighlighter() - for last, stack in loop_last(reversed(self.trace.stacks)): + + @group() + def render_stack(stack: Stack, last: bool) -> RenderResult: if stack.frames: stack_renderable: ConsoleRenderable = Panel( self._render_stack(stack), @@ -632,6 +659,21 @@ def __rich_console__( for note in stack.notes: yield Text.assemble(("[NOTE] ", "traceback.note"), highlighter(note)) + if stack.is_group: + for group_no, group_exception in enumerate(stack.exceptions, 1): + grouped_exceptions: List[Group] = [] + for group_last, group_stack in loop_last(group_exception.stacks): + grouped_exceptions.append(render_stack(group_stack, group_last)) + yield "" + yield Constrain( + Panel( + Group(*grouped_exceptions), + title=f"Sub-exception #{group_no}", + border_style="traceback.group.border", + ), + self.width, + ) + if not last: if stack.is_cause: yield Text.from_markup( @@ -642,6 +684,9 @@ def __rich_console__( "\n[i]During handling of the above exception, another exception occurred:\n", ) + for last, stack in loop_last(reversed(self.trace.stacks)): + yield render_stack(stack, last) + @group() def _render_syntax_error(self, syntax_error: _SyntaxError) -> RenderResult: highlighter = ReprHighlighter()