table styles, readme

This commit is contained in:
Will McGugan 2019-12-23 23:12:12 +00:00
parent 2ef55219fa
commit 9168dbdbec
9 changed files with 126 additions and 26 deletions

View file

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

BIN
imgs/log.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 449 KiB

BIN
imgs/table.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 363 KiB

BIN
imgs/table2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 365 KiB

View file

@ -2,6 +2,7 @@ from __future__ import annotations
from collections import ChainMap
from collections.abc import Mapping, Sequence
from contextlib import contextmanager
from dataclasses import dataclass, replace
from enum import Enum
@ -588,9 +589,13 @@ class Console:
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

View file

@ -56,6 +56,9 @@ DEFAULT_STYLES: Dict[str, Style] = {
"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 = {

View file

@ -3,9 +3,10 @@ from __future__ import annotations
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
@ -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:
@ -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)

View file

@ -9,6 +9,7 @@ from typing import (
Optional,
Sequence,
Tuple,
TypeVar,
TYPE_CHECKING,
Union,
)
@ -34,14 +35,25 @@ from ._render_width import RenderWidth
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
@ -61,7 +73,7 @@ class Column:
@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
)
@ -69,7 +81,7 @@ class Column:
@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
)
@ -90,7 +102,7 @@ class Table:
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,
@ -98,9 +110,10 @@ class Table:
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)
@ -112,11 +125,12 @@ class Table:
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
@ -134,9 +148,9 @@ class Table:
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,
@ -158,9 +172,9 @@ class Table:
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,
@ -196,7 +210,7 @@ class Table:
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"
@ -216,7 +230,7 @@ class Table:
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)
@ -325,9 +339,9 @@ class Table:
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)

View file

@ -150,7 +150,7 @@ class 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:
@ -161,7 +161,7 @@ class Text:
"""
from .markup import render
return render(text)
return render(text, style)
@property
def text(self) -> str: