Skip to content

Commit

Permalink
Merge pull request #1300 from willmcgugan/pretty-classes
Browse files Browse the repository at this point in the history
Pretty classes
  • Loading branch information
willmcgugan authored Jun 18, 2021
2 parents 56e7158 + 6e97016 commit e524643
Show file tree
Hide file tree
Showing 25 changed files with 402 additions and 105 deletions.
12 changes: 11 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,17 @@ 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.4.0] - 2021-06-18

### Added

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

### Fixed

- Fixed error pretty printing classes with special **rich_repr** method

## [10.3.0] - 2021-06-09

### Added
Expand All @@ -21,7 +32,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [10.2.2] - 2021-05-19


### Fixed

- Fixed status not rendering console markup https://github.com/willmcgugan/rich/issues/1244
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 <[email protected]>"]
license = "MIT"
Expand Down
2 changes: 1 addition & 1 deletion rich/_ratio.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
if sys.version_info >= (3, 8):
from typing import Protocol
else:
from typing_extensions import Protocol
from typing_extensions import Protocol # pragma: no cover


class Edge(Protocol):
Expand Down
2 changes: 1 addition & 1 deletion rich/align.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
if sys.version_info >= (3, 8):
from typing import Literal
else:
from typing_extensions import Literal
from typing_extensions import Literal # pragma: no cover

from .constrain import Constrain
from .jupyter import JupyterMixin
Expand Down
2 changes: 1 addition & 1 deletion rich/box.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
if sys.version_info >= (3, 8):
from typing import Literal
else:
from typing_extensions import Literal
from typing_extensions import Literal # pragma: no cover


from ._loop import loop_last
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
11 changes: 8 additions & 3 deletions rich/console.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
from itertools import islice
from time import monotonic
from types import FrameType, TracebackType
from inspect import isclass
from typing import (
IO,
TYPE_CHECKING,
Expand All @@ -36,7 +37,11 @@
if sys.version_info >= (3, 8):
from typing import Literal, Protocol, runtime_checkable
else:
from typing_extensions import Literal, Protocol, runtime_checkable
from typing_extensions import (
Literal,
Protocol,
runtime_checkable,
) # pragma: no cover


from . import errors, themes
Expand Down Expand Up @@ -1185,9 +1190,9 @@ def render(
# No space to render anything. This prevents potential recursion errors.
return
render_iterable: RenderResult
if hasattr(renderable, "__rich__"):
if hasattr(renderable, "__rich__") and not isclass(renderable):
renderable = renderable.__rich__() # type: ignore
if hasattr(renderable, "__rich_console__"):
if hasattr(renderable, "__rich_console__") and not isclass(renderable):
render_iterable = renderable.__rich_console__(self, _options) # type: ignore
elif isinstance(renderable, str):
text_renderable = self.render_str(
Expand Down
10 changes: 5 additions & 5 deletions rich/control.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
ControlType.CURSOR_DOWN: lambda param: f"\x1b[{param}B",
ControlType.CURSOR_FORWARD: lambda param: f"\x1b[{param}C",
ControlType.CURSOR_BACKWARD: lambda param: f"\x1b[{param}D",
ControlType.CURSOR_MOVE_TO_ROW: lambda param: f"\x1b[{param+1}G",
ControlType.CURSOR_MOVE_TO_COLUMN: lambda param: f"\x1b[{param+1}G",
ControlType.ERASE_IN_LINE: lambda param: f"\x1b[{param}K",
ControlType.CURSOR_MOVE_TO: lambda x, y: f"\x1b[{y+1};{x+1}H",
}
Expand Down Expand Up @@ -93,8 +93,8 @@ def get_codes() -> Iterable[ControlCode]:
return control

@classmethod
def move_to_row(cls, x: int, y: int = 0) -> "Control":
"""Move to the given row, optionally add offset to column.
def move_to_column(cls, x: int, y: int = 0) -> "Control":
"""Move to the given column, optionally add offset to row.
Returns:
x (int): absolute x (column)
Expand All @@ -106,14 +106,14 @@ def move_to_row(cls, x: int, y: int = 0) -> "Control":

return (
cls(
(ControlType.CURSOR_MOVE_TO_ROW, x + 1),
(ControlType.CURSOR_MOVE_TO_COLUMN, x),
(
ControlType.CURSOR_DOWN if y > 0 else ControlType.CURSOR_UP,
abs(y),
),
)
if y
else cls((ControlType.CURSOR_MOVE_TO_ROW, x))
else cls((ControlType.CURSOR_MOVE_TO_COLUMN, x))
)

@classmethod
Expand Down
2 changes: 1 addition & 1 deletion rich/live_render.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
if sys.version_info >= (3, 8):
from typing import Literal
else:
from typing_extensions import Literal
from typing_extensions import Literal # pragma: no cover


from ._loop import loop_last
Expand Down
38 changes: 34 additions & 4 deletions rich/markup.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
from ast import literal_eval
from operator import attrgetter
import re
from typing import Callable, Iterable, List, Match, NamedTuple, Optional, Tuple, Union

Expand All @@ -8,7 +10,7 @@


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

Expand Down Expand Up @@ -137,6 +139,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 +156,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 +191,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 +200,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 e524643

Please sign in to comment.