Spaces:
Runtime error
Runtime error
import sys | |
from threading import Event, RLock, Thread | |
from types import TracebackType | |
from typing import IO, Any, Callable, List, Optional, TextIO, Type, cast | |
from . import get_console | |
from .console import Console, ConsoleRenderable, RenderableType, RenderHook | |
from .control import Control | |
from .file_proxy import FileProxy | |
from .jupyter import JupyterMixin | |
from .live_render import LiveRender, VerticalOverflowMethod | |
from .screen import Screen | |
from .text import Text | |
class _RefreshThread(Thread): | |
"""A thread that calls refresh() at regular intervals.""" | |
def __init__(self, live: "Live", refresh_per_second: float) -> None: | |
self.live = live | |
self.refresh_per_second = refresh_per_second | |
self.done = Event() | |
super().__init__(daemon=True) | |
def stop(self) -> None: | |
self.done.set() | |
def run(self) -> None: | |
while not self.done.wait(1 / self.refresh_per_second): | |
with self.live._lock: | |
if not self.done.is_set(): | |
self.live.refresh() | |
class Live(JupyterMixin, RenderHook): | |
"""Renders an auto-updating live display of any given renderable. | |
Args: | |
renderable (RenderableType, optional): The renderable to live display. Defaults to displaying nothing. | |
console (Console, optional): Optional Console instance. Default will an internal Console instance writing to stdout. | |
screen (bool, optional): Enable alternate screen mode. Defaults to False. | |
auto_refresh (bool, optional): Enable auto refresh. If disabled, you will need to call `refresh()` or `update()` with refresh flag. Defaults to True | |
refresh_per_second (float, optional): Number of times per second to refresh the live display. Defaults to 4. | |
transient (bool, optional): Clear the renderable on exit (has no effect when screen=True). Defaults to False. | |
redirect_stdout (bool, optional): Enable redirection of stdout, so ``print`` may be used. Defaults to True. | |
redirect_stderr (bool, optional): Enable redirection of stderr. Defaults to True. | |
vertical_overflow (VerticalOverflowMethod, optional): How to handle renderable when it is too tall for the console. Defaults to "ellipsis". | |
get_renderable (Callable[[], RenderableType], optional): Optional callable to get renderable. Defaults to None. | |
""" | |
def __init__( | |
self, | |
renderable: Optional[RenderableType] = None, | |
*, | |
console: Optional[Console] = None, | |
screen: bool = False, | |
auto_refresh: bool = True, | |
refresh_per_second: float = 4, | |
transient: bool = False, | |
redirect_stdout: bool = True, | |
redirect_stderr: bool = True, | |
vertical_overflow: VerticalOverflowMethod = "ellipsis", | |
get_renderable: Optional[Callable[[], RenderableType]] = None, | |
) -> None: | |
assert refresh_per_second > 0, "refresh_per_second must be > 0" | |
self._renderable = renderable | |
self.console = console if console is not None else get_console() | |
self._screen = screen | |
self._alt_screen = False | |
self._redirect_stdout = redirect_stdout | |
self._redirect_stderr = redirect_stderr | |
self._restore_stdout: Optional[IO[str]] = None | |
self._restore_stderr: Optional[IO[str]] = None | |
self._lock = RLock() | |
self.ipy_widget: Optional[Any] = None | |
self.auto_refresh = auto_refresh | |
self._started: bool = False | |
self.transient = True if screen else transient | |
self._refresh_thread: Optional[_RefreshThread] = None | |
self.refresh_per_second = refresh_per_second | |
self.vertical_overflow = vertical_overflow | |
self._get_renderable = get_renderable | |
self._live_render = LiveRender( | |
self.get_renderable(), vertical_overflow=vertical_overflow | |
) | |
def is_started(self) -> bool: | |
"""Check if live display has been started.""" | |
return self._started | |
def get_renderable(self) -> RenderableType: | |
renderable = ( | |
self._get_renderable() | |
if self._get_renderable is not None | |
else self._renderable | |
) | |
return renderable or "" | |
def start(self, refresh: bool = False) -> None: | |
"""Start live rendering display. | |
Args: | |
refresh (bool, optional): Also refresh. Defaults to False. | |
""" | |
with self._lock: | |
if self._started: | |
return | |
self.console.set_live(self) | |
self._started = True | |
if self._screen: | |
self._alt_screen = self.console.set_alt_screen(True) | |
self.console.show_cursor(False) | |
self._enable_redirect_io() | |
self.console.push_render_hook(self) | |
if refresh: | |
try: | |
self.refresh() | |
except Exception: | |
# If refresh fails, we want to stop the redirection of sys.stderr, | |
# so the error stacktrace is properly displayed in the terminal. | |
# (or, if the code that calls Rich captures the exception and wants to display something, | |
# let this be displayed in the terminal). | |
self.stop() | |
raise | |
if self.auto_refresh: | |
self._refresh_thread = _RefreshThread(self, self.refresh_per_second) | |
self._refresh_thread.start() | |
def stop(self) -> None: | |
"""Stop live rendering display.""" | |
with self._lock: | |
if not self._started: | |
return | |
self.console.clear_live() | |
self._started = False | |
if self.auto_refresh and self._refresh_thread is not None: | |
self._refresh_thread.stop() | |
self._refresh_thread = None | |
# allow it to fully render on the last even if overflow | |
self.vertical_overflow = "visible" | |
with self.console: | |
try: | |
if not self._alt_screen and not self.console.is_jupyter: | |
self.refresh() | |
finally: | |
self._disable_redirect_io() | |
self.console.pop_render_hook() | |
if not self._alt_screen and self.console.is_terminal: | |
self.console.line() | |
self.console.show_cursor(True) | |
if self._alt_screen: | |
self.console.set_alt_screen(False) | |
if self.transient and not self._alt_screen: | |
self.console.control(self._live_render.restore_cursor()) | |
if self.ipy_widget is not None and self.transient: | |
self.ipy_widget.close() # pragma: no cover | |
def __enter__(self) -> "Live": | |
self.start(refresh=self._renderable is not None) | |
return self | |
def __exit__( | |
self, | |
exc_type: Optional[Type[BaseException]], | |
exc_val: Optional[BaseException], | |
exc_tb: Optional[TracebackType], | |
) -> None: | |
self.stop() | |
def _enable_redirect_io(self) -> None: | |
"""Enable redirecting of stdout / stderr.""" | |
if self.console.is_terminal or self.console.is_jupyter: | |
if self._redirect_stdout and not isinstance(sys.stdout, FileProxy): | |
self._restore_stdout = sys.stdout | |
sys.stdout = cast("TextIO", FileProxy(self.console, sys.stdout)) | |
if self._redirect_stderr and not isinstance(sys.stderr, FileProxy): | |
self._restore_stderr = sys.stderr | |
sys.stderr = cast("TextIO", FileProxy(self.console, sys.stderr)) | |
def _disable_redirect_io(self) -> None: | |
"""Disable redirecting of stdout / stderr.""" | |
if self._restore_stdout: | |
sys.stdout = cast("TextIO", self._restore_stdout) | |
self._restore_stdout = None | |
if self._restore_stderr: | |
sys.stderr = cast("TextIO", self._restore_stderr) | |
self._restore_stderr = None | |
def renderable(self) -> RenderableType: | |
"""Get the renderable that is being displayed | |
Returns: | |
RenderableType: Displayed renderable. | |
""" | |
renderable = self.get_renderable() | |
return Screen(renderable) if self._alt_screen else renderable | |
def update(self, renderable: RenderableType, *, refresh: bool = False) -> None: | |
"""Update the renderable that is being displayed | |
Args: | |
renderable (RenderableType): New renderable to use. | |
refresh (bool, optional): Refresh the display. Defaults to False. | |
""" | |
if isinstance(renderable, str): | |
renderable = self.console.render_str(renderable) | |
with self._lock: | |
self._renderable = renderable | |
if refresh: | |
self.refresh() | |
def refresh(self) -> None: | |
"""Update the display of the Live Render.""" | |
with self._lock: | |
self._live_render.set_renderable(self.renderable) | |
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: | |
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._live_render.renderable) | |
elif self.console.is_terminal and not self.console.is_dumb_terminal: | |
with self.console: | |
self.console.print(Control()) | |
elif ( | |
not self._started and not self.transient | |
): # if it is finished allow files or dumb-terminals to see final result | |
with self.console: | |
self.console.print(Control()) | |
def process_renderables( | |
self, renderables: List[ConsoleRenderable] | |
) -> List[ConsoleRenderable]: | |
"""Process renderables to restore cursor and display progress.""" | |
self._live_render.vertical_overflow = self.vertical_overflow | |
if self.console.is_interactive: | |
# lock needs acquiring as user can modify live_render renderable at any time unlike in Progress. | |
with self._lock: | |
reset = ( | |
Control.home() | |
if self._alt_screen | |
else self._live_render.position_cursor() | |
) | |
renderables = [reset, *renderables, self._live_render] | |
elif ( | |
not self._started and not self.transient | |
): # if it is finished render the final output for files or dumb_terminals | |
renderables = [*renderables, self._live_render] | |
return renderables | |
if __name__ == "__main__": # pragma: no cover | |
import random | |
import time | |
from itertools import cycle | |
from typing import Dict, List, Tuple | |
from .align import Align | |
from .console import Console | |
from .live import Live as Live | |
from .panel import Panel | |
from .rule import Rule | |
from .syntax import Syntax | |
from .table import Table | |
console = Console() | |
syntax = Syntax( | |
'''def loop_last(values: Iterable[T]) -> Iterable[Tuple[bool, T]]: | |
"""Iterate and generate a tuple with a flag for last value.""" | |
iter_values = iter(values) | |
try: | |
previous_value = next(iter_values) | |
except StopIteration: | |
return | |
for value in iter_values: | |
yield False, previous_value | |
previous_value = value | |
yield True, previous_value''', | |
"python", | |
line_numbers=True, | |
) | |
table = Table("foo", "bar", "baz") | |
table.add_row("1", "2", "3") | |
progress_renderables = [ | |
"You can make the terminal shorter and taller to see the live table hide" | |
"Text may be printed while the progress bars are rendering.", | |
Panel("In fact, [i]any[/i] renderable will work"), | |
"Such as [magenta]tables[/]...", | |
table, | |
"Pretty printed structures...", | |
{"type": "example", "text": "Pretty printed"}, | |
"Syntax...", | |
syntax, | |
Rule("Give it a try!"), | |
] | |
examples = cycle(progress_renderables) | |
exchanges = [ | |
"SGD", | |
"MYR", | |
"EUR", | |
"USD", | |
"AUD", | |
"JPY", | |
"CNH", | |
"HKD", | |
"CAD", | |
"INR", | |
"DKK", | |
"GBP", | |
"RUB", | |
"NZD", | |
"MXN", | |
"IDR", | |
"TWD", | |
"THB", | |
"VND", | |
] | |
with Live(console=console) as live_table: | |
exchange_rate_dict: Dict[Tuple[str, str], float] = {} | |
for index in range(100): | |
select_exchange = exchanges[index % len(exchanges)] | |
for exchange in exchanges: | |
if exchange == select_exchange: | |
continue | |
time.sleep(0.4) | |
if random.randint(0, 10) < 1: | |
console.log(next(examples)) | |
exchange_rate_dict[(select_exchange, exchange)] = 200 / ( | |
(random.random() * 320) + 1 | |
) | |
if len(exchange_rate_dict) > len(exchanges) - 1: | |
exchange_rate_dict.pop(list(exchange_rate_dict.keys())[0]) | |
table = Table(title="Exchange Rates") | |
table.add_column("Source Currency") | |
table.add_column("Destination Currency") | |
table.add_column("Exchange Rate") | |
for ((source, dest), exchange_rate) in exchange_rate_dict.items(): | |
table.add_row( | |
source, | |
dest, | |
Text( | |
f"{exchange_rate:.4f}", | |
style="red" if exchange_rate < 1.0 else "green", | |
), | |
) | |
live_table.update(Align.center(table)) | |