Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/rich/live.py: 22%

Shortcuts on this page

r m x   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

145 statements  

1import sys 

2from threading import Event, RLock, Thread 

3from types import TracebackType 

4from typing import IO, Any, Callable, List, Optional, TextIO, Type, cast 

5 

6from . import get_console 

7from .console import Console, ConsoleRenderable, RenderableType, RenderHook 

8from .control import Control 

9from .file_proxy import FileProxy 

10from .jupyter import JupyterMixin 

11from .live_render import LiveRender, VerticalOverflowMethod 

12from .screen import Screen 

13from .text import Text 

14 

15 

16class _RefreshThread(Thread): 

17 """A thread that calls refresh() at regular intervals.""" 

18 

19 def __init__(self, live: "Live", refresh_per_second: float) -> None: 

20 self.live = live 

21 self.refresh_per_second = refresh_per_second 

22 self.done = Event() 

23 super().__init__(daemon=True) 

24 

25 def stop(self) -> None: 

26 self.done.set() 

27 

28 def run(self) -> None: 

29 while not self.done.wait(1 / self.refresh_per_second): 

30 with self.live._lock: 

31 if not self.done.is_set(): 

32 self.live.refresh() 

33 

34 

35class Live(JupyterMixin, RenderHook): 

36 """Renders an auto-updating live display of any given renderable. 

37 

38 Args: 

39 renderable (RenderableType, optional): The renderable to live display. Defaults to displaying nothing. 

40 console (Console, optional): Optional Console instance. Defaults to 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. 

49 """ 

50 

51 def __init__( 

52 self, 

53 renderable: Optional[RenderableType] = None, 

54 *, 

55 console: Optional[Console] = None, 

56 screen: bool = False, 

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, 

64 ) -> 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() 

68 self._screen = screen 

69 self._alt_screen = False 

70 

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 

75 

76 self._lock = RLock() 

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 

81 

82 self._refresh_thread: Optional[_RefreshThread] = None 

83 self.refresh_per_second = refresh_per_second 

84 

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 

89 ) 

90 

91 @property 

92 def is_started(self) -> bool: 

93 """Check if live display has been started.""" 

94 return self._started 

95 

96 def get_renderable(self) -> RenderableType: 

97 renderable = ( 

98 self._get_renderable() 

99 if self._get_renderable is not None 

100 else self._renderable 

101 ) 

102 return renderable or "" 

103 

104 def start(self, refresh: bool = False) -> None: 

105 """Start live rendering display. 

106 

107 Args: 

108 refresh (bool, optional): Also refresh. Defaults to False. 

109 """ 

110 with self._lock: 

111 if self._started: 

112 return 

113 self.console.set_live(self) 

114 self._started = True 

115 if self._screen: 

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) 

120 if refresh: 

121 try: 

122 self.refresh() 

123 except Exception: 

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). 

128 self.stop() 

129 raise 

130 if self.auto_refresh: 

131 self._refresh_thread = _RefreshThread(self, self.refresh_per_second) 

132 self._refresh_thread.start() 

133 

134 def stop(self) -> None: 

135 """Stop live rendering display.""" 

136 with self._lock: 

137 if not self._started: 

138 return 

139 self.console.clear_live() 

140 self._started = False 

141 

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" 

147 with self.console: 

148 try: 

149 if not self._alt_screen and not self.console.is_jupyter: 

150 self.refresh() 

151 finally: 

152 self._disable_redirect_io() 

153 self.console.pop_render_hook() 

154 if not self._alt_screen and self.console.is_terminal: 

155 self.console.line() 

156 self.console.show_cursor(True) 

157 if self._alt_screen: 

158 self.console.set_alt_screen(False) 

159 

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 

164 

165 def __enter__(self) -> "Live": 

166 self.start(refresh=self._renderable is not None) 

167 return self 

168 

169 def __exit__( 

170 self, 

171 exc_type: Optional[Type[BaseException]], 

172 exc_val: Optional[BaseException], 

173 exc_tb: Optional[TracebackType], 

174 ) -> None: 

175 self.stop() 

176 

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)) 

186 

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 

195 

196 @property 

197 def renderable(self) -> RenderableType: 

198 """Get the renderable that is being displayed 

199 

200 Returns: 

201 RenderableType: Displayed renderable. 

202 """ 

203 renderable = self.get_renderable() 

204 return Screen(renderable) if self._alt_screen else renderable 

205 

206 def update(self, renderable: RenderableType, *, refresh: bool = False) -> None: 

207 """Update the renderable that is being displayed 

208 

209 Args: 

210 renderable (RenderableType): New renderable to use. 

211 refresh (bool, optional): Refresh the display. Defaults to False. 

212 """ 

213 if isinstance(renderable, str): 

214 renderable = self.console.render_str(renderable) 

215 with self._lock: 

216 self._renderable = renderable 

217 if refresh: 

218 self.refresh() 

219 

220 def refresh(self) -> None: 

221 """Update the display of the Live Render.""" 

222 with self._lock: 

223 self._live_render.set_renderable(self.renderable) 

224 if self.console.is_jupyter: # pragma: no cover 

225 try: 

226 from IPython.display import display 

227 from ipywidgets import Output 

228 except ImportError: 

229 import warnings 

230 

231 warnings.warn('install "ipywidgets" for Jupyter support') 

232 else: 

233 if self.ipy_widget is None: 

234 self.ipy_widget = Output() 

235 display(self.ipy_widget) 

236 

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: 

241 with self.console: 

242 self.console.print(Control()) 

243 elif ( 

244 not self._started and not self.transient 

245 ): # if it is finished allow files or dumb-terminals to see final result 

246 with self.console: 

247 self.console.print(Control()) 

248 

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. 

256 with self._lock: 

257 reset = ( 

258 Control.home() 

259 if self._alt_screen 

260 else self._live_render.position_cursor() 

261 ) 

262 renderables = [reset, *renderables, self._live_render] 

263 elif ( 

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] 

267 

268 return renderables 

269 

270 

271if __name__ == "__main__": # pragma: no cover 

272 import random 

273 import time 

274 from itertools import cycle 

275 from typing import Dict, List, Tuple 

276 

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 

284 

285 console = Console() 

286 

287 syntax = Syntax( 

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) 

291 try: 

292 previous_value = next(iter_values) 

293 except StopIteration: 

294 return 

295 for value in iter_values: 

296 yield False, previous_value 

297 previous_value = value 

298 yield True, previous_value''', 

299 "python", 

300 line_numbers=True, 

301 ) 

302 

303 table = Table("foo", "bar", "baz") 

304 table.add_row("1", "2", "3") 

305 

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[/]...", 

311 table, 

312 "Pretty printed structures...", 

313 {"type": "example", "text": "Pretty printed"}, 

314 "Syntax...", 

315 syntax, 

316 Rule("Give it a try!"), 

317 ] 

318 

319 examples = cycle(progress_renderables) 

320 

321 exchanges = [ 

322 "SGD", 

323 "MYR", 

324 "EUR", 

325 "USD", 

326 "AUD", 

327 "JPY", 

328 "CNH", 

329 "HKD", 

330 "CAD", 

331 "INR", 

332 "DKK", 

333 "GBP", 

334 "RUB", 

335 "NZD", 

336 "MXN", 

337 "IDR", 

338 "TWD", 

339 "THB", 

340 "VND", 

341 ] 

342 with Live(console=console) as live_table: 

343 exchange_rate_dict: Dict[Tuple[str, str], float] = {} 

344 

345 for index in range(100): 

346 select_exchange = exchanges[index % len(exchanges)] 

347 

348 for exchange in exchanges: 

349 if exchange == select_exchange: 

350 continue 

351 time.sleep(0.4) 

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 

356 ) 

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") 

360 

361 table.add_column("Source Currency") 

362 table.add_column("Destination Currency") 

363 table.add_column("Exchange Rate") 

364 

365 for (source, dest), exchange_rate in exchange_rate_dict.items(): 

366 table.add_row( 

367 source, 

368 dest, 

369 Text( 

370 f"{exchange_rate:.4f}", 

371 style="red" if exchange_rate < 1.0 else "green", 

372 ), 

373 ) 

374 

375 live_table.update(Align.center(table))