mirror of
https://github.com/Textualize/rich.git
synced 2025-08-04 18:18:22 +00:00
race condition and tests
This commit is contained in:
parent
53d9eeafe0
commit
07d51ffc1a
11 changed files with 76 additions and 23 deletions
|
@ -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
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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:
|
||||
|
|
31
rich/live.py
31
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
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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
|
||||
|
||||
|
||||
|
|
|
@ -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():
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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>"
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue