status tests

This commit is contained in:
Will McGugan 2020-12-08 12:57:06 +00:00
parent 63a4ecab20
commit be47b3f899
11 changed files with 180 additions and 61 deletions

View file

@ -11,6 +11,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Added rich.live https://github.com/willmcgugan/rich/pull/382
- Added algin parameter to Rule and Console.rule
- Added rich.Status class and Console.status
- Added getitem to Text
- Added style parameter to Console.log
## [9.3.0] - 2020-12-1

View file

@ -83,6 +83,24 @@ The :meth:`~rich.console.Console.rule` method will draw a horizontal line with a
The rule method also accepts a ``style`` parameter to set the style of the line, and an ``align`` parameter to align the title ("left", "center", or "right").
Status
------
Rich can display a status message with a 'spinner' animation that won't interfere with regular console output. Run the following command for a demo of this feature::
python -m rich.status
To display a status message call :meth:`~rich.console.Console.status` with the status message (which may be a string, Text, or other renderable). The result is a context manager which starts and stop the status display around a block of code. Here's an example::
with console.status("Working...")
do_work()
You can change the spinner animation via the ``spinner`` parameter. Run the following command to see the available choices::
python -m rich.spinner
Low level output
----------------

View file

@ -26,6 +26,7 @@ Reference
reference/protocol.rst
reference/rule.rst
reference/segment.rst
reference/status.rst
reference/style.rst
reference/styled.rst
reference/syntax.rst

View file

@ -42,11 +42,13 @@ from .pretty import Pretty
from .scope import render_scope
from .segment import Segment
from .style import Style
from .styled import Styled
from .terminal_theme import DEFAULT_TERMINAL_THEME, TerminalTheme
from .text import Text, TextType
from .theme import Theme, ThemeStack
if TYPE_CHECKING:
from .status import Status
from ._windows import WindowsConsoleFeatures
WINDOWS = platform.system() == "Windows"
@ -758,6 +760,26 @@ class Console:
"""
self.control("\033[2J\033[H" if home else "\033[2J")
def status(
self,
status: RenderableType,
spinner: str = "dots",
spinner_style: str = "status.spinner",
speed: float = 1.0,
refresh_per_second: float = 12.5,
) -> "Status":
from .status import Status
status = Status(
status,
console=self,
spinner=spinner,
spinner_style=spinner_style,
speed=speed,
refresh_per_second=refresh_per_second,
)
return status
def show_cursor(self, show: bool = True) -> None:
"""Show or hide the cursor.
@ -1175,6 +1197,7 @@ class Console:
*objects: Any,
sep=" ",
end="\n",
style: Union[str, Style] = None,
justify: JustifyMethod = None,
emoji: bool = None,
markup: bool = None,
@ -1188,6 +1211,7 @@ class Console:
objects (positional args): Objects to log to the terminal.
sep (str, optional): String to write between print data. Defaults to " ".
end (str, optional): String to write at end of print data. Defaults to "\\n".
style (Union[str, Style], optional): A style to apply to output. Defaults to None.
justify (str, optional): One of "left", "right", "center", or "full". Defaults to ``None``.
overflow (str, optional): Overflow method: "crop", "fold", or "ellipsis". Defaults to None.
emoji (Optional[bool], optional): Enable emoji code, or ``None`` to use console default. Defaults to None.
@ -1210,6 +1234,8 @@ class Console:
markup=markup,
highlight=highlight,
)
if style is not None:
renderables = [Styled(renderable, style) for renderable in renderables]
caller = inspect.stack()[_stack_offset]
link_path = (

View file

@ -117,6 +117,7 @@ DEFAULT_STYLES: Dict[str, Style] = {
"progress.remaining": Style(color="cyan"),
"progress.data.speed": Style(color="red"),
"progress.spinner": Style(color="green"),
"status.spinner": Style(color="green"),
}
MARKDOWN_STYLES = {

View file

@ -1,4 +1,3 @@
import io
import sys
from abc import ABC, abstractmethod
from collections import deque
@ -192,7 +191,13 @@ class ProgressColumn(ABC):
class RenderableColumn(ProgressColumn):
def __init__(self, renderable: RenderableType = None):
"""A column to insert an arbitrary column.
Args:
renderable (RenderableType, optional): Any renderable. Defaults to empty string.
"""
def __init__(self, renderable: RenderableType = ""):
self.renderable = renderable
super().__init__()
@ -206,7 +211,7 @@ class SpinnerColumn(ProgressColumn):
Args:
spinner_name (str, optional): Name of spinner animation. Defaults to "dots".
style (StyleType, optional): Style of spinner. Defaults to "progress.spinner".
speed (float, optional): Speed faxtor of spinner. Defaults to 1.0.
speed (float, optional): Speed factor of spinner. Defaults to 1.0.
finished_text (TextType, optional): Text used when task is finished. Defaults to " ".
"""
@ -228,9 +233,16 @@ class SpinnerColumn(ProgressColumn):
def set_spinner(
self,
spinner_name: str,
spinner_style: Optional[StyleType] = None,
spinner_style: Optional[StyleType] = "progress.spinner",
speed: float = 1.0,
):
"""Set a new spinner.
Args:
spinner_name (str): Spinner name, see python -m rich.spinner.
spinner_style (Optional[StyleType], optional): Spinner style. Defaults to "progress.spinner".
speed (float, optional): Speed factor of spinner. Defaults to 1.0.
"""
self.spinner = Spinner(spinner_name, style=spinner_style, speed=speed)
def render(self, task: "Task") -> Text:

View file

@ -1,5 +1,5 @@
import typing
from typing import Optional
from typing import Optional, TYPE_CHECKING
from ._spinners import SPINNERS
from .console import Console
@ -7,7 +7,7 @@ from .measure import Measurement
from .style import StyleType
from .text import Text, TextType
if typing.TYPE_CHECKING:
if TYPE_CHECKING:
from .console import Console, ConsoleOptions, RenderResult

View file

@ -1,69 +1,97 @@
from typing import Optional
from .console import Console
from .progress import Progress, SpinnerColumn, RenderableColumn
from .console import Console, RenderableType
from .live import Live
from .spinner import Spinner
from .style import StyleType
from .text import TextType
from .table import Table
class Status:
"""Displays a status indicator with a 'spinner' animation.
Args:
status (RenderableType): A status renderable (str or Text typically).
console (Console, optional): Console instance to use, or None for global console. Defaults to None.
spinner (str, optional): Name of spinner animation (see python -m rich.spinner). Defaults to "dots".
spinner_style (StyleType, optional): Style of spinner. Defaults to "status.spinner".
speed (float, optional): Speed factor for spinner animation. Defaults to 1.0.
refresh_per_second (float, optional): Number of refreshes per second. Defaults to 12.5.
"""
def __init__(
self,
status: str,
status: RenderableType,
*,
console: Console = None,
spinner: str = "dots",
spinner_style: StyleType = "progress.spinner",
spinner_style: StyleType = "status.spinner",
speed: float = 1.0,
refresh_per_second: float = 12.5,
):
self.status = status
self.spinner = spinner
self.spinner_style = spinner_style
self.speed = speed
self._spinner_column = SpinnerColumn(spinner, style=spinner_style)
self._renderable_column = RenderableColumn(self.status)
self._progress = Progress(
self._spinner_column,
self._renderable_column,
refresh_per_second=12.5,
self._spinner = Spinner(spinner, style=spinner_style, speed=speed)
self._live = Live(
self.renderable,
console=console,
refresh_per_second=refresh_per_second,
transient=True,
)
self._task_id = self._progress.add_task(status)
self.update(
status=status, spinner=spinner, spinner_style=spinner_style, speed=speed
)
@property
def renderable(self) -> Table:
"""Get the renderable for the status (a table with spinner and status)."""
table = Table.grid(padding=1)
table.add_row(self._spinner, self.status)
return table
@property
def console(self) -> "Console":
return self._progress.console
"""Get the Console used by the Status objects."""
return self._live.console
def update(
self,
*,
status: Optional[str] = None,
status: Optional[RenderableType] = None,
spinner: Optional[str] = None,
spinner_style: Optional[StyleType] = None,
speed: Optional[float] = None,
):
"""Update status.
Args:
status (Optional[RenderableType], optional): New status renderable or None for no change. Defaults to None.
spinner (Optional[str], optional): New spinner or None for no change. Defaults to None.
spinner_style (Optional[StyleType], optional): New spinner style or None for no change. Defaults to None.
speed (Optional[float], optional): Speed factor for spinner animation or None for no change. Defaults to None.
"""
if status is not None:
self._renderable_column.renderable = status
self.status = status
if spinner is not None:
self.spinner = spinner
if spinner_style is not None:
self.spinner_style = spinner_style
if speed is not None:
self.speed = speed
self._spinner_column.spinner = Spinner(
self.spinner,
style=self.spinner_style or "progress.spinner",
speed=self.speed,
self._spinner = Spinner(
self.spinner, style=self.spinner_style, speed=self.speed
)
self._progress.refresh()
self._live.update(self.renderable, refresh=True)
def start(self) -> None:
self._progress.start()
"""Start the status animation."""
self._live.start()
def stop(self) -> None:
self._progress.stop()
"""Stop the spinner animation."""
self._live.stop()
def __enter__(self) -> "Status":
self.start()
@ -77,33 +105,23 @@ if __name__ == "__main__": # pragma: no cover
from time import sleep
with Status("[green]Working") as status:
from .console import Console
def example_text():
status.console.print(
"[dim]Don't pay this any attention, look at the spinner"
)
sleep(1)
status.console.print([1, 2, 3])
sleep(1)
status.console.print({"foo": "bar"})
sleep(1)
example_text()
console = Console()
with console.status("[magenta]Covid detector booting up") as status:
sleep(3)
console.log("Importing advanced AI")
sleep(3)
console.log("Advanced Covid AI Ready")
sleep(3)
status.update(status="[bold blue] Scanning for Covid", spinner="earth")
sleep(3)
console.log("Found 10,000,000,000 copies of Covid32.exe")
sleep(3)
status.update(
status="[cyan]Reticulating [i]splines[/i]",
spinner="dots2",
status="[bold red]Moving Covid32.exe to Trash",
spinner="bouncingBall",
spinner_style="yellow",
)
example_text()
status.update(status="[blink red]Scanning...", spinner="earth")
example_text()
status.update(
status="[bold red]Beautiful is better than ugly.\n[bold blue]Explicit is better than implicit.\n[bold yellow]Simple is better than complex.",
spinner="arrow2",
)
example_text()
status.update(
spinner="dots9", spinner_style="magenta", status="That's all folks!"
)
example_text()
sleep(5)
console.print("[bold green]Covid deleted successfully")

View file

@ -549,12 +549,6 @@ class Text(JupyterMixin):
"""
_Segment = Segment
if not self._spans:
yield _Segment(self.plain)
if self.end:
yield _Segment(end)
return
text = self.plain
enumerated_spans = list(enumerate(self._spans, 1))
get_style = partial(console.get_style, default=Style.null())
@ -962,7 +956,8 @@ class Text(JupyterMixin):
while True:
span, new_span = span.split(line_end)
new_lines[line_index]._spans.append(span.move(-line_start))
if span:
new_lines[line_index]._spans.append(span.move(-line_start))
if new_span is None:
break
span = new_span

View file

@ -13,6 +13,7 @@ from rich.console import CaptureError, Console, ConsoleOptions, render_group
from rich.measure import measure_renderables
from rich.pager import SystemPager
from rich.panel import Panel
from rich.status import Status
from rich.style import Style
@ -97,6 +98,21 @@ def test_print():
assert console.file.getvalue() == "foo\n"
def test_log():
console = Console(
file=io.StringIO(),
width=80,
color_system="truecolor",
log_time_format="TIME",
log_path=False,
)
console.log("foo", style="red")
expected = "\x1b[2;36mTIME\x1b[0m\x1b[2;36m \x1b[0m\x1b[31mfoo\x1b[0m\x1b[31m \x1b[0m\n"
result = console.file.getvalue()
print(repr(result))
assert result == expected
def test_print_empty():
console = Console(file=io.StringIO(), color_system="truecolor")
console.print()
@ -218,6 +234,12 @@ def test_input_password(monkeypatch, capsys):
assert user_input == "bar"
def test_status():
console = Console(file=io.StringIO(), force_terminal=True, width=20)
status = console.status("foo")
assert isinstance(status, Status)
def test_justify_none():
console = Console(file=io.StringIO(), force_terminal=True, width=20)
console.print("FOO", justify=None)

View file

@ -451,6 +451,14 @@ def test_render():
assert output == expected
def test_render_simple():
console = Console(width=80)
console.begin_capture()
console.print(Text("foo"))
result = console.end_capture()
assert result == "foo\n"
@pytest.mark.parametrize(
"print_text,result",
[
@ -636,3 +644,18 @@ foo = [
print(repr(result.plain))
expected = "for a in range(10):\n│ print(a)\n\nfoo = [\n│ 1,\n{\n│ │ 2\n│ }\n]\n"
assert result.plain == expected
def test_slice():
text = Text.from_markup("[red]foo [bold]bar[/red] baz[/bold]")
assert text[0] == Text("f", spans=[Span(0, 1, "red")])
assert text[4] == Text("b", spans=[Span(0, 1, "red"), Span(0, 1, "bold")])
assert text[:3] == Text("foo", spans=[Span(0, 3, "red")])
assert text[:4] == Text("foo ", spans=[Span(0, 4, "red")])
assert text[:5] == Text("foo b", spans=[Span(0, 5, "red"), Span(4, 5, "bold")])
assert text[4:] == Text("bar baz", spans=[Span(0, 3, "red"), Span(0, 7, "bold")])
with pytest.raises(TypeError):
text[::-1]