From e6cdda5f73540373b5425add3ebadab495f634b5 Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Sat, 29 Mar 2025 16:08:03 +0000 Subject: [PATCH 1/4] Don't underline indentation --- rich/default_styles.py | 2 +- rich/traceback.py | 61 ++++++++++++++++++++++++++++++++++++------ 2 files changed, 54 insertions(+), 9 deletions(-) diff --git a/rich/default_styles.py b/rich/default_styles.py index 3975a3615f..42020615f2 100644 --- a/rich/default_styles.py +++ b/rich/default_styles.py @@ -120,7 +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), + "traceback.error_range": Style(underline=True, bold=True), "traceback.note": Style(color="green", bold=True), "bar.back": Style(color="grey23"), "bar.complete": Style(color="rgb(249,38,114)"), diff --git a/rich/traceback.py b/rich/traceback.py index fc9859b93c..c5e84dba59 100644 --- a/rich/traceback.py +++ b/rich/traceback.py @@ -26,7 +26,7 @@ from pygments.util import ClassNotFound from . import pretty -from ._loop import loop_last +from ._loop import loop_first_last, loop_last from .columns import Columns from .console import Console, ConsoleOptions, ConsoleRenderable, RenderResult, group from .constrain import Constrain @@ -34,7 +34,7 @@ from .panel import Panel from .scope import render_scope from .style import Style -from .syntax import Syntax +from .syntax import Syntax, SyntaxPosition from .text import Text from .theme import Theme @@ -44,6 +44,34 @@ LOCALS_MAX_STRING = 80 +def _iter_syntax_lines( + start: SyntaxPosition, end: SyntaxPosition +) -> Iterable[Tuple[SyntaxPosition, SyntaxPosition]]: + """Yield start and end syntax positions per line. + + Args: + start: Start position. + end: End position. + + Returns: + Iterable of pairs of syntax positions. + """ + + line1, column1 = start + line2, column2 = end + + if line1 == line2: + yield start, end + else: + for first, last, line_no in loop_first_last(range(line1, line2 + 1)): + if first: + yield (line_no, column1), (line_no, -1) + elif last: + yield (line_no, 0), (line_no, column2) + else: + yield (line_no, 0), (line_no, -1) + + def install( *, console: Optional[Console] = None, @@ -759,12 +787,29 @@ def render_locals(frame: Frame) -> Iterable[ConsoleRenderable]: 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, - ) + lines = code.splitlines() + + # Stylize a line at a time + # So that indentation isn't underlined (which looks bad) + for (line1, column1), (_, column2) in _iter_syntax_lines( + start, end + ): + try: + if column1 == 0: + line = lines[line1 - 1] + column1 = len(line) - len(line.lstrip()) + if column2 == -1: + column2 = len(lines[line1 - 1]) + except IndexError: + # Being defensive here + # If last_instruction reports a line out-of-bounds, we don't want to crash + continue + + syntax.stylize_range( + style="traceback.error_range", + start=(line1, column1), + end=(line1, column2), + ) yield ( Columns( [ From 8cd99869914d1bef3cf1e3c76d81fc722c4ae2de Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Sat, 29 Mar 2025 20:12:07 +0000 Subject: [PATCH 2/4] simplify --- rich/traceback.py | 14 ++------------ 1 file changed, 2 insertions(+), 12 deletions(-) diff --git a/rich/traceback.py b/rich/traceback.py index c5e84dba59..314a2e6f92 100644 --- a/rich/traceback.py +++ b/rich/traceback.py @@ -686,17 +686,6 @@ def _render_stack(self, stack: Stack) -> RenderResult: path_highlighter = PathHighlighter() theme = self.theme - def read_code(filename: str) -> str: - """Read files, and cache results on filename. - - Args: - filename (str): Filename to read - - Returns: - str: Contents of file - """ - return "".join(linecache.getlines(filename)) - def render_locals(frame: Frame) -> Iterable[ConsoleRenderable]: if frame.locals: yield render_scope( @@ -758,7 +747,8 @@ def render_locals(frame: Frame) -> Iterable[ConsoleRenderable]: continue if not suppressed: try: - code = read_code(frame.filename) + code_lines = linecache.getlines(frame.filename) + code = "".join(code_lines) if not code: # code may be an empty string if the file doesn't exist, OR # if the traceback filename is generated dynamically From e6557f5b6c6bd0a883c060fd103d330038fa6719 Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Sat, 29 Mar 2025 20:38:04 +0000 Subject: [PATCH 3/4] simplify --- rich/traceback.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/rich/traceback.py b/rich/traceback.py index 314a2e6f92..98cf206db7 100644 --- a/rich/traceback.py +++ b/rich/traceback.py @@ -786,10 +786,10 @@ def render_locals(frame: Frame) -> Iterable[ConsoleRenderable]: ): try: if column1 == 0: - line = lines[line1 - 1] + line = code_lines[line1 - 1] column1 = len(line) - len(line.lstrip()) if column2 == -1: - column2 = len(lines[line1 - 1]) + column2 = len(code_lines[line1 - 1]) except IndexError: # Being defensive here # If last_instruction reports a line out-of-bounds, we don't want to crash From 47da97e46a529905a170d609c38bd1380034583a Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Sun, 30 Mar 2025 08:19:11 +0100 Subject: [PATCH 4/4] simplify --- rich/traceback.py | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/rich/traceback.py b/rich/traceback.py index 98cf206db7..86371d6f0f 100644 --- a/rich/traceback.py +++ b/rich/traceback.py @@ -46,30 +46,30 @@ def _iter_syntax_lines( start: SyntaxPosition, end: SyntaxPosition -) -> Iterable[Tuple[SyntaxPosition, SyntaxPosition]]: - """Yield start and end syntax positions per line. +) -> Iterable[Tuple[int, int, int]]: + """Yield start and end positions per line. Args: start: Start position. end: End position. Returns: - Iterable of pairs of syntax positions. + Iterable of (LINE, COLUMN1, COLUMN2). """ line1, column1 = start line2, column2 = end if line1 == line2: - yield start, end + yield line1, column1, column2 else: for first, last, line_no in loop_first_last(range(line1, line2 + 1)): if first: - yield (line_no, column1), (line_no, -1) + yield line_no, column1, -1 elif last: - yield (line_no, 0), (line_no, column2) + yield line_no, 0, column2 else: - yield (line_no, 0), (line_no, -1) + yield line_no, 0, -1 def install( @@ -777,13 +777,10 @@ def render_locals(frame: Frame) -> Iterable[ConsoleRenderable]: else: if frame.last_instruction is not None: start, end = frame.last_instruction - lines = code.splitlines() # Stylize a line at a time # So that indentation isn't underlined (which looks bad) - for (line1, column1), (_, column2) in _iter_syntax_lines( - start, end - ): + for line1, column1, column2 in _iter_syntax_lines(start, end): try: if column1 == 0: line = code_lines[line1 - 1]