| 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 |
| ) |
| |
| @property |
| 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 |
| |
| @property |
| 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. |
| """ |
| 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)) |