Team Ai
Datasetpublic

codekingpro/portable-devtools

sourceHugging Faceupdated 5mo agoView on Hugging Face
1likes14kdownloads
live.py405 linesDownload Raw Back to rich
1from __future__ import annotations2 3import sys4from threading import Event, RLock, Thread5from types import TracebackType6from typing import IO, TYPE_CHECKING, Any, Callable, List, Optional, TextIO, Type, cast7 8from . import get_console9from .console import Console, ConsoleRenderable, Group, RenderableType, RenderHook10from .control import Control11from .file_proxy import FileProxy12from .jupyter import JupyterMixin13from .live_render import LiveRender, VerticalOverflowMethod14from .screen import Screen15from .text import Text16 17if TYPE_CHECKING:18    # Can be replaced with `from typing import Self` in Python 3.11+19    from typing_extensions import Self  # pragma: no cover20 21 22class _RefreshThread(Thread):23    """A thread that calls refresh() at regular intervals."""24 25    def __init__(self, live: "Live", refresh_per_second: float) -> None:26        self.live = live27        self.refresh_per_second = refresh_per_second28        self.done = Event()29        super().__init__(daemon=True)30 31    def stop(self) -> None:32        self.done.set()33 34    def run(self) -> None:35        while not self.done.wait(1 / self.refresh_per_second):36            with self.live._lock:37                if not self.done.is_set():38                    self.live.refresh()39 40 41class Live(JupyterMixin, RenderHook):42    """Renders an auto-updating live display of any given renderable.43 44    Args:45        renderable (RenderableType, optional): The renderable to live display. Defaults to displaying nothing.46        console (Console, optional): Optional Console instance. Defaults to an internal Console instance writing to stdout.47        screen (bool, optional): Enable alternate screen mode. Defaults to False.48        auto_refresh (bool, optional): Enable auto refresh. If disabled, you will need to call `refresh()` or `update()` with refresh flag. Defaults to True49        refresh_per_second (float, optional): Number of times per second to refresh the live display. Defaults to 4.50        transient (bool, optional): Clear the renderable on exit (has no effect when screen=True). Defaults to False.51        redirect_stdout (bool, optional): Enable redirection of stdout, so ``print`` may be used. Defaults to True.52        redirect_stderr (bool, optional): Enable redirection of stderr. Defaults to True.53        vertical_overflow (VerticalOverflowMethod, optional): How to handle renderable when it is too tall for the console. Defaults to "ellipsis".54        get_renderable (Callable[[], RenderableType], optional): Optional callable to get renderable. Defaults to None.55    """56 57    def __init__(58        self,59        renderable: Optional[RenderableType] = None,60        *,61        console: Optional[Console] = None,62        screen: bool = False,63        auto_refresh: bool = True,64        refresh_per_second: float = 4,65        transient: bool = False,66        redirect_stdout: bool = True,67        redirect_stderr: bool = True,68        vertical_overflow: VerticalOverflowMethod = "ellipsis",69        get_renderable: Optional[Callable[[], RenderableType]] = None,70    ) -> None:71        assert refresh_per_second > 0, "refresh_per_second must be > 0"72        self._renderable = renderable73        self.console = console if console is not None else get_console()74        self._screen = screen75        self._alt_screen = False76 77        self._redirect_stdout = redirect_stdout78        self._redirect_stderr = redirect_stderr79        self._restore_stdout: Optional[IO[str]] = None80        self._restore_stderr: Optional[IO[str]] = None81 82        self._lock = RLock()83        self.ipy_widget: Optional[Any] = None84        self.auto_refresh = auto_refresh85        self._started: bool = False86        self.transient = True if screen else transient87 88        self._refresh_thread: Optional[_RefreshThread] = None89        self.refresh_per_second = refresh_per_second90 91        self.vertical_overflow = vertical_overflow92        self._get_renderable = get_renderable93        self._live_render = LiveRender(94            self.get_renderable(), vertical_overflow=vertical_overflow95        )96        self._nested = False97 98    @property99    def is_started(self) -> bool:100        """Check if live display has been started."""101        return self._started102 103    def get_renderable(self) -> RenderableType:104        renderable = (105            self._get_renderable()106            if self._get_renderable is not None107            else self._renderable108        )109        return renderable or ""110 111    def start(self, refresh: bool = False) -> None:112        """Start live rendering display.113 114        Args:115            refresh (bool, optional): Also refresh. Defaults to False.116        """117        with self._lock:118            if self._started:119                return120            self._started = True121 122            if not self.console.set_live(self):123                self._nested = True124                return125 126            if self._screen:127                self._alt_screen = self.console.set_alt_screen(True)128            self.console.show_cursor(False)129            self._enable_redirect_io()130            self.console.push_render_hook(self)131            if refresh:132                try:133                    self.refresh()134                except Exception:135                    # If refresh fails, we want to stop the redirection of sys.stderr,136                    # so the error stacktrace is properly displayed in the terminal.137                    # (or, if the code that calls Rich captures the exception and wants to display something,138                    # let this be displayed in the terminal).139                    self.stop()140                    raise141            if self.auto_refresh:142                self._refresh_thread = _RefreshThread(self, self.refresh_per_second)143                self._refresh_thread.start()144 145    def stop(self) -> None:146        """Stop live rendering display."""147        with self._lock:148            if not self._started:149                return150            self._started = False151            self.console.clear_live()152            if self._nested:153                if not self.transient:154                    self.console.print(self.renderable)155                return156 157            if self.auto_refresh and self._refresh_thread is not None:158                self._refresh_thread.stop()159                self._refresh_thread = None160            # allow it to fully render on the last even if overflow161            self.vertical_overflow = "visible"162            with self.console:163                try:164                    if not self._alt_screen and not self.console.is_jupyter:165                        self.refresh()166                finally:167                    self._disable_redirect_io()168                    self.console.pop_render_hook()169                    if (170                        not self._alt_screen171                        and self.console.is_terminal172                        and self._live_render.last_render_height173                    ):174                        self.console.line()175                    self.console.show_cursor(True)176                    if self._alt_screen:177                        self.console.set_alt_screen(False)178                    if self.transient and not self._alt_screen:179                        self.console.control(self._live_render.restore_cursor())180                    if self.ipy_widget is not None and self.transient:181                        self.ipy_widget.close()  # pragma: no cover182 183    def __enter__(self) -> Self:184        self.start(refresh=self._renderable is not None)185        return self186 187    def __exit__(188        self,189        exc_type: Optional[Type[BaseException]],190        exc_val: Optional[BaseException],191        exc_tb: Optional[TracebackType],192    ) -> None:193        self.stop()194 195    def _enable_redirect_io(self) -> None:196        """Enable redirecting of stdout / stderr."""197        if self.console.is_terminal or self.console.is_jupyter:198            if self._redirect_stdout and not isinstance(sys.stdout, FileProxy):199                self._restore_stdout = sys.stdout200                sys.stdout = cast("TextIO", FileProxy(self.console, sys.stdout))201            if self._redirect_stderr and not isinstance(sys.stderr, FileProxy):202                self._restore_stderr = sys.stderr203                sys.stderr = cast("TextIO", FileProxy(self.console, sys.stderr))204 205    def _disable_redirect_io(self) -> None:206        """Disable redirecting of stdout / stderr."""207        if self._restore_stdout:208            sys.stdout = cast("TextIO", self._restore_stdout)209            self._restore_stdout = None210        if self._restore_stderr:211            sys.stderr = cast("TextIO", self._restore_stderr)212            self._restore_stderr = None213 214    @property215    def renderable(self) -> RenderableType:216        """Get the renderable that is being displayed217 218        Returns:219            RenderableType: Displayed renderable.220        """221        live_stack = self.console._live_stack222        renderable: RenderableType223        if live_stack and self is live_stack[0]:224            # The first Live instance will render everything in the Live stack225            renderable = Group(*[live.get_renderable() for live in live_stack])226        else:227            renderable = self.get_renderable()228        return Screen(renderable) if self._alt_screen else renderable229 230    def update(self, renderable: RenderableType, *, refresh: bool = False) -> None:231        """Update the renderable that is being displayed232 233        Args:234            renderable (RenderableType): New renderable to use.235            refresh (bool, optional): Refresh the display. Defaults to False.236        """237        if isinstance(renderable, str):238            renderable = self.console.render_str(renderable)239        with self._lock:240            self._renderable = renderable241            if refresh:242                self.refresh()243 244    def refresh(self) -> None:245        """Update the display of the Live Render."""246        with self._lock:247            self._live_render.set_renderable(self.renderable)248            if self._nested:249                if self.console._live_stack:250                    self.console._live_stack[0].refresh()251                return252 253            if self.console.is_jupyter:  # pragma: no cover254                try:255                    from IPython.display import display256                    from ipywidgets import Output257                except ImportError:258                    import warnings259 260                    warnings.warn('install "ipywidgets" for Jupyter support')261                else:262                    if self.ipy_widget is None:263                        self.ipy_widget = Output()264                        display(self.ipy_widget)265 266                    with self.ipy_widget:267                        self.ipy_widget.clear_output(wait=True)268                        self.console.print(self._live_render.renderable)269            elif self.console.is_terminal and not self.console.is_dumb_terminal:270                with self.console:271                    self.console.print(Control())272            elif (273                not self._started and not self.transient274            ):  # if it is finished allow files or dumb-terminals to see final result275                with self.console:276                    self.console.print(Control())277 278    def process_renderables(279        self, renderables: List[ConsoleRenderable]280    ) -> List[ConsoleRenderable]:281        """Process renderables to restore cursor and display progress."""282        self._live_render.vertical_overflow = self.vertical_overflow283        if self.console.is_interactive:284            # lock needs acquiring as user can modify live_render renderable at any time unlike in Progress.285            with self._lock:286                reset = (287                    Control.home()288                    if self._alt_screen289                    else self._live_render.position_cursor()290                )291                renderables = [reset, *renderables, self._live_render]292        elif (293            not self._started and not self.transient294        ):  # if it is finished render the final output for files or dumb_terminals295            renderables = [*renderables, self._live_render]296 297        return renderables298 299 300if __name__ == "__main__":  # pragma: no cover301    import random302    import time303    from itertools import cycle304    from typing import Dict, List, Tuple305 306    from .align import Align307    from .console import Console308    from .live import Live as Live309    from .panel import Panel310    from .rule import Rule311    from .syntax import Syntax312    from .table import Table313 314    console = Console()315 316    syntax = Syntax(317        '''def loop_last(values: Iterable[T]) -> Iterable[Tuple[bool, T]]:318    """Iterate and generate a tuple with a flag for last value."""319    iter_values = iter(values)320    try:321        previous_value = next(iter_values)322    except StopIteration:323        return324    for value in iter_values:325        yield False, previous_value326        previous_value = value327    yield True, previous_value''',328        "python",329        line_numbers=True,330    )331 332    table = Table("foo", "bar", "baz")333    table.add_row("1", "2", "3")334 335    progress_renderables = [336        "You can make the terminal shorter and taller to see the live table hide"337        "Text may be printed while the progress bars are rendering.",338        Panel("In fact, [i]any[/i] renderable will work"),339        "Such as [magenta]tables[/]...",340        table,341        "Pretty printed structures...",342        {"type": "example", "text": "Pretty printed"},343        "Syntax...",344        syntax,345        Rule("Give it a try!"),346    ]347 348    examples = cycle(progress_renderables)349 350    exchanges = [351        "SGD",352        "MYR",353        "EUR",354        "USD",355        "AUD",356        "JPY",357        "CNH",358        "HKD",359        "CAD",360        "INR",361        "DKK",362        "GBP",363        "RUB",364        "NZD",365        "MXN",366        "IDR",367        "TWD",368        "THB",369        "VND",370    ]371    with Live(console=console) as live_table:372        exchange_rate_dict: Dict[Tuple[str, str], float] = {}373 374        for index in range(100):375            select_exchange = exchanges[index % len(exchanges)]376 377            for exchange in exchanges:378                if exchange == select_exchange:379                    continue380                time.sleep(0.4)381                if random.randint(0, 10) < 1:382                    console.log(next(examples))383                exchange_rate_dict[(select_exchange, exchange)] = 200 / (384                    (random.random() * 320) + 1385                )386                if len(exchange_rate_dict) > len(exchanges) - 1:387                    exchange_rate_dict.pop(list(exchange_rate_dict.keys())[0])388                table = Table(title="Exchange Rates")389 390                table.add_column("Source Currency")391                table.add_column("Destination Currency")392                table.add_column("Exchange Rate")393 394                for (source, dest), exchange_rate in exchange_rate_dict.items():395                    table.add_row(396                        source,397                        dest,398                        Text(399                            f"{exchange_rate:.4f}",400                            style="red" if exchange_rate < 1.0 else "green",401                        ),402                    )403 404                live_table.update(Align.center(table))405 
codekingpro/portable-devtools · Team Ai