Skip to content

Commit a48a5b3

Browse files
authored
Merge pull request #3677 from Textualize/exception-groups
exception groups
2 parents 4de139e + 8f68c84 commit a48a5b3

File tree

3 files changed

+68
-21
lines changed

3 files changed

+68
-21
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1717
- An empty `FORCE_COLOR` env var is now considered disabled. https://github.com/Textualize/rich/pull/3675
1818
- Rich tracebacks will now render notes on Python 3.11 onwards (added with `Exception.add_note`) https://github.com/Textualize/rich/pull/3676
1919
- Indentation in exceptions won't be underlined https://github.com/Textualize/rich/pull/3678
20+
- Rich tracebacks will now render Exception Groups https://github.com/Textualize/rich/pull/3677
2021

2122
## [13.9.4] - 2024-11-01
2223

rich/default_styles.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,7 @@
122122
"traceback.offset": Style(color="bright_red", bold=True),
123123
"traceback.error_range": Style(underline=True, bold=True),
124124
"traceback.note": Style(color="green", bold=True),
125+
"traceback.group.border": Style(color="magenta"),
125126
"bar.back": Style(color="grey23"),
126127
"bar.complete": Style(color="rgb(249,38,114)"),
127128
"bar.finished": Style(color="rgb(114,156,31)"),

rich/traceback.py

Lines changed: 66 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,14 @@
2828
from . import pretty
2929
from ._loop import loop_first_last, loop_last
3030
from .columns import Columns
31-
from .console import Console, ConsoleOptions, ConsoleRenderable, RenderResult, group
31+
from .console import (
32+
Console,
33+
ConsoleOptions,
34+
ConsoleRenderable,
35+
Group,
36+
RenderResult,
37+
group,
38+
)
3239
from .constrain import Constrain
3340
from .highlighter import RegexHighlighter, ReprHighlighter
3441
from .panel import Panel
@@ -128,26 +135,25 @@ def excepthook(
128135
value: BaseException,
129136
traceback: Optional[TracebackType],
130137
) -> None:
131-
traceback_console.print(
132-
Traceback.from_exception(
133-
type_,
134-
value,
135-
traceback,
136-
width=width,
137-
code_width=code_width,
138-
extra_lines=extra_lines,
139-
theme=theme,
140-
word_wrap=word_wrap,
141-
show_locals=show_locals,
142-
locals_max_length=locals_max_length,
143-
locals_max_string=locals_max_string,
144-
locals_hide_dunder=locals_hide_dunder,
145-
locals_hide_sunder=bool(locals_hide_sunder),
146-
indent_guides=indent_guides,
147-
suppress=suppress,
148-
max_frames=max_frames,
149-
)
138+
exception_traceback = Traceback.from_exception(
139+
type_,
140+
value,
141+
traceback,
142+
width=width,
143+
code_width=code_width,
144+
extra_lines=extra_lines,
145+
theme=theme,
146+
word_wrap=word_wrap,
147+
show_locals=show_locals,
148+
locals_max_length=locals_max_length,
149+
locals_max_string=locals_max_string,
150+
locals_hide_dunder=locals_hide_dunder,
151+
locals_hide_sunder=bool(locals_hide_sunder),
152+
indent_guides=indent_guides,
153+
suppress=suppress,
154+
max_frames=max_frames,
150155
)
156+
traceback_console.print(exception_traceback)
151157

152158
def ipy_excepthook_closure(ip: Any) -> None: # pragma: no cover
153159
tb_data = {} # store information about showtraceback call
@@ -230,6 +236,8 @@ class Stack:
230236
is_cause: bool = False
231237
frames: List[Frame] = field(default_factory=list)
232238
notes: List[str] = field(default_factory=list)
239+
is_group: bool = False
240+
exceptions: List["Trace"] = field(default_factory=list)
233241

234242

235243
@dataclass
@@ -450,6 +458,22 @@ def safe_str(_object: Any) -> str:
450458
notes=notes,
451459
)
452460

461+
if sys.version_info >= (3, 11):
462+
if isinstance(exc_value, (BaseExceptionGroup, ExceptionGroup)):
463+
stack.is_group = True
464+
for exception in exc_value.exceptions:
465+
stack.exceptions.append(
466+
Traceback.extract(
467+
type(exception),
468+
exception,
469+
exception.__traceback__,
470+
show_locals=show_locals,
471+
locals_max_length=locals_max_length,
472+
locals_hide_dunder=locals_hide_dunder,
473+
locals_hide_sunder=locals_hide_sunder,
474+
)
475+
)
476+
453477
if isinstance(exc_value, SyntaxError):
454478
stack.syntax_error = _SyntaxError(
455479
offset=exc_value.offset or 0,
@@ -558,6 +582,7 @@ def get_locals(
558582
break # pragma: no cover
559583

560584
trace = Trace(stacks=stacks)
585+
561586
return trace
562587

563588
def __rich_console__(
@@ -590,7 +615,9 @@ def __rich_console__(
590615
)
591616

592617
highlighter = ReprHighlighter()
593-
for last, stack in loop_last(reversed(self.trace.stacks)):
618+
619+
@group()
620+
def render_stack(stack: Stack, last: bool) -> RenderResult:
594621
if stack.frames:
595622
stack_renderable: ConsoleRenderable = Panel(
596623
self._render_stack(stack),
@@ -632,6 +659,21 @@ def __rich_console__(
632659
for note in stack.notes:
633660
yield Text.assemble(("[NOTE] ", "traceback.note"), highlighter(note))
634661

662+
if stack.is_group:
663+
for group_no, group_exception in enumerate(stack.exceptions, 1):
664+
grouped_exceptions: List[Group] = []
665+
for group_last, group_stack in loop_last(group_exception.stacks):
666+
grouped_exceptions.append(render_stack(group_stack, group_last))
667+
yield ""
668+
yield Constrain(
669+
Panel(
670+
Group(*grouped_exceptions),
671+
title=f"Sub-exception #{group_no}",
672+
border_style="traceback.group.border",
673+
),
674+
self.width,
675+
)
676+
635677
if not last:
636678
if stack.is_cause:
637679
yield Text.from_markup(
@@ -642,6 +684,9 @@ def __rich_console__(
642684
"\n[i]During handling of the above exception, another exception occurred:\n",
643685
)
644686

687+
for last, stack in loop_last(reversed(self.trace.stacks)):
688+
yield render_stack(stack, last)
689+
645690
@group()
646691
def _render_syntax_error(self, syntax_error: _SyntaxError) -> RenderResult:
647692
highlighter = ReprHighlighter()

0 commit comments

Comments
 (0)