Skip to content

Commit

Permalink
Merge pull request Textualize#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
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
@@ -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
@@ -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
71 changes: 57 additions & 14 deletions docs/source/pretty.rst
Original file line number Diff line number Diff line change
@@ -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::

@@ -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
-------

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

2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -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"
2 changes: 1 addition & 1 deletion rich/_ratio.py
Original file line number Diff line number Diff line change
@@ -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):
2 changes: 1 addition & 1 deletion rich/align.py
Original file line number Diff line number Diff line change
@@ -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
2 changes: 1 addition & 1 deletion rich/box.py
Original file line number Diff line number Diff line change
@@ -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
21 changes: 14 additions & 7 deletions rich/color.py
Original file line number Diff line number Diff line change
@@ -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
@@ -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."""
@@ -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,
@@ -257,6 +264,7 @@ class ColorParseError(Exception):
)


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

@@ -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
@@ -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."""
11 changes: 8 additions & 3 deletions rich/console.py
Original file line number Diff line number Diff line change
@@ -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,
@@ -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
@@ -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(
10 changes: 5 additions & 5 deletions rich/control.py
Original file line number Diff line number Diff line change
@@ -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",
}
@@ -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)
@@ -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
2 changes: 1 addition & 1 deletion rich/live_render.py
Original file line number Diff line number Diff line change
@@ -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
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

@@ -8,7 +10,7 @@


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

@@ -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:
@@ -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))
@@ -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


@@ -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")
Loading

0 comments on commit e524643

Please sign in to comment.