Skip to content

exception groups #3677

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 8 commits into from
Mar 30, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
1 change: 1 addition & 0 deletions rich/default_styles.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)"),
Expand Down
87 changes: 66 additions & 21 deletions rich/traceback.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -558,6 +582,7 @@ def get_locals(
break # pragma: no cover

trace = Trace(stacks=stacks)

return trace

def __rich_console__(
Expand Down Expand Up @@ -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),
Expand Down Expand Up @@ -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(
Expand All @@ -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()
Expand Down
Loading