mirror of
https://github.com/Textualize/rich.git
synced 2025-08-04 18:18:22 +00:00
status tests
This commit is contained in:
parent
63a4ecab20
commit
be47b3f899
11 changed files with 180 additions and 61 deletions
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
----------------
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 = (
|
||||
|
|
|
@ -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 = {
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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
|
||||
|
||||
|
||||
|
|
114
rich/status.py
114
rich/status.py
|
@ -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")
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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]
|
Loading…
Add table
Add a link
Reference in a new issue