diff --git a/CHANGELOG.md b/CHANGELOG.md index 6c7332fee..27a6fc01e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,11 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## Unreleased + +### Added + +- Rich tracebacks will now render notes on Python 3.11 onwards (added with `Exception.add_note`) https://github.com/Textualize/rich/pull/3676 ## [13.9.4] - 2024-11-01 diff --git a/rich/default_styles.py b/rich/default_styles.py index 28e8f6f94..3975a3615 100644 --- a/rich/default_styles.py +++ b/rich/default_styles.py @@ -121,6 +121,7 @@ "traceback.exc_value": Style.null(), "traceback.offset": Style(color="bright_red", bold=True), "traceback.error_range": Style(underline=True, bold=True, dim=False), + "traceback.note": Style(color="green", bold=True), "bar.back": Style(color="grey23"), "bar.complete": Style(color="rgb(249,38,114)"), "bar.finished": Style(color="rgb(114,156,31)"), diff --git a/rich/traceback.py b/rich/traceback.py index 3bf5baa7b..fc9859b93 100644 --- a/rich/traceback.py +++ b/rich/traceback.py @@ -191,6 +191,7 @@ class _SyntaxError: line: str lineno: int msg: str + notes: List[str] = field(default_factory=list) @dataclass @@ -200,6 +201,7 @@ class Stack: syntax_error: Optional[_SyntaxError] = None is_cause: bool = False frames: List[Frame] = field(default_factory=list) + notes: List[str] = field(default_factory=list) @dataclass @@ -403,6 +405,8 @@ def extract( from rich import _IMPORT_CWD + notes: List[str] = getattr(exc_value, "__notes__", None) or [] + def safe_str(_object: Any) -> str: """Don't allow exceptions from __str__ to propagate.""" try: @@ -415,6 +419,7 @@ def safe_str(_object: Any) -> str: exc_type=safe_str(exc_type.__name__), exc_value=safe_str(exc_value), is_cause=is_cause, + notes=notes, ) if isinstance(exc_value, SyntaxError): @@ -424,13 +429,14 @@ def safe_str(_object: Any) -> str: lineno=exc_value.lineno or 0, line=exc_value.text or "", msg=exc_value.msg, + notes=notes, ) stacks.append(stack) append = stack.frames.append def get_locals( - iter_locals: Iterable[Tuple[str, object]] + iter_locals: Iterable[Tuple[str, object]], ) -> Iterable[Tuple[str, object]]: """Extract locals from an iterator of key pairs.""" if not (locals_hide_dunder or locals_hide_sunder): @@ -569,6 +575,7 @@ def __rich_console__( stack_renderable = Constrain(stack_renderable, self.width) with console.use_theme(traceback_theme): yield stack_renderable + if stack.syntax_error is not None: with console.use_theme(traceback_theme): yield Constrain( @@ -594,6 +601,9 @@ def __rich_console__( else: yield Text.assemble((f"{stack.exc_type}", "traceback.exc_type")) + for note in stack.notes: + yield Text.assemble(("[NOTE] ", "traceback.note"), highlighter(note)) + if not last: if stack.is_cause: yield Text.from_markup( diff --git a/tests/test_traceback.py b/tests/test_traceback.py index ed80d1ba7..bc9bc91e9 100644 --- a/tests/test_traceback.py +++ b/tests/test_traceback.py @@ -358,3 +358,18 @@ def test_traceback_finely_grained() -> None: start, end = last_instruction print(start, end) assert start[0] == end[0] + + +@pytest.mark.skipif( + sys.version_info.minor < 11, reason="Not supported before Python 3.11" +) +def test_notes() -> None: + """Check traceback captures __note__.""" + try: + 1 / 0 + except Exception as error: + error.add_note("Hello") + error.add_note("World") + traceback = Traceback() + + assert traceback.trace.stacks[0].notes == ["Hello", "World"]