race condition and tests

This commit is contained in:
Will McGugan 2021-11-27 19:41:29 +00:00
parent 53d9eeafe0
commit 07d51ffc1a
11 changed files with 76 additions and 23 deletions

View file

@ -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

View file

@ -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:

View file

@ -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:

View file

@ -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

View file

@ -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:

View file

@ -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

View file

@ -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():

View file

@ -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)

View file

@ -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

View file

@ -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)

View file

@ -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"))) == "<Bar 'hello' 'hello' egg=1>"
assert (repr(Bar("hello", bar=3))) == "<Bar 'hello' 'hello' bar=3 egg=1>"