Skip to content

Commit 2d0276c

Browse files
committed
progress improvements
1 parent 5bebc17 commit 2d0276c

14 files changed

+372
-79
lines changed

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1010
### Added
1111

1212
- Added -p switch to python -m rich.markdown to page output
13+
- Added Console.control to output control codes
14+
15+
### Changed
16+
17+
- Changed Console log_time_format to no longer require a space at the end
18+
- Added print and log to Progress to render terminal output when progress is active
1319

1420
## [1.1.1] - 2020-05-12
1521

docs/source/progress.rst

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,24 @@ The following column objects are available:
8181
- :class:`~rich.progress.DownloadColumn` Displays download progress (assumes the steps are bytes).
8282
- :class:`~rich.progress.TransferSpeedColumn` Displays transfer speed (assumes the steps are bytes.
8383

84+
Print / log
85+
~~~~~~~~~~~
86+
87+
When a progress display is running, printing or logging anything directly to the console will break the visuals. To work around this, the Progress class provides :meth:`~rich.progress.Progress.print` and :meth:`~rich.progress.Progress.log` which work the same as their counterparts on :class:`~rich.console.Console` but will move the cursor and refresh automatically -- ensure that everything renders properly.
88+
89+
90+
Extending
91+
~~~~~~~~~
92+
93+
If the progress API doesn't offer exactly what you need in terms of a progress display, you can extend the :class:`~rich.progress.Progress` class by overriding the :class:`~rich.progress.Progress.get_renderables` method. For example, the following class will render a :class:`~rich.panel.Panel` around the progress display::
94+
95+
from rich.panel import Panel
96+
from rich.progress import Progress
97+
98+
class MyProgress(Progress):
99+
def get_renderables(self):
100+
yield Panel(self.make_tasks_table(self.tasks))
101+
84102

85103
Example
86104
-------

rich/console.py

Lines changed: 43 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -129,10 +129,10 @@ def __console__(
129129

130130

131131
"""A type that may be rendered by Console."""
132-
RenderableType = Union[ConsoleRenderable, RichCast, Control, str]
132+
RenderableType = Union[ConsoleRenderable, RichCast, str]
133133

134134
"""The result of calling a __console__ method."""
135-
RenderResult = Iterable[Union[RenderableType, Segment, Control]]
135+
RenderResult = Iterable[Union[RenderableType, Segment]]
136136

137137

138138
_null_highlighter = NullHighlighter()
@@ -207,15 +207,14 @@ class ConsoleThreadLocals(threading.local):
207207

208208
buffer: List[Segment] = field(default_factory=list)
209209
buffer_index: int = 0
210-
control: List[str] = field(default_factory=list)
211210

212211

213212
def detect_legacy_windows() -> bool:
214213
"""Detect legacy Windows."""
215214
return "WINDIR" in os.environ and "WT_SESSION" not in os.environ
216215

217216

218-
if detect_legacy_windows():
217+
if detect_legacy_windows(): # pragma: no cover
219218
from colorama import init
220219

221220
init()
@@ -274,18 +273,16 @@ def __init__(
274273

275274
self._color_system: Optional[ColorSystem]
276275
self._force_terminal = force_terminal
277-
if self.legacy_windows:
278-
self.file = file or sys.stdout
279-
self._color_system = COLOR_SYSTEMS["windows"]
276+
self.file = file or sys.stdout
277+
278+
if color_system is None:
279+
self._color_system = None
280+
elif color_system == "auto":
281+
self._color_system = self._detect_color_system()
280282
else:
281-
self.file = file or sys.stdout
282-
if color_system is None:
283-
self._color_system = None
284-
elif color_system == "auto":
285-
self._color_system = self._detect_color_system()
286-
else:
287-
self._color_system = COLOR_SYSTEMS[color_system]
283+
self._color_system = COLOR_SYSTEMS[color_system]
288284

285+
self._lock = threading.RLock()
289286
self._log_render = LogRender(
290287
show_time=log_time, show_path=log_path, time_format=log_time_format
291288
)
@@ -312,15 +309,12 @@ def _buffer_index(self) -> int:
312309
def _buffer_index(self, value: int) -> None:
313310
self._thread_locals.buffer_index = value
314311

315-
@property
316-
def _control(self) -> List[str]:
317-
"""Get control codes buffer."""
318-
return self._thread_locals.control
319-
320312
def _detect_color_system(self) -> Optional[ColorSystem]:
321313
"""Detect color system from env vars."""
322314
if not self.is_terminal:
323315
return None
316+
if self.legacy_windows: # pragma: no cover
317+
return ColorSystem.WINDOWS
324318
color_term = os.environ.get("COLORTERM", "").strip().lower()
325319
return (
326320
ColorSystem.TRUECOLOR
@@ -402,10 +396,8 @@ def size(self) -> ConsoleDimensions:
402396
return ConsoleDimensions(self._width, self._height)
403397

404398
width, height = shutil.get_terminal_size()
405-
if self.legacy_windows:
406-
width -= 1
407399
return ConsoleDimensions(
408-
width if self._width is None else self._width,
400+
(width - self.legacy_windows) if self._width is None else self._width,
409401
height if self._height is None else self._height,
410402
)
411403

@@ -441,9 +433,7 @@ def show_cursor(self, show: bool = True) -> None:
441433
self.file.write("\033[?25h" if show else "\033[?25l")
442434

443435
def _render(
444-
self,
445-
renderable: Union[RenderableType, Control],
446-
options: Optional[ConsoleOptions],
436+
self, renderable: RenderableType, options: Optional[ConsoleOptions],
447437
) -> Iterable[Segment]:
448438
"""Render an object in to an iterable of `Segment` instances.
449439
@@ -459,9 +449,6 @@ def _render(
459449
Iterable[Segment]: An iterable of segments that may be rendered.
460450
"""
461451
render_iterable: RenderResult
462-
if isinstance(renderable, Control):
463-
self._control.append(renderable.codes)
464-
return
465452
render_options = options or self.options
466453
if isinstance(renderable, ConsoleRenderable):
467454
render_iterable = renderable.__console__(self, render_options)
@@ -487,7 +474,7 @@ def _render(
487474
yield from self.render(render_output, render_options)
488475

489476
def render(
490-
self, renderable: RenderableType, options: Optional[ConsoleOptions]
477+
self, renderable: RenderableType, options: Optional[ConsoleOptions] = None
491478
) -> Iterable[Segment]:
492479
"""Render an object in to an iterable of `Segment` instances.
493480
@@ -504,7 +491,7 @@ def render(
504491
Iterable[Segment]: An iterable of segments that may be rendered.
505492
"""
506493

507-
yield from self._render(renderable, options)
494+
yield from self._render(renderable, options or self.options)
508495

509496
def render_lines(
510497
self,
@@ -685,6 +672,16 @@ def rule(
685672
rule = Rule(title=title, character=character, style=style)
686673
self.print(rule)
687674

675+
def control(self, control_codes: Union["Control", str]) -> None:
676+
"""Insert non-printing control codes.
677+
678+
Args:
679+
control_codes (str): Control codes, such as those that may move the cursor.
680+
"""
681+
682+
self._buffer.append(Segment.control(str(control_codes)))
683+
self._check_buffer()
684+
688685
def print(
689686
self,
690687
*objects: Any,
@@ -802,10 +799,11 @@ def log(
802799

803800
def _check_buffer(self) -> None:
804801
"""Check if the buffer may be rendered."""
805-
if self._buffer_index == 0:
806-
text = self._render_buffer()
807-
self.file.write(text)
808-
self.file.flush()
802+
with self._lock:
803+
if self._buffer_index == 0:
804+
text = self._render_buffer()
805+
self.file.write(text)
806+
self.file.flush()
809807

810808
def _render_buffer(self) -> str:
811809
"""Render buffered output, and clear buffer."""
@@ -818,14 +816,13 @@ def _render_buffer(self) -> str:
818816
self._record_buffer.extend(buffer)
819817
del self._buffer[:]
820818
for line in Segment.split_and_crop_lines(buffer, self.width, pad=False):
821-
for text, style in line:
822-
if style:
819+
for text, style, is_control in line:
820+
if style and not is_control:
823821
append(style.render(text, color_system=color_system))
824822
else:
825823
append(text)
826824

827-
rendered = "".join(self._control) + "".join(output)
828-
del self._control[:]
825+
rendered = "".join(output)
829826
return rendered
830827

831828
def export_text(self, clear: bool = True, styles: bool = False) -> str:
@@ -848,10 +845,10 @@ def export_text(self, clear: bool = True, styles: bool = False) -> str:
848845
if styles:
849846
text = "".join(
850847
(style.render(text) if style else text)
851-
for text, style in self._record_buffer
848+
for text, style, _ in self._record_buffer
852849
)
853850
else:
854-
text = "".join(text for text, _ in self._record_buffer)
851+
text = "".join(text for text, _, _ in self._record_buffer)
855852
if clear:
856853
del self._record_buffer[:]
857854
return text
@@ -907,7 +904,9 @@ def escape(text: str) -> str:
907904

908905
with self._record_buffer_lock:
909906
if inline_styles:
910-
for text, style in Segment.simplify(self._record_buffer):
907+
for text, style, _ in Segment.filter_control(
908+
Segment.simplify(self._record_buffer)
909+
):
911910
text = escape(text)
912911
if style:
913912
rule = style.get_html_style(_theme)
@@ -916,7 +915,9 @@ def escape(text: str) -> str:
916915
append(text)
917916
else:
918917
styles: Dict[str, int] = {}
919-
for text, style in Segment.simplify(self._record_buffer):
918+
for text, style, _ in Segment.filter_control(
919+
Segment.simplify(self._record_buffer)
920+
):
920921
text = escape(text)
921922
if style:
922923
rule = style.get_html_style(_theme)

rich/control.py

Lines changed: 23 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
1-
from typing import NamedTuple
1+
from typing import NamedTuple, TYPE_CHECKING
22

3+
from .segment import Segment
4+
5+
if TYPE_CHECKING:
6+
from .console import Console, ConsoleOptions, RenderResult
37

48
STRIP_CONTROL_CODES = [
59
8, # Backspace
@@ -10,11 +14,25 @@
1014
_CONTROL_TRANSLATE = {_codepoint: None for _codepoint in STRIP_CONTROL_CODES}
1115

1216

13-
class Control(NamedTuple):
14-
"""Control codes that are not printable."""
17+
class Control:
18+
"""A renderable that inserts a control code (non printable but may move cursor).
19+
20+
Args:
21+
control_codes (str): A string containing control codes.
22+
"""
23+
24+
__slots__ = ["_control_codes"]
25+
26+
def __init__(self, control_codes: str) -> None:
27+
self._control_codes = Segment.control(control_codes)
28+
29+
def __str__(self) -> str:
30+
return self._control_codes.text
1531

16-
# May define pre and post control codes eventually
17-
codes: str
32+
def __console__(
33+
self, console: "Console", options: "ConsoleOptions"
34+
) -> "RenderResult":
35+
yield self._control_codes
1836

1937

2038
def strip_control_codes(text: str, _translate_table=_CONTROL_TRANSLATE) -> str:

rich/live_render.py

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from typing import Optional, Tuple
1+
from typing import Iterable, Optional, Tuple
22

33
from .console import Console, ConsoleOptions, RenderableType, RenderResult
44
from .control import Control
@@ -16,14 +16,21 @@ def __init__(self, renderable: RenderableType, style: StyleType = "") -> None:
1616
def set_renderable(self, renderable: RenderableType) -> None:
1717
self.renderable = renderable
1818

19-
def __console__(self, console: Console, options: ConsoleOptions) -> RenderResult:
19+
def position_cursor(self) -> Control:
20+
"""Get control codes to move cursor to beggining of live render.
2021
22+
Returns:
23+
str: String containing control codes.
24+
"""
2125
if self._shape is not None:
22-
width, height = self._shape
26+
_, height = self._shape
2327
if height > 1:
24-
yield Control(f"\r\x1b[{height - 1}A")
28+
return Control(f"\r\x1b[{height - 1}A\x1b[2K")
2529
else:
26-
yield Control("\r")
30+
return Control("\r")
31+
return Control("")
32+
33+
def __console__(self, console: Console, options: ConsoleOptions) -> RenderResult:
2734
style = console.get_style(self.style)
2835
lines = console.render_lines(self.renderable, options, style, pad=False)
2936

0 commit comments

Comments
 (0)