diff --git a/CHANGELOG.md b/CHANGELOG.md index e4601883..775a978a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Some optimizations for simple strings (with only single cell widths) +### Fixed + +- Fixed issue with progress bar not rendering markup https://github.com/willmcgugan/rich/issues/1721 +- Fixed race condition when exiting Live https://github.com/willmcgugan/rich/issues/1530 + ## [10.14.0] - 2021-11-16 ### Fixed diff --git a/rich/console.py b/rich/console.py index 901d19e7..fc340bc2 100644 --- a/rich/console.py +++ b/rich/console.py @@ -825,20 +825,25 @@ class Console: Args: hook (RenderHook): Render hook instance. """ - - self._render_hooks.append(hook) + with self._lock: + self._render_hooks.append(hook) def pop_render_hook(self) -> None: """Pop the last renderhook from the stack.""" - self._render_hooks.pop() + with self._lock: + self._render_hooks.pop() def __enter__(self) -> "Console": """Own context manager to enter buffer context.""" self._enter_buffer() + if self._live: + self._live._lock.acquire() return self def __exit__(self, exc_type: Any, exc_value: Any, traceback: Any) -> None: """Exit buffer context.""" + if self._live: + self._live._lock.release() self._exit_buffer() def begin_capture(self) -> None: diff --git a/rich/file_proxy.py b/rich/file_proxy.py index 99a6922c..3ec593a5 100644 --- a/rich/file_proxy.py +++ b/rich/file_proxy.py @@ -44,7 +44,7 @@ class FileProxy(io.TextIOBase): output = Text("\n").join( self.__ansi_decoder.decode_line(line) for line in lines ) - console.print(output, markup=False, emoji=False, highlight=False) + console.print(output) return len(text) def flush(self) -> None: diff --git a/rich/live.py b/rich/live.py index 8097f7d2..c61f233c 100644 --- a/rich/live.py +++ b/rich/live.py @@ -133,12 +133,16 @@ class Live(JupyterMixin, RenderHook): try: if self.auto_refresh and self._refresh_thread is not None: self._refresh_thread.stop() + self._refresh_thread.join() # allow it to fully render on the last even if overflow self.vertical_overflow = "visible" if not self._alt_screen and not self.console.is_jupyter: self.refresh() finally: + if self._refresh_thread is not None: + self._refresh_thread = None + self._disable_redirect_io() self.console.pop_render_hook() if not self._alt_screen and self.console.is_terminal: @@ -147,18 +151,15 @@ class Live(JupyterMixin, RenderHook): if self._alt_screen: self.console.set_alt_screen(False) - if self._refresh_thread is not None: - self._refresh_thread.join() - self._refresh_thread = None - if self.transient and not self._alt_screen: - self.console.control(self._live_render.restore_cursor()) - if self.ipy_widget is not None: # pragma: no cover - if self.transient: - self.ipy_widget.close() - else: - # jupyter last refresh must occur after console pop render hook - # i am not sure why this is needed - self.refresh() + if self.transient and not self._alt_screen: + self.console.control(self._live_render.restore_cursor()) + if self.ipy_widget is not None: # pragma: no cover + if self.transient: + self.ipy_widget.close() + else: + # jupyter last refresh must occur after console pop render hook + # i am not sure why this is needed + self.refresh() def __enter__(self) -> "Live": self.start(refresh=self._renderable is not None) @@ -255,11 +256,7 @@ class Live(JupyterMixin, RenderHook): if self._alt_screen else self._live_render.position_cursor() ) - renderables = [ - reset, - *renderables, - self._live_render, - ] + 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 diff --git a/rich/pager.py b/rich/pager.py index ea9bdf08..dbfb973e 100644 --- a/rich/pager.py +++ b/rich/pager.py @@ -17,7 +17,7 @@ class Pager(ABC): class SystemPager(Pager): """Uses the pager installed on the system.""" - def _pager(self, content: str) -> Any: + def _pager(self, content: str) -> Any: #  pragma: no cover return __import__("pydoc").pager(content) def show(self, content: str) -> None: diff --git a/rich/pretty.py b/rich/pretty.py index 7e916511..f7059112 100644 --- a/rich/pretty.py +++ b/rich/pretty.py @@ -78,7 +78,7 @@ def _is_dataclass_repr(obj: object) -> bool: # Catching all exceptions in case something is missing on a non CPython implementation try: return obj.__repr__.__code__.co_filename == dataclasses.__file__ - except Exception: + except Exception: # pragma: no coverage return False diff --git a/tests/test_cells.py b/tests/test_cells.py index c332f2a5..d64317f3 100644 --- a/tests/test_cells.py +++ b/tests/test_cells.py @@ -13,6 +13,7 @@ def test_set_cell_size(): assert cells.set_cell_size("😽😽", 3) == "😽 " assert cells.set_cell_size("😽😽", 2) == "😽" assert cells.set_cell_size("😽😽", 1) == " " + assert cells.set_cell_size("😽😽", 5) == "😽😽 " def test_set_cell_size_infinite(): diff --git a/tests/test_pretty.py b/tests/test_pretty.py index 9fd2bdd9..d2e82996 100644 --- a/tests/test_pretty.py +++ b/tests/test_pretty.py @@ -108,6 +108,19 @@ def test_broken_repr(): assert result == expected +def test_broken_getattr(): + class BrokenAttr: + def __getattr__(self, name): + 1 / 0 + + def __repr__(self): + return "BrokenAttr()" + + test = BrokenAttr() + result = pretty_repr(test) + assert result == "BrokenAttr()" + + def test_recursive(): test = [] test.append(test) diff --git a/tests/test_progress.py b/tests/test_progress.py index 303c2e08..2020f91f 100644 --- a/tests/test_progress.py +++ b/tests/test_progress.py @@ -68,6 +68,13 @@ def test_text_column(): assert text == Text("[b]bar") +def test_time_elapsed_column(): + column = TimeElapsedColumn() + task = Task(1, "test", 100, 20, _get_time=lambda: 1.0) + text = column.render(task) + assert str(text) == "-:--:--" + + def test_time_remaining_column(): class FakeTask(Task): time_remaining = 60 diff --git a/tests/test_protocol.py b/tests/test_protocol.py index 50d129a7..6337ef5d 100644 --- a/tests/test_protocol.py +++ b/tests/test_protocol.py @@ -18,6 +18,21 @@ def test_rich_cast(): assert console.file.getvalue() == "Foo\n" +class Fake: + def __getattr__(self, name): + return 12 + + def __repr__(self) -> str: + return "Fake()" + + +def test_rich_cast_fake(): + fake = Fake() + console = Console(file=io.StringIO()) + console.print(fake) + assert console.file.getvalue() == "Fake()\n" + + def test_rich_cast_container(): foo = Foo() console = Console(file=io.StringIO(), legacy_windows=False) diff --git a/tests/test_repr.py b/tests/test_repr.py index c4f8bd09..c47b76b1 100644 --- a/tests/test_repr.py +++ b/tests/test_repr.py @@ -59,6 +59,16 @@ def test_rich_repr() -> None: assert (repr(Foo("hello", bar=3))) == "Foo('hello', 'hello', bar=3, egg=1)" +def test_rich_repr_positional_only() -> None: + @rich.repr.auto + class PosOnly: + def __init__(self, foo, /): + self.foo = 1 + + p = PosOnly(1) + assert repr(p) == "PosOnly(1)" + + def test_rich_angular() -> None: assert (repr(Bar("hello"))) == "" assert (repr(Bar("hello", bar=3))) == ""