mirror of
https://github.com/Textualize/rich.git
synced 2025-08-04 18:18:22 +00:00
docs for spinner and status
This commit is contained in:
parent
be47b3f899
commit
df1dc1ddcb
10 changed files with 216 additions and 35 deletions
|
@ -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
|
||||
|
|
22
README.md
22
README.md
|
@ -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.
|
||||
|
||||

|
||||
|
||||
## 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:
|
||||
|
|
|
@ -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.
|
||||
|
||||
|
|
5
docs/source/reference/status.rst
Normal file
5
docs/source/reference/status.rst
Normal file
|
@ -0,0 +1,5 @@
|
|||
rich.status
|
||||
============
|
||||
|
||||
.. automodule:: rich.status
|
||||
:members:
|
14
examples/status.py
Normal file
14
examples/status.py
Normal 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
BIN
imgs/status.gif
Normal file
Binary file not shown.
After Width: | Height: | Size: 588 KiB |
37
progress.html
Normal file
37
progress.html
Normal 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>
|
103
rich/table.py
103
rich/table.py
|
@ -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
42
tests/test_spinner.py
Normal 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
21
tests/test_status.py
Normal 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)
|
Loading…
Add table
Add a link
Reference in a new issue