Skip to content

Commit

Permalink
table styles, readme
Browse files Browse the repository at this point in the history
  • Loading branch information
willmcgugan committed Dec 23, 2019
1 parent 2ef5521 commit 9168dbd
Show file tree
Hide file tree
Showing 9 changed files with 126 additions and 26 deletions.
79 changes: 78 additions & 1 deletion README.md
@@ -1,3 +1,5 @@
**Note:** This library is currently work in progress. Documentation and tests are in progress...

# Rich

Rich is a Python library for _rich_ text and high level formatting in the terminal.
Expand Down Expand Up @@ -81,7 +83,38 @@ Style attributes and colors may appear in any order, i.e. `"bold magenta on yell

## Console Logging

TODO
The Console object has a `log()` method which has a similar interface to `print()`, but also renders a column for the current time and the file and line which made the call. By default, Rich will do syntax highlighting for Python structures and for repr strings. If you log a collection (i.e. a dict or a list) Rich will pretty print it so that it fits in the available space. Here's an example of some of these features.

```python
from rich.console import Console
console = Console()

test_data = [
{"jsonrpc": "2.0", "method": "sum", "params": [None, 1, 2, 4, False, True], "id": "1",},
{"jsonrpc": "2.0", "method": "notify_hello", "params": [7]},
{"jsonrpc": "2.0", "method": "subtract", "params": [42, 23], "id": "2"},
]

def test_log():
enabled = False
context = {
"foo": "bar",
}
movies = ["Deadpool", "Rise of the Skywalker"]
console.log("Hello from", console, "!")
console.log(test_data, log_locals=True)


test_log()
```

The above produces the following output:

![Log](./imgs/log.png)

Note the `log_locals` argument, which outputs a table containing the local variables where the log method was called.

The log method could be used for logging to the terminal for long running applications such as servers, but is also a very nice debugging aid.

## Emoji

Expand All @@ -94,6 +127,50 @@ To insert an emoji in to console output place the name between two colons. Here'

Please use this feature wisely.

## Tables

Rich can render flexible tables with unicode box characters. There is a large variety of formatting options for borders, styles, cell alignment etc. Here's a simple example:

```python
from rich.console import Console
from rich.table import Column, Table

console = Console()

table = Table(show_header=True, header_style="bold magenta")
table.add_column("Date", style="dim", width=12)
table.add_column("Title")
table.add_column("Production Budget", justify="right")
table.add_column("Box Office", justify="right")
table.add_row(
"Dev 20, 2019", "Star Wars: The Rise of Skywalker", "$275,000,0000", "$375,126,118"
)
table.add_row(
"May 25, 2018",
"[red]Solo[/red]: A Star Wars Story",
"$275,000,0000",
"$393,151,347",
)
table.add_row(
"Dec 15, 2017",
"Star Wars Ep. VIII: The Last Jedi",
"$262,000,000",
"[bold]$1,332,539,889[/bold]",
)

console.print(table)
```

This produces the following output:

![table](./imgs/table.png)

Note that console markup is rendered in the same was as `print()` and `log()`. In fact, anything that is renderable by Rich may be included in the headers / rows (even other tables).

The `Table` class is smart enough to resize columns to fit the available width of the terminal, wrapping text as required. Here's the same example, with the terminal made smaller than the table above:

![table2](./imgs/table2.png)

## Markdown

Rich can render markdown and does a reasonable job of translating the formatting to the terminal.
Expand Down
Binary file added imgs/log.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added imgs/table.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added imgs/table2.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
7 changes: 6 additions & 1 deletion rich/console.py
Expand Up @@ -2,6 +2,7 @@


from collections import ChainMap
from collections.abc import Mapping, Sequence
from contextlib import contextmanager
from dataclasses import dataclass, replace
from enum import Enum
Expand Down Expand Up @@ -588,9 +589,13 @@ def check_text() -> None:
append_text(
highlight(repr(renderable)) if highlight else repr(renderable)
)
else:
elif isinstance(renderable, (Mapping, Sequence)):
check_text()
append(Pretty(renderable))
else:
append_text(
highlight(repr(renderable)) if highlight else repr(renderable)
)

check_text()
return renderables
Expand Down
3 changes: 3 additions & 0 deletions rich/default_styles.py
Expand Up @@ -56,6 +56,9 @@
"repr.url": Style(underline=True, color="default"),
"inspect.key": Style(italic=True),
"inspect.value": Style(),
"table.header": Style(bold=True),
"table.footer": Style(bold=True),
"table.cell": Style(),
}

MARKDOWN_STYLES = {
Expand Down
7 changes: 4 additions & 3 deletions rich/markup.py
Expand Up @@ -3,9 +3,10 @@
from collections import defaultdict
from operator import itemgetter
import re
from typing import Dict, Iterable, List, Optional, Tuple
from typing import Dict, Iterable, List, Optional, Tuple, Union

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


Expand Down Expand Up @@ -36,7 +37,7 @@ def _parse(markup: str) -> Iterable[Tuple[Optional[str], Optional[str]]]:
yield markup[position:], None


def render(markup: str) -> Text:
def render(markup: str, style: Union[str, Style] = "") -> Text:
"""Render console markup in to a Text instance.
Args:
Expand All @@ -48,7 +49,7 @@ def render(markup: str) -> Text:
Returns:
Text: A test instance.
"""
text = Text()
text = Text(style=style)
stylize = text.stylize

styles: Dict[str, List[int]] = defaultdict(list)
Expand Down
52 changes: 33 additions & 19 deletions rich/table.py
Expand Up @@ -9,6 +9,7 @@
Optional,
Sequence,
Tuple,
TypeVar,
TYPE_CHECKING,
Union,
)
Expand All @@ -34,14 +35,25 @@
from ._tools import iter_first_last, iter_first, iter_last, ratio_divide


T = TypeVar("T")


def _pick_first(values: Iterable[Optional[T]], final: T) -> T:
"""Pick first non-None value."""
for value in values:
if value is not None:
return value
return final


@dataclass
class Column:
"""Defines a column in a table."""

header: Union[str, ConsoleRenderable] = ""
footer: Union[str, ConsoleRenderable] = ""
header_style: Union[str, Style] = "none"
footer_style: Union[str, Style] = "none"
header_style: Union[str, Style] = "table.header"
footer_style: Union[str, Style] = "table.footer"
style: Union[str, Style] = "none"
justify: JustifyValues = "left"
width: Optional[int] = None
Expand All @@ -61,15 +73,15 @@ def flexible(self) -> bool:
@property
def header_renderable(self) -> ConsoleRenderable:
return (
Text(self.header, style=self.header_style)
Text.from_markup(self.header, style=self.header_style or "")
if isinstance(self.header, str)
else self.header
)

@property
def footer_renderable(self) -> ConsoleRenderable:
return (
Text(self.footer, style=self.footer_style)
Text.from_markup(self.footer, style=self.footer_style or "")
if isinstance(self.footer, str)
else self.footer
)
Expand All @@ -90,17 +102,18 @@ def __init__(
title: str = None,
footer: str = None,
width: int = None,
box: Optional[box.Box] = box.MINIMAL_DOUBLE_HEAD,
box: Optional[box.Box] = box.DOUBLE_EDGE,
padding: PaddingDimensions = (0, 1),
pad_edge: bool = True,
expand: bool = False,
show_header: bool = True,
show_footer: bool = False,
show_edge: bool = True,
style: Union[str, Style] = "none",
header_style: Union[str, Style] = "bold",
border_style: Union[str, Style] = "",
title_style: Union[str, Style] = "italic blue",
header_style: Union[str, Style] = None,
footer_style: Union[str, Style] = None,
border_style: Union[str, Style] = None,
title_style: Union[str, Style] = None,
) -> None:
self.columns = [
(Column(header) if isinstance(header, str) else header)
Expand All @@ -112,11 +125,12 @@ def __init__(
self._padding = Padding.unpack(padding)
self.pad_edge = pad_edge
self.expand = expand
self.show_header = show_header and headers
self.show_header = show_header
self.show_footer = show_footer
self.show_edge = show_edge
self.style = style
self.header_style = header_style
self.footer_style = footer_style
self.border_style = border_style
self.title_style = title_style
self._row_count = 0
Expand All @@ -134,9 +148,9 @@ def add_column(
self,
header: Union[str, ConsoleRenderable] = "",
footer: Union[str, ConsoleRenderable] = "",
header_style: Union[str, Style] = "none",
footer_style: Union[str, Style] = "none",
style: Union[str, Style] = "none",
header_style: Union[str, Style] = None,
footer_style: Union[str, Style] = None,
style: Union[str, Style] = None,
justify: JustifyValues = "left",
width: int = None,
ratio: int = None,
Expand All @@ -158,9 +172,9 @@ def add_column(
column = Column(
header=header,
footer=footer,
header_style=header_style,
footer_style=footer_style,
style=style,
header_style=_pick_first((header_style, self.header_style), "table.header"),
footer_style=_pick_first((footer_style, self.footer_style), "table.footer"),
style=_pick_first((style, self.style), "table.cell"),
justify=justify,
width=width,
ratio=ratio,
Expand Down Expand Up @@ -196,7 +210,7 @@ def add_cell(column: Column, renderable: ConsoleRenderable):
elif renderable is None:
add_cell(column, Text(""))
elif isinstance(renderable, str):
add_cell(column, Text(renderable))
add_cell(column, Text.from_markup(renderable))
else:
raise errors.NotRenderableError(
f"unable to render {renderable!r}; str or object with a __console__ method is required"
Expand All @@ -216,7 +230,7 @@ def __console__(self, console: Console, options: ConsoleOptions) -> RenderResult
widths = self._calculate_column_widths(max_width)
table_width = sum(widths) + len(self.columns) + (2 if self.box else 0)
if self.title:
title_text = Text(self.title, self.title_style)
title_text = Text.from_markup(self.title, style=self.title_style or "")
wrapped_title = title_text.wrap(table_width, "center")
yield wrapped_title
yield from self._render(console, options, widths)
Expand Down Expand Up @@ -325,9 +339,9 @@ def _measure_column(
def _render(
self, console: Console, options: ConsoleOptions, widths: List[int]
) -> RenderResult:
table_style = console.get_style(self.style)
table_style = console.get_style(self.style or "")

border_style = table_style + console.get_style(self.border_style)
border_style = table_style + console.get_style(self.border_style or "")
rows: Iterable[Tuple[_Cell, ...]] = zip(
*(
self._get_cells(column_index, column)
Expand Down
4 changes: 2 additions & 2 deletions rich/text.py
Expand Up @@ -150,7 +150,7 @@ def __add__(self, other: Any) -> Text:
return NotImplemented

@classmethod
def from_markup(cls, text: str) -> Text:
def from_markup(cls, text: str, style: Union[str, Style] = "") -> Text:
"""Create Text instance from markup.
Args:
Expand All @@ -161,7 +161,7 @@ def from_markup(cls, text: str) -> Text:
"""
from .markup import render

return render(text)
return render(text, style)

@property
def text(self) -> str:
Expand Down

0 comments on commit 9168dbd

Please sign in to comment.