Skip to content

Commit

Permalink
auto repr
Browse files Browse the repository at this point in the history
  • Loading branch information
willmcgugan committed Jun 18, 2021
1 parent 41cc6a6 commit 3281af6
Show file tree
Hide file tree
Showing 15 changed files with 310 additions and 102 deletions.
3 changes: 2 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,12 @@ 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).

## [10.3.1] - Unreleased
## [10.4.0] - 2021-06-18

### Added

- Added Style.meta
- Added rich.repr.auto decorator

### Fixed

Expand Down
71 changes: 57 additions & 14 deletions docs/source/pretty.rst
Original file line number Diff line number Diff line change
Expand Up @@ -67,24 +67,28 @@ Rich Repr Protocol

Rich is able to syntax highlight any output, but the formatting is restricted to builtin containers, dataclasses, and other objects Rich knows about, such as objects generated by the `attrs <https://www.attrs.org/en/stable/>`_ library. To add Rich formatting capabilities to custom objects, you can implement the *rich repr protocol*.

Run the following command to see an example of what the Rich repr protocol can generate::

python -m rich.repr

First, let's look at a class that might benefit from a Rich repr::

class Bird:
def __init__(self, name, eats=None, fly=True, extinct=False):
self.name = name
self.eats = list(eats) if eats else []
self.fly = fly
self.extinct = extinct
class Bird:
def __init__(self, name, eats=None, fly=True, extinct=False):
self.name = name
self.eats = list(eats) if eats else []
self.fly = fly
self.extinct = extinct

def __repr__(self):
return f"Bird({self.name!r}, eats={self.eats!r}, fly={self.fly!r}, extinct={self.extinct!r})"
def __repr__(self):
return f"Bird({self.name!r}, eats={self.eats!r}, fly={self.fly!r}, extinct={self.extinct!r})"

BIRDS = {
"gull": Bird("gull", eats=["fish", "chips", "ice cream", "sausage rolls"]),
"penguin": Bird("penguin", eats=["fish"], fly=False),
"dodo": Bird("dodo", eats=["fruit"], fly=False, extinct=True)
}
print(BIRDS)
BIRDS = {
"gull": Bird("gull", eats=["fish", "chips", "ice cream", "sausage rolls"]),
"penguin": Bird("penguin", eats=["fish"], fly=False),
"dodo": Bird("dodo", eats=["fruit"], fly=False, extinct=True)
}
print(BIRDS)

The result of this script would be::

Expand Down Expand Up @@ -168,6 +172,45 @@ This will change the output of the Rich repr example to the following::

Note that you can add ``__rich_repr__`` methods to third-party libraries *without* including Rich as a dependency. If Rich is not installed, then nothing will break. Hopefully more third-party libraries will adopt Rich repr methods in the future.

Automatic Rich Repr
~~~~~~~~~~~~~~~~~~~

Rich can generate a rich repr automatically if the parameters are named the same as your attributes.

To automatically build a rich repr, use the :meth:`~rich.repr.auto` class decorator. The Bird example above follows the above rule, so we wouldn't even need to implement our own `__rich_repr__`::

import rich.repr

@rich.repr.auto
class Bird:
def __init__(self, name, eats=None, fly=True, extinct=False):
self.name = name
self.eats = list(eats) if eats else []
self.fly = fly
self.extinct = extinct


BIRDS = {
"gull": Bird("gull", eats=["fish", "chips", "ice cream", "sausage rolls"]),
"penguin": Bird("penguin", eats=["fish"], fly=False),
"dodo": Bird("dodo", eats=["fruit"], fly=False, extinct=True)
}
from rich import print
print(BIRDS)

Note that the decorator will also create a `__repr__`, so you you will get an auto-generated repr even if you don't print with Rich.

If you want to auto-generate the angular type of repr, then set ``angular=True`` on the decorator::

@rich.repr.auto(angular=True)
class Bird:
def __init__(self, name, eats=None, fly=True, extinct=False):
self.name = name
self.eats = list(eats) if eats else []
self.fly = fly
self.extinct = extinct


Example
-------

Expand Down
13 changes: 4 additions & 9 deletions examples/repr.py
Original file line number Diff line number Diff line change
@@ -1,22 +1,17 @@
from rich.repr import rich_repr
import rich.repr


@rich_repr
@rich.repr.auto
class Bird:
def __init__(self, name, eats=None, fly=True, extinct=False):
self.name = name
self.eats = list(eats) if eats else []
self.fly = fly
self.extinct = extinct

def __rich_repr__(self):
yield self.name
yield "eats", self.eats
yield "fly", self.fly, True
yield "extinct", self.extinct, False

# __rich_repr__.angular = True

# Note that the repr is still generate without Rich
# Try commenting out the following lin

from rich import print

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 = "10.3.0"
version = "10.4.0"
description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal"
authors = ["Will McGugan <willmcgugan@gmail.com>"]
license = "MIT"
Expand Down
21 changes: 14 additions & 7 deletions rich/color.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

from ._palettes import EIGHT_BIT_PALETTE, STANDARD_PALETTE, WINDOWS_PALETTE
from .color_triplet import ColorTriplet
from .repr import rich_repr, RichReprResult
from .terminal_theme import DEFAULT_TERMINAL_THEME

if TYPE_CHECKING: # pragma: no cover
Expand All @@ -25,6 +26,9 @@ class ColorSystem(IntEnum):
TRUECOLOR = 3
WINDOWS = 4

def __repr__(self) -> str:
return f"ColorSystem.{self.name}"


class ColorType(IntEnum):
"""Type of color stored in Color class."""
Expand All @@ -35,6 +39,9 @@ class ColorType(IntEnum):
TRUECOLOR = 3
WINDOWS = 4

def __repr__(self) -> str:
return f"ColorType.{self.name}"


ANSI_COLOR_NAMES = {
"black": 0,
Expand Down Expand Up @@ -257,6 +264,7 @@ class ColorParseError(Exception):
)


@rich_repr
class Color(NamedTuple):
"""Terminal color definition."""

Expand All @@ -269,13 +277,6 @@ class Color(NamedTuple):
triplet: Optional[ColorTriplet] = None
"""A triplet of color components, if an RGB color."""

def __repr__(self) -> str:
return (
f"<color {self.name!r} ({self.type.name.lower()})>"
if self.number is None
else f"<color {self.name!r} {self.number} ({self.type.name.lower()})>"
)

def __rich__(self) -> "Text":
"""Dispays the actual color if Rich printed."""
from .text import Text
Expand All @@ -287,6 +288,12 @@ def __rich__(self) -> "Text":
" >",
)

def __rich_repr__(self) -> RichReprResult:
yield self.name
yield self.type
yield "number", self.number, None
yield "triplet", self.triplet, None

@property
def system(self) -> ColorSystem:
"""Get the native color system for this color."""
Expand Down
40 changes: 36 additions & 4 deletions rich/markup.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,18 @@
from ast import literal_eval
from operator import attrgetter
import re
from typing import Callable, Iterable, List, Match, NamedTuple, Optional, Tuple, Union

from black import E

from .errors import MarkupError
from .style import Style
from .text import Span, Text
from ._emoji_replace import _emoji_replace


RE_TAGS = re.compile(
r"""((\\*)\[([a-z#\/].*?)\])""",
r"""((\\*)\[([a-z#\/@].*?)\])""",
re.VERBOSE,
)

Expand Down Expand Up @@ -137,6 +141,7 @@ def pop_style(style_name: str) -> Tuple[int, Tag]:
elif tag is not None:
if tag.name.startswith("/"): # Closing tag
style_name = tag.name[1:].strip()

if style_name: # explicit close
style_name = normalize(style_name)
try:
Expand All @@ -153,7 +158,30 @@ def pop_style(style_name: str) -> Tuple[int, Tag]:
f"closing tag '[/]' at position {position} has nothing to close"
) from None

append_span(_Span(start, len(text), str(open_tag)))
if open_tag.name.startswith("@"):
if open_tag.parameters:
try:
meta_params = literal_eval(open_tag.parameters)
except SyntaxError as error:
raise MarkupError(
f"error parsing {open_tag.parameters!r}; {error.msg}"
)
except Exception as error:
raise MarkupError(
f"error parsing {open_tag.parameters!r}; {error}"
) from None

else:
meta_params = ()

append_span(
_Span(
start, len(text), Style(meta={open_tag.name: meta_params})
)
)
else:
append_span(_Span(start, len(text), str(open_tag)))

else: # Opening tag
normalized_tag = _Tag(normalize(tag.name), tag.parameters)
style_stack.append((len(text), normalized_tag))
Expand All @@ -165,7 +193,7 @@ def pop_style(style_name: str) -> Tuple[int, Tag]:
if style:
append_span(_Span(start, text_length, style))

text.spans = sorted(spans)
text.spans = sorted(spans, key=attrgetter("start", "end"))
return text


Expand All @@ -174,7 +202,11 @@ def pop_style(style_name: str) -> Tuple[int, Tag]:
from rich.console import Console
from rich.text import Text

console = Console(highlight=False)
console = Console(highlight=True)

t = render("[b]Hello[/b] [@click='view.toggle', 'left']World[/]")
console.print(t)
console.print(t._spans)

console.print("Hello [1], [1,2,3] ['hello']")
console.print("foo")
Expand Down
Loading

0 comments on commit 3281af6

Please sign in to comment.