spinner column

This commit is contained in:
Will McGugan 2020-12-05 15:56:08 +00:00
parent 279b62b932
commit f018047c25
8 changed files with 111 additions and 53 deletions

View file

@ -81,7 +81,7 @@ The :meth:`~rich.console.Console.rule` method will draw a horizontal line with a
<pre style="font-family:Menlo,\'DejaVu Sans Mono\',consolas,\'Courier New\',monospace"><span style="color: #00ff00">─────────────────────────────── </span><span style="color: #800000; font-weight: bold">Chapter 2</span><span style="color: #00ff00"> ───────────────────────────────</span></pre>
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").
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").
Low level output
----------------

View file

@ -20,7 +20,7 @@ class Align(JupyterMixin):
Args:
renderable (RenderableType): A console renderable.
align (AlignValues): One of "left", "center", or "right""
align (AlignMethod): One of "left", "center", or "right""
style (StyleType, optional): An optional style to apply to the renderable.
pad (bool, optional): Pad the right with spaces. Defaults to True.
width (int, optional): Restrict contents to given width, or None to use default width. Defaults to None.

View file

@ -3,7 +3,7 @@ from itertools import chain
from operator import itemgetter
from typing import Dict, Iterable, List, Optional, Tuple
from .align import Align, AlignValues
from .align import Align, AlignMethod
from .console import Console, ConsoleOptions, RenderableType, RenderResult
from .constrain import Constrain
from .measure import Measurement
@ -38,7 +38,7 @@ class Columns(JupyterMixin):
equal: bool = False,
column_first: bool = False,
right_to_left: bool = False,
align: AlignValues = None,
align: AlignMethod = None,
title: TextType = None,
) -> None:
self.renderables = list(renderables or [])

View file

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

View file

@ -2,7 +2,7 @@ from typing import Optional, TYPE_CHECKING
from .box import Box, ROUNDED
from .align import AlignValues
from .align import AlignMethod
from .jupyter import JupyterMixin
from .measure import Measurement, measure_renderables
from .padding import Padding, PaddingDimensions
@ -39,7 +39,7 @@ class Panel(JupyterMixin):
box: Box = ROUNDED,
*,
title: TextType = None,
title_align: AlignValues = "center",
title_align: AlignMethod = "center",
safe_box: Optional[bool] = None,
expand: bool = True,
style: StyleType = "none",
@ -65,7 +65,7 @@ class Panel(JupyterMixin):
box: Box = ROUNDED,
*,
title: TextType = None,
title_align: AlignValues = "center",
title_align: AlignMethod = "center",
safe_box: Optional[bool] = None,
style: StyleType = "none",
border_style: StyleType = "none",

View file

@ -25,7 +25,6 @@ from typing import (
)
from . import filesize, get_console
from .console import (
Console,
ConsoleRenderable,
@ -40,9 +39,10 @@ from .highlighter import Highlighter
from .jupyter import JupyterMixin
from .live_render import LiveRender
from .progress_bar import ProgressBar
from .spinner import Spinner
from .style import StyleType
from .table import Table
from .text import Text
from .text import Text, TextType
TaskID = NewType("TaskID", int)
@ -100,6 +100,7 @@ def track(
finished_style: StyleType = "bar.finished",
pulse_style: StyleType = "bar.pulse",
update_period: float = 0.1,
disable: bool = False,
) -> Iterable[ProgressType]:
"""Track progress by iterating over a sequence.
@ -116,6 +117,7 @@ def track(
finished_style (StyleType, optional): Style for a finished bar. Defaults to "bar.done".
pulse_style (StyleType, optional): Style for pulsing bars. Defaults to "bar.pulse".
update_period (float, optional): Minimum time (in seconds) between calls to update(). Defaults to 0.1.
disable (bool, optional): Disable display of progress.
Returns:
Iterable[ProgressType]: An iterable of the values in the sequence.
@ -143,6 +145,7 @@ def track(
transient=transient,
get_time=get_time,
refresh_per_second=refresh_per_second,
disable=disable,
)
with progress:
@ -188,6 +191,38 @@ class ProgressColumn(ABC):
"""Should return a renderable object."""
class SpinnerColumn(ProgressColumn):
"""A column with a 'spinner' animation.
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.
finished_text (TextType, optional): Text used when task is finished. Defaults to " ".
"""
def __init__(
self,
spinner_name: str = "dots",
style: StyleType = "progress.spinner",
speed: float = 1.0,
finished_text: TextType = " ",
):
self.spinner = Spinner(spinner_name, style=style, speed=speed)
self.finished_text = (
Text.from_markup(finished_text)
if isinstance(finished_text, str)
else finished_text
)
super().__init__()
def render(self, task: "Task") -> Text:
if task.finished:
return self.finished_text
text = self.spinner.render(task._get_time())
return text
class TextColumn(ProgressColumn):
"""A column containing text."""
@ -811,33 +846,30 @@ class Progress(JupyterMixin, RenderHook):
def refresh(self) -> None:
"""Refresh (render) the progress information."""
if self.console.is_jupyter: # pragma: no cover
try:
from IPython.display import display
from ipywidgets import Output
except ImportError:
import warnings
if not self.disable:
if self.console.is_jupyter: # pragma: no cover
try:
from IPython.display import display
from ipywidgets import Output
except ImportError:
import warnings
warnings.warn('install "ipywidgets" for Jupyter support')
else:
warnings.warn('install "ipywidgets" for Jupyter support')
else:
with self._lock:
if self.ipy_widget is None:
self.ipy_widget = Output()
display(self.ipy_widget)
with self.ipy_widget:
self.ipy_widget.clear_output(wait=True)
self.console.print(self.get_renderable())
elif self.console.is_terminal and not self.console.is_dumb_terminal:
with self._lock:
if self.ipy_widget is None:
self.ipy_widget = Output()
display(self.ipy_widget)
with self.ipy_widget:
self.ipy_widget.clear_output(wait=True)
self.console.print(self.get_renderable())
elif (
self.console.is_terminal
and not self.console.is_dumb_terminal
and not self.disable
):
with self._lock:
self._live_render.set_renderable(self.get_renderable())
with self.console:
self.console.print(Control(""))
self._live_render.set_renderable(self.get_renderable())
with self.console:
self.console.print(Control(""))
def get_renderable(self) -> RenderableType:
"""Get a renderable for the progress display."""
@ -990,7 +1022,15 @@ if __name__ == "__main__": # pragma: no coverage
console = Console(record=True)
try:
with Progress(console=console, transient=True) as progress:
with Progress(
SpinnerColumn(),
TextColumn("[progress.description]{task.description}"),
BarColumn(),
TextColumn("[progress.percentage]{task.percentage:>3.0f}%"),
TimeRemainingColumn(),
console=console,
transient=True,
) as progress:
task1 = progress.add_task("[red]Downloading", total=1000)
task2 = progress.add_task("[green]Processing", total=1000)

View file

@ -1,22 +1,31 @@
import typing
from typing import Optional
from ._spinners import SPINNERS
from .console import Console
from .measure import Measurement
from .style import StyleType
from .text import Text, TextType
from ._spinners import SPINNERS
if typing.TYPE_CHECKING:
from .console import Console, ConsoleOptions, RenderResult
class Spinner:
"""Base class for a spinner."""
def __init__(
self, name: str, text: TextType = "", style: StyleType = None, speed=1.0
self, name: str, text: TextType = "", *, style: StyleType = None, speed=1.0
) -> None:
"""A spinner animation.
Args:
name (str): Name of spinner (run python -m rich.spinner).
text (TextType, optional): Text to display at the right of the spinner. Defaults to "".
style (StyleType, optional): Style for sinner amimation. Defaults to None.
speed (float, optional): Speed factor for animation. Defaults to 1.0.
Raises:
KeyError: If name isn't one of the supported spinner animations.
"""
try:
spinner = SPINNERS[name]
except KeyError:
@ -43,20 +52,24 @@ class Spinner:
return Measurement.get(console, text, max_width)
def render(self, time: float) -> Text:
frame_no = int((time * self.speed) / (self.interval / 1000.0)) % len(
self.frames
)
frame = Text(self.frames[frame_no])
if self.style is not None:
frame.stylize(self.style)
"""Render the spinner for a given time.
Args:
time (float): Time in seconds.
Returns:
Text: A Text instance containing animation frame.
"""
frame_no = int((time * self.speed) / (self.interval / 1000.0))
frame = Text(self.frames[frame_no % len(self.frames)], style=self.style or "")
return Text.assemble(frame, " ", self.text) if self.text else frame
if __name__ == "__main__": # pragma: no cover
from .live import Live
from time import sleep
from .columns import Columns
from .live import Live
all_spinners = Columns(
[
@ -68,4 +81,3 @@ if __name__ == "__main__": # pragma: no cover
with Live(all_spinners, refresh_per_second=20) as live:
while True:
sleep(0.1)
live.refresh()

View file

@ -20,7 +20,7 @@ from typing import (
from ._loop import loop_last
from ._pick import pick_bool
from ._wrap import divide_line
from .align import AlignValues
from .align import AlignMethod
from .cells import cell_len, set_cell_size
from .containers import Lines
from .control import strip_control_codes
@ -548,10 +548,16 @@ class Text(JupyterMixin):
Iterable[Segment]: Result of render that may be written to the console.
"""
_Segment = Segment
if not self._spans:
yield _Segment(self.plain)
if self.end:
yield _Segment(end)
return
text = self.plain
null_style = Style.null()
enumerated_spans = list(enumerate(self._spans, 1))
get_style = partial(console.get_style, default=null_style)
get_style = partial(console.get_style, default=Style.null())
style_map = {index: get_style(span.style) for index, span in enumerated_spans}
style_map[0] = get_style(self.style)
@ -567,7 +573,6 @@ class Text(JupyterMixin):
stack_append = stack.append
stack_pop = stack.remove
_Segment = Segment
style_cache: Dict[Tuple[Style, ...], Style] = {}
style_cache_get = style_cache.get
combine = Style.combine
@ -752,11 +757,11 @@ class Text(JupyterMixin):
if count:
self.plain = f"{self.plain}{character * count}"
def align(self, align: AlignValues, width: int, character: str = " ") -> None:
def align(self, align: AlignMethod, width: int, character: str = " ") -> None:
"""Align text to a given width.
Args:
align (AlignValues): One of "left", "center", or "right".
align (AlignMethod): One of "left", "center", or "right".
width (int): Desired width.
character (str, optional): Character to pad with. Defaults to " ".
"""