Skip to content

Commit 4101991

Browse files
authored
Merge pull request #3486 from Textualize/fine-grained-errors
WIP report fine grained error locations
2 parents d0de442 + a92b399 commit 4101991

File tree

8 files changed

+131
-221
lines changed

8 files changed

+131
-221
lines changed

.github/workflows/pythonpackage.yml

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,8 @@ jobs:
1010
matrix:
1111
os: [windows-latest, ubuntu-latest, macos-latest]
1212
python-version: ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13"]
13-
include:
14-
- { os: ubuntu-latest, python-version: "3.7" }
15-
- { os: windows-latest, python-version: "3.7" }
16-
- { os: macos-12, python-version: "3.7" }
13+
exclude:
14+
- { os: windows-latest, python-version: "3.13" }
1715
defaults:
1816
run:
1917
shell: bash

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,12 @@ All notable changes to this project will be documented in this file.
55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

8+
## Unreleased
9+
10+
### Changed
11+
12+
- Rich will display tracebacks with finely grained error locations on python 3.11+ https://github.com/Textualize/rich/pull/3486
13+
814
## [13.8.1] - 2024-09-10
915

1016
### Fixed

poetry.lock

Lines changed: 17 additions & 192 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ mypy = "^0.971"
4444
pytest-cov = "^3.0.0"
4545
attrs = "^21.4.0"
4646
pre-commit = "^2.17.0"
47-
asv = "^0.6.4"
47+
asv = "^0.5.1"
4848
importlib-metadata = { version = "*", python = "<3.8" }
4949

5050
[build-system]

rich/default_styles.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,7 @@
120120
"traceback.exc_type": Style(color="bright_red", bold=True),
121121
"traceback.exc_value": Style.null(),
122122
"traceback.offset": Style(color="bright_red", bold=True),
123+
"traceback.error_range": Style(underline=True, bold=True, dim=False),
123124
"bar.back": Style(color="grey23"),
124125
"bar.complete": Style(color="rgb(249,38,114)"),
125126
"bar.finished": Style(color="rgb(114,156,31)"),

rich/syntax.py

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -221,6 +221,7 @@ class _SyntaxHighlightRange(NamedTuple):
221221
style: StyleType
222222
start: SyntaxPosition
223223
end: SyntaxPosition
224+
style_before: bool = False
224225

225226

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

536537
def stylize_range(
537-
self, style: StyleType, start: SyntaxPosition, end: SyntaxPosition
538+
self,
539+
style: StyleType,
540+
start: SyntaxPosition,
541+
end: SyntaxPosition,
542+
style_before: bool = False,
538543
) -> None:
539544
"""
540545
Adds a custom style on a part of the code, that will be applied to the syntax display when it's rendered.
@@ -544,8 +549,11 @@ def stylize_range(
544549
style (StyleType): The style to apply.
545550
start (Tuple[int, int]): The start of the range, in the form `[line number, column index]`.
546551
end (Tuple[int, int]): The end of the range, in the form `[line number, column index]`.
552+
style_before (bool): Apply the style before any existing styles.
547553
"""
548-
self._stylized_ranges.append(_SyntaxHighlightRange(style, start, end))
554+
self._stylized_ranges.append(
555+
_SyntaxHighlightRange(style, start, end, style_before)
556+
)
549557

550558
def _get_line_numbers_color(self, blend: float = 0.3) -> Color:
551559
background_style = self._theme.get_background_style() + self.background_style
@@ -785,7 +793,10 @@ def _apply_stylized_ranges(self, text: Text) -> None:
785793
newlines_offsets, stylized_range.end
786794
)
787795
if start is not None and end is not None:
788-
text.stylize(stylized_range.style, start, end)
796+
if stylized_range.style_before:
797+
text.stylize_before(stylized_range.style, start, end)
798+
else:
799+
text.stylize(stylized_range.style, start, end)
789800

790801
def _process_code(self, code: str) -> Tuple[bool, str]:
791802
"""

rich/traceback.py

Lines changed: 59 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
1+
import inspect
12
import linecache
23
import os
34
import sys
45
from dataclasses import dataclass, field
6+
from itertools import islice
57
from traceback import walk_tb
68
from types import ModuleType, TracebackType
79
from typing import (
@@ -179,6 +181,7 @@ class Frame:
179181
name: str
180182
line: str = ""
181183
locals: Optional[Dict[str, pretty.Node]] = None
184+
last_instruction: Optional[Tuple[Tuple[int, int], Tuple[int, int]]] = None
182185

183186

184187
@dataclass
@@ -442,6 +445,35 @@ def get_locals(
442445

443446
for frame_summary, line_no in walk_tb(traceback):
444447
filename = frame_summary.f_code.co_filename
448+
449+
last_instruction: Optional[Tuple[Tuple[int, int], Tuple[int, int]]]
450+
last_instruction = None
451+
if sys.version_info >= (3, 11):
452+
instruction_index = frame_summary.f_lasti // 2
453+
instruction_position = next(
454+
islice(
455+
frame_summary.f_code.co_positions(),
456+
instruction_index,
457+
instruction_index + 1,
458+
)
459+
)
460+
(
461+
start_line,
462+
end_line,
463+
start_column,
464+
end_column,
465+
) = instruction_position
466+
if (
467+
start_line is not None
468+
and end_line is not None
469+
and start_column is not None
470+
and end_column is not None
471+
):
472+
last_instruction = (
473+
(start_line, start_column),
474+
(end_line, end_column),
475+
)
476+
445477
if filename and not filename.startswith("<"):
446478
if not os.path.isabs(filename):
447479
filename = os.path.join(_IMPORT_CWD, filename)
@@ -452,16 +484,20 @@ def get_locals(
452484
filename=filename or "?",
453485
lineno=line_no,
454486
name=frame_summary.f_code.co_name,
455-
locals={
456-
key: pretty.traverse(
457-
value,
458-
max_length=locals_max_length,
459-
max_string=locals_max_string,
460-
)
461-
for key, value in get_locals(frame_summary.f_locals.items())
462-
}
463-
if show_locals
464-
else None,
487+
locals=(
488+
{
489+
key: pretty.traverse(
490+
value,
491+
max_length=locals_max_length,
492+
max_string=locals_max_string,
493+
)
494+
for key, value in get_locals(frame_summary.f_locals.items())
495+
if not (inspect.isfunction(value) or inspect.isclass(value))
496+
}
497+
if show_locals
498+
else None
499+
),
500+
last_instruction=last_instruction,
465501
)
466502
append(frame)
467503
if frame_summary.f_locals.get("_rich_traceback_guard", False):
@@ -711,6 +747,14 @@ def render_locals(frame: Frame) -> Iterable[ConsoleRenderable]:
711747
(f"\n{error}", "traceback.error"),
712748
)
713749
else:
750+
if frame.last_instruction is not None:
751+
start, end = frame.last_instruction
752+
syntax.stylize_range(
753+
style="traceback.error_range",
754+
start=start,
755+
end=end,
756+
style_before=True,
757+
)
714758
yield (
715759
Columns(
716760
[
@@ -725,12 +769,12 @@ def render_locals(frame: Frame) -> Iterable[ConsoleRenderable]:
725769

726770

727771
if __name__ == "__main__": # pragma: no cover
728-
from .console import Console
729-
730-
console = Console()
772+
install(show_locals=True)
731773
import sys
732774

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

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

750794
def error() -> None:
751-
try:
752-
try:
753-
foo(0)
754-
except:
755-
slfkjsldkfj # type: ignore[name-defined]
756-
except:
757-
console.print_exception(show_locals=True)
795+
foo(0)
758796

759797
error()

tests/test_traceback.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -327,3 +327,34 @@ def level3():
327327
assert len(frames) == expected_frames_length
328328
frame_names = [f.name for f in frames]
329329
assert frame_names == expected_frame_names
330+
331+
332+
@pytest.mark.skipif(
333+
sys.version_info.minor >= 11, reason="Not applicable after Python 3.11"
334+
)
335+
def test_traceback_finely_grained_missing() -> None:
336+
"""Before 3.11, the last_instruction should be None"""
337+
try:
338+
1 / 0
339+
except:
340+
traceback = Traceback()
341+
last_instruction = traceback.trace.stacks[-1].frames[-1].last_instruction
342+
assert last_instruction is None
343+
344+
345+
@pytest.mark.skipif(
346+
sys.version_info.minor < 11, reason="Not applicable before Python 3.11"
347+
)
348+
def test_traceback_finely_grained() -> None:
349+
"""Check that last instruction is populated."""
350+
try:
351+
1 / 0
352+
except:
353+
traceback = Traceback()
354+
last_instruction = traceback.trace.stacks[-1].frames[-1].last_instruction
355+
assert last_instruction is not None
356+
assert isinstance(last_instruction, tuple)
357+
assert len(last_instruction) == 2
358+
start, end = last_instruction
359+
print(start, end)
360+
assert start[0] == end[0]

0 commit comments

Comments
 (0)