Skip to content
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

ExceptionGroup PEP654 proof of concept. #3033

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
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
36 changes: 36 additions & 0 deletions examples/exception_group.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
"""

Demonstrates Rich tracebacks for recursion errors.

Rich can exclude frames in the middle to avoid huge tracebacks.

"""

from rich.console import Console

console = Console()

def a():
b()

def b():
try:
c()
except Exception as exception:
raise ExceptionGroup(
"Created in B",
[exception, exception]
)

def c():
raise RuntimeError("I was raised in C")
raise ExceptionGroup(
"exception group",
[RuntimeError("I'm a runtime error"), ValueError("I'm a value error")]
)

try:
a()
except Exception:
console.print_exception(max_frames=20)
raise
2 changes: 2 additions & 0 deletions rich/default_styles.py
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,8 @@
"traceback.title": Style(color="red", bold=True),
"traceback.exc_type": Style(color="bright_red", bold=True),
"traceback.exc_value": Style.null(),
"exception_group.title": Style(color="magenta", bold=True),
"exception_group.border": Style(color="magenta"),
"traceback.offset": Style(color="bright_red", bold=True),
"bar.back": Style(color="grey23"),
"bar.complete": Style(color="rgb(249,38,114)"),
Expand Down
57 changes: 51 additions & 6 deletions rich/traceback.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
Iterable,
List,
Optional,
Self,
Sequence,
Tuple,
Type,
Expand All @@ -29,7 +30,14 @@
from . import pretty
from ._loop import 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 @@ -197,6 +205,7 @@ class Stack:
syntax_error: Optional[_SyntaxError] = None
is_cause: bool = False
frames: List[Frame] = field(default_factory=list)
grouped_traces: List["Trace"] = field(default_factory=list)


@dataclass
Expand Down Expand Up @@ -417,6 +426,20 @@ def safe_str(_object: Any) -> str:
msg=exc_value.msg,
)


if isinstance(exc_value, BaseExceptionGroup):
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be very easy to make this support older Python versions, with or without the exceptiongroup backport:

Suggested change
if isinstance(exc_value, BaseExceptionGroup):
if sys.version_info[:2] < (3, 11):
BaseExceptionGroup = getattr(sys.modules.get("exceptiongroup"), "BaseExceptionGroup", ())
if isinstance(exc_value, BaseExceptionGroup):

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, this was the plan but hasn't been implemented because I wanted to get a PoC out first with some feedback. See my first post as well at the bottom.

for exception in exc_value.exceptions:
stack.grouped_traces.append(cls.extract(
exc_type=type(exception),
exc_value=exception,
traceback=exception.__traceback__,
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=locals_hide_sunder,
))

stacks.append(stack)
append = stack.frames.append

Expand Down Expand Up @@ -484,9 +507,8 @@ def get_locals(
trace = Trace(stacks=stacks)
return trace

def __rich_console__(
self, console: Console, options: ConsoleOptions
) -> RenderResult:

def _render_trace(self, console: Console, options: ConsoleOptions, trace: Trace, child_index: int | None = None) -> RenderResult:
theme = self.theme
background_style = theme.get_background_style()
token_style = theme.get_style_for_token
Expand Down Expand Up @@ -514,11 +536,17 @@ def __rich_console__(
)

highlighter = ReprHighlighter()
for last, stack in loop_last(reversed(self.trace.stacks)):
for last, stack in loop_last(reversed(trace.stacks)):
is_exception_group = bool(stack.grouped_traces)
if stack.frames:
traceback_title = "Traceback"
if is_exception_group:
traceback_title = f"Exception Group {traceback_title}"
if child_index is not None:
traceback_title += f" {child_index}"
stack_renderable: ConsoleRenderable = Panel(
self._render_stack(stack),
title="[traceback.title]Traceback [dim](most recent call last)",
title=f"[traceback.title]{traceback_title} [dim](most recent call last)",
style=background_style,
border_style="traceback.border",
expand=True,
Expand All @@ -544,6 +572,18 @@ def __rich_console__(
(f"{stack.exc_type}: ", "traceback.exc_type"),
highlighter(stack.syntax_error.msg),
)
elif stack.grouped_traces:
group_exception_renderable = Panel(
Group(*[Group(*self._render_trace(console=console, options=options, trace=trace, child_index=index + 1)) for index, trace in enumerate(stack.grouped_traces)]),
title=f"[exception_group.title]{stack.exc_type} [dim]{highlighter(stack.exc_value)}",
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd love to see support for PEP-678 __notes__ here (and in the next elif clause, for non-grouped exceptions). Probably easiest if we add a new method .render_exc_value(stack) cribbing from this part of the backport?

Copy link
Author

@AndreasBackx AndreasBackx Oct 17, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also wanted to do this, but again wanted to get initial feedback first as this will depend on how the maintainer was possibly envisioning how it'd fit in the "design". If I have the go ahead for how it should be achieved, I can make the changes. Right now we're stuck waiting for feedback.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My intuition as a maintainer is that fewer rounds of review makes everything easier - if I can just say "thanks, merging" that's likely to happen sooner than if we iterate. I understand not wanting to do more work which might not be merged though. 🙂

style=background_style,
border_style="exception_group.border",
expand=True,
padding=(0, 1),
)
group_exception_renderable = Constrain(group_exception_renderable, self.width)
with console.use_theme(traceback_theme):
yield group_exception_renderable
elif stack.exc_value:
yield Text.assemble(
(f"{stack.exc_type}: ", "traceback.exc_type"),
Expand All @@ -562,6 +602,11 @@ def __rich_console__(
"\n[i]During handling of the above exception, another exception occurred:\n",
)

def __rich_console__(
self, console: Console, options: ConsoleOptions
) -> RenderResult:
return self._render_trace(console=console, options=options, trace=self.trace)

@group()
def _render_syntax_error(self, syntax_error: _SyntaxError) -> RenderResult:
highlighter = ReprHighlighter()
Expand Down