diff --git a/CHANGELOG.md b/CHANGELOG.md index e98bbe67..5e7073b4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,11 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## [2.0.2] - Unreleased +## [2.1.0] - Unreleased + +### Added + +- Added 'transient' option to Progress ### Changed @@ -470,4 +474,4 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added -- First official release, API still bto be stabilized +- First official release, API still to be stabilized diff --git a/docs/source/progress.rst b/docs/source/progress.rst index c0b4a1d1..fce137a1 100644 --- a/docs/source/progress.rst +++ b/docs/source/progress.rst @@ -44,13 +44,24 @@ Here's a simple example:: progress.update(task3, advance=0.9) time.sleep(0.02) -Starting Tasks +Transient progress +~~~~~~~~~~~~~~~~~~ + +Normally when you exit the progress context manager (or call :meth:`~rich.progress.Progress.stop`) the last refreshed display remains in the terminal with the cursor on the following line. You can also make the progress display disappear on exit by setting ``transient=False`` on the Progress constructor. Here's an example + + with Progress(transient=False) as progress: + task = progress.add_task("Working", total=100) + do_work(task) + +Transient progress displays are useful if you want more minimal output in the terminal when tasks are complete. + +Starting tasks ~~~~~~~~~~~~~~ When you add a task it is automatically *started* which means it will show a progress bar at 0% and the time remaining will be calculated from the current time. Occasionally this may not work well if there is a long delay before you can start updating progress, you may need to wait for a response from a server, or count files in a directory (for example) before you can begin tracking progress. In these cases you can call :meth:`~rich.progress.Progress.add_task` with ``start=False`` which will display a pulsing animation that lets the user know something is working. When you have the number of steps you can call :meth:`~rich.progress.Progress.start_task` which will display the progress bar at 0%, then :meth:`~rich.progress.Progress.update` as normal. -Updating Tasks +Updating tasks ~~~~~~~~~~~~~~ When you add a task you get back a `Task ID`. Use this ID to call :meth:`~rich.progress.Progress.update` whenever you have completed some work, or any information has changed. @@ -58,7 +69,9 @@ When you add a task you get back a `Task ID`. Use this ID to call :meth:`~rich.p Auto refresh ~~~~~~~~~~~~ -By default, the progress information will auto refresh at 10 times a second. Refreshing in a predictable rate can make numbers more readable if they are updating very quickly. Auto refresh can also prevent excessive rendering to the terminal. +By default, the progress information will refresh 10 times a second. Refreshing at a predictable rate can make numbers more readable if they are updating quickly. Auto refresh can also prevent excessive rendering to the terminal. + +You can set the refresh rate with the ``refresh_per_second`` argument on the :class:`~rich.progress.Progress` constructor. You could set this to something lower than 10 if you know your updates will not be that frequent. You can disable auto-refresh by setting ``auto_refresh=False`` on the constructor and call :meth:`~rich.progress.Progress.refresh` manually when there are updates to display. @@ -93,7 +106,6 @@ Print / log When a progress display is running, printing or logging anything directly to the console will break the visuals. To work around this, the Progress class provides :meth:`~rich.progress.Progress.print` and :meth:`~rich.progress.Progress.log` which work the same as their counterparts on :class:`~rich.console.Console` but will move the cursor and refresh automatically -- ensure that everything renders properly. - Extending ~~~~~~~~~ @@ -106,7 +118,6 @@ If the progress API doesn't offer exactly what you need in terms of a progress d def get_renderables(self): yield Panel(self.make_tasks_table(self.tasks)) - Example ------- diff --git a/pyproject.toml b/pyproject.toml index c6a390f9..f9ab0cb9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -2,7 +2,7 @@ name = "rich" homepage = "https://github.com/willmcgugan/rich" documentation = "https://rich.readthedocs.io/en/latest/" -version = "2.0.1" +version = "2.1.0" description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal" authors = ["Will McGugan "] license = "MIT" diff --git a/rich/constrain.py b/rich/constrain.py index c0a856b4..ec7828a0 100644 --- a/rich/constrain.py +++ b/rich/constrain.py @@ -1,10 +1,11 @@ from typing import Optional from .console import Console, ConsoleOptions, RenderableType, RenderResult +from .jupyter import JupyterMixin from .measure import Measurement -class Constrain: +class Constrain(JupyterMixin): """Constrain the width of a renderable to a given number of characters. Args: diff --git a/rich/live_render.py b/rich/live_render.py index eb029776..208d8acf 100644 --- a/rich/live_render.py +++ b/rich/live_render.py @@ -8,19 +8,31 @@ from ._loop import loop_last class LiveRender: + """Creates a renderable that may be updated. + + Args: + renderable (RenderableType): Any renderable object. + style (StyleType, optional): An optional style to apply to the renderable. Defaults to "". + """ + def __init__(self, renderable: RenderableType, style: StyleType = "") -> None: self.renderable = renderable self.style = style self._shape: Optional[Tuple[int, int]] = None def set_renderable(self, renderable: RenderableType) -> None: + """Set a new renderable. + + Args: + renderable (RenderableType): Any renderable object, including str. + """ self.renderable = renderable def position_cursor(self) -> Control: """Get control codes to move cursor to beggining of live render. Returns: - str: String containing control codes. + Control: A control instance that may be printed. """ if self._shape is not None: _, height = self._shape @@ -30,6 +42,17 @@ class LiveRender: return Control("\r\x1b[2K") return Control("") + def restore_cursor(self) -> Control: + """Get control codes to clear the render and restore the cursor to its previous position. + + Returns: + Control: A Control instance that may be printed. + """ + if self._shape is not None: + _, height = self._shape + return Control("\r" + "\x1b[1A\x1b[2K" * height) + return Control("") + def __rich_console__( self, console: Console, options: ConsoleOptions ) -> RenderResult: diff --git a/rich/markup.py b/rich/markup.py index 7543313f..6d67e234 100644 --- a/rich/markup.py +++ b/rich/markup.py @@ -50,7 +50,6 @@ def _parse(markup: str) -> Iterable[Tuple[int, Optional[str], Optional[Tag]]]: """ position = 0 - normalize = Style.normalize for match in re_tags.finditer(markup): escape_open, escape_close, tag_text = match.groups() start, end = match.span() diff --git a/rich/progress.py b/rich/progress.py index 15ae06f6..96f48037 100644 --- a/rich/progress.py +++ b/rich/progress.py @@ -50,6 +50,7 @@ def track( total: int = None, auto_refresh=True, console: Optional[Console] = None, + transient: bool = False, get_time: Callable[[], float] = None, ) -> Iterable[ProgressType]: """Track progress of processing a sequence. @@ -59,13 +60,19 @@ def track( description (str, optional): Description of task show next to progress bar. Defaults to "Working". total: (int, optional): Total number of steps. Default is len(sequence). auto_refresh (bool, optional): Automatic refresh, disable to force a refresh after each iteration. Default is True. + transient: (bool, optional): Clear the progress on exit. Defaults to False. console (Console, optional): Console to write to. Default creates internal Console instance. Returns: Iterable[ProgressType]: An iterable of the values in the sequence. """ - progress = Progress(auto_refresh=auto_refresh, console=console, get_time=get_time) + progress = Progress( + auto_refresh=auto_refresh, + console=console, + transient=transient, + get_time=get_time, + ) task_total = total if task_total is None: @@ -369,6 +376,8 @@ class Progress: auto_refresh (bool, optional): Enable auto refresh. If disabled, you will need to call `refresh()`. refresh_per_second (int, optional): Number of times per second to refresh the progress information. Defaults to 10. speed_estimate_period: (float, optional): Period (in seconds) used to calculate the speed estimate. Defaults to 30. + transient: (bool, optional): Clear the progress on exit. Defaults to False. + get_time: (Callable, optional): A callable that gets the current time, or None to use time.monotonic. Defaults to None. """ def __init__( @@ -378,6 +387,7 @@ class Progress: auto_refresh: bool = True, refresh_per_second: int = 10, speed_estimate_period: float = 30.0, + transient: bool = False, get_time: GetTimeCallable = None, ) -> None: assert refresh_per_second > 0, "refresh_per_second must be > 0" @@ -392,6 +402,7 @@ class Progress: self.auto_refresh = auto_refresh self.refresh_per_second = refresh_per_second self.speed_estimate_period = speed_estimate_period + self.transient = transient self.get_time = get_time or monotonic self._tasks: Dict[TaskID, Task] = {} self._live_render = LiveRender(self.get_renderable()) @@ -449,6 +460,8 @@ class Progress: if self._refresh_thread is not None: self._refresh_thread.join() self._refresh_thread = None + if self.transient: + self.console.control(self._live_render.restore_cursor()) def __enter__(self) -> "Progress": self.start() @@ -467,13 +480,13 @@ class Progress: """[summary] Args: - sequence (Sequence[ProgressType]): [description] + sequence (Sequence[ProgressType]): A sequence of values you want to iterate over and track progress. total: (int, optional): Total number of steps. Default is len(sequence). task_id: (TaskID): Task to track. Default is new task. description: (str, optional): Description of task, if new task is created. Returns: - Iterable[ProgressType]: [description] + Iterable[ProgressType]: An iterable of values taken from the provided sequence. """ if total is None: if isinstance(sequence, Sized): @@ -786,7 +799,7 @@ yield True, previous_value''', examples = cycle(progress_renderables) - with Progress() as progress: + with Progress(transient=True) as progress: task1 = progress.add_task(" [red]Downloading", total=1000) task2 = progress.add_task(" [green]Processing", total=1000)