Skip to content

Commit ef80460

Browse files
authored
Merge pull request #111 from willmcgugan/transparent-progress
add new render hooks for transparent progress
2 parents 8b5a8dd + 8a9ad81 commit ef80460

File tree

9 files changed

+271
-123
lines changed

9 files changed

+271
-123
lines changed

CHANGELOG.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,16 @@ All notable changes to this project will be documented in this file.
55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

8+
## [2.2.0] - 2020-06-14
9+
10+
### Added
11+
12+
- Added redirect_stdout and redirect_stderr to Progress
13+
14+
### Changed
15+
16+
- printing to console with an active Progress doesn't break visuals
17+
818
## [2.1.0] - 2020-06-11
919

1020
### Added

docs/source/progress.rst

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -118,7 +118,29 @@ To implement your own columns, extend the :class:`~rich.progress.Progress` and u
118118
Print / log
119119
~~~~~~~~~~~
120120

121-
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.
121+
The Progress class will create an internal Console object which you can access via ``progress.console``. If you print or log to this console, the output will be displayed *above* the progress display. Here's an example::
122+
123+
with Progress() as progress:
124+
task = progress.add_task(total=10)
125+
for job in range(10):
126+
progress.console.print("Working on job #{job}")
127+
run_job(job)
128+
progress.advance(task)
129+
130+
If you have another Console object you want to use, pass it in to the :class:`~rich.progress.Progress` constructor. Here's an example::
131+
132+
from my_project import my_console
133+
134+
with Progress(console=my_console) as progress:
135+
my_console.print("[bold blue]Starting work!")
136+
do_work(progress)
137+
138+
139+
Redirecting stdout / stderr
140+
~~~~~~~~~~~~~~~~~~~~~~~~~~~
141+
142+
To avoid breaking the progress display visuals, Rich will redirect ``stdout`` and ``stdout`` so that you can use the builtin ``print`` statement. This feature is enabled by default, but you can disable by setting ``redirect_stdout`` or ``redirect_stderr`` to ``False``
143+
122144

123145
Customizing
124146
~~~~~~~~~~~

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
name = "rich"
33
homepage = "https://github.com/willmcgugan/rich"
44
documentation = "https://rich.readthedocs.io/en/latest/"
5-
version = "2.1.0"
5+
version = "2.2.0"
66
description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal"
77
authors = ["Will McGugan <willmcgugan@gmail.com>"]
88
license = "MIT"

rich/console.py

Lines changed: 68 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -235,6 +235,25 @@ class ConsoleThreadLocals(threading.local):
235235
buffer_index: int = 0
236236

237237

238+
class RenderHook:
239+
"""Provides hooks in to the render process."""
240+
241+
def process_renderables(
242+
self, renderables: List[ConsoleRenderable]
243+
) -> List[ConsoleRenderable]:
244+
"""Called with a list of objects to render.
245+
246+
This method can return a new list of renderables, or modify and return the same list.
247+
248+
Args:
249+
renderables (List[ConsoleRenderable]): A number of renderable objects.
250+
251+
Returns:
252+
List[ConsoleRenderable]: A replacement list of renderables.
253+
"""
254+
return renderables
255+
256+
238257
def detect_legacy_windows() -> bool:
239258
"""Detect legacy Windows."""
240259
return "WINDIR" in os.environ and "WT_SESSION" not in os.environ
@@ -325,6 +344,7 @@ def __init__(
325344
self._record_buffer_lock = threading.RLock()
326345
self._thread_locals = ConsoleThreadLocals()
327346
self._record_buffer: List[Segment] = []
347+
self._render_hooks: List[RenderHook] = []
328348

329349
def __repr__(self) -> str:
330350
return f"<console width={self.width} {str(self._color_system)}>"
@@ -368,6 +388,19 @@ def _exit_buffer(self) -> None:
368388
self._buffer_index -= 1
369389
self._check_buffer()
370390

391+
def push_render_hook(self, hook: RenderHook) -> None:
392+
"""Add a new render hook to the stack.
393+
394+
Args:
395+
hook (RenderHook): Render hook instance.
396+
"""
397+
398+
self._render_hooks.append(hook)
399+
400+
def pop_render_hook(self) -> None:
401+
"""Pop the last renderhook from the stack."""
402+
self._render_hooks.pop()
403+
371404
def __enter__(self) -> "Console":
372405
"""Own context manager to enter buffer context."""
373406
self._enter_buffer()
@@ -754,7 +787,7 @@ def print(
754787
end (str, optional): String to write at end of print data. Defaults to "\n".
755788
style (Union[str, Style], optional): A style to apply to output. Defaults to None.
756789
justify (str, optional): Justify method: "default", "left", "right", "center", or "full". Defaults to ``None``.
757-
overflow (str, optional): Overflow method: "crop", "fold", or "ellipisis". Defaults to None.
790+
overflow (str, optional): Overflow method: "crop", "fold", or "ellipsis". Defaults to None.
758791
no_wrap (Optional[bool], optional): Disable word wrapping. Defaults to None.
759792
emoji (Optional[bool], optional): Enable emoji code, or ``None`` to use console default. Defaults to ``None``.
760793
markup (Optional[bool], optional): Enable markup, or ``None`` to use console default. Defaults to ``None``.
@@ -775,6 +808,8 @@ def print(
775808
markup=markup,
776809
highlight=highlight,
777810
)
811+
for hook in self._render_hooks:
812+
renderables = hook.process_renderables(renderables)
778813
render_options = self.options.update(
779814
justify=justify, overflow=overflow, width=width, no_wrap=no_wrap
780815
)
@@ -843,35 +878,39 @@ def log(
843878
if not objects:
844879
self.line()
845880
return
846-
renderables = self._collect_renderables(
847-
objects,
848-
sep,
849-
end,
850-
justify=justify,
851-
emoji=emoji,
852-
markup=markup,
853-
highlight=highlight,
854-
)
855-
856-
caller = inspect.stack()[_stack_offset]
857-
path = caller.filename.rpartition(os.sep)[-1]
858-
line_no = caller.lineno
859-
if log_locals:
860-
locals_map = {
861-
key: value
862-
for key, value in caller.frame.f_locals.items()
863-
if not key.startswith("__")
864-
}
865-
renderables.append(tabulate_mapping(locals_map, title="Locals"))
866-
867881
with self:
868-
self._buffer.extend(
869-
self.render(
870-
self._log_render(self, renderables, path=path, line_no=line_no),
871-
self.options,
872-
)
882+
renderables = self._collect_renderables(
883+
objects,
884+
sep,
885+
end,
886+
justify=justify,
887+
emoji=emoji,
888+
markup=markup,
889+
highlight=highlight,
873890
)
874891

892+
caller = inspect.stack()[_stack_offset]
893+
path = caller.filename.rpartition(os.sep)[-1]
894+
line_no = caller.lineno
895+
if log_locals:
896+
locals_map = {
897+
key: value
898+
for key, value in caller.frame.f_locals.items()
899+
if not key.startswith("__")
900+
}
901+
renderables.append(tabulate_mapping(locals_map, title="Locals"))
902+
903+
renderables = [
904+
self._log_render(self, renderables, path=path, line_no=line_no)
905+
]
906+
for hook in self._render_hooks:
907+
renderables = hook.process_renderables(renderables)
908+
extend = self._buffer.extend
909+
render = self.render
910+
render_options = self.options
911+
for renderable in renderables:
912+
extend(render(renderable, render_options))
913+
875914
def _check_buffer(self) -> None:
876915
"""Check if the buffer may be rendered."""
877916
with self._lock:
@@ -883,7 +922,8 @@ def _check_buffer(self) -> None:
883922
del self._buffer[:]
884923
else:
885924
text = self._render_buffer()
886-
self.file.write(text)
925+
if text:
926+
self.file.write(text)
887927
self.file.flush()
888928

889929
def _render_buffer(self) -> str:

rich/live_render.py

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -36,10 +36,7 @@ def position_cursor(self) -> Control:
3636
"""
3737
if self._shape is not None:
3838
_, height = self._shape
39-
if height > 1:
40-
return Control(f"\r\x1b[{height - 1}A\x1b[2K")
41-
else:
42-
return Control("\r\x1b[2K")
39+
return Control("\r\x1b[2K" + "\x1b[1A\x1b[2K" * (height - 1))
4340
return Control("")
4441

4542
def restore_cursor(self) -> Control:

rich/logging.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
from logging import Handler, LogRecord
44
from pathlib import Path
55

6+
from . import get_console
67
from rich._log_render import LogRender
78
from rich.console import Console
89
from rich.highlighter import ReprHighlighter
@@ -25,7 +26,7 @@ class RichHandler(Handler):
2526

2627
def __init__(self, level: int = logging.NOTSET, console: Console = None) -> None:
2728
super().__init__(level=level)
28-
self.console = Console() if console is None else console
29+
self.console = console or get_console()
2930
self.highlighter = ReprHighlighter()
3031
self._log_render = LogRender(show_level=True)
3132

0 commit comments

Comments
 (0)