Skip to content

Commit

Permalink
Merge pull request #1146 from willmcgugan/progress-race-condition
Browse files Browse the repository at this point in the history
fix for race condition
  • Loading branch information
willmcgugan authored Mar 30, 2021
2 parents 0758f2e + bcb2923 commit 11c305e
Show file tree
Hide file tree
Showing 9 changed files with 70 additions and 56 deletions.
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 https://github.com/willmcgugan/rich/issues/1144

## [10.0.0] - 2021-03-27

### Changed
Expand Down
2 changes: 1 addition & 1 deletion docs/source/text.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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 <[email protected]>"]
license = "MIT"
Expand Down
53 changes: 27 additions & 26 deletions rich/console.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
3 changes: 2 additions & 1 deletion rich/control.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
44 changes: 24 additions & 20 deletions rich/live.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 = (
Expand Down Expand Up @@ -205,32 +209,32 @@ 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)

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]
Expand Down
7 changes: 5 additions & 2 deletions rich/live_render.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
from threading import RLock
from typing import Optional, Tuple

from typing_extensions import Literal

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

Expand Down Expand Up @@ -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
Expand Down
6 changes: 3 additions & 3 deletions rich/progress.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -872,7 +872,6 @@ def make_tasks_table(self, tasks: Iterable[Task]) -> Table:
Returns:
Table: A table instance.
"""

table_columns = (
(
Column(no_wrap=True)
Expand All @@ -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,
Expand Down
3 changes: 1 addition & 2 deletions tests/test_progress.py
Original file line number Diff line number Diff line change
Expand Up @@ -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


Expand Down

0 comments on commit 11c305e

Please sign in to comment.