|
26 | 26 | from pygments.util import ClassNotFound
|
27 | 27 |
|
28 | 28 | from . import pretty
|
29 |
| -from ._loop import loop_last |
| 29 | +from ._loop import loop_first_last, loop_last |
30 | 30 | from .columns import Columns
|
31 | 31 | from .console import (
|
32 | 32 | Console,
|
|
41 | 41 | from .panel import Panel
|
42 | 42 | from .scope import render_scope
|
43 | 43 | from .style import Style
|
44 |
| -from .syntax import Syntax |
| 44 | +from .syntax import Syntax, SyntaxPosition |
45 | 45 | from .text import Text
|
46 | 46 | from .theme import Theme
|
47 | 47 |
|
|
51 | 51 | LOCALS_MAX_STRING = 80
|
52 | 52 |
|
53 | 53 |
|
| 54 | +def _iter_syntax_lines( |
| 55 | + start: SyntaxPosition, end: SyntaxPosition |
| 56 | +) -> Iterable[Tuple[int, int, int]]: |
| 57 | + """Yield start and end positions per line. |
| 58 | +
|
| 59 | + Args: |
| 60 | + start: Start position. |
| 61 | + end: End position. |
| 62 | +
|
| 63 | + Returns: |
| 64 | + Iterable of (LINE, COLUMN1, COLUMN2). |
| 65 | + """ |
| 66 | + |
| 67 | + line1, column1 = start |
| 68 | + line2, column2 = end |
| 69 | + |
| 70 | + if line1 == line2: |
| 71 | + yield line1, column1, column2 |
| 72 | + else: |
| 73 | + for first, last, line_no in loop_first_last(range(line1, line2 + 1)): |
| 74 | + if first: |
| 75 | + yield line_no, column1, -1 |
| 76 | + elif last: |
| 77 | + yield line_no, 0, column2 |
| 78 | + else: |
| 79 | + yield line_no, 0, -1 |
| 80 | + |
| 81 | + |
54 | 82 | def install(
|
55 | 83 | *,
|
56 | 84 | console: Optional[Console] = None,
|
@@ -703,17 +731,6 @@ def _render_stack(self, stack: Stack) -> RenderResult:
|
703 | 731 | path_highlighter = PathHighlighter()
|
704 | 732 | theme = self.theme
|
705 | 733 |
|
706 |
| - def read_code(filename: str) -> str: |
707 |
| - """Read files, and cache results on filename. |
708 |
| -
|
709 |
| - Args: |
710 |
| - filename (str): Filename to read |
711 |
| -
|
712 |
| - Returns: |
713 |
| - str: Contents of file |
714 |
| - """ |
715 |
| - return "".join(linecache.getlines(filename)) |
716 |
| - |
717 | 734 | def render_locals(frame: Frame) -> Iterable[ConsoleRenderable]:
|
718 | 735 | if frame.locals:
|
719 | 736 | yield render_scope(
|
@@ -775,7 +792,8 @@ def render_locals(frame: Frame) -> Iterable[ConsoleRenderable]:
|
775 | 792 | continue
|
776 | 793 | if not suppressed:
|
777 | 794 | try:
|
778 |
| - code = read_code(frame.filename) |
| 795 | + code_lines = linecache.getlines(frame.filename) |
| 796 | + code = "".join(code_lines) |
779 | 797 | if not code:
|
780 | 798 | # code may be an empty string if the file doesn't exist, OR
|
781 | 799 | # if the traceback filename is generated dynamically
|
@@ -804,12 +822,26 @@ def render_locals(frame: Frame) -> Iterable[ConsoleRenderable]:
|
804 | 822 | else:
|
805 | 823 | if frame.last_instruction is not None:
|
806 | 824 | start, end = frame.last_instruction
|
807 |
| - syntax.stylize_range( |
808 |
| - style="traceback.error_range", |
809 |
| - start=start, |
810 |
| - end=end, |
811 |
| - style_before=True, |
812 |
| - ) |
| 825 | + |
| 826 | + # Stylize a line at a time |
| 827 | + # So that indentation isn't underlined (which looks bad) |
| 828 | + for line1, column1, column2 in _iter_syntax_lines(start, end): |
| 829 | + try: |
| 830 | + if column1 == 0: |
| 831 | + line = code_lines[line1 - 1] |
| 832 | + column1 = len(line) - len(line.lstrip()) |
| 833 | + if column2 == -1: |
| 834 | + column2 = len(code_lines[line1 - 1]) |
| 835 | + except IndexError: |
| 836 | + # Being defensive here |
| 837 | + # If last_instruction reports a line out-of-bounds, we don't want to crash |
| 838 | + continue |
| 839 | + |
| 840 | + syntax.stylize_range( |
| 841 | + style="traceback.error_range", |
| 842 | + start=(line1, column1), |
| 843 | + end=(line1, column2), |
| 844 | + ) |
813 | 845 | yield (
|
814 | 846 | Columns(
|
815 | 847 | [
|
|
0 commit comments