2 from threading
import Event
, RLock
, Thread
3 from types
import TracebackType
4 from typing
import IO
, Any
, Callable
, List
, Optional
, TextIO
, Type
, cast
6 from . import get_console
7 from .console
import Console
, ConsoleRenderable
, RenderableType
, RenderHook
8 from .control
import Control
9 from .file_proxy
import FileProxy
10 from .jupyter
import JupyterMixin
11 from .live_render
import LiveRender
, VerticalOverflowMethod
12 from .screen
import Screen
13 from .text
import Text
16 class _RefreshThread(Thread
):
17 """A thread that calls refresh() at regular intervals."""
19 def __init__(self
, live
: "Live", refresh_per_second
: float) -> None:
21 self
.refresh_per_second
= refresh_per_second
23 super().__init
__(daemon
=True)
25 def stop(self
) -> None:
28 def run(self
) -> None:
29 while not self
.done
.wait(1 / self
.refresh_per_second
):
31 if not self
.done
.is_set():
35 class Live(JupyterMixin
, RenderHook
):
36 """Renders an auto-updating live display of any given renderable.
39 renderable (RenderableType, optional): The renderable to live display. Defaults to displaying nothing.
40 console (Console, optional): Optional Console instance. Default will an internal Console instance writing to stdout.
41 screen (bool, optional): Enable alternate screen mode. Defaults to False.
42 auto_refresh (bool, optional): Enable auto refresh. If disabled, you will need to call `refresh()` or `update()` with refresh flag. Defaults to True
43 refresh_per_second (float, optional): Number of times per second to refresh the live display. Defaults to 4.
44 transient (bool, optional): Clear the renderable on exit (has no effect when screen=True). Defaults to False.
45 redirect_stdout (bool, optional): Enable redirection of stdout, so ``print`` may be used. Defaults to True.
46 redirect_stderr (bool, optional): Enable redirection of stderr. Defaults to True.
47 vertical_overflow (VerticalOverflowMethod, optional): How to handle renderable when it is too tall for the console. Defaults to "ellipsis".
48 get_renderable (Callable[[], RenderableType], optional): Optional callable to get renderable. Defaults to None.
53 renderable
: Optional
[RenderableType
] = None,
55 console
: Optional
[Console
] = None,
57 auto_refresh
: bool = True,
58 refresh_per_second
: float = 4,
59 transient
: bool = False,
60 redirect_stdout
: bool = True,
61 redirect_stderr
: bool = True,
62 vertical_overflow
: VerticalOverflowMethod
= "ellipsis",
63 get_renderable
: Optional
[Callable
[[], RenderableType
]] = None,
65 assert refresh_per_second
> 0, "refresh_per_second must be > 0"
66 self
._renderable
= renderable
67 self
.console
= console
if console
is not None else get_console()
69 self
._alt
_screen
= False
71 self
._redirect
_stdout
= redirect_stdout
72 self
._redirect
_stderr
= redirect_stderr
73 self
._restore
_stdout
: Optional
[IO
[str]] = None
74 self
._restore
_stderr
: Optional
[IO
[str]] = None
77 self
.ipy_widget
: Optional
[Any
] = None
78 self
.auto_refresh
= auto_refresh
79 self
._started
: bool = False
80 self
.transient
= True if screen
else transient
82 self
._refresh
_thread
: Optional
[_RefreshThread
] = None
83 self
.refresh_per_second
= refresh_per_second
85 self
.vertical_overflow
= vertical_overflow
86 self
._get
_renderable
= get_renderable
87 self
._live
_render
= LiveRender(
88 self
.get_renderable(), vertical_overflow
=vertical_overflow
92 def is_started(self
) -> bool:
93 """Check if live display has been started."""
96 def get_renderable(self
) -> RenderableType
:
98 self
._get
_renderable
()
99 if self
._get
_renderable
is not None
100 else self
._renderable
102 return renderable
or ""
104 def start(self
, refresh
: bool = False) -> None:
105 """Start live rendering display.
108 refresh (bool, optional): Also refresh. Defaults to False.
113 self
.console
.set_live(self
)
116 self
._alt
_screen
= self
.console
.set_alt_screen(True)
117 self
.console
.show_cursor(False)
118 self
._enable
_redirect
_io
()
119 self
.console
.push_render_hook(self
)
124 # If refresh fails, we want to stop the redirection of sys.stderr,
125 # so the error stacktrace is properly displayed in the terminal.
126 # (or, if the code that calls Rich captures the exception and wants to display something,
127 # let this be displayed in the terminal).
130 if self
.auto_refresh
:
131 self
._refresh
_thread
= _RefreshThread(self
, self
.refresh_per_second
)
132 self
._refresh
_thread
.start()
134 def stop(self
) -> None:
135 """Stop live rendering display."""
137 if not self
._started
:
139 self
.console
.clear_live()
140 self
._started
= False
142 if self
.auto_refresh
and self
._refresh
_thread
is not None:
143 self
._refresh
_thread
.stop()
144 self
._refresh
_thread
= None
145 # allow it to fully render on the last even if overflow
146 self
.vertical_overflow
= "visible"
149 if not self
._alt
_screen
and not self
.console
.is_jupyter
:
152 self
._disable
_redirect
_io
()
153 self
.console
.pop_render_hook()
154 if not self
._alt
_screen
and self
.console
.is_terminal
:
156 self
.console
.show_cursor(True)
158 self
.console
.set_alt_screen(False)
160 if self
.transient
and not self
._alt
_screen
:
161 self
.console
.control(self
._live
_render
.restore_cursor())
162 if self
.ipy_widget
is not None and self
.transient
:
163 self
.ipy_widget
.close() # pragma: no cover
165 def __enter__(self
) -> "Live":
166 self
.start(refresh
=self
._renderable
is not None)
171 exc_type
: Optional
[Type
[BaseException
]],
172 exc_val
: Optional
[BaseException
],
173 exc_tb
: Optional
[TracebackType
],
177 def _enable_redirect_io(self
) -> None:
178 """Enable redirecting of stdout / stderr."""
179 if self
.console
.is_terminal
or self
.console
.is_jupyter
:
180 if self
._redirect
_stdout
and not isinstance(sys
.stdout
, FileProxy
):
181 self
._restore
_stdout
= sys
.stdout
182 sys
.stdout
= cast("TextIO", FileProxy(self
.console
, sys
.stdout
))
183 if self
._redirect
_stderr
and not isinstance(sys
.stderr
, FileProxy
):
184 self
._restore
_stderr
= sys
.stderr
185 sys
.stderr
= cast("TextIO", FileProxy(self
.console
, sys
.stderr
))
187 def _disable_redirect_io(self
) -> None:
188 """Disable redirecting of stdout / stderr."""
189 if self
._restore
_stdout
:
190 sys
.stdout
= cast("TextIO", self
._restore
_stdout
)
191 self
._restore
_stdout
= None
192 if self
._restore
_stderr
:
193 sys
.stderr
= cast("TextIO", self
._restore
_stderr
)
194 self
._restore
_stderr
= None
197 def renderable(self
) -> RenderableType
:
198 """Get the renderable that is being displayed
201 RenderableType: Displayed renderable.
203 renderable
= self
.get_renderable()
204 return Screen(renderable
) if self
._alt
_screen
else renderable
206 def update(self
, renderable
: RenderableType
, *, refresh
: bool = False) -> None:
207 """Update the renderable that is being displayed
210 renderable (RenderableType): New renderable to use.
211 refresh (bool, optional): Refresh the display. Defaults to False.
213 if isinstance(renderable
, str):
214 renderable
= self
.console
.render_str(renderable
)
216 self
._renderable
= renderable
220 def refresh(self
) -> None:
221 """Update the display of the Live Render."""
223 self
._live
_render
.set_renderable(self
.renderable
)
224 if self
.console
.is_jupyter
: # pragma: no cover
226 from IPython
.display
import display
227 from ipywidgets
import Output
231 warnings
.warn('install "ipywidgets" for Jupyter support')
233 if self
.ipy_widget
is None:
234 self
.ipy_widget
= Output()
235 display(self
.ipy_widget
)
237 with self
.ipy_widget
:
238 self
.ipy_widget
.clear_output(wait
=True)
239 self
.console
.print(self
._live
_render
.renderable
)
240 elif self
.console
.is_terminal
and not self
.console
.is_dumb_terminal
:
242 self
.console
.print(Control())
244 not self
._started
and not self
.transient
245 ): # if it is finished allow files or dumb-terminals to see final result
247 self
.console
.print(Control())
249 def process_renderables(
250 self
, renderables
: List
[ConsoleRenderable
]
251 ) -> List
[ConsoleRenderable
]:
252 """Process renderables to restore cursor and display progress."""
253 self
._live
_render
.vertical_overflow
= self
.vertical_overflow
254 if self
.console
.is_interactive
:
255 # lock needs acquiring as user can modify live_render renderable at any time unlike in Progress.
260 else self
._live
_render
.position_cursor()
262 renderables
= [reset
, *renderables
, self
._live
_render
]
264 not self
._started
and not self
.transient
265 ): # if it is finished render the final output for files or dumb_terminals
266 renderables
= [*renderables
, self
._live
_render
]
271 if __name__
== "__main__": # pragma: no cover
274 from itertools
import cycle
275 from typing
import Dict
, List
, Tuple
277 from .align
import Align
278 from .console
import Console
279 from .live
import Live
as Live
280 from .panel
import Panel
281 from .rule
import Rule
282 from .syntax
import Syntax
283 from .table
import Table
288 '''def loop_last(values: Iterable[T]) -> Iterable[Tuple[bool, T]]:
289 """Iterate and generate a tuple with a flag for last value."""
290 iter_values = iter(values)
292 previous_value = next(iter_values)
293 except StopIteration:
295 for value in iter_values:
296 yield False, previous_value
297 previous_value = value
298 yield True, previous_value''',
303 table
= Table("foo", "bar", "baz")
304 table
.add_row("1", "2", "3")
306 progress_renderables
= [
307 "You can make the terminal shorter and taller to see the live table hide"
308 "Text may be printed while the progress bars are rendering.",
309 Panel("In fact, [i]any[/i] renderable will work"),
310 "Such as [magenta]tables[/]...",
312 "Pretty printed structures...",
313 {"type": "example", "text": "Pretty printed"}
,
316 Rule("Give it a try!"),
319 examples
= cycle(progress_renderables
)
342 with Live(console
=console
) as live_table
:
343 exchange_rate_dict
: Dict
[Tuple
[str, str], float] = {}
345 for index
in range(100):
346 select_exchange
= exchanges
[index
% len(exchanges
)]
348 for exchange
in exchanges
:
349 if exchange
== select_exchange
:
352 if random
.randint(0, 10) < 1:
353 console
.log(next(examples
))
354 exchange_rate_dict
[(select_exchange
, exchange
)] = 200 / (
355 (random
.random() * 320) + 1
357 if len(exchange_rate_dict
) > len(exchanges
) - 1:
358 exchange_rate_dict
.pop(list(exchange_rate_dict
.keys())[0])
359 table
= Table(title
="Exchange Rates")
361 table
.add_column("Source Currency")
362 table
.add_column("Destination Currency")
363 table
.add_column("Exchange Rate")
365 for ((source
, dest
), exchange_rate
) in exchange_rate_dict
.items():
370 f
"{exchange_rate:.4f}",
371 style
="red" if exchange_rate
< 1.0 else "green",
375 live_table
.update(Align
.center(table
))