-
-
Notifications
You must be signed in to change notification settings - Fork 1.7k
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
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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 |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -14,6 +14,7 @@ | |
Iterable, | ||
List, | ||
Optional, | ||
Self, | ||
Sequence, | ||
Tuple, | ||
Type, | ||
|
@@ -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 | ||
|
@@ -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 | ||
|
@@ -417,6 +426,20 @@ def safe_str(_object: Any) -> str: | |
msg=exc_value.msg, | ||
) | ||
|
||
|
||
if isinstance(exc_value, BaseExceptionGroup): | ||
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 | ||
|
||
|
@@ -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 | ||
|
@@ -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, | ||
|
@@ -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)}", | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'd love to see support for PEP-678 There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. There was a problem hiding this comment. Choose a reason for hiding this commentThe 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"), | ||
|
@@ -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() | ||
|
There was a problem hiding this comment.
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:There was a problem hiding this comment.
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.