Skip to content

Commit

Permalink
pretty printable dataclasses
Browse files Browse the repository at this point in the history
  • Loading branch information
willmcgugan committed Mar 6, 2021
1 parent 7dd6eb1 commit a05ff5f
Show file tree
Hide file tree
Showing 11 changed files with 217 additions and 56 deletions.
14 changes: 14 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,20 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [9.12.5] - Unreleased

### Added

- Pretty printer now supports dataclasses

### Fixed

- Fixed Syntax background https://github.com/willmcgugan/rich/issues/1088

### Changed

- Added ws and wss to url highlighter

## [9.12.4] - 2021-03-01

### Fixed
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
name = "rich"
homepage = "https://github.com/willmcgugan/rich"
documentation = "https://rich.readthedocs.io/en/latest/"
version = "9.12.4"
version = "9.13.0"
description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal"
authors = ["Will McGugan <[email protected]>"]
license = "MIT"
Expand Down
6 changes: 3 additions & 3 deletions rich/console.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
import threading
from abc import ABC, abstractmethod
from collections import abc
from dataclasses import dataclass, field, replace
from dataclasses import dataclass, field, is_dataclass
from datetime import datetime
from functools import wraps
from getpass import getpass
Expand Down Expand Up @@ -40,7 +40,7 @@
from .markup import render as render_markup
from .measure import Measurement, measure_renderables
from .pager import Pager, SystemPager
from .pretty import Pretty
from .pretty import is_expandable, Pretty
from .scope import render_scope
from .screen import Screen
from .segment import Segment
Expand Down Expand Up @@ -1276,7 +1276,7 @@ def check_text() -> None:
elif isinstance(renderable, ConsoleRenderable):
check_text()
append(renderable)
elif isinstance(renderable, (abc.Mapping, abc.Sequence, abc.Set)):
elif is_expandable(renderable):
check_text()
append(Pretty(renderable, highlighter=_highlighter))
else:
Expand Down
2 changes: 1 addition & 1 deletion rich/highlighter.py
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ class ReprHighlighter(RegexHighlighter):
r"(?P<path>\B(\/[\w\.\-\_\+]+)*\/)(?P<filename>[\w\.\-\_\+]*)?",
r"(?<!\\)(?P<str>b?\'\'\'.*?(?<!\\)\'\'\'|b?\'.*?(?<!\\)\'|b?\"\"\".*?(?<!\\)\"\"\"|b?\".*?(?<!\\)\")",
r"(?P<uuid>[a-fA-F0-9]{8}\-[a-fA-F0-9]{4}\-[a-fA-F0-9]{4}\-[a-fA-F0-9]{4}\-[a-fA-F0-9]{12})",
r"(?P<url>https?:\/\/[0-9a-zA-Z\$\-\_\+\!`\(\)\,\.\?\/\;\:\&\=\%\#]*)",
r"(?P<url>(https|http|ws|wss):\/\/[0-9a-zA-Z\$\-\_\+\!`\(\)\,\.\?\/\;\:\&\=\%\#]*)",
),
]

Expand Down
57 changes: 50 additions & 7 deletions rich/pretty.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@
import os
import sys
from array import array
from collections import Counter, abc, defaultdict, deque
from dataclasses import dataclass
from collections import Counter, defaultdict, deque
from dataclasses import dataclass, fields, is_dataclass
from itertools import islice
from typing import (
TYPE_CHECKING,
Expand All @@ -19,9 +19,10 @@

from rich.highlighter import ReprHighlighter

from .abc import RichRenderable
from . import get_console
from ._loop import loop_last
from ._pick import pick_bool
from .abc import RichRenderable
from .cells import cell_len
from .highlighter import ReprHighlighter
from .jupyter import JupyterRenderable
Expand Down Expand Up @@ -61,6 +62,7 @@ def install(
expand_all (bool, optional): Expand all containers. Defaults to False
"""
from rich import get_console

from .console import ConsoleRenderable # needed here to prevent circular import

console = console or get_console()
Expand Down Expand Up @@ -195,7 +197,7 @@ def __rich_console__(
pretty_text = (
self.highlighter(pretty_text)
if pretty_text
else Text("__repr__ return empty string", style="dim italic")
else Text("__repr__ returned empty string", style="dim italic")
)
if self.indent_guides and not options.ascii_only:
pretty_text = pretty_text.with_indent_guides(
Expand Down Expand Up @@ -247,6 +249,13 @@ def _get_braces_for_array(_object: array) -> Tuple[str, str, str]:
_MAPPING_CONTAINERS = (dict, os._Environ)


def is_expandable(obj: Any) -> bool:
"""Check if an object may be expanded by pretty print."""
return isinstance(obj, _CONTAINERS) or (
is_dataclass(obj) and not isinstance(obj, type)
)


@dataclass
class Node:
"""A node in a repr tree. May be atomic or a container."""
Expand All @@ -259,6 +268,7 @@ class Node:
last: bool = False
is_tuple: bool = False
children: Optional[List["Node"]] = None
key_separator = ": "

@property
def separator(self) -> str:
Expand All @@ -269,7 +279,7 @@ def iter_tokens(self) -> Iterable[str]:
"""Generate tokens for this node."""
if self.key_repr:
yield self.key_repr
yield ": "
yield self.key_separator
if self.value_repr:
yield self.value_repr
elif self.children is not None:
Expand Down Expand Up @@ -366,7 +376,8 @@ def expand(self, indent_size: int) -> Iterable["_Line"]:
assert node.children
if node.key_repr:
yield _Line(
text=f"{node.key_repr}: {node.open_brace}", whitespace=whitespace
text=f"{node.key_repr}{node.key_separator}{node.open_brace}",
whitespace=whitespace,
)
else:
yield _Line(text=node.open_brace, whitespace=whitespace)
Expand Down Expand Up @@ -428,13 +439,45 @@ def to_repr(obj: Any) -> str:
def _traverse(obj: Any, root: bool = False) -> Node:
"""Walk the object depth first."""
obj_type = type(obj)
if obj_type in _CONTAINERS:
if (
is_dataclass(obj)
and not isinstance(obj, type)
and (
"__create_fn__" in obj.__repr__.__qualname__
) # Check if __repr__ wasn't overriden
):
obj_id = id(obj)
if obj_id in visited_ids:
# Recursion detected
return Node(value_repr="...")
push_visited(obj_id)

children: List[Node] = []
append = children.append
node = Node(
open_brace=f"{obj.__class__.__name__}(",
close_brace=")",
children=children,
last=root,
)

for last, field in loop_last(fields(obj)):
if field.repr:
child_node = _traverse(getattr(obj, field.name))
child_node.key_repr = field.name
child_node.last = last
child_node.key_separator = "="
append(child_node)

pop_visited(obj_id)

elif obj_type in _CONTAINERS:
obj_id = id(obj)
if obj_id in visited_ids:
# Recursion detected
return Node(value_repr="...")
push_visited(obj_id)

open_brace, close_brace, empty = _BRACES[obj_type](obj)

if obj:
Expand Down
83 changes: 61 additions & 22 deletions rich/syntax.py
Original file line number Diff line number Diff line change
Expand Up @@ -249,6 +249,9 @@ def __init__(
self.tab_size = tab_size
self.word_wrap = word_wrap
self.background_color = background_color
self.background_style = (
Style(bgcolor=background_color) if background_color else Style()
)
self.indent_guides = indent_guides

self._theme = self.get_theme(theme)
Expand Down Expand Up @@ -328,11 +331,12 @@ def from_path(

def _get_base_style(self) -> Style:
"""Get the base style."""
default_style = (
Style(bgcolor=self.background_color)
if self.background_color is not None
else self._theme.get_background_style()
)
# default_style = (
# Style(bgcolor=self.background_color)
# if self.background_color is not None
# else self._theme.get_background_style()
# )
default_style = self._theme.get_background_style() + self.background_style
return default_style

def _get_token_color(self, token_type: TokenType) -> Optional[Color]:
Expand Down Expand Up @@ -419,9 +423,10 @@ def tokens_to_spans() -> Iterable[Tuple[str, Optional[Style]]]:
return text

def _get_line_numbers_color(self, blend: float = 0.3) -> Color:
background_color = self._theme.get_background_style().bgcolor
background_style = self._theme.get_background_style() + self.background_style
background_color = background_style.bgcolor
if background_color is None or background_color.is_system_defined:
return background_color or Color.default()
return Color.default()
foreground_color = self._get_token_color(Token.Text)
if foreground_color is None or foreground_color.is_system_defined:
return foreground_color or Color.default()
Expand Down Expand Up @@ -450,11 +455,13 @@ def _get_number_styles(self, console: Console) -> Tuple[Style, Style, Style]:
background_style,
self._theme.get_style_for_token(Token.Text),
Style(color=self._get_line_numbers_color()),
self.background_style,
)
highlight_number_style = Style.chain(
background_style,
self._theme.get_style_for_token(Token.Text),
Style(bold=True, color=self._get_line_numbers_color(0.9)),
self.background_style,
)
else:
number_style = background_style + Style(dim=True)
Expand All @@ -473,7 +480,11 @@ def __rich_console__(

transparent_background = self._get_base_style().transparent_background
code_width = (
(options.max_width - self._numbers_column_width - 1)
(
(options.max_width - self._numbers_column_width - 1)
if self.line_numbers
else options.max_width
)
if self.code_width is None
else self.code_width
)
Expand All @@ -494,9 +505,31 @@ def __rich_console__(
highlight_number_style,
) = self._get_number_styles(console)

if not self.line_numbers:
if not self.line_numbers and not self.word_wrap and not self.line_range:
# Simple case of just rendering text
yield from console.render(text, options=options.update(width=code_width))
style = (
self._get_base_style()
+ self._theme.get_style_for_token(Comment)
+ Style(dim=True)
+ self.background_style
)
if self.indent_guides and not options.ascii_only:
text = text.with_indent_guides(self.tab_size, style=style)
text.overflow = "crop"
if style.transparent_background:
yield from console.render(
text, options=options.update(width=code_width)
)
else:
lines = console.render_lines(
text,
options.update(width=code_width, height=None),
style=self.background_style,
pad=True,
new_lines=True,
)
for line in lines:
yield from line
return

lines = text.split("\n")
Expand All @@ -508,6 +541,7 @@ def __rich_console__(
self._get_base_style()
+ self._theme.get_style_for_token(Comment)
+ Style(dim=True)
+ self.background_style
)
lines = (
Text("\n")
Expand Down Expand Up @@ -548,19 +582,24 @@ def __rich_console__(
pad=not transparent_background,
)
]
for first, wrapped_line in loop_first(wrapped_lines):
if first:
line_column = str(line_no).rjust(numbers_column_width - 2) + " "
if highlight_line(line_no):
yield _Segment(line_pointer, Style(color="red"))
yield _Segment(line_column, highlight_number_style)
if self.line_numbers:
for first, wrapped_line in loop_first(wrapped_lines):
if first:
line_column = str(line_no).rjust(numbers_column_width - 2) + " "
if highlight_line(line_no):
yield _Segment(line_pointer, Style(color="red"))
yield _Segment(line_column, highlight_number_style)
else:
yield _Segment(" ", highlight_number_style)
yield _Segment(line_column, number_style)
else:
yield _Segment(" ", highlight_number_style)
yield _Segment(line_column, number_style)
else:
yield padding
yield from wrapped_line
yield new_line
yield padding
yield from wrapped_line
yield new_line
else:
for line in wrapped_lines:
yield from line
yield new_line


if __name__ == "__main__": # pragma: no cover
Expand Down
5 changes: 3 additions & 2 deletions rich/text.py
Original file line number Diff line number Diff line change
Expand Up @@ -329,9 +329,10 @@ def spans(self, spans: List[Span]) -> None:
"""Set spans."""
self._spans = spans[:]

def blank_copy(self) -> "Text":
def blank_copy(self, plain:str="") -> "Text":
"""Return a new Text instance with copied meta data (but not the string or spans)."""
copy_self = Text(
plain,
style=self.style,
justify=self.justify,
overflow=self.overflow,
Expand Down Expand Up @@ -1116,7 +1117,7 @@ def with_indent_guides(
if blank_lines:
new_lines.extend([Text("", style=style)] * blank_lines)

new_text = Text("\n").join(new_lines)
new_text = text.blank_copy("\n").join(new_lines)
return new_text


Expand Down
Loading

0 comments on commit a05ff5f

Please sign in to comment.