Skip to content

Commit

Permalink
changed console customization
Browse files Browse the repository at this point in the history
  • Loading branch information
willmcgugan committed Feb 23, 2020
1 parent de5e23b commit d471375
Show file tree
Hide file tree
Showing 5 changed files with 46 additions and 25 deletions.
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +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).

## [0.5.1] - 2020-02-23

### Changed

- Replaced `__console_str__` with `__rich__`

## [0.4.1] - 2020-02-22

### Fixed
Expand Down
29 changes: 16 additions & 13 deletions docs/source/protocol.rst
Original file line number Diff line number Diff line change
Expand Up @@ -9,32 +9,36 @@ Rich supports a simple protocol to add rich formatting capabilities to custom ob
Use this for presentation or to display additional debugging information that might be hard to parse from a typical ``__repr__`` string.


Console Str
-----------
Console Customization
---------------------

The easiest way to add console output to your custom object is to implement a ``__console_str__`` method. This method accepts no arguments, and should return a :class:`~rich.text.Text` instance. Here's an example::
The easiest way to customize console output for your object is to implement a ``__rich__`` method. This method accepts no arguments, and should return an object that Rich knows how to render, such as a :class:`~rich.text.Text` or :class:`~rich.table.Table`. If you return a plain string it will be rendered as :ref:`console_markup`. Here's an example::

class MyObject:
def __console_str__(self) -> Text:
return Text.from_markup("[bold]MyObject()[/bold]")
def __rich__(self) -> str:
return "[bold cyan]MyObject()"

If you were to print or log an instance of ``MyObject`` it would render as ``MyObject()`` in bold. Naturally, you would want to put this to better use, perhaps by adding specialized syntax highlighting.
If you were to print or log an instance of ``MyObject`` it would render as ``MyObject()`` in bold cyan. Naturally, you would want to put this to better use, perhaps by adding specialized syntax highlighting.


Console Render
--------------

The ``__console_str__`` method is limited to styled text. For more advanced rendering, Rich supports a ``__console__`` method which you can use to generate custom output with other renderable objects. For instance, a complex data type might be best represented as a :class:`~rich.table.Table`.
The ``__rich__`` method is limited to a single renderable object. For more advanced rendering, add a ``__console__`` method to your class.

The ``__console__`` method should accept a :class:`~rich.console.Console` and a :class:`~rich.console.ConsoleOptions` instance. It should return an iterable of other renderable objects. Although that means it *could* return a container such as a list, it is customary to ``yield`` output (making the method a generator)
The ``__console__`` method should accept a :class:`~rich.console.Console` and a :class:`~rich.console.ConsoleOptions` instance. It should return an iterable of other renderable objects. Although that means it *could* return a container such as a list, it generally easier implemented by using the ``yield`` statement (making the method a generator).

Here's an example of a ``__console__`` method::

from rich.console import Console, ConsoleOptions, RenderResult

@dataclass
class Student:
id: int
name: str
age: int
def __console__(self, console: Console, options: ConsoleOptions) -> Iterable[Table]:
def __console__(self, console: Console, options: ConsoleOptions) -> RenderResult:
yield f"[b]Student:[/b] #{self.id}"
my_table = Table("Attribute, "Value")
my_table.add_row("name", self.name)
my_table.add_row("age", str(self.age))
Expand All @@ -48,9 +52,8 @@ Low Level Render

For complete control over how a custom object is rendered to the terminal, you can yield :class:`~rich.segment.Segment` objects. A Segment consists of a piece of text and an optional Style. The following example, writes multi-colored text when rendering a ``MyObject`` instance::


class MyObject:
def __console__(self, console: Console, options: ConsoleOptions) -> Iterable[Table]:
def __console__(self, console: Console, options: ConsoleOptions) -> RenderResult:
yield Segment("My", "magenta")
yield Segment("Object", green")
yield Segment("()", "cyan")
Expand All @@ -59,9 +62,9 @@ For complete control over how a custom object is rendered to the terminal, you c
Console Width
~~~~~~~~~~~~~

Sometimes Rich needs to know how many characters an object will take up when rendering. The :class:`~rich.table.Table` class for instance, will use this information to calculate the optimal dimensions for the columns. If you aren't using one of the standard classes, you will need to supply a ``__console_width__`` method which accepts the maximum width as an integer and returns a :class:`~rich.render_width.RenderWidth` object. The RenderWidth object should contain the *minimum* and *maximum* number of characters required to render.
Sometimes Rich needs to know how many characters an object will take up when rendering. The :class:`~rich.table.Table` class, for instance, will use this information to calculate the optimal dimensions for the columns. If you aren't using one of the standard classes, you will need to supply a ``__console_width__`` method which accepts the maximum width as an integer and returns a :class:`~rich.render_width.RenderWidth` object. The RenderWidth object should contain the *minimum* and *maximum* number of characters required to render.

For example, if we are rendering a chess board, it would require a minimum of 8 characters to render. The maximum can be left as the maximum available width (assuming a centered board)::
For example, if we are rendering a chess board, it would require a minimum of 8 characters to render. The maximum can be left as the maximum available width (assuming a centered board)::

class ChessBoard:
def __console_width__(self, max_width: int) -> RenderWidth:
Expand Down
19 changes: 13 additions & 6 deletions rich/console.py
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,16 @@ def __console__(
_null_highlighter = NullHighlighter()


class RichRenderable:
def __init__(self, rich_cast: Callable[[], ConsoleRenderable]) -> None:
self.rich_cast = rich_cast

def __console__(
self, console: "Console", options: "ConsoleOptions"
) -> RenderResult:
yield self.rich_cast()


class RenderGroup:
def __init__(
self, renderables: Iterable[RenderableType], fit: bool = False
Expand Down Expand Up @@ -664,14 +674,13 @@ def check_text() -> None:
del text[:]

for renderable in objects:
rich_cast = getattr(renderable, "__rich__", None)
if rich_cast:
renderable = rich_cast()
if isinstance(renderable, ConsoleRenderable):
check_text()
append(renderable)
continue
console_str_callable = getattr(renderable, "__console_str__", None)
if console_str_callable is not None:
append_text(console_str_callable())
continue
if isinstance(renderable, str):
render_str = renderable
if emoji:
Expand All @@ -680,8 +689,6 @@ def check_text() -> None:
append_text(_highlighter(render_text))
elif isinstance(renderable, Text):
append_text(renderable)
elif isinstance(renderable, (int, float, bool, bytes, type(None))):
append_text(_highlighter(repr(renderable)))
elif isinstance(renderable, (Mapping, Sequence)):
check_text()
append(Pretty(renderable, highlighter=_highlighter))
Expand Down
8 changes: 4 additions & 4 deletions rich/containers.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,8 @@
class Renderables:
"""A list subclass which renders its contents to the console."""

def __init__(self, renderables: Iterable["ConsoleRenderable"] = None) -> None:
self._renderables: List["ConsoleRenderable"] = (
def __init__(self, renderables: Iterable["RenderableType"] = None) -> None:
self._renderables: List["RenderableType"] = (
list(renderables) if renderables is not None else []
)

Expand All @@ -41,10 +41,10 @@ def __console_width__(self, max_width: int) -> "RenderWidth":
_max = max(dimension.maximum for dimension in dimensions)
return RenderWidth(_min, _max)

def append(self, renderable: "ConsoleRenderable") -> None:
def append(self, renderable: "RenderableType") -> None:
self._renderables.append(renderable)

def __iter__(self) -> Iterable["ConsoleRenderable"]:
def __iter__(self) -> Iterable["RenderableType"]:
return iter(self._renderables)


Expand Down
9 changes: 7 additions & 2 deletions rich/segment.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,12 @@


class Segment(NamedTuple):
"""A piece of text with associated style."""
"""A piece of text with associated style.
Args:
text (str): A piece of text.
style (:class:`rich.style.Style`, optional): An optional style to apply to the text.
"""

text: str
style: Optional[Style] = None
Expand Down Expand Up @@ -131,7 +136,7 @@ def get_line_length(cls, line: List["Segment"]) -> int:

@classmethod
def get_shape(cls, lines: List[List["Segment"]]) -> Tuple[int, int]:
"""Get the shape (enclosing rectangle) of a list of lines.
r"""Get the shape (enclosing rectangle) of a list of lines.
Args:
lines (List[List[Segment]]): A list of lines (no '\n' characters).
Expand Down

0 comments on commit d471375

Please sign in to comment.