indentaion guides

This commit is contained in:
Will McGugan 2020-10-22 21:17:03 +01:00
parent cbac633ab3
commit be1b14d42b
11 changed files with 214 additions and 14 deletions

View File

@ -55,6 +55,7 @@ DEFAULT_STYLES: Dict[str, Style] = {
"log.time": Style(color="cyan", dim=True),
"log.message": Style.null(),
"log.path": Style(dim=True),
"repr.indent": Style(color="green", dim=True),
"repr.error": Style(color="red", bold=True),
"repr.str": Style(color="green", italic=False, bold=False),
"repr.brace": Style(bold=True),

View File

@ -40,6 +40,7 @@ def install(
console: "Console" = None,
overflow: "OverflowMethod" = "ignore",
crop: bool = False,
indent_guides: bool = False,
) -> None:
"""Install automatic pretty printing in the Python REPL.
@ -47,6 +48,7 @@ def install(
console (Console, optional): Console instance or ``None`` to use global console. Defaults to None.
overflow (Optional[OverflowMethod], optional): Overflow method. Defaults to "ignore".
crop (Optional[bool], optional): Enable cropping of long lines. Defaults to False.
indent_guides (bool, optional): Enable indentation guides. Defaults to False.
"""
from rich import get_console
@ -60,7 +62,7 @@ def install(
console.print(
value
if hasattr(value, "__rich_console__") or hasattr(value, "__rich__")
else Pretty(value, overflow=overflow),
else Pretty(value, overflow=overflow, indent_guides=indent_guides),
crop=crop,
)
builtins._ = value # type: ignore
@ -78,6 +80,7 @@ class Pretty:
justify (JustifyMethod, optional): Justify method, or None for default. Defaults to None.
overflow (OverflowMethod, optional): Overflow method, or None for default. Defaults to None.
no_wrap (Optional[bool], optional): Disable word wrapping. Defaults to False.
indent_guides (bool, optional): Enable indentation guides. Defaults to False.
"""
def __init__(
@ -89,6 +92,7 @@ class Pretty:
justify: "JustifyMethod" = None,
overflow: Optional["OverflowMethod"] = "crop",
no_wrap: Optional[bool] = False,
indent_guides: bool = False,
) -> None:
self._object = _object
self.highlighter = highlighter or ReprHighlighter()
@ -96,6 +100,7 @@ class Pretty:
self.justify = justify
self.overflow = overflow
self.no_wrap = no_wrap
self.indent_guides = indent_guides
def __rich_console__(
self, console: "Console", options: "ConsoleOptions"
@ -111,6 +116,10 @@ class Pretty:
style="pretty",
)
pretty_text = self.highlighter(pretty_text)
if self.indent_guides and not options.ascii_only:
pretty_text = pretty_text.with_indent_guides(
self.indent_size, style="repr.indent"
)
yield pretty_text
def __rich_measure__(self, console: "Console", max_width: int) -> "Measurement":
@ -415,4 +424,4 @@ if __name__ == "__main__": # pragma: no cover
from rich import print
print(Pretty(data))
print(Pretty(data, indent_guides=True))

View File

@ -780,7 +780,8 @@ class Progress(JupyterMixin, RenderHook):
popleft()
while len(_progress) > 1000:
popleft()
_progress.append(ProgressSample(current_time, update_completed))
if update_completed > 0:
_progress.append(ProgressSample(current_time, update_completed))
def reset(
self,

View File

@ -14,7 +14,11 @@ if TYPE_CHECKING:
def render_scope(
scope: Mapping, *, title: TextType = None, sort_keys: bool = True
scope: Mapping,
*,
title: TextType = None,
sort_keys: bool = True,
indent_guides: bool = False
) -> "ConsoleRenderable":
"""Render python variables in a given scope.
@ -22,6 +26,7 @@ def render_scope(
scope (Mapping): A mapping containing variable names and values.
title (str, optional): Optional title. Defaults to None.
sort_keys (bool, optional): Enable sorting of items. Defaults to True.
indent_guides (bool, optional): Enable indentaton guides. Defaults to False.
Returns:
RenderableType: A renderable object.
@ -41,7 +46,10 @@ def render_scope(
(key, "scope.key.special" if key.startswith("__") else "scope.key"),
(" =", "scope.equals"),
)
items_table.add_row(key_text, Pretty(value, highlighter=highlighter))
items_table.add_row(
key_text,
Pretty(value, highlighter=highlighter, indent_guides=indent_guides),
)
return Panel.fit(
items_table,
title=title,

View File

@ -203,6 +203,7 @@ class Syntax(JupyterMixin):
tab_size (int, optional): Size of tabs. Defaults to 4.
word_wrap (bool, optional): Enable word wrapping.
background_color (str, optional): Optional background color, or None to use theme color. Defaults to None.
indent_guides (bool, optional): Show indent guides. Defaults to False.
"""
_pygments_style_class: Type[PygmentsStyle]
@ -235,6 +236,7 @@ class Syntax(JupyterMixin):
tab_size: int = 4,
word_wrap: bool = False,
background_color: str = None,
indent_guides: bool = False,
) -> None:
self.code = code
self.lexer_name = lexer_name
@ -247,6 +249,7 @@ class Syntax(JupyterMixin):
self.tab_size = tab_size
self.word_wrap = word_wrap
self.background_color = background_color
self.indent_guides = indent_guides
self._theme = self.get_theme(theme)
@ -265,6 +268,7 @@ class Syntax(JupyterMixin):
tab_size: int = 4,
word_wrap: bool = False,
background_color: str = None,
indent_guides: bool = False,
) -> "Syntax":
"""Construct a Syntax object from a file.
@ -281,6 +285,7 @@ class Syntax(JupyterMixin):
tab_size (int, optional): Size of tabs. Defaults to 4.
word_wrap (bool, optional): Enable word wrapping of code.
background_color (str, optional): Optional background color, or None to use theme color. Defaults to None.
indent_guides (bool, optional): Show indent guides. Defaults to False.
Returns:
[Syntax]: A Syntax object that may be printed to the console
@ -318,6 +323,7 @@ class Syntax(JupyterMixin):
tab_size=tab_size,
word_wrap=word_wrap,
background_color=background_color,
indent_guides=indent_guides,
)
def _get_base_style(self) -> Style:
@ -439,6 +445,17 @@ class Syntax(JupyterMixin):
text = self.highlight(code)
text.remove_suffix("\n")
text.expand_tabs(self.tab_size)
(
background_style,
number_style,
highlight_number_style,
) = self._get_number_styles(console)
if self.indent_guides and not options.ascii_only:
style = self._get_base_style() + self._theme.get_style_for_token(Comment)
text = text.with_indent_guides(self.tab_size, style=style)
if not self.line_numbers:
# Simple case of just rendering text
yield from console.render(text, options=options.update(width=code_width))
@ -454,12 +471,6 @@ class Syntax(JupyterMixin):
numbers_column_width = self._numbers_column_width
render_options = options.update(width=code_width)
(
background_style,
number_style,
highlight_number_style,
) = self._get_number_styles(console)
highlight_line = self.highlight_lines.__contains__
_Segment = Segment
padding = _Segment(" " * numbers_column_width + " ", background_style)
@ -519,6 +530,14 @@ if __name__ == "__main__": # pragma: no cover
default=None,
help="force color for non-terminals",
)
parser.add_argument(
"-i",
"--indent-guides",
dest="indent_guides",
action="store_true",
default=False,
help="display indent guides",
)
parser.add_argument(
"-l",
"--line-numbers",
@ -572,5 +591,6 @@ if __name__ == "__main__": # pragma: no cover
word_wrap=args.word_wrap,
theme=args.theme,
background_color=args.background_color,
indent_guides=args.indent_guides,
)
console.print(syntax, soft_wrap=args.soft_wrap)

View File

@ -1,4 +1,5 @@
from functools import partial
from functools import partial, reduce
from math import gcd
import re
from operator import itemgetter
from typing import (
@ -1008,6 +1009,75 @@ class Text(JupyterMixin):
append(line)
return lines
def detect_indentation(self) -> int:
"""Auto-detect indentation of code.
Returns:
int: Number of spaces used to indent code.
"""
_indentations = {
len(match.group(1))
for match in re.finditer(r"^( *)(.*)$", self.plain, flags=re.MULTILINE)
}
try:
indentation = (
reduce(gcd, [indent for indent in _indentations if not indent % 2]) or 1
)
except TypeError:
indentation = 1
return indentation
def with_indent_guides(
self,
indent_size: int = None,
*,
character: str = "",
style: StyleType = "dim green",
) -> "Text":
"""Adds indent guide lines to text.
Args:
indent_size (int): Size of indentation.
character (str, optional): Character to use for indentation. Defaults to "".
style (Union[Style, str], optional): Style of indent guides.
Returns:
[type]: [description]
"""
_indent_size = self.detect_indentation() if indent_size is None else indent_size
text = self.copy()
text.expand_tabs()
indent_line = f"{character}{' ' * (_indent_size - 1)}"
re_indent = re.compile(r"^( *)(.*)$")
new_lines: List[Text] = []
add_line = new_lines.append
blank_lines = 0
for line in text.split():
match = re_indent.match(line.plain)
if not match.group(2):
blank_lines += 1
continue
indent = match.group(1)
full_indents, remaining_space = divmod(len(indent), _indent_size)
new_indent = f"{indent_line * full_indents}{' ' * remaining_space}"
line.plain = new_indent + line.plain[len(new_indent) :]
line.stylize(style, 0, len(new_indent))
if blank_lines:
new_lines.extend([Text(new_indent, style=style)] * blank_lines)
blank_lines = 0
add_line(line)
if blank_lines:
new_lines.extend([Text("", style=style)] * blank_lines)
new_text = Text("\n").join(new_lines)
return new_text
if __name__ == "__main__": # pragma: no cover
from rich import print
@ -1015,3 +1085,15 @@ if __name__ == "__main__": # pragma: no cover
text = Text("<span>\n\tHello\n</span>")
text.expand_tabs(4)
print(text)
code = """
def __add__(self, other: Any) -> "Text":
if isinstance(other, (str, Text)):
result = self.copy()
result.append(other)
return result
return NotImplemented
"""
text = Text(code)
text = text.with_indent_guides()
print(text)

View File

@ -3,12 +3,14 @@ from __future__ import absolute_import
import platform
import sys
from dataclasses import dataclass, field
from textwrap import indent
from traceback import walk_tb
from types import TracebackType
from typing import Callable, Dict, List, Optional, Type
from pygments.lexers import guess_lexer_for_filename
from pygments.token import (
Comment,
Keyword,
Name,
Number,
@ -279,6 +281,7 @@ class Traceback:
"pygments.string": token_style(String),
"pygments.function": token_style(Name.Function),
"pygments.number": token_style(Number),
"repr.indent": token_style(Comment),
"repr.str": token_style(String),
"repr.brace": token_style(TextToken) + Style(bold=True),
"repr.number": token_style(Number),
@ -407,6 +410,7 @@ class Traceback:
highlight_lines={frame.lineno},
word_wrap=self.word_wrap,
code_width=88,
indent_guides=False,
)
yield ""
except Exception:
@ -437,6 +441,7 @@ if __name__ == "__main__": # pragma: no cover
print(one / a)
def foo(a):
zed = {
"characters": {
"Paul Atriedies",

View File

@ -4,7 +4,7 @@ import io
import sys
from rich.console import Console
from rich.pretty import install, pretty_repr, Node
from rich.pretty import install, Pretty, pretty_repr, Node
def test_install():
@ -77,3 +77,19 @@ def test_tuple_of_one():
def test_node():
node = Node("abc")
assert pretty_repr(node) == "abc: "
def test_indent_lines():
console = Console(width=100, color_system=None)
console.begin_capture()
console.print(Pretty([100, 200], indent_guides=True), width=8)
expected = """\
[
100,
200
]
"""
result = console.end_capture()
print(repr(result))
print(result)
assert result == expected

View File

@ -54,6 +54,26 @@ def test_python_render():
assert rendered_syntax == expected
def test_python_render_indent_guides():
syntax = Panel.fit(
Syntax(
CODE,
lexer_name="python",
line_numbers=True,
line_range=(2, 10),
theme="foo",
code_width=60,
word_wrap=True,
indent_guides=True,
),
padding=0,
)
rendered_syntax = render(syntax)
print(repr(rendered_syntax))
expected = '╭────────────────────────────────────────────────────────────────╮\n\x1b[1;38;2;24;24;24;48;2;248;248;248m \x1b[0m\x1b[38;2;173;173;173;48;2;248;248;248m 2 \x1b[0m\x1b[3;38;2;64;128;128;48;2;248;248;248m│ \x1b[0m\x1b[3;38;2;186;33;33;48;2;248;248;248m"""Iterate and generate a tuple with a flag for first \x1b[0m\x1b[48;2;248;248;248m \x1b[0m│\n\x1b[48;2;248;248;248m \x1b[0m\x1b[3;38;2;186;33;33;48;2;248;248;248mand last value."""\x1b[0m\x1b[48;2;248;248;248m \x1b[0m│\n\x1b[1;38;2;24;24;24;48;2;248;248;248m \x1b[0m\x1b[38;2;173;173;173;48;2;248;248;248m 3 \x1b[0m\x1b[3;38;2;64;128;128;48;2;248;248;248m│ \x1b[0m\x1b[38;2;0;0;0;48;2;248;248;248miter_values\x1b[0m\x1b[38;2;0;0;0;48;2;248;248;248m \x1b[0m\x1b[38;2;102;102;102;48;2;248;248;248m=\x1b[0m\x1b[38;2;0;0;0;48;2;248;248;248m \x1b[0m\x1b[38;2;0;128;0;48;2;248;248;248miter\x1b[0m\x1b[38;2;0;0;0;48;2;248;248;248m(\x1b[0m\x1b[38;2;0;0;0;48;2;248;248;248mvalues\x1b[0m\x1b[38;2;0;0;0;48;2;248;248;248m)\x1b[0m\x1b[48;2;248;248;248m \x1b[0m│\n\x1b[1;38;2;24;24;24;48;2;248;248;248m \x1b[0m\x1b[38;2;173;173;173;48;2;248;248;248m 4 \x1b[0m\x1b[3;38;2;64;128;128;48;2;248;248;248m│ \x1b[0m\x1b[1;38;2;0;128;0;48;2;248;248;248mtry\x1b[0m\x1b[38;2;0;0;0;48;2;248;248;248m:\x1b[0m\x1b[48;2;248;248;248m \x1b[0m│\n\x1b[1;38;2;24;24;24;48;2;248;248;248m \x1b[0m\x1b[38;2;173;173;173;48;2;248;248;248m 5 \x1b[0m\x1b[3;38;2;64;128;128;48;2;248;248;248m│ │ \x1b[0m\x1b[38;2;0;0;0;48;2;248;248;248mprevious_value\x1b[0m\x1b[38;2;0;0;0;48;2;248;248;248m \x1b[0m\x1b[38;2;102;102;102;48;2;248;248;248m=\x1b[0m\x1b[38;2;0;0;0;48;2;248;248;248m \x1b[0m\x1b[38;2;0;128;0;48;2;248;248;248mnext\x1b[0m\x1b[38;2;0;0;0;48;2;248;248;248m(\x1b[0m\x1b[38;2;0;0;0;48;2;248;248;248miter_values\x1b[0m\x1b[38;2;0;0;0;48;2;248;248;248m)\x1b[0m\x1b[48;2;248;248;248m \x1b[0m│\n\x1b[1;38;2;24;24;24;48;2;248;248;248m \x1b[0m\x1b[38;2;173;173;173;48;2;248;248;248m 6 \x1b[0m\x1b[3;38;2;64;128;128;48;2;248;248;248m│ \x1b[0m\x1b[1;38;2;0;128;0;48;2;248;248;248mexcept\x1b[0m\x1b[38;2;0;0;0;48;2;248;248;248m \x1b[0m\x1b[1;38;2;210;65;58;48;2;248;248;248mStopIteration\x1b[0m\x1b[38;2;0;0;0;48;2;248;248;248m:\x1b[0m\x1b[48;2;248;248;248m \x1b[0m│\n\x1b[1;38;2;24;24;24;48;2;248;248;248m \x1b[0m\x1b[38;2;173;173;173;48;2;248;248;248m 7 \x1b[0m\x1b[3;38;2;64;128;128;48;2;248;248;248m│ │ \x1b[0m\x1b[1;38;2;0;128;0;48;2;248;248;248mreturn\x1b[0m\x1b[48;2;248;248;248m \x1b[0m│\n\x1b[1;38;2;24;24;24;48;2;248;248;248m \x1b[0m\x1b[38;2;173;173;173;48;2;248;248;248m 8 \x1b[0m\x1b[3;38;2;64;128;128;48;2;248;248;248m│ \x1b[0m\x1b[38;2;0;0;0;48;2;248;248;248mfirst\x1b[0m\x1b[38;2;0;0;0;48;2;248;248;248m \x1b[0m\x1b[38;2;102;102;102;48;2;248;248;248m=\x1b[0m\x1b[38;2;0;0;0;48;2;248;248;248m \x1b[0m\x1b[1;38;2;0;128;0;48;2;248;248;248mTrue\x1b[0m\x1b[48;2;248;248;248m \x1b[0m│\n\x1b[1;38;2;24;24;24;48;2;248;248;248m \x1b[0m\x1b[38;2;173;173;173;48;2;248;248;248m 9 \x1b[0m\x1b[3;38;2;64;128;128;48;2;248;248;248m│ \x1b[0m\x1b[1;38;2;0;128;0;48;2;248;248;248mfor\x1b[0m\x1b[38;2;0;0;0;48;2;248;248;248m \x1b[0m\x1b[38;2;0;0;0;48;2;248;248;248mvalue\x1b[0m\x1b[38;2;0;0;0;48;2;248;248;248m \x1b[0m\x1b[1;38;2;170;34;255;48;2;248;248;248min\x1b[0m\x1b[38;2;0;0;0;48;2;248;248;248m \x1b[0m\x1b[38;2;0;0;0;48;2;248;248;248miter_values\x1b[0m\x1b[38;2;0;0;0;48;2;248;248;248m:\x1b[0m\x1b[48;2;248;248;248m \x1b[0m│\n\x1b[1;38;2;24;24;24;48;2;248;248;248m \x1b[0m\x1b[38;2;173;173;173;48;2;248;248;248m10 \x1b[0m\x1b[3;38;2;64;128;128;48;2;248;248;248m│ │ \x1b[0m\x1b[1;38;2;0;128;0;48;2;248;248;248myield\x1b[0m\x1b[38;2;0;0;0;48;2;248;248;248m \x1b[0m\x1b[38;2;0;0;0;48;2;248;248;248mfirst\x1b[0m\x1b[38;2;0;0;0;48;2;248;248;248m,\x1b[0m\x1b[38;2;0;0;0;48;2;248;248;248m \x1b[0m\x1b[1;38;2;0;128;0;48;2;248;248;248mFalse\x1b[0m\x1b[38;2;0;0;0;48;2;248;248;248m,\x1b[0m\x1b[38;2;0;0;0;48;2;248;248;248m \x1b[0m\x1b[38;2;0;0;0;48;2;248;248;248mprevious_value\x1b[0m\x1b[48;2;248;248;248m \x1b[0m│\n╰────────────────────────────────────────────────────────────────╯\n'
assert rendered_syntax == expected
def test_pygments_syntax_theme_non_str():
from pygments.style import Style as PygmentsStyle

View File

@ -598,3 +598,41 @@ def test_align_center():
test = Text("foo")
test.align("center", 10)
assert test.plain == " foo "
def test_detect_indentation():
test = """\
foo
bar
"""
assert Text(test).detect_indentation() == 4
test = """\
foo
bar
baz
"""
assert Text(test).detect_indentation() == 2
assert Text("").detect_indentation() == 1
assert Text(" ").detect_indentation() == 1
def test_indentation_guides():
test = Text(
"""\
for a in range(10):
print(a)
foo = [
1,
{
2
}
]
"""
)
result = test.with_indent_guides()
print(result.plain)
print(repr(result.plain))
expected = "for a in range(10):\n│ print(a)\n\nfoo = [\n│ 1,\n{\n│ │ 2\n│ }\n]\n"
assert result.plain == expected

View File

@ -16,4 +16,4 @@ DATA = {
}
console = Console()
for w in range(130):
console.print(Panel(Pretty(DATA), width=w))
console.print(Panel(Pretty(DATA, indent_guides=True), width=w))