diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index b19a15c1..c72dc5ae 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -6,19 +6,18 @@ labels: Needs triage assignees: "" --- -**Read the docs** -You might find a solution to your problem in the [docs](https://rich.readthedocs.io/en/latest/introduction.html) -- consider using the search function there. +You may find a solution to your problem in the [docs](https://rich.readthedocs.io/en/latest/introduction.html) or [issues](https://github.com/willmcgugan/rich/issues). **Describe the bug** -A clear and concise description of what the bug is. -**To Reproduce** -A minimal code example that reproduces the problem would be a big help if you can provide it. If the issue is visual in nature, consider posting a screenshot. +Edit this with a clear and concise description of what the bug. + +Provide a minimal code example that demonstrates the issue if you can. If the issue is visual in nature, consider posting a screenshot. **Platform** + What platform (Win/Linux/Mac) are you running on? What terminal software are you using? -**Diagnose** I may ask you to cut and paste the output of the following commands. It may save some time if you do it now. ``` @@ -26,7 +25,3 @@ python -m rich.diagnose python -m rich._windows pip freeze | grep rich ``` - -**Did I help?** - -If I was able to resolve your problem, consider [sponsoring](https://github.com/sponsors/willmcgugan) my work on Rich, or [buy me a coffee](https://ko-fi.com/willmcgugan) to say thanks. diff --git a/CHANGELOG.md b/CHANGELOG.md index 8c9ecf60..21484dac 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.15.1] - 2021-11-29 + +### Fixed + +- Reverted thread-safety fix for Live that introduced deadlock potential + ## [10.15.0] - 2021-11-28 ### Added diff --git a/rich/console.py b/rich/console.py index fc340bc2..b6a66106 100644 --- a/rich/console.py +++ b/rich/console.py @@ -1,7 +1,6 @@ import inspect import os import platform -import shutil import sys import threading from abc import ABC, abstractmethod @@ -13,6 +12,7 @@ from html import escape from inspect import isclass from itertools import islice from time import monotonic +from threading import RLock from types import FrameType, TracebackType, ModuleType from typing import ( IO, @@ -25,7 +25,6 @@ from typing import ( Mapping, NamedTuple, Optional, - Set, TextIO, Tuple, Type, @@ -836,14 +835,10 @@ class Console: 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: @@ -1512,9 +1507,8 @@ class Console: control_codes (str): Control codes, such as those that may move the cursor. """ if not self.is_dumb_terminal: - for _control in control: - self._buffer.append(_control.segment) - self._check_buffer() + with self: + self._buffer.extend(_control.segment for _control in control) def out( self, @@ -1596,7 +1590,7 @@ class Console: if overflow is None: overflow = "ignore" crop = False - + render_hooks = self._render_hooks[:] with self: renderables = self._collect_renderables( objects, @@ -1607,7 +1601,7 @@ class Console: markup=markup, highlight=highlight, ) - for hook in self._render_hooks: + for hook in render_hooks: renderables = hook.process_renderables(renderables) render_options = self.options.update( justify=justify, @@ -1864,6 +1858,8 @@ class Console: if not objects: objects = (NewLine(),) + render_hooks = self._render_hooks[:] + with self: renderables = self._collect_renderables( objects, @@ -1898,7 +1894,7 @@ class Console: link_path=link_path, ) ] - for hook in self._render_hooks: + for hook in render_hooks: renderables = hook.process_renderables(renderables) new_segments: List[Segment] = [] extend = new_segments.extend diff --git a/rich/live.py b/rich/live.py index c61f233c..59478dd9 100644 --- a/rich/live.py +++ b/rich/live.py @@ -128,38 +128,37 @@ class Live(JupyterMixin, RenderHook): with self._lock: if not self._started: return - self.console.clear_live() self._started = False - 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: - self.console.line() - self.console.show_cursor(True) - if self._alt_screen: - self.console.set_alt_screen(False) - - 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 + if self.auto_refresh and self._refresh_thread is not None: + self._refresh_thread.stop() + self._refresh_thread.join() + self._refresh_thread = None + # allow it to fully render on the last even if overflow + self.vertical_overflow = "visible" + with self.console: + try: + if not self._alt_screen and not self.console.is_jupyter: self.refresh() + finally: + self._disable_redirect_io() + self.console.pop_render_hook() + if not self._alt_screen and self.console.is_terminal: + self.console.line() + self.console.show_cursor(True) + if self._alt_screen: + self.console.set_alt_screen(False) + + 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() + self.console.clear_live() def __enter__(self) -> "Live": self.start(refresh=self._renderable is not None) diff --git a/rich/live_render.py b/rich/live_render.py index ca5ad23b..f6fa7b2d 100644 --- a/rich/live_render.py +++ b/rich/live_render.py @@ -84,16 +84,15 @@ class LiveRender: ) -> RenderResult: renderable = self.renderable - _Segment = Segment style = console.get_style(self.style) lines = console.render_lines(renderable, options, style=style, pad=False) - shape = _Segment.get_shape(lines) + shape = Segment.get_shape(lines) _, height = shape if height > options.size.height: if self.vertical_overflow == "crop": lines = lines[: options.size.height] - shape = _Segment.get_shape(lines) + shape = Segment.get_shape(lines) elif self.vertical_overflow == "ellipsis": lines = lines[: (options.size.height - 1)] overflow_text = Text( @@ -104,10 +103,11 @@ class LiveRender: style="live.ellipsis", ) lines.append(list(console.render(overflow_text))) - shape = _Segment.get_shape(lines) + shape = Segment.get_shape(lines) self._shape = shape + new_line = Segment.line() for last, line in loop_last(lines): yield from line if not last: - yield _Segment.line() + yield new_line