-
-
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
Enable rendering of rich exception objects in traceback #3325
base: master
Are you sure you want to change the base?
Changes from 4 commits
fb3eeb2
12b0f83
5bef760
9cb8a34
9d1c316
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 |
---|---|---|
|
@@ -28,6 +28,7 @@ | |
|
||
from . import pretty | ||
from ._loop import loop_last | ||
from .abc import RichRenderable | ||
from .columns import Columns | ||
from .console import Console, ConsoleOptions, ConsoleRenderable, RenderResult, group | ||
from .constrain import Constrain | ||
|
@@ -194,6 +195,7 @@ class _SyntaxError: | |
class Stack: | ||
exc_type: str | ||
exc_value: str | ||
exc_renderable: Optional[RichRenderable] = None | ||
syntax_error: Optional[_SyntaxError] = None | ||
is_cause: bool = False | ||
frames: List[Frame] = field(default_factory=list) | ||
|
@@ -416,6 +418,8 @@ def safe_str(_object: Any) -> str: | |
line=exc_value.text or "", | ||
msg=exc_value.msg, | ||
) | ||
if isinstance(exc_value, RichRenderable): | ||
stack.exc_renderable = exc_value | ||
|
||
stacks.append(stack) | ||
append = stack.frames.append | ||
|
@@ -544,6 +548,19 @@ def __rich_console__( | |
(f"{stack.exc_type}: ", "traceback.exc_type"), | ||
highlighter(stack.syntax_error.msg), | ||
) | ||
elif stack.exc_renderable: | ||
exc_renderable = Constrain(stack.exc_renderable, self.width) | ||
with console.use_theme(traceback_theme): | ||
try: | ||
segments = tuple(console.render(renderable=exc_renderable)) | ||
except Exception: | ||
yield Text("<exception rich render failed>") | ||
yield Text.assemble( | ||
(f"{stack.exc_type}: ", "traceback.exc_type"), | ||
highlighter(stack.exc_value), | ||
) | ||
else: | ||
yield from segments | ||
elif stack.exc_value: | ||
yield Text.assemble( | ||
(f"{stack.exc_type}: ", "traceback.exc_type"), | ||
|
@@ -722,7 +739,11 @@ def render_locals(frame: Frame) -> Iterable[ConsoleRenderable]: | |
from .console import Console | ||
|
||
console = Console() | ||
import sys | ||
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. sys was unused at this point, and there's already a top-level |
||
|
||
class RichError(Exception): | ||
def __rich_console__(self, console, options): | ||
yield "[bold][red]ERROR[/]:[/] This is a [i]renderable[/] exception!" | ||
yield Panel(self.args[0], expand=False, border_style="blue") | ||
|
||
def bar(a: Any) -> None: # 这是对亚洲语言支持的测试。面对模棱两可的想法,拒绝猜测的诱惑 | ||
one = 1 | ||
|
@@ -746,7 +767,7 @@ def error() -> None: | |
try: | ||
foo(0) | ||
except: | ||
slfkjsldkfj # type: ignore[name-defined] | ||
raise RichError("Woah! Look at this text!") | ||
except: | ||
console.print_exception(show_locals=True) | ||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -248,6 +248,27 @@ def test_traceback_console_theme_applies(): | |
assert f"\\x1b[38;2;{r};{g};{b}mTraceback \\x1b[0m" in repr(result) | ||
|
||
|
||
def test_rich_exception(): | ||
class RichError(Exception): | ||
def __rich_console__(self, console, options): | ||
yield f"[bold][red]error[/red]:[/bold] [red]this is red[/red]" | ||
|
||
console = Console( | ||
width=100, | ||
file=io.StringIO(), | ||
color_system="truecolor", | ||
legacy_windows=False, | ||
) | ||
try: | ||
raise RichError() | ||
except Exception: | ||
console.print_exception() | ||
result = console.file.getvalue() | ||
print(result) | ||
assert "\x1b[1;31merror\x1b[0m\x1b[1m:\x1b[0m" in result | ||
assert "\x1b[31mthis is red\x1b[0m" in result | ||
|
||
|
||
def test_broken_str(): | ||
class BrokenStr(Exception): | ||
def __str__(self): | ||
|
@@ -263,6 +284,36 @@ def __str__(self): | |
assert "<exception str() failed>" in result | ||
|
||
|
||
def test_broken_rich_exception(): | ||
class BrokenRichError(Exception): | ||
def __rich_console__(self, console, options): | ||
raise Exception("broken") | ||
|
||
console = Console(width=100, file=io.StringIO()) | ||
try: | ||
raise BrokenRichError() | ||
except Exception: | ||
console.print_exception() | ||
result = console.file.getvalue() | ||
print(result) | ||
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. These prints are useful for diagnosing failures, and pytest will capture this output appropriately. :) |
||
assert "<exception rich render failed>" in result | ||
|
||
|
||
def test_broken_rich_bad_markup(): | ||
class BrokenRichError(Exception): | ||
def __rich_console__(self, console, options): | ||
yield "[red]broken[/green]" | ||
|
||
console = Console(width=100, file=io.StringIO()) | ||
try: | ||
raise BrokenRichError() | ||
except Exception: | ||
console.print_exception() | ||
result = console.file.getvalue() | ||
print(result) | ||
assert "<exception rich render failed>" in result | ||
|
||
|
||
def test_guess_lexer(): | ||
assert Traceback._guess_lexer("foo.py", "code") == "python" | ||
code_python = "#! usr/bin/env python\nimport this" | ||
|
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.
My concern here is that if we offer arbitrary renderables, then the Traceback object may no longer be trivially sererializeable. Which was one of the goals for this class.
And since a renderable may be mutable, it may become out of date if it isn't immediately printed.