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..86371d6f0f 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[int, int, int]]: + """Yield start and end positions per line. + + Args: + start: Start position. + end: End position. + + Returns: + Iterable of (LINE, COLUMN1, COLUMN2). + """ + + line1, column1 = start + line2, column2 = end + + if line1 == line2: + yield line1, column1, column2 + else: + for first, last, line_no in loop_first_last(range(line1, line2 + 1)): + if first: + yield line_no, column1, -1 + elif last: + yield line_no, 0, column2 + else: + yield line_no, 0, -1 + + def install( *, console: Optional[Console] = None, @@ -658,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( @@ -730,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 @@ -759,12 +777,26 @@ 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, - ) + + # 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 = code_lines[line1 - 1] + column1 = len(line) - len(line.lstrip()) + if column2 == -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 + continue + + syntax.stylize_range( + style="traceback.error_range", + start=(line1, column1), + end=(line1, column2), + ) yield ( Columns( [