Skip to content

Commit

Permalink
Merge pull request #3486 from Textualize/fine-grained-errors
Browse files Browse the repository at this point in the history
WIP report fine grained error locations
  • Loading branch information
willmcgugan authored Sep 11, 2024
2 parents d0de442 + a92b399 commit 4101991
Show file tree
Hide file tree
Showing 8 changed files with 131 additions and 221 deletions.
6 changes: 2 additions & 4 deletions .github/workflows/pythonpackage.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,8 @@ jobs:
matrix:
os: [windows-latest, ubuntu-latest, macos-latest]
python-version: ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13"]
include:
- { os: ubuntu-latest, python-version: "3.7" }
- { os: windows-latest, python-version: "3.7" }
- { os: macos-12, python-version: "3.7" }
exclude:
- { os: windows-latest, python-version: "3.13" }
defaults:
run:
shell: bash
Expand Down
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,12 @@ 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

### Changed

- Rich will display tracebacks with finely grained error locations on python 3.11+ https://github.com/Textualize/rich/pull/3486

## [13.8.1] - 2024-09-10

### Fixed
Expand Down
209 changes: 17 additions & 192 deletions poetry.lock

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ mypy = "^0.971"
pytest-cov = "^3.0.0"
attrs = "^21.4.0"
pre-commit = "^2.17.0"
asv = "^0.6.4"
asv = "^0.5.1"
importlib-metadata = { version = "*", python = "<3.8" }

[build-system]
Expand Down
1 change: 1 addition & 0 deletions rich/default_styles.py
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,7 @@
"traceback.exc_type": Style(color="bright_red", bold=True),
"traceback.exc_value": Style.null(),
"traceback.offset": Style(color="bright_red", bold=True),
"traceback.error_range": Style(underline=True, bold=True, dim=False),
"bar.back": Style(color="grey23"),
"bar.complete": Style(color="rgb(249,38,114)"),
"bar.finished": Style(color="rgb(114,156,31)"),
Expand Down
17 changes: 14 additions & 3 deletions rich/syntax.py
Original file line number Diff line number Diff line change
Expand Up @@ -221,6 +221,7 @@ class _SyntaxHighlightRange(NamedTuple):
style: StyleType
start: SyntaxPosition
end: SyntaxPosition
style_before: bool = False


class Syntax(JupyterMixin):
Expand Down Expand Up @@ -534,7 +535,11 @@ def tokens_to_spans() -> Iterable[Tuple[str, Optional[Style]]]:
return text

def stylize_range(
self, style: StyleType, start: SyntaxPosition, end: SyntaxPosition
self,
style: StyleType,
start: SyntaxPosition,
end: SyntaxPosition,
style_before: bool = False,
) -> None:
"""
Adds a custom style on a part of the code, that will be applied to the syntax display when it's rendered.
Expand All @@ -544,8 +549,11 @@ def stylize_range(
style (StyleType): The style to apply.
start (Tuple[int, int]): The start of the range, in the form `[line number, column index]`.
end (Tuple[int, int]): The end of the range, in the form `[line number, column index]`.
style_before (bool): Apply the style before any existing styles.
"""
self._stylized_ranges.append(_SyntaxHighlightRange(style, start, end))
self._stylized_ranges.append(
_SyntaxHighlightRange(style, start, end, style_before)
)

def _get_line_numbers_color(self, blend: float = 0.3) -> Color:
background_style = self._theme.get_background_style() + self.background_style
Expand Down Expand Up @@ -785,7 +793,10 @@ def _apply_stylized_ranges(self, text: Text) -> None:
newlines_offsets, stylized_range.end
)
if start is not None and end is not None:
text.stylize(stylized_range.style, start, end)
if stylized_range.style_before:
text.stylize_before(stylized_range.style, start, end)
else:
text.stylize(stylized_range.style, start, end)

def _process_code(self, code: str) -> Tuple[bool, str]:
"""
Expand Down
80 changes: 59 additions & 21 deletions rich/traceback.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import inspect
import linecache
import os
import sys
from dataclasses import dataclass, field
from itertools import islice
from traceback import walk_tb
from types import ModuleType, TracebackType
from typing import (
Expand Down Expand Up @@ -179,6 +181,7 @@ class Frame:
name: str
line: str = ""
locals: Optional[Dict[str, pretty.Node]] = None
last_instruction: Optional[Tuple[Tuple[int, int], Tuple[int, int]]] = None


@dataclass
Expand Down Expand Up @@ -442,6 +445,35 @@ def get_locals(

for frame_summary, line_no in walk_tb(traceback):
filename = frame_summary.f_code.co_filename

last_instruction: Optional[Tuple[Tuple[int, int], Tuple[int, int]]]
last_instruction = None
if sys.version_info >= (3, 11):
instruction_index = frame_summary.f_lasti // 2
instruction_position = next(
islice(
frame_summary.f_code.co_positions(),
instruction_index,
instruction_index + 1,
)
)
(
start_line,
end_line,
start_column,
end_column,
) = instruction_position
if (
start_line is not None
and end_line is not None
and start_column is not None
and end_column is not None
):
last_instruction = (
(start_line, start_column),
(end_line, end_column),
)

if filename and not filename.startswith("<"):
if not os.path.isabs(filename):
filename = os.path.join(_IMPORT_CWD, filename)
Expand All @@ -452,16 +484,20 @@ def get_locals(
filename=filename or "?",
lineno=line_no,
name=frame_summary.f_code.co_name,
locals={
key: pretty.traverse(
value,
max_length=locals_max_length,
max_string=locals_max_string,
)
for key, value in get_locals(frame_summary.f_locals.items())
}
if show_locals
else None,
locals=(
{
key: pretty.traverse(
value,
max_length=locals_max_length,
max_string=locals_max_string,
)
for key, value in get_locals(frame_summary.f_locals.items())
if not (inspect.isfunction(value) or inspect.isclass(value))
}
if show_locals
else None
),
last_instruction=last_instruction,
)
append(frame)
if frame_summary.f_locals.get("_rich_traceback_guard", False):
Expand Down Expand Up @@ -711,6 +747,14 @@ def render_locals(frame: Frame) -> Iterable[ConsoleRenderable]:
(f"\n{error}", "traceback.error"),
)
else:
if frame.last_instruction is not None:
start, end = frame.last_instruction
syntax.stylize_range(
style="traceback.error_range",
start=start,
end=end,
style_before=True,
)
yield (
Columns(
[
Expand All @@ -725,12 +769,12 @@ def render_locals(frame: Frame) -> Iterable[ConsoleRenderable]:


if __name__ == "__main__": # pragma: no cover
from .console import Console

console = Console()
install(show_locals=True)
import sys

def bar(a: Any) -> None: # 这是对亚洲语言支持的测试。面对模棱两可的想法,拒绝猜测的诱惑
def bar(
a: Any,
) -> None: # 这是对亚洲语言支持的测试。面对模棱两可的想法,拒绝猜测的诱惑
one = 1
print(one / a)

Expand All @@ -748,12 +792,6 @@ def foo(a: Any) -> None:
bar(a)

def error() -> None:
try:
try:
foo(0)
except:
slfkjsldkfj # type: ignore[name-defined]
except:
console.print_exception(show_locals=True)
foo(0)

error()
31 changes: 31 additions & 0 deletions tests/test_traceback.py
Original file line number Diff line number Diff line change
Expand Up @@ -327,3 +327,34 @@ def level3():
assert len(frames) == expected_frames_length
frame_names = [f.name for f in frames]
assert frame_names == expected_frame_names


@pytest.mark.skipif(
sys.version_info.minor >= 11, reason="Not applicable after Python 3.11"
)
def test_traceback_finely_grained_missing() -> None:
"""Before 3.11, the last_instruction should be None"""
try:
1 / 0
except:
traceback = Traceback()
last_instruction = traceback.trace.stacks[-1].frames[-1].last_instruction
assert last_instruction is None


@pytest.mark.skipif(
sys.version_info.minor < 11, reason="Not applicable before Python 3.11"
)
def test_traceback_finely_grained() -> None:
"""Check that last instruction is populated."""
try:
1 / 0
except:
traceback = Traceback()
last_instruction = traceback.trace.stacks[-1].frames[-1].last_instruction
assert last_instruction is not None
assert isinstance(last_instruction, tuple)
assert len(last_instruction) == 2
start, end = last_instruction
print(start, end)
assert start[0] == end[0]

0 comments on commit 4101991

Please sign in to comment.