docs for spinner and status

This commit is contained in:
Will McGugan 2020-12-10 17:07:11 +00:00
parent be47b3f899
commit df1dc1ddcb
10 changed files with 216 additions and 35 deletions

View file

@ -15,6 +15,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Added getitem to Text
- Added style parameter to Console.log
### Changed
- Table.add_row style argument now applies to entire line and not just cells
- Added end_section parameter to Table.add_row to force a line underneath row
## [9.3.0] - 2020-12-1
### Added

View file

@ -230,6 +230,28 @@ The columns may be configured to show any details you want. Built-in columns inc
To try this out yourself, see [examples/downloader.py](https://github.com/willmcgugan/rich/blob/master/examples/downloader.py) which can download multiple URLs simultaneously while displaying progress.
## Status
For situations where it is hard to calculate progress, you can use the status method which will display a 'spinner' animation with a status message that won't prevent you from writing to the terminal. Here's an example:
```python
from time import sleep
from rich.console import Console
console = Console()
tasks = [f"task {n}" for n in range(1, 11)]
with console.status("[bold green]Working on tasks...") as status:
while tasks:
task = tasks.pop(0)
sleep(1)
console.log(f"{task} complete")
```
This generates the following output in the terminal.
![status](https://github.com/willmcgugan/rich/raw/master/imgs/status.gif)
## Columns
Rich can render content in neat [columns](https://rich.readthedocs.io/en/latest/columns.html) with equal or optimal width. Here's a very basic clone of the (MacOS / Linux) `ls` command which displays a directory listing in columns:

View file

@ -114,6 +114,8 @@ The following column objects are available:
- :class:`~rich.progress.TotalFileSizeColumn` Displays total file size (assumes the steps are bytes).
- :class:`~rich.progress.DownloadColumn` Displays download progress (assumes the steps are bytes).
- :class:`~rich.progress.TransferSpeedColumn` Displays transfer speed (assumes the steps are bytes.
- :class:`~rich.progress.SpinnerColumn` Displays a "spinner" animation.
- :class:`~rich.progress.RenderableColumn` Displays an arbitrary Rich renderable in the column.
To implement your own columns, extend the :class:`~rich.progress.Progress` and use it as you would the other columns.

View file

@ -0,0 +1,5 @@
rich.status
============
.. automodule:: rich.status
:members:

14
examples/status.py Normal file
View file

@ -0,0 +1,14 @@
from time import sleep
from rich.console import Console
console = Console()
console.print()
tasks = [f"task {n}" for n in range(1, 11)]
with console.status("[bold green]Working on tasks...") as status:
while tasks:
task = tasks.pop(0)
sleep(1)
console.log(f"{task} complete")

BIN
imgs/status.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 588 KiB

37
progress.html Normal file
View file

@ -0,0 +1,37 @@
<!DOCTYPE html>
<head>
<meta charset="UTF-8">
<style>
.r1 {color: #7fbfbf}
.r2 {color: #7f7f7f}
.r3 {font-style: italic}
.r4 {color: #800080}
.r5 {color: #808000}
.r6 {font-weight: bold}
.r7 {color: #008000}
body {
color: #000000;
background-color: #ffffff;
}
</style>
</head>
<html>
<body>
<code>
<pre style="font-family:Menlo,'DejaVu Sans Mono',consolas,'Courier New',monospace"><span class="r1">[18:34:12] </span>Text may be printed while the progress bars are rendering. <a href="file:///Users/willmcgugan/projects/rich/rich/progress.py"><span class="r2">progress.py</span></a><span class="r2">:1073</span>
<span class="r1"> </span>╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ <a href="file:///Users/willmcgugan/projects/rich/rich/progress.py"><span class="r2">progress.py</span></a><span class="r2">:1073</span>
│ In fact, <span class="r3">any</span> renderable will work │
╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
<span class="r1">[18:34:13] </span>Such as <span class="r4">tables</span><span class="r5">...</span> <a href="file:///Users/willmcgugan/projects/rich/rich/progress.py"><span class="r2">progress.py</span></a><span class="r2">:1073</span>
<span class="r1">[18:34:14] </span>┏━━━━━┳━━━━━┳━━━━━┓ <a href="file:///Users/willmcgugan/projects/rich/rich/progress.py"><span class="r2">progress.py</span></a><span class="r2">:1073</span>
<span class="r6"> foo </span><span class="r6"> bar </span><span class="r6"> baz </span>
┡━━━━━╇━━━━━╇━━━━━┩
│ 1 │ 2 │ 3 │
└─────┴─────┴─────┘
<span class="r1"> </span>Pretty printed structures<span class="r5">...</span> <a href="file:///Users/willmcgugan/projects/rich/rich/progress.py"><span class="r2">progress.py</span></a><span class="r2">:1073</span>
<span class="r1"> </span><span class="r6">{</span><span class="r7">'type'</span>: <span class="r7">'example'</span>, <span class="r7">'text'</span>: <span class="r7">'Pretty printed'</span><span class="r6">}</span> <a href="file:///Users/willmcgugan/projects/rich/rich/progress.py"><span class="r2">progress.py</span></a><span class="r2">:1073</span>
</pre>
</code>
</body>
</html>

View file

@ -11,7 +11,6 @@ from .padding import Padding, PaddingDimensions
from .protocol import is_renderable
from .segment import Segment
from .style import Style, StyleType
from .styled import Styled
from .text import Text, TextType
if TYPE_CHECKING:
@ -81,6 +80,17 @@ class Column:
return self.ratio is not None
@dataclass
class Row:
"""Information regarding a row."""
style: Optional[StyleType] = None
"""Style to apply to row."""
end_section: bool = False
"""Indicated end of section, which will force a line beneath the row."""
class _Cell(NamedTuple):
"""A single cell in a table."""
@ -122,6 +132,7 @@ class Table(JupyterMixin):
"""
columns: List[Column]
rows: List[Row]
def __init__(
self,
@ -153,6 +164,7 @@ class Table(JupyterMixin):
) -> None:
self.columns: List[Column] = []
self.rows: List[Row] = []
append_column = self.columns.append
for index, header in enumerate(headers):
if isinstance(header, str):
@ -184,7 +196,6 @@ class Table(JupyterMixin):
self.caption_style = caption_style
self.title_justify = title_justify
self.caption_justify = caption_justify
self._row_count = 0
self.row_styles = list(row_styles or [])
@classmethod
@ -240,13 +251,17 @@ class Table(JupyterMixin):
@property
def row_count(self) -> int:
"""Get the current number of rows."""
return self._row_count
return len(self.rows)
def get_row_style(self, index: int) -> StyleType:
def get_row_style(self, console: "Console", index: int) -> StyleType:
"""Get the current row style."""
style = Style.null()
if self.row_styles:
return self.row_styles[index % len(self.row_styles)]
return Style.null()
style += console.get_style(self.row_styles[index % len(self.row_styles)])
row_style = self.rows[index].style
if row_style is not None:
style += console.get_style(row_style)
return style
def __rich_measure__(self, console: "Console", max_width: int) -> Measurement:
if self.width is not None:
@ -255,9 +270,7 @@ class Table(JupyterMixin):
return Measurement(0, 0)
extra_width = self._extra_width
max_width = sum(self._calculate_column_widths(console, max_width - extra_width))
_measure_column = self._measure_column
measurements = [
@ -342,7 +355,10 @@ class Table(JupyterMixin):
self.columns.append(column)
def add_row(
self, *renderables: Optional["RenderableType"], style: StyleType = None
self,
*renderables: Optional["RenderableType"],
style: StyleType = None,
end_section: bool = False,
) -> None:
"""Add a row of renderables.
@ -350,15 +366,14 @@ class Table(JupyterMixin):
*renderables (None or renderable): Each cell in a row must be a renderable object (including str),
or ``None`` for a blank cell.
style (StyleType, optional): An optional style to apply to the entire row. Defaults to None.
end_section (bool, optional): End a section and draw a line. Defaults to False.
Raises:
errors.NotRenderableError: If you add something that can't be rendered.
"""
def add_cell(column: Column, renderable: "RenderableType") -> None:
column._cells.append(
renderable if style is None else Styled(renderable, style)
)
column._cells.append(renderable)
cell_renderables: List[Optional["RenderableType"]] = list(renderables)
@ -371,7 +386,7 @@ class Table(JupyterMixin):
for index, renderable in enumerate(cell_renderables):
if index == len(columns):
column = Column(_index=index)
for _ in range(self._row_count):
for _ in self.rows:
add_cell(column, Text(""))
self.columns.append(column)
else:
@ -384,7 +399,7 @@ class Table(JupyterMixin):
raise errors.NotRenderableError(
f"unable to render {type(renderable).__name__}; a string or other renderable object is required"
)
self._row_count += 1
self.rows.append(Row(style=style, end_section=end_section))
def __rich_console__(
self, console: "Console", options: "ConsoleOptions"
@ -623,14 +638,11 @@ class Table(JupyterMixin):
table_style = console.get_style(self.style or "")
border_style = table_style + console.get_style(self.border_style or "")
rows: List[Tuple[_Cell, ...]] = list(
zip(
*(
self._get_cells(column_index, column)
for column_index, column in enumerate(self.columns)
)
)
_column_cells = (
self._get_cells(column_index, column)
for column_index, column in enumerate(self.columns)
)
row_cells: List[Tuple[_Cell, ...]] = list(zip(*_column_cells))
_box = (
self.box.substitute(
options, safe=pick_bool(self.safe_box, console.safe_box)
@ -677,18 +689,23 @@ class Table(JupyterMixin):
get_row_style = self.get_row_style
get_style = console.get_style
for index, (first, last, row) in enumerate(loop_first_last(rows)):
for index, (first, last, row_cell) in enumerate(loop_first_last(row_cells)):
header_row = first and show_header
footer_row = last and show_footer
row = (
self.rows[index - show_header]
if (not header_row and not footer_row)
else None
)
max_height = 1
cells: List[List[List[Segment]]] = []
if header_row or footer_row:
row_style = Style.null()
else:
row_style = get_style(
get_row_style(index - 1 if show_header else index)
get_row_style(console, index - 1 if show_header else index)
)
for width, cell, column in zip(widths, row, columns):
for width, cell, column in zip(widths, row_cell, columns):
render_options = options.update(
width=width,
justify=column.justify,
@ -703,7 +720,9 @@ class Table(JupyterMixin):
cells.append(lines)
cells[:] = [
_Segment.set_shape(_cell, width, max_height, style=table_style)
_Segment.set_shape(
_cell, width, max_height, style=table_style + row_style
)
for width, _cell in zip(widths, cells)
]
@ -743,18 +762,18 @@ class Table(JupyterMixin):
_box.get_row(widths, "head", edge=show_edge), border_style
)
yield new_line
if _box and (show_lines or leading):
end_section = row and row.end_section
if _box and (show_lines or leading or end_section):
if (
not last
and not (show_footer and index >= len(rows) - 2)
and not (show_footer and index >= len(row_cells) - 2)
and not (show_header and header_row)
):
if leading:
for _ in range(leading):
yield _Segment(
_box.get_row(widths, "mid", edge=show_edge),
border_style,
)
yield _Segment(
_box.get_row(widths, "mid", edge=show_edge) * leading,
border_style,
)
else:
yield _Segment(
_box.get_row(widths, "row", edge=show_edge), border_style
@ -781,10 +800,24 @@ if __name__ == "__main__": # pragma: no cover
table.add_column("Title", style="magenta")
table.add_column("Box Office", justify="right", style="green")
table.add_row("Dec 20, 2019", "Star Wars: The Rise of Skywalker", "$952,110,690")
table.add_row(
"Dec 20, 2019",
"Star Wars: The Rise of Skywalker",
"$952,110,690",
)
table.add_row("May 25, 2018", "Solo: A Star Wars Story", "$393,151,347")
table.add_row("Dec 15, 2017", "Star Wars Ep. V111: The Last Jedi", "$1,332,539,889")
table.add_row("Dec 16, 2016", "Rogue One: A Star Wars Story", "$1,332,439,889")
table.add_row(
"Dec 15, 2017",
"Star Wars Ep. V111: The Last Jedi",
"$1,332,539,889",
style="on black",
end_section=True,
)
table.add_row(
"Dec 16, 2016",
"Rogue One: A Star Wars Story",
"$1,332,439,889",
)
def header(text: str) -> None:
console.print()

42
tests/test_spinner.py Normal file
View file

@ -0,0 +1,42 @@
from time import time
import pytest
from rich.console import Console
from rich.measure import Measurement
from rich.spinner import Spinner
def test_spinner_create():
spinner = Spinner("dots")
assert spinner.time == 0.0
with pytest.raises(KeyError):
Spinner("foobar")
def test_spinner_render():
time = 0.0
def get_time():
nonlocal time
return time
console = Console(
width=80, color_system=None, force_terminal=True, get_time=get_time
)
console.begin_capture()
spinner = Spinner("dots", "Foo")
console.print(spinner)
time += 80 / 1000
console.print(spinner)
result = console.end_capture()
print(repr(result))
expected = "⠋ Foo\n⠙ Foo\n"
assert result == expected
def test_rich_measure():
console = Console(width=80, color_system=None, force_terminal=True)
spinner = Spinner("dots", "Foo")
min_width, max_width = Measurement.get(console, spinner, 80)
assert min_width == 3
assert max_width == 5

21
tests/test_status.py Normal file
View file

@ -0,0 +1,21 @@
from time import sleep
from rich.console import Console
from rich.status import Status
from rich.table import Table
def test_status():
console = Console(
color_system=None, width=80, legacy_windows=False, get_time=lambda: 0.0
)
status = Status("foo", console=console)
assert status.console == console
status.update(status="bar", spinner="dots2", spinner_style="red", speed=2.0)
assert isinstance(status.renderable, Table)
# TODO: Testing output is tricky with threads
with status:
sleep(0.2)