From 23497ed2303d52d72caf0d15749cbde8b89c9507 Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Tue, 30 Mar 2021 18:02:05 +0100 Subject: [PATCH 1/2] fix for race condition --- CHANGELOG.md | 6 +++++ docs/source/text.rst | 2 +- rich/console.py | 53 +++++++++++++++++++++--------------------- rich/control.py | 3 ++- rich/live.py | 44 +++++++++++++++++++---------------- rich/live_render.py | 7 ++++-- rich/progress.py | 6 ++--- tests/test_progress.py | 3 +-- 8 files changed, 69 insertions(+), 55 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 20ab61523..dfac8efb6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,12 @@ 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). +## [10.0.1] - 2021-03-30 + +### Fixed + +- Fixed race condition that duplicated lines in progress + ## [10.0.0] - 2021-03-27 ### Changed diff --git a/docs/source/text.rst b/docs/source/text.rst index 50113d372..b5f2eaf8f 100644 --- a/docs/source/text.rst +++ b/docs/source/text.rst @@ -44,7 +44,7 @@ The Text class has a number of parameters you can set on the constructor to modi - ``no_wrap`` prevents wrapping if the text is longer then the available width. - ``tab_size`` Sets the number of characters in a tab. -A Text instance may be used in place of a plain string virtually everywhere in the Rich API, which gives you a lot of control in how text renders within other Rich renderables. For instance, the following example right aligns text within a :class:`rich.panel.Panel`:: +A Text instance may be used in place of a plain string virtually everywhere in the Rich API, which gives you a lot of control in how text renders within other Rich renderables. For instance, the following example right aligns text within a :class:`~rich.panel.Panel`:: from rich import print from rich.panel import Panel diff --git a/rich/console.py b/rich/console.py index 5d434eaf9..c01aee3d6 100644 --- a/rich/console.py +++ b/rich/console.py @@ -1159,33 +1159,34 @@ def render_lines( Returns: List[List[Segment]]: A list of lines, where a line is a list of Segment objects. """ - render_options = options or self.options - _rendered = self.render(renderable, render_options) - if style: - _rendered = Segment.apply_style(_rendered, style) - lines = list( - islice( - Segment.split_and_crop_lines( - _rendered, - render_options.max_width, - include_new_lines=new_lines, - pad=pad, - ), - None, - render_options.height, + with self._lock: + render_options = options or self.options + _rendered = self.render(renderable, render_options) + if style: + _rendered = Segment.apply_style(_rendered, style) + lines = list( + islice( + Segment.split_and_crop_lines( + _rendered, + render_options.max_width, + include_new_lines=new_lines, + pad=pad, + ), + None, + render_options.height, + ) ) - ) - if render_options.height is not None: - extra_lines = render_options.height - len(lines) - if extra_lines > 0: - pad_line = [ - [Segment(" " * render_options.max_width, style), Segment("\n")] - if new_lines - else [Segment(" " * render_options.max_width, style)] - ] - lines.extend(pad_line * extra_lines) - - return lines + if render_options.height is not None: + extra_lines = render_options.height - len(lines) + if extra_lines > 0: + pad_line = [ + [Segment(" " * render_options.max_width, style), Segment("\n")] + if new_lines + else [Segment(" " * render_options.max_width, style)] + ] + lines.extend(pad_line * extra_lines) + + return lines def render_str( self, diff --git a/rich/control.py b/rich/control.py index 43fe02227..8a6236437 100644 --- a/rich/control.py +++ b/rich/control.py @@ -153,7 +153,8 @@ def __str__(self) -> str: def __rich_console__( self, console: "Console", options: "ConsoleOptions" ) -> "RenderResult": - yield self.segment + if self.segment.text: + yield self.segment def strip_control_codes(text: str, _translate_table=_CONTROL_TRANSLATE) -> str: diff --git a/rich/live.py b/rich/live.py index 0746d7a5b..641a47bf7 100644 --- a/rich/live.py +++ b/rich/live.py @@ -86,7 +86,11 @@ def __init__( self._live_render = LiveRender( self.get_renderable(), vertical_overflow=vertical_overflow ) - # cant store just clear_control as the live_render shape is lazily computed on render + + @property + def is_started(self) -> bool: + """Check if live display has been started.""" + return self._started def get_renderable(self) -> RenderableType: renderable = ( @@ -205,17 +209,17 @@ def update(self, renderable: RenderableType, *, refresh: bool = False) -> None: def refresh(self) -> None: """Update the display of the Live Render.""" - 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: - with self._lock: + 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) @@ -223,14 +227,14 @@ def refresh(self) -> None: 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._lock, 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()) + 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] diff --git a/rich/live_render.py b/rich/live_render.py index e6dbc7aef..4294b9923 100644 --- a/rich/live_render.py +++ b/rich/live_render.py @@ -1,3 +1,4 @@ +from threading import RLock from typing import Optional, Tuple from typing_extensions import Literal @@ -5,7 +6,7 @@ from ._loop import loop_last from .console import Console, ConsoleOptions, RenderableType, RenderResult from .control import Control -from .segment import ControlCode, ControlType, Segment +from .segment import ControlType, Segment from .style import StyleType from .text import Text @@ -77,9 +78,11 @@ def restore_cursor(self) -> Control: def __rich_console__( self, console: Console, options: ConsoleOptions ) -> RenderResult: + + renderable = self.renderable _Segment = Segment style = console.get_style(self.style) - lines = console.render_lines(self.renderable, options, style=style, pad=False) + lines = console.render_lines(renderable, options, style=style, pad=False) shape = _Segment.get_shape(lines) _, height = shape diff --git a/rich/progress.py b/rich/progress.py index e050fef4f..12545ebf1 100644 --- a/rich/progress.py +++ b/rich/progress.py @@ -850,7 +850,7 @@ def advance(self, task_id: TaskID, advance: float = 1) -> None: def refresh(self) -> None: """Refresh (render) the progress information.""" - if not self.disable: + if not self.disable and self.live.is_started: self.live.refresh() def get_renderable(self) -> RenderableType: @@ -872,7 +872,6 @@ def make_tasks_table(self, tasks: Iterable[Task]) -> Table: Returns: Table: A table instance. """ - table_columns = ( ( Column(no_wrap=True) @@ -899,7 +898,8 @@ def make_tasks_table(self, tasks: Iterable[Task]) -> Table: def __rich__(self) -> RenderableType: """Makes the Progress class itself renderable.""" - return self.get_renderable() + with self._lock: + return self.get_renderable() def add_task( self, diff --git a/tests/test_progress.py b/tests/test_progress.py index 046912052..e20c6ab63 100644 --- a/tests/test_progress.py +++ b/tests/test_progress.py @@ -310,8 +310,7 @@ def test_columns() -> None: result = replace_link_ids(console.file.getvalue()) print(repr(result)) - expected = "\x1b[?25ltest foo \x1b[38;5;237m━━━━━━━━━━━━━━━━━━━━━━━━━\x1b[0m \x1b[36m-:--:--\x1b[0m \x1b[33m0:00:37\x1b[0m \x1b[32m0 bytes\x1b[0m \x1b[32m10 bytes\x1b[0m \x1b[32m0/10 bytes\x1b[0m \x1b[31m?\x1b[0m\ntest bar \x1b[38;5;237m━━━━━━━━━━━━━━━━━━━━━━━━━\x1b[0m \x1b[36m-:--:--\x1b[0m \x1b[33m0:00:36\x1b[0m \x1b[32m0 bytes\x1b[0m \x1b[32m7 bytes \x1b[0m \x1b[32m0/7 bytes \x1b[0m \x1b[31m?\x1b[0m\r\x1b[2K\x1b[1A\x1b[2Kfoo\ntest foo \x1b[38;5;237m━━━━━━━━━━━━━━━━━━━━━━━━━\x1b[0m \x1b[36m-:--:--\x1b[0m \x1b[33m0:00:37\x1b[0m \x1b[32m0 bytes\x1b[0m \x1b[32m10 bytes\x1b[0m \x1b[32m0/10 bytes\x1b[0m \x1b[31m?\x1b[0m\ntest bar \x1b[38;5;237m━━━━━━━━━━━━━━━━━━━━━━━━━\x1b[0m \x1b[36m-:--:--\x1b[0m \x1b[33m0:00:36\x1b[0m \x1b[32m0 bytes\x1b[0m \x1b[32m7 bytes \x1b[0m \x1b[32m0/7 bytes \x1b[0m \x1b[31m?\x1b[0m\r\x1b[2K\x1b[1A\x1b[2K\x1b[2;36m[TIME]\x1b[0m\x1b[2;36m \x1b[0mhello \ntest foo \x1b[38;5;237m━━━━━━━━━━━━━━━━━━━━━━━━━\x1b[0m \x1b[36m-:--:--\x1b[0m \x1b[33m0:00:37\x1b[0m \x1b[32m0 bytes\x1b[0m \x1b[32m10 bytes\x1b[0m \x1b[32m0/10 bytes\x1b[0m \x1b[31m?\x1b[0m\ntest bar \x1b[38;5;237m━━━━━━━━━━━━━━━━━━━━━━━━━\x1b[0m \x1b[36m-:--:--\x1b[0m \x1b[33m0:00:36\x1b[0m \x1b[32m0 bytes\x1b[0m \x1b[32m7 bytes \x1b[0m \x1b[32m0/7 bytes \x1b[0m \x1b[31m?\x1b[0m\r\x1b[2K\x1b[1A\x1b[2Kworld\ntest foo \x1b[38;5;237m━━━━━━━━━━━━━━━━━━━━━━━━━\x1b[0m \x1b[36m-:--:--\x1b[0m \x1b[33m0:00:37\x1b[0m \x1b[32m0 bytes\x1b[0m \x1b[32m10 bytes\x1b[0m \x1b[32m0/10 bytes\x1b[0m \x1b[31m?\x1b[0m\ntest bar \x1b[38;5;237m━━━━━━━━━━━━━━━━━━━━━━━━━\x1b[0m \x1b[36m-:--:--\x1b[0m \x1b[33m0:00:36\x1b[0m \x1b[32m0 bytes\x1b[0m \x1b[32m7 bytes \x1b[0m \x1b[32m0/7 bytes \x1b[0m \x1b[31m?\x1b[0m\r\x1b[2K\x1b[1A\x1b[2Ktest foo \x1b[38;2;114;156;31m━━━━━━━━━━━━━━━\x1b[0m \x1b[36m0:00:00\x1b[0m \x1b[33m0:01:00\x1b[0m \x1b[32m12 bytes\x1b[0m \x1b[32m10 bytes\x1b[0m \x1b[32m12/10 bytes\x1b[0m \x1b[31m1 byte/s \x1b[0m\ntest bar \x1b[38;2;114;156;31m━━━━━━━━━━━━━━━\x1b[0m \x1b[36m0:00:00\x1b[0m \x1b[33m0:00:45\x1b[0m \x1b[32m16 bytes\x1b[0m \x1b[32m7 bytes \x1b[0m \x1b[32m16/7 bytes \x1b[0m \x1b[31m2 bytes/s\x1b[0m\r\x1b[2K\x1b[1A\x1b[2Ktest foo \x1b[38;2;114;156;31m━━━━━━━━━━━━━━━\x1b[0m \x1b[36m0:00:00\x1b[0m \x1b[33m0:01:00\x1b[0m \x1b[32m12 bytes\x1b[0m \x1b[32m10 bytes\x1b[0m \x1b[32m12/10 bytes\x1b[0m \x1b[31m1 byte/s \x1b[0m\ntest bar \x1b[38;2;114;156;31m━━━━━━━━━━━━━━━\x1b[0m \x1b[36m0:00:00\x1b[0m \x1b[33m0:00:45\x1b[0m \x1b[32m16 bytes\x1b[0m \x1b[32m7 bytes \x1b[0m \x1b[32m16/7 bytes \x1b[0m \x1b[31m2 bytes/s\x1b[0m\n\x1b[?25h\r\x1b[1A\x1b[2K\x1b[1A\x1b[2K" - + expected = "\x1b[?25ltest foo \x1b[38;5;237m━━━━━━━━━━━━━━━━━━━━━━━━━\x1b[0m \x1b[36m-:--:--\x1b[0m \x1b[33m0:00:07\x1b[0m \x1b[32m0 bytes\x1b[0m \x1b[32m10 bytes\x1b[0m \x1b[32m0/10 bytes\x1b[0m \x1b[31m?\x1b[0m\ntest bar \x1b[38;5;237m━━━━━━━━━━━━━━━━━━━━━━━━━\x1b[0m \x1b[36m-:--:--\x1b[0m \x1b[33m0:00:16\x1b[0m \x1b[32m0 bytes\x1b[0m \x1b[32m7 bytes \x1b[0m \x1b[32m0/7 bytes \x1b[0m \x1b[31m?\x1b[0m\r\x1b[2K\x1b[1A\x1b[2Kfoo\ntest foo \x1b[38;5;237m━━━━━━━━━━━━━━━━━━━━━━━━━\x1b[0m \x1b[36m-:--:--\x1b[0m \x1b[33m0:00:07\x1b[0m \x1b[32m0 bytes\x1b[0m \x1b[32m10 bytes\x1b[0m \x1b[32m0/10 bytes\x1b[0m \x1b[31m?\x1b[0m\ntest bar \x1b[38;5;237m━━━━━━━━━━━━━━━━━━━━━━━━━\x1b[0m \x1b[36m-:--:--\x1b[0m \x1b[33m0:00:16\x1b[0m \x1b[32m0 bytes\x1b[0m \x1b[32m7 bytes \x1b[0m \x1b[32m0/7 bytes \x1b[0m \x1b[31m?\x1b[0m\r\x1b[2K\x1b[1A\x1b[2K\x1b[2;36m[TIME]\x1b[0m\x1b[2;36m \x1b[0mhello \ntest foo \x1b[38;5;237m━━━━━━━━━━━━━━━━━━━━━━━━━\x1b[0m \x1b[36m-:--:--\x1b[0m \x1b[33m0:00:07\x1b[0m \x1b[32m0 bytes\x1b[0m \x1b[32m10 bytes\x1b[0m \x1b[32m0/10 bytes\x1b[0m \x1b[31m?\x1b[0m\ntest bar \x1b[38;5;237m━━━━━━━━━━━━━━━━━━━━━━━━━\x1b[0m \x1b[36m-:--:--\x1b[0m \x1b[33m0:00:16\x1b[0m \x1b[32m0 bytes\x1b[0m \x1b[32m7 bytes \x1b[0m \x1b[32m0/7 bytes \x1b[0m \x1b[31m?\x1b[0m\r\x1b[2K\x1b[1A\x1b[2Kworld\ntest foo \x1b[38;5;237m━━━━━━━━━━━━━━━━━━━━━━━━━\x1b[0m \x1b[36m-:--:--\x1b[0m \x1b[33m0:00:07\x1b[0m \x1b[32m0 bytes\x1b[0m \x1b[32m10 bytes\x1b[0m \x1b[32m0/10 bytes\x1b[0m \x1b[31m?\x1b[0m\ntest bar \x1b[38;5;237m━━━━━━━━━━━━━━━━━━━━━━━━━\x1b[0m \x1b[36m-:--:--\x1b[0m \x1b[33m0:00:16\x1b[0m \x1b[32m0 bytes\x1b[0m \x1b[32m7 bytes \x1b[0m \x1b[32m0/7 bytes \x1b[0m \x1b[31m?\x1b[0m\r\x1b[2K\x1b[1A\x1b[2Ktest foo \x1b[38;2;114;156;31m━━━━━━━━━━━━━━━\x1b[0m \x1b[36m0:00:00\x1b[0m \x1b[33m0:00:30\x1b[0m \x1b[32m12 bytes\x1b[0m \x1b[32m10 bytes\x1b[0m \x1b[32m12/10 bytes\x1b[0m \x1b[31m1 byte/s \x1b[0m\ntest bar \x1b[38;2;114;156;31m━━━━━━━━━━━━━━━\x1b[0m \x1b[36m0:00:00\x1b[0m \x1b[33m0:00:25\x1b[0m \x1b[32m16 bytes\x1b[0m \x1b[32m7 bytes \x1b[0m \x1b[32m16/7 bytes \x1b[0m \x1b[31m2 bytes/s\x1b[0m\r\x1b[2K\x1b[1A\x1b[2Ktest foo \x1b[38;2;114;156;31m━━━━━━━━━━━━━━━\x1b[0m \x1b[36m0:00:00\x1b[0m \x1b[33m0:00:30\x1b[0m \x1b[32m12 bytes\x1b[0m \x1b[32m10 bytes\x1b[0m \x1b[32m12/10 bytes\x1b[0m \x1b[31m1 byte/s \x1b[0m\ntest bar \x1b[38;2;114;156;31m━━━━━━━━━━━━━━━\x1b[0m \x1b[36m0:00:00\x1b[0m \x1b[33m0:00:25\x1b[0m \x1b[32m16 bytes\x1b[0m \x1b[32m7 bytes \x1b[0m \x1b[32m16/7 bytes \x1b[0m \x1b[31m2 bytes/s\x1b[0m\n\x1b[?25h\r\x1b[1A\x1b[2K\x1b[1A\x1b[2K" assert result == expected From bcb2923bf59ae0c6a3e6a2ae78b1d6f8d4b7bff0 Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Tue, 30 Mar 2021 18:05:25 +0100 Subject: [PATCH 2/2] version bump --- CHANGELOG.md | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index dfac8efb6..5c81dcb26 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,7 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed -- Fixed race condition that duplicated lines in progress +- Fixed race condition that duplicated lines in progress https://github.com/willmcgugan/rich/issues/1144 ## [10.0.0] - 2021-03-27 diff --git a/pyproject.toml b/pyproject.toml index 696288905..2eea08648 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 = "10.0.0" +version = "10.0.1" description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal" authors = ["Will McGugan "] license = "MIT"