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

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

1002 statements  

1import inspect 

2import os 

3import sys 

4import threading 

5import zlib 

6from abc import ABC, abstractmethod 

7from dataclasses import dataclass, field 

8from datetime import datetime 

9from functools import wraps 

10from getpass import getpass 

11from html import escape 

12from inspect import isclass 

13from itertools import islice 

14from math import ceil 

15from time import monotonic 

16from types import FrameType, ModuleType, TracebackType 

17from typing import ( 

18 IO, 

19 TYPE_CHECKING, 

20 Any, 

21 Callable, 

22 Dict, 

23 Iterable, 

24 List, 

25 Literal, 

26 Mapping, 

27 NamedTuple, 

28 Optional, 

29 Protocol, 

30 TextIO, 

31 Tuple, 

32 Type, 

33 Union, 

34 cast, 

35 runtime_checkable, 

36) 

37 

38from rich._null_file import NULL_FILE 

39 

40from . import errors, themes 

41from ._emoji_replace import _emoji_replace 

42from ._export_format import CONSOLE_HTML_FORMAT, CONSOLE_SVG_FORMAT 

43from ._fileno import get_fileno 

44from ._log_render import FormatTimeCallable, LogRender 

45from .align import Align, AlignMethod 

46from .color import ColorSystem, blend_rgb 

47from .control import Control 

48from .emoji import EmojiVariant 

49from .highlighter import NullHighlighter, ReprHighlighter 

50from .markup import render as render_markup 

51from .measure import Measurement, measure_renderables 

52from .pager import Pager, SystemPager 

53from .pretty import Pretty, is_expandable 

54from .protocol import rich_cast 

55from .region import Region 

56from .scope import render_scope 

57from .screen import Screen 

58from .segment import Segment 

59from .style import Style, StyleType 

60from .styled import Styled 

61from .terminal_theme import DEFAULT_TERMINAL_THEME, SVG_EXPORT_THEME, TerminalTheme 

62from .text import Text, TextType 

63from .theme import Theme, ThemeStack 

64 

65if TYPE_CHECKING: 

66 from ._windows import WindowsConsoleFeatures 

67 from .live import Live 

68 from .status import Status 

69 

70JUPYTER_DEFAULT_COLUMNS = 115 

71JUPYTER_DEFAULT_LINES = 100 

72WINDOWS = sys.platform == "win32" 

73 

74HighlighterType = Callable[[Union[str, "Text"]], "Text"] 

75JustifyMethod = Literal["default", "left", "center", "right", "full"] 

76OverflowMethod = Literal["fold", "crop", "ellipsis", "ignore"] 

77 

78 

79class NoChange: 

80 pass 

81 

82 

83NO_CHANGE = NoChange() 

84 

85try: 

86 _STDIN_FILENO = sys.__stdin__.fileno() # type: ignore[union-attr] 

87except Exception: 

88 _STDIN_FILENO = 0 

89try: 

90 _STDOUT_FILENO = sys.__stdout__.fileno() # type: ignore[union-attr] 

91except Exception: 

92 _STDOUT_FILENO = 1 

93try: 

94 _STDERR_FILENO = sys.__stderr__.fileno() # type: ignore[union-attr] 

95except Exception: 

96 _STDERR_FILENO = 2 

97 

98_STD_STREAMS = (_STDIN_FILENO, _STDOUT_FILENO, _STDERR_FILENO) 

99_STD_STREAMS_OUTPUT = (_STDOUT_FILENO, _STDERR_FILENO) 

100 

101 

102_TERM_COLORS = { 

103 "kitty": ColorSystem.EIGHT_BIT, 

104 "256color": ColorSystem.EIGHT_BIT, 

105 "16color": ColorSystem.STANDARD, 

106} 

107 

108 

109class ConsoleDimensions(NamedTuple): 

110 """Size of the terminal.""" 

111 

112 width: int 

113 """The width of the console in 'cells'.""" 

114 height: int 

115 """The height of the console in lines.""" 

116 

117 

118@dataclass 

119class ConsoleOptions: 

120 """Options for __rich_console__ method.""" 

121 

122 size: ConsoleDimensions 

123 """Size of console.""" 

124 legacy_windows: bool 

125 """legacy_windows: flag for legacy windows.""" 

126 min_width: int 

127 """Minimum width of renderable.""" 

128 max_width: int 

129 """Maximum width of renderable.""" 

130 is_terminal: bool 

131 """True if the target is a terminal, otherwise False.""" 

132 encoding: str 

133 """Encoding of terminal.""" 

134 max_height: int 

135 """Height of container (starts as terminal)""" 

136 justify: Optional[JustifyMethod] = None 

137 """Justify value override for renderable.""" 

138 overflow: Optional[OverflowMethod] = None 

139 """Overflow value override for renderable.""" 

140 no_wrap: Optional[bool] = False 

141 """Disable wrapping for text.""" 

142 highlight: Optional[bool] = None 

143 """Highlight override for render_str.""" 

144 markup: Optional[bool] = None 

145 """Enable markup when rendering strings.""" 

146 height: Optional[int] = None 

147 

148 @property 

149 def ascii_only(self) -> bool: 

150 """Check if renderables should use ascii only.""" 

151 return not self.encoding.startswith("utf") 

152 

153 def copy(self) -> "ConsoleOptions": 

154 """Return a copy of the options. 

155 

156 Returns: 

157 ConsoleOptions: a copy of self. 

158 """ 

159 options: ConsoleOptions = ConsoleOptions.__new__(ConsoleOptions) 

160 options.__dict__ = self.__dict__.copy() 

161 return options 

162 

163 def update( 

164 self, 

165 *, 

166 width: Union[int, NoChange] = NO_CHANGE, 

167 min_width: Union[int, NoChange] = NO_CHANGE, 

168 max_width: Union[int, NoChange] = NO_CHANGE, 

169 justify: Union[Optional[JustifyMethod], NoChange] = NO_CHANGE, 

170 overflow: Union[Optional[OverflowMethod], NoChange] = NO_CHANGE, 

171 no_wrap: Union[Optional[bool], NoChange] = NO_CHANGE, 

172 highlight: Union[Optional[bool], NoChange] = NO_CHANGE, 

173 markup: Union[Optional[bool], NoChange] = NO_CHANGE, 

174 height: Union[Optional[int], NoChange] = NO_CHANGE, 

175 ) -> "ConsoleOptions": 

176 """Update values, return a copy.""" 

177 options = self.copy() 

178 if not isinstance(width, NoChange): 

179 options.min_width = options.max_width = max(0, width) 

180 if not isinstance(min_width, NoChange): 

181 options.min_width = min_width 

182 if not isinstance(max_width, NoChange): 

183 options.max_width = max_width 

184 if not isinstance(justify, NoChange): 

185 options.justify = justify 

186 if not isinstance(overflow, NoChange): 

187 options.overflow = overflow 

188 if not isinstance(no_wrap, NoChange): 

189 options.no_wrap = no_wrap 

190 if not isinstance(highlight, NoChange): 

191 options.highlight = highlight 

192 if not isinstance(markup, NoChange): 

193 options.markup = markup 

194 if not isinstance(height, NoChange): 

195 if height is not None: 

196 options.max_height = height 

197 options.height = None if height is None else max(0, height) 

198 return options 

199 

200 def update_width(self, width: int) -> "ConsoleOptions": 

201 """Update just the width, return a copy. 

202 

203 Args: 

204 width (int): New width (sets both min_width and max_width) 

205 

206 Returns: 

207 ~ConsoleOptions: New console options instance. 

208 """ 

209 options = self.copy() 

210 options.min_width = options.max_width = max(0, width) 

211 return options 

212 

213 def update_height(self, height: int) -> "ConsoleOptions": 

214 """Update the height, and return a copy. 

215 

216 Args: 

217 height (int): New height 

218 

219 Returns: 

220 ~ConsoleOptions: New Console options instance. 

221 """ 

222 options = self.copy() 

223 options.max_height = options.height = height 

224 return options 

225 

226 def reset_height(self) -> "ConsoleOptions": 

227 """Return a copy of the options with height set to ``None``. 

228 

229 Returns: 

230 ~ConsoleOptions: New console options instance. 

231 """ 

232 options = self.copy() 

233 options.height = None 

234 return options 

235 

236 def update_dimensions(self, width: int, height: int) -> "ConsoleOptions": 

237 """Update the width and height, and return a copy. 

238 

239 Args: 

240 width (int): New width (sets both min_width and max_width). 

241 height (int): New height. 

242 

243 Returns: 

244 ~ConsoleOptions: New console options instance. 

245 """ 

246 options = self.copy() 

247 options.min_width = options.max_width = max(0, width) 

248 options.height = options.max_height = height 

249 return options 

250 

251 

252@runtime_checkable 

253class RichCast(Protocol): 

254 """An object that may be 'cast' to a console renderable.""" 

255 

256 def __rich__( 

257 self, 

258 ) -> Union["ConsoleRenderable", "RichCast", str]: # pragma: no cover 

259 ... 

260 

261 

262@runtime_checkable 

263class ConsoleRenderable(Protocol): 

264 """An object that supports the console protocol.""" 

265 

266 def __rich_console__( 

267 self, console: "Console", options: "ConsoleOptions" 

268 ) -> "RenderResult": # pragma: no cover 

269 ... 

270 

271 

272# A type that may be rendered by Console. 

273RenderableType = Union[ConsoleRenderable, RichCast, str] 

274"""A string or any object that may be rendered by Rich.""" 

275 

276# The result of calling a __rich_console__ method. 

277RenderResult = Iterable[Union[RenderableType, Segment]] 

278 

279_null_highlighter = NullHighlighter() 

280 

281 

282class CaptureError(Exception): 

283 """An error in the Capture context manager.""" 

284 

285 

286class NewLine: 

287 """A renderable to generate new line(s)""" 

288 

289 def __init__(self, count: int = 1) -> None: 

290 self.count = count 

291 

292 def __rich_console__( 

293 self, console: "Console", options: "ConsoleOptions" 

294 ) -> Iterable[Segment]: 

295 yield Segment("\n" * self.count) 

296 

297 

298class ScreenUpdate: 

299 """Render a list of lines at a given offset.""" 

300 

301 def __init__(self, lines: List[List[Segment]], x: int, y: int) -> None: 

302 self._lines = lines 

303 self.x = x 

304 self.y = y 

305 

306 def __rich_console__( 

307 self, console: "Console", options: ConsoleOptions 

308 ) -> RenderResult: 

309 x = self.x 

310 move_to = Control.move_to 

311 for offset, line in enumerate(self._lines, self.y): 

312 yield move_to(x, offset) 

313 yield from line 

314 

315 

316class Capture: 

317 """Context manager to capture the result of printing to the console. 

318 See :meth:`~rich.console.Console.capture` for how to use. 

319 

320 Args: 

321 console (Console): A console instance to capture output. 

322 """ 

323 

324 def __init__(self, console: "Console") -> None: 

325 self._console = console 

326 self._result: Optional[str] = None 

327 

328 def __enter__(self) -> "Capture": 

329 self._console.begin_capture() 

330 return self 

331 

332 def __exit__( 

333 self, 

334 exc_type: Optional[Type[BaseException]], 

335 exc_val: Optional[BaseException], 

336 exc_tb: Optional[TracebackType], 

337 ) -> None: 

338 self._result = self._console.end_capture() 

339 

340 def get(self) -> str: 

341 """Get the result of the capture.""" 

342 if self._result is None: 

343 raise CaptureError( 

344 "Capture result is not available until context manager exits." 

345 ) 

346 return self._result 

347 

348 

349class ThemeContext: 

350 """A context manager to use a temporary theme. See :meth:`~rich.console.Console.use_theme` for usage.""" 

351 

352 def __init__(self, console: "Console", theme: Theme, inherit: bool = True) -> None: 

353 self.console = console 

354 self.theme = theme 

355 self.inherit = inherit 

356 

357 def __enter__(self) -> "ThemeContext": 

358 self.console.push_theme(self.theme) 

359 return self 

360 

361 def __exit__( 

362 self, 

363 exc_type: Optional[Type[BaseException]], 

364 exc_val: Optional[BaseException], 

365 exc_tb: Optional[TracebackType], 

366 ) -> None: 

367 self.console.pop_theme() 

368 

369 

370class PagerContext: 

371 """A context manager that 'pages' content. See :meth:`~rich.console.Console.pager` for usage.""" 

372 

373 def __init__( 

374 self, 

375 console: "Console", 

376 pager: Optional[Pager] = None, 

377 styles: bool = False, 

378 links: bool = False, 

379 ) -> None: 

380 self._console = console 

381 self.pager = SystemPager() if pager is None else pager 

382 self.styles = styles 

383 self.links = links 

384 

385 def __enter__(self) -> "PagerContext": 

386 self._console._enter_buffer() 

387 return self 

388 

389 def __exit__( 

390 self, 

391 exc_type: Optional[Type[BaseException]], 

392 exc_val: Optional[BaseException], 

393 exc_tb: Optional[TracebackType], 

394 ) -> None: 

395 if exc_type is None: 

396 with self._console._lock: 

397 buffer: List[Segment] = self._console._buffer[:] 

398 del self._console._buffer[:] 

399 segments: Iterable[Segment] = buffer 

400 if not self.styles: 

401 segments = Segment.strip_styles(segments) 

402 elif not self.links: 

403 segments = Segment.strip_links(segments) 

404 content = self._console._render_buffer(segments) 

405 self.pager.show(content) 

406 self._console._exit_buffer() 

407 

408 

409class ScreenContext: 

410 """A context manager that enables an alternative screen. See :meth:`~rich.console.Console.screen` for usage.""" 

411 

412 def __init__( 

413 self, console: "Console", hide_cursor: bool, style: StyleType = "" 

414 ) -> None: 

415 self.console = console 

416 self.hide_cursor = hide_cursor 

417 self.screen = Screen(style=style) 

418 self._changed = False 

419 

420 def update( 

421 self, *renderables: RenderableType, style: Optional[StyleType] = None 

422 ) -> None: 

423 """Update the screen. 

424 

425 Args: 

426 renderable (RenderableType, optional): Optional renderable to replace current renderable, 

427 or None for no change. Defaults to None. 

428 style: (Style, optional): Replacement style, or None for no change. Defaults to None. 

429 """ 

430 if renderables: 

431 self.screen.renderable = ( 

432 Group(*renderables) if len(renderables) > 1 else renderables[0] 

433 ) 

434 if style is not None: 

435 self.screen.style = style 

436 self.console.print(self.screen, end="") 

437 

438 def __enter__(self) -> "ScreenContext": 

439 self._changed = self.console.set_alt_screen(True) 

440 if self._changed and self.hide_cursor: 

441 self.console.show_cursor(False) 

442 return self 

443 

444 def __exit__( 

445 self, 

446 exc_type: Optional[Type[BaseException]], 

447 exc_val: Optional[BaseException], 

448 exc_tb: Optional[TracebackType], 

449 ) -> None: 

450 if self._changed: 

451 self.console.set_alt_screen(False) 

452 if self.hide_cursor: 

453 self.console.show_cursor(True) 

454 

455 

456class Group: 

457 """Takes a group of renderables and returns a renderable object that renders the group. 

458 

459 Args: 

460 renderables (Iterable[RenderableType]): An iterable of renderable objects. 

461 fit (bool, optional): Fit dimension of group to contents, or fill available space. Defaults to True. 

462 """ 

463 

464 def __init__(self, *renderables: "RenderableType", fit: bool = True) -> None: 

465 self._renderables = renderables 

466 self.fit = fit 

467 self._render: Optional[List[RenderableType]] = None 

468 

469 @property 

470 def renderables(self) -> List["RenderableType"]: 

471 if self._render is None: 

472 self._render = list(self._renderables) 

473 return self._render 

474 

475 def __rich_measure__( 

476 self, console: "Console", options: "ConsoleOptions" 

477 ) -> "Measurement": 

478 if self.fit: 

479 return measure_renderables(console, options, self.renderables) 

480 else: 

481 return Measurement(options.max_width, options.max_width) 

482 

483 def __rich_console__( 

484 self, console: "Console", options: "ConsoleOptions" 

485 ) -> RenderResult: 

486 yield from self.renderables 

487 

488 

489def group(fit: bool = True) -> Callable[..., Callable[..., Group]]: 

490 """A decorator that turns an iterable of renderables in to a group. 

491 

492 Args: 

493 fit (bool, optional): Fit dimension of group to contents, or fill available space. Defaults to True. 

494 """ 

495 

496 def decorator( 

497 method: Callable[..., Iterable[RenderableType]], 

498 ) -> Callable[..., Group]: 

499 """Convert a method that returns an iterable of renderables in to a Group.""" 

500 

501 @wraps(method) 

502 def _replace(*args: Any, **kwargs: Any) -> Group: 

503 renderables = method(*args, **kwargs) 

504 return Group(*renderables, fit=fit) 

505 

506 return _replace 

507 

508 return decorator 

509 

510 

511def _is_jupyter() -> bool: # pragma: no cover 

512 """Check if we're running in a Jupyter notebook.""" 

513 try: 

514 get_ipython # type: ignore[name-defined] 

515 except NameError: 

516 return False 

517 ipython = get_ipython() # type: ignore[name-defined] 

518 shell = ipython.__class__.__name__ 

519 if ( 

520 "google.colab" in str(ipython.__class__) 

521 or os.getenv("DATABRICKS_RUNTIME_VERSION") 

522 or shell == "ZMQInteractiveShell" 

523 ): 

524 return True # Jupyter notebook or qtconsole 

525 elif shell == "TerminalInteractiveShell": 

526 return False # Terminal running IPython 

527 else: 

528 return False # Other type (?) 

529 

530 

531COLOR_SYSTEMS = { 

532 "standard": ColorSystem.STANDARD, 

533 "256": ColorSystem.EIGHT_BIT, 

534 "truecolor": ColorSystem.TRUECOLOR, 

535 "windows": ColorSystem.WINDOWS, 

536} 

537 

538_COLOR_SYSTEMS_NAMES = {system: name for name, system in COLOR_SYSTEMS.items()} 

539 

540 

541@dataclass 

542class ConsoleThreadLocals(threading.local): 

543 """Thread local values for Console context.""" 

544 

545 theme_stack: ThemeStack 

546 buffer: List[Segment] = field(default_factory=list) 

547 buffer_index: int = 0 

548 

549 

550class RenderHook(ABC): 

551 """Provides hooks in to the render process.""" 

552 

553 @abstractmethod 

554 def process_renderables( 

555 self, renderables: List[ConsoleRenderable] 

556 ) -> List[ConsoleRenderable]: 

557 """Called with a list of objects to render. 

558 

559 This method can return a new list of renderables, or modify and return the same list. 

560 

561 Args: 

562 renderables (List[ConsoleRenderable]): A number of renderable objects. 

563 

564 Returns: 

565 List[ConsoleRenderable]: A replacement list of renderables. 

566 """ 

567 

568 

569_windows_console_features: Optional["WindowsConsoleFeatures"] = None 

570 

571 

572def get_windows_console_features() -> "WindowsConsoleFeatures": # pragma: no cover 

573 global _windows_console_features 

574 if _windows_console_features is not None: 

575 return _windows_console_features 

576 from ._windows import get_windows_console_features 

577 

578 _windows_console_features = get_windows_console_features() 

579 return _windows_console_features 

580 

581 

582def detect_legacy_windows() -> bool: 

583 """Detect legacy Windows.""" 

584 return WINDOWS and not get_windows_console_features().vt 

585 

586 

587class Console: 

588 """A high level console interface. 

589 

590 Args: 

591 color_system (str, optional): The color system supported by your terminal, 

592 either ``"standard"``, ``"256"`` or ``"truecolor"``. Leave as ``"auto"`` to autodetect. 

593 force_terminal (Optional[bool], optional): Enable/disable terminal control codes, or None to auto-detect terminal. Defaults to None. 

594 force_jupyter (Optional[bool], optional): Enable/disable Jupyter rendering, or None to auto-detect Jupyter. Defaults to None. 

595 force_interactive (Optional[bool], optional): Enable/disable interactive mode, or None to auto detect. Defaults to None. 

596 soft_wrap (Optional[bool], optional): Set soft wrap default on print method. Defaults to False. 

597 theme (Theme, optional): An optional style theme object, or ``None`` for default theme. 

598 stderr (bool, optional): Use stderr rather than stdout if ``file`` is not specified. Defaults to False. 

599 file (IO, optional): A file object where the console should write to. Defaults to stdout. 

600 quiet (bool, Optional): Boolean to suppress all output. Defaults to False. 

601 width (int, optional): The width of the terminal. Leave as default to auto-detect width. 

602 height (int, optional): The height of the terminal. Leave as default to auto-detect height. 

603 style (StyleType, optional): Style to apply to all output, or None for no style. Defaults to None. 

604 no_color (Optional[bool], optional): Enabled no color mode, or None to auto detect. Defaults to None. 

605 tab_size (int, optional): Number of spaces used to replace a tab character. Defaults to 8. 

606 record (bool, optional): Boolean to enable recording of terminal output, 

607 required to call :meth:`export_html`, :meth:`export_svg`, and :meth:`export_text`. Defaults to False. 

608 markup (bool, optional): Boolean to enable :ref:`console_markup`. Defaults to True. 

609 emoji (bool, optional): Enable emoji code. Defaults to True. 

610 emoji_variant (str, optional): Optional emoji variant, either "text" or "emoji". Defaults to None. 

611 highlight (bool, optional): Enable automatic highlighting. Defaults to True. 

612 log_time (bool, optional): Boolean to enable logging of time by :meth:`log` methods. Defaults to True. 

613 log_path (bool, optional): Boolean to enable the logging of the caller by :meth:`log`. Defaults to True. 

614 log_time_format (Union[str, TimeFormatterCallable], optional): If ``log_time`` is enabled, either string for strftime or callable that formats the time. Defaults to "[%X] ". 

615 highlighter (HighlighterType, optional): Default highlighter. 

616 legacy_windows (bool, optional): Enable legacy Windows mode, or ``None`` to auto detect. Defaults to ``None``. 

617 safe_box (bool, optional): Restrict box options that don't render on legacy Windows. 

618 get_datetime (Callable[[], datetime], optional): Callable that gets the current time as a datetime.datetime object (used by Console.log), 

619 or None for datetime.now. 

620 get_time (Callable[[], time], optional): Callable that gets the current time in seconds, default uses time.monotonic. 

621 """ 

622 

623 _environ: Mapping[str, str] = os.environ 

624 

625 def __init__( 

626 self, 

627 *, 

628 color_system: Optional[ 

629 Literal["auto", "standard", "256", "truecolor", "windows"] 

630 ] = "auto", 

631 force_terminal: Optional[bool] = None, 

632 force_jupyter: Optional[bool] = None, 

633 force_interactive: Optional[bool] = None, 

634 soft_wrap: bool = False, 

635 theme: Optional[Theme] = None, 

636 stderr: bool = False, 

637 file: Optional[IO[str]] = None, 

638 quiet: bool = False, 

639 width: Optional[int] = None, 

640 height: Optional[int] = None, 

641 style: Optional[StyleType] = None, 

642 no_color: Optional[bool] = None, 

643 tab_size: int = 8, 

644 record: bool = False, 

645 markup: bool = True, 

646 emoji: bool = True, 

647 emoji_variant: Optional[EmojiVariant] = None, 

648 highlight: bool = True, 

649 log_time: bool = True, 

650 log_path: bool = True, 

651 log_time_format: Union[str, FormatTimeCallable] = "[%X]", 

652 highlighter: Optional["HighlighterType"] = ReprHighlighter(), 

653 legacy_windows: Optional[bool] = None, 

654 safe_box: bool = True, 

655 get_datetime: Optional[Callable[[], datetime]] = None, 

656 get_time: Optional[Callable[[], float]] = None, 

657 _environ: Optional[Mapping[str, str]] = None, 

658 ): 

659 # Copy of os.environ allows us to replace it for testing 

660 if _environ is not None: 

661 self._environ = _environ 

662 

663 self.is_jupyter = _is_jupyter() if force_jupyter is None else force_jupyter 

664 if self.is_jupyter: 

665 if width is None: 

666 jupyter_columns = self._environ.get("JUPYTER_COLUMNS") 

667 if jupyter_columns is not None and jupyter_columns.isdigit(): 

668 width = int(jupyter_columns) 

669 else: 

670 width = JUPYTER_DEFAULT_COLUMNS 

671 if height is None: 

672 jupyter_lines = self._environ.get("JUPYTER_LINES") 

673 if jupyter_lines is not None and jupyter_lines.isdigit(): 

674 height = int(jupyter_lines) 

675 else: 

676 height = JUPYTER_DEFAULT_LINES 

677 

678 self.tab_size = tab_size 

679 self.record = record 

680 self._markup = markup 

681 self._emoji = emoji 

682 self._emoji_variant: Optional[EmojiVariant] = emoji_variant 

683 self._highlight = highlight 

684 self.legacy_windows: bool = ( 

685 (detect_legacy_windows() and not self.is_jupyter) 

686 if legacy_windows is None 

687 else legacy_windows 

688 ) 

689 

690 if width is None: 

691 columns = self._environ.get("COLUMNS") 

692 if columns is not None and columns.isdigit(): 

693 width = int(columns) - self.legacy_windows 

694 if height is None: 

695 lines = self._environ.get("LINES") 

696 if lines is not None and lines.isdigit(): 

697 height = int(lines) 

698 

699 self.soft_wrap = soft_wrap 

700 self._width = width 

701 self._height = height 

702 

703 self._color_system: Optional[ColorSystem] 

704 

705 self._force_terminal = None 

706 if force_terminal is not None: 

707 self._force_terminal = force_terminal 

708 

709 self._file = file 

710 self.quiet = quiet 

711 self.stderr = stderr 

712 

713 if color_system is None: 

714 self._color_system = None 

715 elif color_system == "auto": 

716 self._color_system = self._detect_color_system() 

717 else: 

718 self._color_system = COLOR_SYSTEMS[color_system] 

719 

720 self._lock = threading.RLock() 

721 self._log_render = LogRender( 

722 show_time=log_time, 

723 show_path=log_path, 

724 time_format=log_time_format, 

725 ) 

726 self.highlighter: HighlighterType = highlighter or _null_highlighter 

727 self.safe_box = safe_box 

728 self.get_datetime = get_datetime or datetime.now 

729 self.get_time = get_time or monotonic 

730 self.style = style 

731 self.no_color = ( 

732 no_color 

733 if no_color is not None 

734 else self._environ.get("NO_COLOR", "") != "" 

735 ) 

736 if force_interactive is None: 

737 tty_interactive = self._environ.get("TTY_INTERACTIVE", None) 

738 if tty_interactive is not None: 

739 if tty_interactive == "0": 

740 force_interactive = False 

741 elif tty_interactive == "1": 

742 force_interactive = True 

743 

744 self.is_interactive = ( 

745 (self.is_terminal and not self.is_dumb_terminal) 

746 if force_interactive is None 

747 else force_interactive 

748 ) 

749 

750 self._record_buffer_lock = threading.RLock() 

751 self._thread_locals = ConsoleThreadLocals( 

752 theme_stack=ThemeStack(themes.DEFAULT if theme is None else theme) 

753 ) 

754 self._record_buffer: List[Segment] = [] 

755 self._render_hooks: List[RenderHook] = [] 

756 self._live_stack: List[Live] = [] 

757 self._is_alt_screen = False 

758 

759 def __repr__(self) -> str: 

760 return f"<console width={self.width} {self._color_system!s}>" 

761 

762 @property 

763 def file(self) -> IO[str]: 

764 """Get the file object to write to.""" 

765 file = self._file or (sys.stderr if self.stderr else sys.stdout) 

766 file = getattr(file, "rich_proxied_file", file) 

767 if file is None: 

768 file = NULL_FILE 

769 return file 

770 

771 @file.setter 

772 def file(self, new_file: IO[str]) -> None: 

773 """Set a new file object.""" 

774 self._file = new_file 

775 

776 @property 

777 def _buffer(self) -> List[Segment]: 

778 """Get a thread local buffer.""" 

779 return self._thread_locals.buffer 

780 

781 @property 

782 def _buffer_index(self) -> int: 

783 """Get a thread local buffer.""" 

784 return self._thread_locals.buffer_index 

785 

786 @_buffer_index.setter 

787 def _buffer_index(self, value: int) -> None: 

788 self._thread_locals.buffer_index = value 

789 

790 @property 

791 def _theme_stack(self) -> ThemeStack: 

792 """Get the thread local theme stack.""" 

793 return self._thread_locals.theme_stack 

794 

795 def _detect_color_system(self) -> Optional[ColorSystem]: 

796 """Detect color system from env vars.""" 

797 if self.is_jupyter: 

798 return ColorSystem.TRUECOLOR 

799 if not self.is_terminal or self.is_dumb_terminal: 

800 return None 

801 if WINDOWS: # pragma: no cover 

802 if self.legacy_windows: # pragma: no cover 

803 return ColorSystem.WINDOWS 

804 windows_console_features = get_windows_console_features() 

805 return ( 

806 ColorSystem.TRUECOLOR 

807 if windows_console_features.truecolor 

808 else ColorSystem.EIGHT_BIT 

809 ) 

810 else: 

811 color_term = self._environ.get("COLORTERM", "").strip().lower() 

812 if color_term in ("truecolor", "24bit"): 

813 return ColorSystem.TRUECOLOR 

814 term = self._environ.get("TERM", "").strip().lower() 

815 _term_name, _hyphen, colors = term.rpartition("-") 

816 color_system = _TERM_COLORS.get(colors, ColorSystem.STANDARD) 

817 return color_system 

818 

819 def _enter_buffer(self) -> None: 

820 """Enter in to a buffer context, and buffer all output.""" 

821 self._buffer_index += 1 

822 

823 def _exit_buffer(self) -> None: 

824 """Leave buffer context, and render content if required.""" 

825 self._buffer_index -= 1 

826 self._check_buffer() 

827 

828 def set_live(self, live: "Live") -> bool: 

829 """Set Live instance. Used by Live context manager (no need to call directly). 

830 

831 Args: 

832 live (Live): Live instance using this Console. 

833 

834 Returns: 

835 Boolean that indicates if the live is the topmost of the stack. 

836 

837 Raises: 

838 errors.LiveError: If this Console has a Live context currently active. 

839 """ 

840 with self._lock: 

841 self._live_stack.append(live) 

842 return len(self._live_stack) == 1 

843 

844 def clear_live(self) -> None: 

845 """Clear the Live instance. Used by the Live context manager (no need to call directly).""" 

846 with self._lock: 

847 self._live_stack.pop() 

848 

849 def push_render_hook(self, hook: RenderHook) -> None: 

850 """Add a new render hook to the stack. 

851 

852 Args: 

853 hook (RenderHook): Render hook instance. 

854 """ 

855 with self._lock: 

856 self._render_hooks.append(hook) 

857 

858 def pop_render_hook(self) -> None: 

859 """Pop the last renderhook from the stack.""" 

860 with self._lock: 

861 self._render_hooks.pop() 

862 

863 def __enter__(self) -> "Console": 

864 """Own context manager to enter buffer context.""" 

865 self._enter_buffer() 

866 return self 

867 

868 def __exit__(self, exc_type: Any, exc_value: Any, traceback: Any) -> None: 

869 """Exit buffer context.""" 

870 self._exit_buffer() 

871 

872 def begin_capture(self) -> None: 

873 """Begin capturing console output. Call :meth:`end_capture` to exit capture mode and return output.""" 

874 self._enter_buffer() 

875 

876 def end_capture(self) -> str: 

877 """End capture mode and return captured string. 

878 

879 Returns: 

880 str: Console output. 

881 """ 

882 render_result = self._render_buffer(self._buffer) 

883 del self._buffer[:] 

884 self._exit_buffer() 

885 return render_result 

886 

887 def push_theme(self, theme: Theme, *, inherit: bool = True) -> None: 

888 """Push a new theme on to the top of the stack, replacing the styles from the previous theme. 

889 Generally speaking, you should call :meth:`~rich.console.Console.use_theme` to get a context manager, rather 

890 than calling this method directly. 

891 

892 Args: 

893 theme (Theme): A theme instance. 

894 inherit (bool, optional): Inherit existing styles. Defaults to True. 

895 """ 

896 self._theme_stack.push_theme(theme, inherit=inherit) 

897 

898 def pop_theme(self) -> None: 

899 """Remove theme from top of stack, restoring previous theme.""" 

900 self._theme_stack.pop_theme() 

901 

902 def use_theme(self, theme: Theme, *, inherit: bool = True) -> ThemeContext: 

903 """Use a different theme for the duration of the context manager. 

904 

905 Args: 

906 theme (Theme): Theme instance to user. 

907 inherit (bool, optional): Inherit existing console styles. Defaults to True. 

908 

909 Returns: 

910 ThemeContext: [description] 

911 """ 

912 return ThemeContext(self, theme, inherit) 

913 

914 @property 

915 def color_system(self) -> Optional[str]: 

916 """Get color system string. 

917 

918 Returns: 

919 Optional[str]: "standard", "256" or "truecolor". 

920 """ 

921 

922 if self._color_system is not None: 

923 return _COLOR_SYSTEMS_NAMES[self._color_system] 

924 else: 

925 return None 

926 

927 @property 

928 def encoding(self) -> str: 

929 """Get the encoding of the console file, e.g. ``"utf-8"``. 

930 

931 Returns: 

932 str: A standard encoding string. 

933 """ 

934 return (getattr(self.file, "encoding", "utf-8") or "utf-8").lower() 

935 

936 @property 

937 def is_terminal(self) -> bool: 

938 """Check if the console is writing to a terminal. 

939 

940 Returns: 

941 bool: True if the console writing to a device capable of 

942 understanding escape sequences, otherwise False. 

943 """ 

944 # If dev has explicitly set this value, return it 

945 if self._force_terminal is not None: 

946 return self._force_terminal 

947 

948 # Fudge for Idle 

949 if hasattr(sys.stdin, "__module__") and sys.stdin.__module__.startswith( 

950 "idlelib" 

951 ): 

952 # Return False for Idle which claims to be a tty but can't handle ansi codes 

953 return False 

954 

955 if self.is_jupyter: 

956 # return False for Jupyter, which may have FORCE_COLOR set 

957 return False 

958 

959 environ = self._environ 

960 

961 tty_compatible = environ.get("TTY_COMPATIBLE", "") 

962 # 0 indicates device is not tty compatible 

963 if tty_compatible == "0": 

964 return False 

965 # 1 indicates device is tty compatible 

966 if tty_compatible == "1": 

967 return True 

968 

969 # https://force-color.org/ 

970 force_color = environ.get("FORCE_COLOR") 

971 if force_color is not None: 

972 return force_color != "" 

973 

974 # Any other value defaults to auto detect 

975 isatty: Optional[Callable[[], bool]] = getattr(self.file, "isatty", None) 

976 try: 

977 return False if isatty is None else isatty() 

978 except ValueError: 

979 # in some situation (at the end of a pytest run for example) isatty() can raise 

980 # ValueError: I/O operation on closed file 

981 # return False because we aren't in a terminal anymore 

982 return False 

983 

984 @property 

985 def is_dumb_terminal(self) -> bool: 

986 """Detect dumb terminal. 

987 

988 Returns: 

989 bool: True if writing to a dumb terminal, otherwise False. 

990 

991 """ 

992 _term = self._environ.get("TERM", "") 

993 is_dumb = _term.lower() in ("dumb", "unknown") 

994 return self.is_terminal and is_dumb 

995 

996 @property 

997 def options(self) -> ConsoleOptions: 

998 """Get default console options.""" 

999 size = self.size 

1000 return ConsoleOptions( 

1001 max_height=size.height, 

1002 size=size, 

1003 legacy_windows=self.legacy_windows, 

1004 min_width=1, 

1005 max_width=size.width, 

1006 encoding=self.encoding, 

1007 is_terminal=self.is_terminal, 

1008 ) 

1009 

1010 @property 

1011 def size(self) -> ConsoleDimensions: 

1012 """Get the size of the console. 

1013 

1014 Returns: 

1015 ConsoleDimensions: A named tuple containing the dimensions. 

1016 """ 

1017 

1018 if self._width is not None and self._height is not None: 

1019 return ConsoleDimensions(self._width - self.legacy_windows, self._height) 

1020 

1021 if self.is_dumb_terminal: 

1022 return ConsoleDimensions(80, 25) 

1023 

1024 width: Optional[int] = None 

1025 height: Optional[int] = None 

1026 

1027 streams = _STD_STREAMS_OUTPUT if WINDOWS else _STD_STREAMS 

1028 for file_descriptor in streams: 

1029 try: 

1030 width, height = os.get_terminal_size(file_descriptor) 

1031 except (AttributeError, ValueError, OSError): # Probably not a terminal 

1032 pass 

1033 else: 

1034 break 

1035 

1036 columns = self._environ.get("COLUMNS") 

1037 if columns is not None and columns.isdigit(): 

1038 width = int(columns) 

1039 lines = self._environ.get("LINES") 

1040 if lines is not None and lines.isdigit(): 

1041 height = int(lines) 

1042 

1043 # get_terminal_size can report 0, 0 if run from pseudo-terminal 

1044 width = width or 80 

1045 height = height or 25 

1046 return ConsoleDimensions( 

1047 width - self.legacy_windows if self._width is None else self._width, 

1048 height if self._height is None else self._height, 

1049 ) 

1050 

1051 @size.setter 

1052 def size(self, new_size: Tuple[int, int]) -> None: 

1053 """Set a new size for the terminal. 

1054 

1055 Args: 

1056 new_size (Tuple[int, int]): New width and height. 

1057 """ 

1058 width, height = new_size 

1059 self._width = width 

1060 self._height = height 

1061 

1062 @property 

1063 def width(self) -> int: 

1064 """Get the width of the console. 

1065 

1066 Returns: 

1067 int: The width (in characters) of the console. 

1068 """ 

1069 return self.size.width 

1070 

1071 @width.setter 

1072 def width(self, width: int) -> None: 

1073 """Set width. 

1074 

1075 Args: 

1076 width (int): New width. 

1077 """ 

1078 self._width = width 

1079 

1080 @property 

1081 def height(self) -> int: 

1082 """Get the height of the console. 

1083 

1084 Returns: 

1085 int: The height (in lines) of the console. 

1086 """ 

1087 return self.size.height 

1088 

1089 @height.setter 

1090 def height(self, height: int) -> None: 

1091 """Set height. 

1092 

1093 Args: 

1094 height (int): new height. 

1095 """ 

1096 self._height = height 

1097 

1098 def bell(self) -> None: 

1099 """Play a 'bell' sound (if supported by the terminal).""" 

1100 self.control(Control.bell()) 

1101 

1102 def capture(self) -> Capture: 

1103 """A context manager to *capture* the result of print() or log() in a string, 

1104 rather than writing it to the console. 

1105 

1106 Example: 

1107 >>> from rich.console import Console 

1108 >>> console = Console() 

1109 >>> with console.capture() as capture: 

1110 ... console.print("[bold magenta]Hello World[/]") 

1111 >>> print(capture.get()) 

1112 

1113 Returns: 

1114 Capture: Context manager with disables writing to the terminal. 

1115 """ 

1116 capture = Capture(self) 

1117 return capture 

1118 

1119 def pager( 

1120 self, pager: Optional[Pager] = None, styles: bool = False, links: bool = False 

1121 ) -> PagerContext: 

1122 """A context manager to display anything printed within a "pager". The pager application 

1123 is defined by the system and will typically support at least pressing a key to scroll. 

1124 

1125 Args: 

1126 pager (Pager, optional): A pager object, or None to use :class:`~rich.pager.SystemPager`. Defaults to None. 

1127 styles (bool, optional): Show styles in pager. Defaults to False. 

1128 links (bool, optional): Show links in pager. Defaults to False. 

1129 

1130 Example: 

1131 >>> from rich.console import Console 

1132 >>> from rich.__main__ import make_test_card 

1133 >>> console = Console() 

1134 >>> with console.pager(): 

1135 console.print(make_test_card()) 

1136 

1137 Returns: 

1138 PagerContext: A context manager. 

1139 """ 

1140 return PagerContext(self, pager=pager, styles=styles, links=links) 

1141 

1142 def line(self, count: int = 1) -> None: 

1143 """Write new line(s). 

1144 

1145 Args: 

1146 count (int, optional): Number of new lines. Defaults to 1. 

1147 """ 

1148 

1149 assert count >= 0, "count must be >= 0" 

1150 self.print(NewLine(count)) 

1151 

1152 def clear(self, home: bool = True) -> None: 

1153 """Clear the screen. 

1154 

1155 Args: 

1156 home (bool, optional): Also move the cursor to 'home' position. Defaults to True. 

1157 """ 

1158 if home: 

1159 self.control(Control.clear(), Control.home()) 

1160 else: 

1161 self.control(Control.clear()) 

1162 

1163 def status( 

1164 self, 

1165 status: RenderableType, 

1166 *, 

1167 spinner: str = "dots", 

1168 spinner_style: StyleType = "status.spinner", 

1169 speed: float = 1.0, 

1170 refresh_per_second: float = 12.5, 

1171 ) -> "Status": 

1172 """Display a status and spinner. 

1173 

1174 Args: 

1175 status (RenderableType): A status renderable (str or Text typically). 

1176 spinner (str, optional): Name of spinner animation (see python -m rich.spinner). Defaults to "dots". 

1177 spinner_style (StyleType, optional): Style of spinner. Defaults to "status.spinner". 

1178 speed (float, optional): Speed factor for spinner animation. Defaults to 1.0. 

1179 refresh_per_second (float, optional): Number of refreshes per second. Defaults to 12.5. 

1180 

1181 Returns: 

1182 Status: A Status object that may be used as a context manager. 

1183 """ 

1184 from .status import Status 

1185 

1186 status_renderable = Status( 

1187 status, 

1188 console=self, 

1189 spinner=spinner, 

1190 spinner_style=spinner_style, 

1191 speed=speed, 

1192 refresh_per_second=refresh_per_second, 

1193 ) 

1194 return status_renderable 

1195 

1196 def show_cursor(self, show: bool = True) -> bool: 

1197 """Show or hide the cursor. 

1198 

1199 Args: 

1200 show (bool, optional): Set visibility of the cursor. 

1201 """ 

1202 if self.is_terminal: 

1203 self.control(Control.show_cursor(show)) 

1204 return True 

1205 return False 

1206 

1207 def set_alt_screen(self, enable: bool = True) -> bool: 

1208 """Enables alternative screen mode. 

1209 

1210 Note, if you enable this mode, you should ensure that is disabled before 

1211 the application exits. See :meth:`~rich.Console.screen` for a context manager 

1212 that handles this for you. 

1213 

1214 Args: 

1215 enable (bool, optional): Enable (True) or disable (False) alternate screen. Defaults to True. 

1216 

1217 Returns: 

1218 bool: True if the control codes were written. 

1219 

1220 """ 

1221 changed = False 

1222 if self.is_terminal and not self.legacy_windows: 

1223 self.control(Control.alt_screen(enable)) 

1224 changed = True 

1225 self._is_alt_screen = enable 

1226 return changed 

1227 

1228 @property 

1229 def is_alt_screen(self) -> bool: 

1230 """Check if the alt screen was enabled. 

1231 

1232 Returns: 

1233 bool: True if the alt screen was enabled, otherwise False. 

1234 """ 

1235 return self._is_alt_screen 

1236 

1237 def set_window_title(self, title: str) -> bool: 

1238 """Set the title of the console terminal window. 

1239 

1240 Warning: There is no means within Rich of "resetting" the window title to its 

1241 previous value, meaning the title you set will persist even after your application 

1242 exits. 

1243 

1244 ``fish`` shell resets the window title before and after each command by default, 

1245 negating this issue. Windows Terminal and command prompt will also reset the title for you. 

1246 Most other shells and terminals, however, do not do this. 

1247 

1248 Some terminals may require configuration changes before you can set the title. 

1249 Some terminals may not support setting the title at all. 

1250 

1251 Other software (including the terminal itself, the shell, custom prompts, plugins, etc.) 

1252 may also set the terminal window title. This could result in whatever value you write 

1253 using this method being overwritten. 

1254 

1255 Args: 

1256 title (str): The new title of the terminal window. 

1257 

1258 Returns: 

1259 bool: True if the control code to change the terminal title was 

1260 written, otherwise False. Note that a return value of True 

1261 does not guarantee that the window title has actually changed, 

1262 since the feature may be unsupported/disabled in some terminals. 

1263 """ 

1264 if self.is_terminal: 

1265 self.control(Control.title(title)) 

1266 return True 

1267 return False 

1268 

1269 def screen( 

1270 self, hide_cursor: bool = True, style: Optional[StyleType] = None 

1271 ) -> "ScreenContext": 

1272 """Context manager to enable and disable 'alternative screen' mode. 

1273 

1274 Args: 

1275 hide_cursor (bool, optional): Also hide the cursor. Defaults to False. 

1276 style (Style, optional): Optional style for screen. Defaults to None. 

1277 

1278 Returns: 

1279 ~ScreenContext: Context which enables alternate screen on enter, and disables it on exit. 

1280 """ 

1281 return ScreenContext(self, hide_cursor=hide_cursor, style=style or "") 

1282 

1283 def measure( 

1284 self, renderable: RenderableType, *, options: Optional[ConsoleOptions] = None 

1285 ) -> Measurement: 

1286 """Measure a renderable. Returns a :class:`~rich.measure.Measurement` object which contains 

1287 information regarding the number of characters required to print the renderable. 

1288 

1289 Args: 

1290 renderable (RenderableType): Any renderable or string. 

1291 options (Optional[ConsoleOptions], optional): Options to use when measuring, or None 

1292 to use default options. Defaults to None. 

1293 

1294 Returns: 

1295 Measurement: A measurement of the renderable. 

1296 """ 

1297 measurement = Measurement.get(self, options or self.options, renderable) 

1298 return measurement 

1299 

1300 def render( 

1301 self, renderable: RenderableType, options: Optional[ConsoleOptions] = None 

1302 ) -> Iterable[Segment]: 

1303 """Render an object in to an iterable of `Segment` instances. 

1304 

1305 This method contains the logic for rendering objects with the console protocol. 

1306 You are unlikely to need to use it directly, unless you are extending the library. 

1307 

1308 Args: 

1309 renderable (RenderableType): An object supporting the console protocol, or 

1310 an object that may be converted to a string. 

1311 options (ConsoleOptions, optional): An options object, or None to use self.options. Defaults to None. 

1312 

1313 Returns: 

1314 Iterable[Segment]: An iterable of segments that may be rendered. 

1315 """ 

1316 

1317 _options = options or self.options 

1318 if _options.max_width < 1: 

1319 # No space to render anything. This prevents potential recursion errors. 

1320 return 

1321 render_iterable: RenderResult 

1322 

1323 renderable = rich_cast(renderable) 

1324 if hasattr(renderable, "__rich_console__") and not isclass(renderable): 

1325 render_iterable = renderable.__rich_console__(self, _options) 

1326 elif isinstance(renderable, str): 

1327 text_renderable = self.render_str( 

1328 renderable, highlight=_options.highlight, markup=_options.markup 

1329 ) 

1330 render_iterable = text_renderable.__rich_console__(self, _options) 

1331 else: 

1332 raise errors.NotRenderableError( 

1333 f"Unable to render {renderable!r}; " 

1334 "A str, Segment or object with __rich_console__ method is required" 

1335 ) 

1336 

1337 try: 

1338 iter_render = iter(render_iterable) 

1339 except TypeError: 

1340 raise errors.NotRenderableError( 

1341 f"object {render_iterable!r} is not renderable" 

1342 ) 

1343 _Segment = Segment 

1344 _options = _options.reset_height() 

1345 for render_output in iter_render: 

1346 if isinstance(render_output, _Segment): 

1347 yield render_output 

1348 else: 

1349 yield from self.render(render_output, _options) 

1350 

1351 def render_lines( 

1352 self, 

1353 renderable: RenderableType, 

1354 options: Optional[ConsoleOptions] = None, 

1355 *, 

1356 style: Optional[Style] = None, 

1357 pad: bool = True, 

1358 new_lines: bool = False, 

1359 ) -> List[List[Segment]]: 

1360 """Render objects in to a list of lines. 

1361 

1362 The output of render_lines is useful when further formatting of rendered console text 

1363 is required, such as the Panel class which draws a border around any renderable object. 

1364 

1365 Args: 

1366 renderable (RenderableType): Any object renderable in the console. 

1367 options (Optional[ConsoleOptions], optional): Console options, or None to use self.options. Default to ``None``. 

1368 style (Style, optional): Optional style to apply to renderables. Defaults to ``None``. 

1369 pad (bool, optional): Pad lines shorter than render width. Defaults to ``True``. 

1370 new_lines (bool, optional): Include "\n" characters at end of lines. 

1371 

1372 Returns: 

1373 List[List[Segment]]: A list of lines, where a line is a list of Segment objects. 

1374 """ 

1375 with self._lock: 

1376 render_options = options or self.options 

1377 _rendered = self.render(renderable, render_options) 

1378 if style: 

1379 _rendered = Segment.apply_style(_rendered, style) 

1380 

1381 render_height = render_options.height 

1382 if render_height is not None: 

1383 render_height = max(0, render_height) 

1384 

1385 lines = list( 

1386 islice( 

1387 Segment.split_and_crop_lines( 

1388 _rendered, 

1389 render_options.max_width, 

1390 include_new_lines=new_lines, 

1391 pad=pad, 

1392 style=style, 

1393 ), 

1394 None, 

1395 render_height, 

1396 ) 

1397 ) 

1398 if render_options.height is not None: 

1399 extra_lines = render_options.height - len(lines) 

1400 if extra_lines > 0: 

1401 pad_line = [ 

1402 ( 

1403 [ 

1404 Segment(" " * render_options.max_width, style), 

1405 Segment("\n"), 

1406 ] 

1407 if new_lines 

1408 else [Segment(" " * render_options.max_width, style)] 

1409 ) 

1410 ] 

1411 lines.extend(pad_line * extra_lines) 

1412 

1413 return lines 

1414 

1415 def render_str( 

1416 self, 

1417 text: str, 

1418 *, 

1419 style: Union[str, Style] = "", 

1420 justify: Optional[JustifyMethod] = None, 

1421 overflow: Optional[OverflowMethod] = None, 

1422 emoji: Optional[bool] = None, 

1423 markup: Optional[bool] = None, 

1424 highlight: Optional[bool] = None, 

1425 highlighter: Optional[HighlighterType] = None, 

1426 ) -> "Text": 

1427 """Convert a string to a Text instance. This is called automatically if 

1428 you print or log a string. 

1429 

1430 Args: 

1431 text (str): Text to render. 

1432 style (Union[str, Style], optional): Style to apply to rendered text. 

1433 justify (str, optional): Justify method: "default", "left", "center", "full", or "right". Defaults to ``None``. 

1434 overflow (str, optional): Overflow method: "crop", "fold", or "ellipsis". Defaults to ``None``. 

1435 emoji (Optional[bool], optional): Enable emoji, or ``None`` to use Console default. 

1436 markup (Optional[bool], optional): Enable markup, or ``None`` to use Console default. 

1437 highlight (Optional[bool], optional): Enable highlighting, or ``None`` to use Console default. 

1438 highlighter (HighlighterType, optional): Optional highlighter to apply. 

1439 Returns: 

1440 ConsoleRenderable: Renderable object. 

1441 

1442 """ 

1443 emoji_enabled = emoji or (emoji is None and self._emoji) 

1444 markup_enabled = markup or (markup is None and self._markup) 

1445 highlight_enabled = highlight or (highlight is None and self._highlight) 

1446 

1447 if markup_enabled: 

1448 rich_text = render_markup( 

1449 text, 

1450 style=style, 

1451 emoji=emoji_enabled, 

1452 emoji_variant=self._emoji_variant, 

1453 ) 

1454 rich_text.justify = justify 

1455 rich_text.overflow = overflow 

1456 else: 

1457 rich_text = Text( 

1458 ( 

1459 _emoji_replace(text, default_variant=self._emoji_variant) 

1460 if emoji_enabled 

1461 else text 

1462 ), 

1463 justify=justify, 

1464 overflow=overflow, 

1465 style=style, 

1466 ) 

1467 

1468 _highlighter = (highlighter or self.highlighter) if highlight_enabled else None 

1469 if _highlighter is not None: 

1470 highlight_text = _highlighter(str(rich_text)) 

1471 highlight_text.copy_styles(rich_text) 

1472 return highlight_text 

1473 

1474 return rich_text 

1475 

1476 def get_style( 

1477 self, name: Union[str, Style], *, default: Optional[Union[Style, str]] = None 

1478 ) -> Style: 

1479 """Get a Style instance by its theme name or parse a definition. 

1480 

1481 Args: 

1482 name (str): The name of a style or a style definition. 

1483 

1484 Returns: 

1485 Style: A Style object. 

1486 

1487 Raises: 

1488 MissingStyle: If no style could be parsed from name. 

1489 

1490 """ 

1491 if isinstance(name, Style): 

1492 return name 

1493 

1494 try: 

1495 style = self._theme_stack.get(name) 

1496 if style is None: 

1497 style = Style.parse(name) 

1498 return style.copy() if style.link else style 

1499 except errors.StyleSyntaxError as error: 

1500 if default is not None: 

1501 return self.get_style(default) 

1502 raise errors.MissingStyle( 

1503 f"Failed to get style {name!r}; {error}" 

1504 ) from None 

1505 

1506 def _collect_renderables( 

1507 self, 

1508 objects: Iterable[Any], 

1509 sep: str, 

1510 end: str, 

1511 *, 

1512 justify: Optional[JustifyMethod] = None, 

1513 emoji: Optional[bool] = None, 

1514 markup: Optional[bool] = None, 

1515 highlight: Optional[bool] = None, 

1516 ) -> List[ConsoleRenderable]: 

1517 """Combine a number of renderables and text into one renderable. 

1518 

1519 Args: 

1520 objects (Iterable[Any]): Anything that Rich can render. 

1521 sep (str): String to write between print data. 

1522 end (str): String to write at end of print data. 

1523 justify (str, optional): One of "left", "right", "center", or "full". Defaults to ``None``. 

1524 emoji (Optional[bool], optional): Enable emoji code, or ``None`` to use console default. 

1525 markup (Optional[bool], optional): Enable markup, or ``None`` to use console default. 

1526 highlight (Optional[bool], optional): Enable automatic highlighting, or ``None`` to use console default. 

1527 

1528 Returns: 

1529 List[ConsoleRenderable]: A list of things to render. 

1530 """ 

1531 renderables: List[ConsoleRenderable] = [] 

1532 _append = renderables.append 

1533 text: List[Text] = [] 

1534 append_text = text.append 

1535 

1536 append = _append 

1537 if justify in ("left", "center", "right"): 

1538 

1539 def align_append(renderable: RenderableType) -> None: 

1540 _append(Align(renderable, cast(AlignMethod, justify))) 

1541 

1542 append = align_append 

1543 

1544 _highlighter: HighlighterType = _null_highlighter 

1545 if highlight or (highlight is None and self._highlight): 

1546 _highlighter = self.highlighter 

1547 

1548 def check_text() -> None: 

1549 if text: 

1550 sep_text = Text(sep, justify=justify, end=end) 

1551 append(sep_text.join(text)) 

1552 text.clear() 

1553 

1554 for renderable in objects: 

1555 renderable = rich_cast(renderable) 

1556 if isinstance(renderable, str): 

1557 append_text( 

1558 self.render_str( 

1559 renderable, 

1560 emoji=emoji, 

1561 markup=markup, 

1562 highlight=highlight, 

1563 highlighter=_highlighter, 

1564 ) 

1565 ) 

1566 elif isinstance(renderable, Text): 

1567 append_text(renderable) 

1568 elif isinstance(renderable, ConsoleRenderable): 

1569 check_text() 

1570 append(renderable) 

1571 elif is_expandable(renderable): 

1572 check_text() 

1573 append(Pretty(renderable, highlighter=_highlighter)) 

1574 else: 

1575 append_text(_highlighter(str(renderable))) 

1576 

1577 check_text() 

1578 

1579 if self.style is not None: 

1580 style = self.get_style(self.style) 

1581 renderables = [Styled(renderable, style) for renderable in renderables] 

1582 

1583 return renderables 

1584 

1585 def rule( 

1586 self, 

1587 title: TextType = "", 

1588 *, 

1589 characters: str = "─", 

1590 style: Union[str, Style] = "rule.line", 

1591 align: AlignMethod = "center", 

1592 ) -> None: 

1593 """Draw a line with optional centered title. 

1594 

1595 Args: 

1596 title (str, optional): Text to render over the rule. Defaults to "". 

1597 characters (str, optional): Character(s) to form the line. Defaults to "─". 

1598 style (str, optional): Style of line. Defaults to "rule.line". 

1599 align (str, optional): How to align the title, one of "left", "center", or "right". Defaults to "center". 

1600 """ 

1601 from .rule import Rule 

1602 

1603 rule = Rule(title=title, characters=characters, style=style, align=align) 

1604 self.print(rule) 

1605 

1606 def control(self, *control: Control) -> None: 

1607 """Insert non-printing control codes. 

1608 

1609 Args: 

1610 control_codes (str): Control codes, such as those that may move the cursor. 

1611 """ 

1612 if not self.is_dumb_terminal: 

1613 with self: 

1614 self._buffer.extend(_control.segment for _control in control) 

1615 

1616 def out( 

1617 self, 

1618 *objects: Any, 

1619 sep: str = " ", 

1620 end: str = "\n", 

1621 style: Optional[Union[str, Style]] = None, 

1622 highlight: Optional[bool] = None, 

1623 ) -> None: 

1624 """Output to the terminal. This is a low-level way of writing to the terminal which unlike 

1625 :meth:`~rich.console.Console.print` won't pretty print, wrap text, or apply markup, but will 

1626 optionally apply highlighting and a basic style. 

1627 

1628 Args: 

1629 sep (str, optional): String to write between print data. Defaults to " ". 

1630 end (str, optional): String to write at end of print data. Defaults to "\\\\n". 

1631 style (Union[str, Style], optional): A style to apply to output. Defaults to None. 

1632 highlight (Optional[bool], optional): Enable automatic highlighting, or ``None`` to use 

1633 console default. Defaults to ``None``. 

1634 """ 

1635 raw_output: str = sep.join(str(_object) for _object in objects) 

1636 self.print( 

1637 raw_output, 

1638 style=style, 

1639 highlight=highlight, 

1640 emoji=False, 

1641 markup=False, 

1642 no_wrap=True, 

1643 overflow="ignore", 

1644 crop=False, 

1645 end=end, 

1646 ) 

1647 

1648 def print( 

1649 self, 

1650 *objects: Any, 

1651 sep: str = " ", 

1652 end: str = "\n", 

1653 style: Optional[Union[str, Style]] = None, 

1654 justify: Optional[JustifyMethod] = None, 

1655 overflow: Optional[OverflowMethod] = None, 

1656 no_wrap: Optional[bool] = None, 

1657 emoji: Optional[bool] = None, 

1658 markup: Optional[bool] = None, 

1659 highlight: Optional[bool] = None, 

1660 width: Optional[int] = None, 

1661 height: Optional[int] = None, 

1662 crop: bool = True, 

1663 soft_wrap: Optional[bool] = None, 

1664 new_line_start: bool = False, 

1665 ) -> None: 

1666 """Print to the console. 

1667 

1668 Args: 

1669 objects (positional args): Objects to log to the terminal. 

1670 sep (str, optional): String to write between print data. Defaults to " ". 

1671 end (str, optional): String to write at end of print data. Defaults to "\\\\n". 

1672 style (Union[str, Style], optional): A style to apply to output. Defaults to None. 

1673 justify (str, optional): Justify method: "default", "left", "right", "center", or "full". Defaults to ``None``. 

1674 overflow (str, optional): Overflow method: "ignore", "crop", "fold", or "ellipsis". Defaults to None. 

1675 no_wrap (Optional[bool], optional): Disable word wrapping. Defaults to None. 

1676 emoji (Optional[bool], optional): Enable emoji code, or ``None`` to use console default. Defaults to ``None``. 

1677 markup (Optional[bool], optional): Enable markup, or ``None`` to use console default. Defaults to ``None``. 

1678 highlight (Optional[bool], optional): Enable automatic highlighting, or ``None`` to use console default. Defaults to ``None``. 

1679 width (Optional[int], optional): Width of output, or ``None`` to auto-detect. Defaults to ``None``. 

1680 crop (Optional[bool], optional): Crop output to width of terminal. Defaults to True. 

1681 soft_wrap (bool, optional): Enable soft wrap mode which disables word wrapping and cropping of text or ``None`` for 

1682 Console default. Defaults to ``None``. 

1683 new_line_start (bool, False): Insert a new line at the start if the output contains more than one line. Defaults to ``False``. 

1684 """ 

1685 if not objects: 

1686 objects = (NewLine(),) 

1687 

1688 if soft_wrap is None: 

1689 soft_wrap = self.soft_wrap 

1690 if soft_wrap: 

1691 if no_wrap is None: 

1692 no_wrap = True 

1693 if overflow is None: 

1694 overflow = "ignore" 

1695 crop = False 

1696 render_hooks = self._render_hooks[:] 

1697 with self: 

1698 renderables = self._collect_renderables( 

1699 objects, 

1700 sep, 

1701 end, 

1702 justify=justify, 

1703 emoji=emoji, 

1704 markup=markup, 

1705 highlight=highlight, 

1706 ) 

1707 for hook in render_hooks: 

1708 renderables = hook.process_renderables(renderables) 

1709 render_options = self.options.update( 

1710 justify=justify, 

1711 overflow=overflow, 

1712 width=min(width, self.width) if width is not None else NO_CHANGE, 

1713 height=height, 

1714 no_wrap=no_wrap, 

1715 markup=markup, 

1716 highlight=highlight, 

1717 ) 

1718 

1719 new_segments: List[Segment] = [] 

1720 extend = new_segments.extend 

1721 render = self.render 

1722 if style is None: 

1723 for renderable in renderables: 

1724 extend(render(renderable, render_options)) 

1725 else: 

1726 render_style = self.get_style(style) 

1727 new_line = Segment.line() 

1728 for renderable in renderables: 

1729 for line, add_new_line in Segment.split_lines_terminator( 

1730 render(renderable, render_options) 

1731 ): 

1732 extend(Segment.apply_style(line, render_style)) 

1733 if add_new_line: 

1734 new_segments.append(new_line) 

1735 

1736 if new_line_start: 

1737 if ( 

1738 len("".join(segment.text for segment in new_segments).splitlines()) 

1739 > 1 

1740 ): 

1741 new_segments.insert(0, Segment.line()) 

1742 if crop: 

1743 buffer_extend = self._buffer.extend 

1744 for line in Segment.split_and_crop_lines( 

1745 new_segments, self.width, pad=False 

1746 ): 

1747 buffer_extend(line) 

1748 else: 

1749 self._buffer.extend(new_segments) 

1750 

1751 def print_json( 

1752 self, 

1753 json: Optional[str] = None, 

1754 *, 

1755 data: Any = None, 

1756 indent: Union[None, int, str] = 2, 

1757 highlight: bool = True, 

1758 skip_keys: bool = False, 

1759 ensure_ascii: bool = False, 

1760 check_circular: bool = True, 

1761 allow_nan: bool = True, 

1762 default: Optional[Callable[[Any], Any]] = None, 

1763 sort_keys: bool = False, 

1764 ) -> None: 

1765 """Pretty prints JSON. Output will be valid JSON. 

1766 

1767 Args: 

1768 json (Optional[str]): A string containing JSON. 

1769 data (Any): If json is not supplied, then encode this data. 

1770 indent (Union[None, int, str], optional): Number of spaces to indent. Defaults to 2. 

1771 highlight (bool, optional): Enable highlighting of output: Defaults to True. 

1772 skip_keys (bool, optional): Skip keys not of a basic type. Defaults to False. 

1773 ensure_ascii (bool, optional): Escape all non-ascii characters. Defaults to False. 

1774 check_circular (bool, optional): Check for circular references. Defaults to True. 

1775 allow_nan (bool, optional): Allow NaN and Infinity values. Defaults to True. 

1776 default (Callable, optional): A callable that converts values that can not be encoded 

1777 in to something that can be JSON encoded. Defaults to None. 

1778 sort_keys (bool, optional): Sort dictionary keys. Defaults to False. 

1779 """ 

1780 from rich.json import JSON 

1781 

1782 if json is None: 

1783 json_renderable = JSON.from_data( 

1784 data, 

1785 indent=indent, 

1786 highlight=highlight, 

1787 skip_keys=skip_keys, 

1788 ensure_ascii=ensure_ascii, 

1789 check_circular=check_circular, 

1790 allow_nan=allow_nan, 

1791 default=default, 

1792 sort_keys=sort_keys, 

1793 ) 

1794 else: 

1795 if not isinstance(json, str): 

1796 raise TypeError( 

1797 f"json must be str. Did you mean print_json(data={json!r}) ?" 

1798 ) 

1799 json_renderable = JSON( 

1800 json, 

1801 indent=indent, 

1802 highlight=highlight, 

1803 skip_keys=skip_keys, 

1804 ensure_ascii=ensure_ascii, 

1805 check_circular=check_circular, 

1806 allow_nan=allow_nan, 

1807 default=default, 

1808 sort_keys=sort_keys, 

1809 ) 

1810 self.print(json_renderable, soft_wrap=True) 

1811 

1812 def update_screen( 

1813 self, 

1814 renderable: RenderableType, 

1815 *, 

1816 region: Optional[Region] = None, 

1817 options: Optional[ConsoleOptions] = None, 

1818 ) -> None: 

1819 """Update the screen at a given offset. 

1820 

1821 Args: 

1822 renderable (RenderableType): A Rich renderable. 

1823 region (Region, optional): Region of screen to update, or None for entire screen. Defaults to None. 

1824 x (int, optional): x offset. Defaults to 0. 

1825 y (int, optional): y offset. Defaults to 0. 

1826 

1827 Raises: 

1828 errors.NoAltScreen: If the Console isn't in alt screen mode. 

1829 

1830 """ 

1831 if not self.is_alt_screen: 

1832 raise errors.NoAltScreen("Alt screen must be enabled to call update_screen") 

1833 render_options = options or self.options 

1834 if region is None: 

1835 x = y = 0 

1836 render_options = render_options.update_dimensions( 

1837 render_options.max_width, render_options.height or self.height 

1838 ) 

1839 else: 

1840 x, y, width, height = region 

1841 render_options = render_options.update_dimensions(width, height) 

1842 

1843 lines = self.render_lines(renderable, options=render_options) 

1844 self.update_screen_lines(lines, x, y) 

1845 

1846 def update_screen_lines( 

1847 self, lines: List[List[Segment]], x: int = 0, y: int = 0 

1848 ) -> None: 

1849 """Update lines of the screen at a given offset. 

1850 

1851 Args: 

1852 lines (List[List[Segment]]): Rendered lines (as produced by :meth:`~rich.Console.render_lines`). 

1853 x (int, optional): x offset (column no). Defaults to 0. 

1854 y (int, optional): y offset (column no). Defaults to 0. 

1855 

1856 Raises: 

1857 errors.NoAltScreen: If the Console isn't in alt screen mode. 

1858 """ 

1859 if not self.is_alt_screen: 

1860 raise errors.NoAltScreen("Alt screen must be enabled to call update_screen") 

1861 screen_update = ScreenUpdate(lines, x, y) 

1862 segments = self.render(screen_update) 

1863 self._buffer.extend(segments) 

1864 self._check_buffer() 

1865 

1866 def print_exception( 

1867 self, 

1868 *, 

1869 width: Optional[int] = 100, 

1870 extra_lines: int = 3, 

1871 theme: Optional[str] = None, 

1872 word_wrap: bool = False, 

1873 show_locals: bool = False, 

1874 suppress: Iterable[Union[str, ModuleType]] = (), 

1875 max_frames: int = 100, 

1876 ) -> None: 

1877 """Prints a rich render of the last exception and traceback. 

1878 

1879 Args: 

1880 width (Optional[int], optional): Number of characters used to render code. Defaults to 100. 

1881 extra_lines (int, optional): Additional lines of code to render. Defaults to 3. 

1882 theme (str, optional): Override pygments theme used in traceback 

1883 word_wrap (bool, optional): Enable word wrapping of long lines. Defaults to False. 

1884 show_locals (bool, optional): Enable display of local variables. Defaults to False. 

1885 suppress (Iterable[Union[str, ModuleType]]): Optional sequence of modules or paths to exclude from traceback. 

1886 max_frames (int): Maximum number of frames to show in a traceback, 0 for no maximum. Defaults to 100. 

1887 """ 

1888 from .traceback import Traceback 

1889 

1890 traceback = Traceback( 

1891 width=width, 

1892 extra_lines=extra_lines, 

1893 theme=theme, 

1894 word_wrap=word_wrap, 

1895 show_locals=show_locals, 

1896 suppress=suppress, 

1897 max_frames=max_frames, 

1898 ) 

1899 self.print(traceback) 

1900 

1901 @staticmethod 

1902 def _caller_frame_info( 

1903 offset: int, 

1904 currentframe: Callable[[], Optional[FrameType]] = inspect.currentframe, 

1905 ) -> Tuple[str, int, Dict[str, Any]]: 

1906 """Get caller frame information. 

1907 

1908 Args: 

1909 offset (int): the caller offset within the current frame stack. 

1910 currentframe (Callable[[], Optional[FrameType]], optional): the callable to use to 

1911 retrieve the current frame. Defaults to ``inspect.currentframe``. 

1912 

1913 Returns: 

1914 Tuple[str, int, Dict[str, Any]]: A tuple containing the filename, the line number and 

1915 the dictionary of local variables associated with the caller frame. 

1916 

1917 Raises: 

1918 RuntimeError: If the stack offset is invalid. 

1919 """ 

1920 # Ignore the frame of this local helper 

1921 offset += 1 

1922 

1923 frame = currentframe() 

1924 if frame is not None: 

1925 # Use the faster currentframe where implemented 

1926 while offset and frame is not None: 

1927 frame = frame.f_back 

1928 offset -= 1 

1929 assert frame is not None 

1930 return frame.f_code.co_filename, frame.f_lineno, frame.f_locals 

1931 else: 

1932 # Fallback to the slower stack 

1933 frame_info = inspect.stack()[offset] 

1934 return frame_info.filename, frame_info.lineno, frame_info.frame.f_locals 

1935 

1936 def log( 

1937 self, 

1938 *objects: Any, 

1939 sep: str = " ", 

1940 end: str = "\n", 

1941 style: Optional[Union[str, Style]] = None, 

1942 justify: Optional[JustifyMethod] = None, 

1943 emoji: Optional[bool] = None, 

1944 markup: Optional[bool] = None, 

1945 highlight: Optional[bool] = None, 

1946 log_locals: bool = False, 

1947 _stack_offset: int = 1, 

1948 ) -> None: 

1949 """Log rich content to the terminal. 

1950 

1951 Args: 

1952 objects (positional args): Objects to log to the terminal. 

1953 sep (str, optional): String to write between print data. Defaults to " ". 

1954 end (str, optional): String to write at end of print data. Defaults to "\\\\n". 

1955 style (Union[str, Style], optional): A style to apply to output. Defaults to None. 

1956 justify (str, optional): One of "left", "right", "center", or "full". Defaults to ``None``. 

1957 emoji (Optional[bool], optional): Enable emoji code, or ``None`` to use console default. Defaults to None. 

1958 markup (Optional[bool], optional): Enable markup, or ``None`` to use console default. Defaults to None. 

1959 highlight (Optional[bool], optional): Enable automatic highlighting, or ``None`` to use console default. Defaults to None. 

1960 log_locals (bool, optional): Boolean to enable logging of locals where ``log()`` 

1961 was called. Defaults to False. 

1962 _stack_offset (int, optional): Offset of caller from end of call stack. Defaults to 1. 

1963 """ 

1964 if not objects: 

1965 objects = (NewLine(),) 

1966 

1967 render_hooks = self._render_hooks[:] 

1968 

1969 with self: 

1970 renderables = self._collect_renderables( 

1971 objects, 

1972 sep, 

1973 end, 

1974 justify=justify, 

1975 emoji=emoji, 

1976 markup=markup, 

1977 highlight=highlight, 

1978 ) 

1979 if style is not None: 

1980 renderables = [Styled(renderable, style) for renderable in renderables] 

1981 

1982 filename, line_no, locals = self._caller_frame_info(_stack_offset) 

1983 link_path = None if filename.startswith("<") else os.path.abspath(filename) 

1984 path = filename.rpartition(os.sep)[-1] 

1985 if log_locals: 

1986 locals_map = { 

1987 key: value 

1988 for key, value in locals.items() 

1989 if not key.startswith("__") 

1990 } 

1991 renderables.append(render_scope(locals_map, title="[i]locals")) 

1992 

1993 renderables = [ 

1994 self._log_render( 

1995 self, 

1996 renderables, 

1997 log_time=self.get_datetime(), 

1998 path=path, 

1999 line_no=line_no, 

2000 link_path=link_path, 

2001 ) 

2002 ] 

2003 for hook in render_hooks: 

2004 renderables = hook.process_renderables(renderables) 

2005 new_segments: List[Segment] = [] 

2006 extend = new_segments.extend 

2007 render = self.render 

2008 render_options = self.options 

2009 for renderable in renderables: 

2010 extend(render(renderable, render_options)) 

2011 buffer_extend = self._buffer.extend 

2012 for line in Segment.split_and_crop_lines( 

2013 new_segments, self.width, pad=False 

2014 ): 

2015 buffer_extend(line) 

2016 

2017 def on_broken_pipe(self) -> None: 

2018 """This function is called when a `BrokenPipeError` is raised. 

2019 

2020 This can occur when piping Textual output in Linux and macOS. 

2021 The default implementation is to exit the app, but you could implement 

2022 this method in a subclass to change the behavior. 

2023 

2024 See https://docs.python.org/3/library/signal.html#note-on-sigpipe for details. 

2025 """ 

2026 self.quiet = True 

2027 devnull = os.open(os.devnull, os.O_WRONLY) 

2028 os.dup2(devnull, sys.stdout.fileno()) 

2029 raise SystemExit(1) 

2030 

2031 def _check_buffer(self) -> None: 

2032 """Check if the buffer may be rendered. Render it if it can (e.g. Console.quiet is False) 

2033 Rendering is supported on Windows, Unix and Jupyter environments. For 

2034 legacy Windows consoles, the win32 API is called directly. 

2035 This method will also record what it renders if recording is enabled via Console.record. 

2036 """ 

2037 if self.quiet: 

2038 del self._buffer[:] 

2039 return 

2040 

2041 try: 

2042 self._write_buffer() 

2043 except BrokenPipeError: 

2044 self.on_broken_pipe() 

2045 

2046 def _write_buffer(self) -> None: 

2047 """Write the buffer to the output file.""" 

2048 

2049 with self._lock: 

2050 if self.record and not self._buffer_index: 

2051 with self._record_buffer_lock: 

2052 self._record_buffer.extend(self._buffer[:]) 

2053 

2054 if self._buffer_index == 0: 

2055 if self.is_jupyter: # pragma: no cover 

2056 from .jupyter import display 

2057 

2058 display(self._buffer, self._render_buffer(self._buffer[:])) 

2059 del self._buffer[:] 

2060 else: 

2061 if WINDOWS: 

2062 use_legacy_windows_render = False 

2063 if self.legacy_windows: 

2064 fileno = get_fileno(self.file) 

2065 if fileno is not None: 

2066 use_legacy_windows_render = ( 

2067 fileno in _STD_STREAMS_OUTPUT 

2068 ) 

2069 

2070 if use_legacy_windows_render: 

2071 from rich._win32_console import LegacyWindowsTerm 

2072 from rich._windows_renderer import legacy_windows_render 

2073 

2074 buffer = self._buffer[:] 

2075 if self.no_color and self._color_system: 

2076 buffer = list(Segment.remove_color(buffer)) 

2077 

2078 legacy_windows_render(buffer, LegacyWindowsTerm(self.file)) 

2079 else: 

2080 # Either a non-std stream on legacy Windows, or modern Windows. 

2081 text = self._render_buffer(self._buffer[:]) 

2082 # https://bugs.python.org/issue37871 

2083 # https://github.com/python/cpython/issues/82052 

2084 # We need to avoid writing more than 32Kb in a single write, due to the above bug 

2085 write = self.file.write 

2086 # Worse case scenario, every character is 4 bytes of utf-8 

2087 MAX_WRITE = 32 * 1024 // 4 

2088 try: 

2089 if len(text) <= MAX_WRITE: 

2090 write(text) 

2091 else: 

2092 batch: List[str] = [] 

2093 batch_append = batch.append 

2094 size = 0 

2095 for line in text.splitlines(True): 

2096 if size + len(line) > MAX_WRITE and batch: 

2097 write("".join(batch)) 

2098 batch.clear() 

2099 size = 0 

2100 batch_append(line) 

2101 size += len(line) 

2102 if batch: 

2103 write("".join(batch)) 

2104 batch.clear() 

2105 except UnicodeEncodeError as error: 

2106 error.reason = f"{error.reason}\n*** You may need to add PYTHONIOENCODING=utf-8 to your environment ***" 

2107 raise 

2108 else: 

2109 text = self._render_buffer(self._buffer[:]) 

2110 try: 

2111 self.file.write(text) 

2112 except UnicodeEncodeError as error: 

2113 error.reason = f"{error.reason}\n*** You may need to add PYTHONIOENCODING=utf-8 to your environment ***" 

2114 raise 

2115 

2116 self.file.flush() 

2117 del self._buffer[:] 

2118 

2119 def _render_buffer(self, buffer: Iterable[Segment]) -> str: 

2120 """Render buffered output, and clear buffer.""" 

2121 output: List[str] = [] 

2122 append = output.append 

2123 color_system = self._color_system 

2124 legacy_windows = self.legacy_windows 

2125 not_terminal = not self.is_terminal 

2126 if self.no_color and color_system: 

2127 buffer = Segment.remove_color(buffer) 

2128 for text, style, control in buffer: 

2129 if style: 

2130 append( 

2131 style.render( 

2132 text, 

2133 color_system=color_system, 

2134 legacy_windows=legacy_windows, 

2135 ) 

2136 ) 

2137 elif not (not_terminal and control): 

2138 append(text) 

2139 

2140 rendered = "".join(output) 

2141 return rendered 

2142 

2143 def input( 

2144 self, 

2145 prompt: TextType = "", 

2146 *, 

2147 markup: bool = True, 

2148 emoji: bool = True, 

2149 password: bool = False, 

2150 stream: Optional[TextIO] = None, 

2151 ) -> str: 

2152 """Displays a prompt and waits for input from the user. The prompt may contain color / style. 

2153 

2154 It works in the same way as Python's builtin :func:`input` function and provides elaborate line editing and history features if Python's builtin :mod:`readline` module is previously loaded. 

2155 

2156 Args: 

2157 prompt (Union[str, Text]): Text to render in the prompt. 

2158 markup (bool, optional): Enable console markup (requires a str prompt). Defaults to True. 

2159 emoji (bool, optional): Enable emoji (requires a str prompt). Defaults to True. 

2160 password: (bool, optional): Hide typed text. Defaults to False. 

2161 stream: (TextIO, optional): Optional file to read input from (rather than stdin). Defaults to None. 

2162 

2163 Returns: 

2164 str: Text read from stdin. 

2165 """ 

2166 if prompt: 

2167 self.print(prompt, markup=markup, emoji=emoji, end="") 

2168 if password: 

2169 result = getpass("", stream=stream) 

2170 else: 

2171 if stream: 

2172 result = stream.readline() 

2173 else: 

2174 result = input() 

2175 return result 

2176 

2177 def export_text(self, *, clear: bool = True, styles: bool = False) -> str: 

2178 """Generate text from console contents (requires record=True argument in constructor). 

2179 

2180 Args: 

2181 clear (bool, optional): Clear record buffer after exporting. Defaults to ``True``. 

2182 styles (bool, optional): If ``True``, ansi escape codes will be included. ``False`` for plain text. 

2183 Defaults to ``False``. 

2184 

2185 Returns: 

2186 str: String containing console contents. 

2187 

2188 """ 

2189 assert ( 

2190 self.record 

2191 ), "To export console contents set record=True in the constructor or instance" 

2192 

2193 with self._record_buffer_lock: 

2194 if styles: 

2195 text = "".join( 

2196 (style.render(text) if style else text) 

2197 for text, style, _ in self._record_buffer 

2198 ) 

2199 else: 

2200 text = "".join( 

2201 segment.text 

2202 for segment in self._record_buffer 

2203 if not segment.control 

2204 ) 

2205 if clear: 

2206 del self._record_buffer[:] 

2207 return text 

2208 

2209 def save_text(self, path: str, *, clear: bool = True, styles: bool = False) -> None: 

2210 """Generate text from console and save to a given location (requires record=True argument in constructor). 

2211 

2212 Args: 

2213 path (str): Path to write text files. 

2214 clear (bool, optional): Clear record buffer after exporting. Defaults to ``True``. 

2215 styles (bool, optional): If ``True``, ansi style codes will be included. ``False`` for plain text. 

2216 Defaults to ``False``. 

2217 

2218 """ 

2219 text = self.export_text(clear=clear, styles=styles) 

2220 with open(path, "w", encoding="utf-8") as write_file: 

2221 write_file.write(text) 

2222 

2223 def export_html( 

2224 self, 

2225 *, 

2226 theme: Optional[TerminalTheme] = None, 

2227 clear: bool = True, 

2228 code_format: Optional[str] = None, 

2229 inline_styles: bool = False, 

2230 ) -> str: 

2231 """Generate HTML from console contents (requires record=True argument in constructor). 

2232 

2233 Args: 

2234 theme (TerminalTheme, optional): TerminalTheme object containing console colors. 

2235 clear (bool, optional): Clear record buffer after exporting. Defaults to ``True``. 

2236 code_format (str, optional): Format string to render HTML. In addition to '{foreground}', 

2237 '{background}', and '{code}', should contain '{stylesheet}' if inline_styles is ``False``. 

2238 inline_styles (bool, optional): If ``True`` styles will be inlined in to spans, which makes files 

2239 larger but easier to cut and paste markup. If ``False``, styles will be embedded in a style tag. 

2240 Defaults to False. 

2241 

2242 Returns: 

2243 str: String containing console contents as HTML. 

2244 """ 

2245 assert ( 

2246 self.record 

2247 ), "To export console contents set record=True in the constructor or instance" 

2248 fragments: List[str] = [] 

2249 append = fragments.append 

2250 _theme = theme or DEFAULT_TERMINAL_THEME 

2251 stylesheet = "" 

2252 

2253 render_code_format = CONSOLE_HTML_FORMAT if code_format is None else code_format 

2254 

2255 with self._record_buffer_lock: 

2256 if inline_styles: 

2257 for text, style, _ in Segment.filter_control( 

2258 Segment.simplify(self._record_buffer) 

2259 ): 

2260 text = escape(text) 

2261 if style: 

2262 rule = style.get_html_style(_theme) 

2263 if style.link: 

2264 text = f'<a href="{style.link}">{text}</a>' 

2265 text = f'<span style="{rule}">{text}</span>' if rule else text 

2266 append(text) 

2267 else: 

2268 styles: Dict[str, int] = {} 

2269 for text, style, _ in Segment.filter_control( 

2270 Segment.simplify(self._record_buffer) 

2271 ): 

2272 text = escape(text) 

2273 if style: 

2274 rule = style.get_html_style(_theme) 

2275 style_number = styles.setdefault(rule, len(styles) + 1) 

2276 if style.link: 

2277 text = f'<a class="r{style_number}" href="{style.link}">{text}</a>' 

2278 else: 

2279 text = f'<span class="r{style_number}">{text}</span>' 

2280 append(text) 

2281 stylesheet_rules: List[str] = [] 

2282 stylesheet_append = stylesheet_rules.append 

2283 for style_rule, style_number in styles.items(): 

2284 if style_rule: 

2285 stylesheet_append(f".r{style_number} {{{style_rule}}}") 

2286 stylesheet = "\n".join(stylesheet_rules) 

2287 

2288 rendered_code = render_code_format.format( 

2289 code="".join(fragments), 

2290 stylesheet=stylesheet, 

2291 foreground=_theme.foreground_color.hex, 

2292 background=_theme.background_color.hex, 

2293 ) 

2294 if clear: 

2295 del self._record_buffer[:] 

2296 return rendered_code 

2297 

2298 def save_html( 

2299 self, 

2300 path: str, 

2301 *, 

2302 theme: Optional[TerminalTheme] = None, 

2303 clear: bool = True, 

2304 code_format: str = CONSOLE_HTML_FORMAT, 

2305 inline_styles: bool = False, 

2306 ) -> None: 

2307 """Generate HTML from console contents and write to a file (requires record=True argument in constructor). 

2308 

2309 Args: 

2310 path (str): Path to write html file. 

2311 theme (TerminalTheme, optional): TerminalTheme object containing console colors. 

2312 clear (bool, optional): Clear record buffer after exporting. Defaults to ``True``. 

2313 code_format (str, optional): Format string to render HTML. In addition to '{foreground}', 

2314 '{background}', and '{code}', should contain '{stylesheet}' if inline_styles is ``False``. 

2315 inline_styles (bool, optional): If ``True`` styles will be inlined in to spans, which makes files 

2316 larger but easier to cut and paste markup. If ``False``, styles will be embedded in a style tag. 

2317 Defaults to False. 

2318 

2319 """ 

2320 html = self.export_html( 

2321 theme=theme, 

2322 clear=clear, 

2323 code_format=code_format, 

2324 inline_styles=inline_styles, 

2325 ) 

2326 with open(path, "w", encoding="utf-8") as write_file: 

2327 write_file.write(html) 

2328 

2329 def export_svg( 

2330 self, 

2331 *, 

2332 title: str = "Rich", 

2333 theme: Optional[TerminalTheme] = None, 

2334 clear: bool = True, 

2335 code_format: str = CONSOLE_SVG_FORMAT, 

2336 font_aspect_ratio: float = 0.61, 

2337 unique_id: Optional[str] = None, 

2338 ) -> str: 

2339 """ 

2340 Generate an SVG from the console contents (requires record=True in Console constructor). 

2341 

2342 Args: 

2343 title (str, optional): The title of the tab in the output image 

2344 theme (TerminalTheme, optional): The ``TerminalTheme`` object to use to style the terminal 

2345 clear (bool, optional): Clear record buffer after exporting. Defaults to ``True`` 

2346 code_format (str, optional): Format string used to generate the SVG. Rich will inject a number of variables 

2347 into the string in order to form the final SVG output. The default template used and the variables 

2348 injected by Rich can be found by inspecting the ``console.CONSOLE_SVG_FORMAT`` variable. 

2349 font_aspect_ratio (float, optional): The width to height ratio of the font used in the ``code_format`` 

2350 string. Defaults to 0.61, which is the width to height ratio of Fira Code (the default font). 

2351 If you aren't specifying a different font inside ``code_format``, you probably don't need this. 

2352 unique_id (str, optional): unique id that is used as the prefix for various elements (CSS styles, node 

2353 ids). If not set, this defaults to a computed value based on the recorded content. 

2354 """ 

2355 

2356 from rich.cells import cell_len 

2357 

2358 style_cache: Dict[Style, str] = {} 

2359 

2360 def get_svg_style(style: Style) -> str: 

2361 """Convert a Style to CSS rules for SVG.""" 

2362 if style in style_cache: 

2363 return style_cache[style] 

2364 css_rules = [] 

2365 color = ( 

2366 _theme.foreground_color 

2367 if (style.color is None or style.color.is_default) 

2368 else style.color.get_truecolor(_theme) 

2369 ) 

2370 bgcolor = ( 

2371 _theme.background_color 

2372 if (style.bgcolor is None or style.bgcolor.is_default) 

2373 else style.bgcolor.get_truecolor(_theme) 

2374 ) 

2375 if style.reverse: 

2376 color, bgcolor = bgcolor, color 

2377 if style.dim: 

2378 color = blend_rgb(color, bgcolor, 0.4) 

2379 css_rules.append(f"fill: {color.hex}") 

2380 if style.bold: 

2381 css_rules.append("font-weight: bold") 

2382 if style.italic: 

2383 css_rules.append("font-style: italic;") 

2384 if style.underline: 

2385 css_rules.append("text-decoration: underline;") 

2386 if style.strike: 

2387 css_rules.append("text-decoration: line-through;") 

2388 

2389 css = ";".join(css_rules) 

2390 style_cache[style] = css 

2391 return css 

2392 

2393 _theme = theme or SVG_EXPORT_THEME 

2394 

2395 width = self.width 

2396 char_height = 20 

2397 char_width = char_height * font_aspect_ratio 

2398 line_height = char_height * 1.22 

2399 

2400 margin_top = 1 

2401 margin_right = 1 

2402 margin_bottom = 1 

2403 margin_left = 1 

2404 

2405 padding_top = 40 

2406 padding_right = 8 

2407 padding_bottom = 8 

2408 padding_left = 8 

2409 

2410 padding_width = padding_left + padding_right 

2411 padding_height = padding_top + padding_bottom 

2412 margin_width = margin_left + margin_right 

2413 margin_height = margin_top + margin_bottom 

2414 

2415 text_backgrounds: List[str] = [] 

2416 text_group: List[str] = [] 

2417 classes: Dict[str, int] = {} 

2418 style_no = 1 

2419 

2420 def escape_text(text: str) -> str: 

2421 """HTML escape text and replace spaces with nbsp.""" 

2422 return escape(text).replace(" ", "&#160;") 

2423 

2424 def make_tag( 

2425 name: str, content: Optional[str] = None, **attribs: object 

2426 ) -> str: 

2427 """Make a tag from name, content, and attributes.""" 

2428 

2429 def stringify(value: object) -> str: 

2430 if isinstance(value, (float)): 

2431 return format(value, "g") 

2432 return str(value) 

2433 

2434 tag_attribs = " ".join( 

2435 f'{k.lstrip("_").replace("_", "-")}="{stringify(v)}"' 

2436 for k, v in attribs.items() 

2437 ) 

2438 return ( 

2439 f"<{name} {tag_attribs}>{content}</{name}>" 

2440 if content 

2441 else f"<{name} {tag_attribs}/>" 

2442 ) 

2443 

2444 with self._record_buffer_lock: 

2445 segments = list(Segment.filter_control(self._record_buffer)) 

2446 if clear: 

2447 self._record_buffer.clear() 

2448 

2449 if unique_id is None: 

2450 unique_id = "terminal-" + str( 

2451 zlib.adler32( 

2452 ("".join(repr(segment) for segment in segments)).encode( 

2453 "utf-8", 

2454 "ignore", 

2455 ) 

2456 + title.encode("utf-8", "ignore") 

2457 ) 

2458 ) 

2459 y = 0 

2460 for y, line in enumerate(Segment.split_and_crop_lines(segments, length=width)): 

2461 x = 0 

2462 for text, style, _control in line: 

2463 style = style or Style() 

2464 rules = get_svg_style(style) 

2465 if rules not in classes: 

2466 classes[rules] = style_no 

2467 style_no += 1 

2468 class_name = f"r{classes[rules]}" 

2469 

2470 if style.reverse: 

2471 has_background = True 

2472 background = ( 

2473 _theme.foreground_color.hex 

2474 if style.color is None 

2475 else style.color.get_truecolor(_theme).hex 

2476 ) 

2477 else: 

2478 bgcolor = style.bgcolor 

2479 has_background = bgcolor is not None and not bgcolor.is_default 

2480 background = ( 

2481 _theme.background_color.hex 

2482 if style.bgcolor is None 

2483 else style.bgcolor.get_truecolor(_theme).hex 

2484 ) 

2485 

2486 text_length = cell_len(text) 

2487 if has_background: 

2488 text_backgrounds.append( 

2489 make_tag( 

2490 "rect", 

2491 fill=background, 

2492 x=x * char_width, 

2493 y=y * line_height + 1.5, 

2494 width=char_width * text_length, 

2495 height=line_height + 0.25, 

2496 shape_rendering="crispEdges", 

2497 ) 

2498 ) 

2499 

2500 if text != " " * len(text): 

2501 text_group.append( 

2502 make_tag( 

2503 "text", 

2504 escape_text(text), 

2505 _class=f"{unique_id}-{class_name}", 

2506 x=x * char_width, 

2507 y=y * line_height + char_height, 

2508 textLength=char_width * len(text), 

2509 clip_path=f"url(#{unique_id}-line-{y})", 

2510 ) 

2511 ) 

2512 x += cell_len(text) 

2513 

2514 line_offsets = [line_no * line_height + 1.5 for line_no in range(y)] 

2515 lines = "\n".join( 

2516 f"""<clipPath id="{unique_id}-line-{line_no}"> 

2517 {make_tag("rect", x=0, y=offset, width=char_width * width, height=line_height + 0.25)} 

2518 </clipPath>""" 

2519 for line_no, offset in enumerate(line_offsets) 

2520 ) 

2521 

2522 styles = "\n".join( 

2523 f".{unique_id}-r{rule_no} {{ {css} }}" for css, rule_no in classes.items() 

2524 ) 

2525 backgrounds = "".join(text_backgrounds) 

2526 matrix = "".join(text_group) 

2527 

2528 terminal_width = ceil(width * char_width + padding_width) 

2529 terminal_height = (y + 1) * line_height + padding_height 

2530 chrome = make_tag( 

2531 "rect", 

2532 fill=_theme.background_color.hex, 

2533 stroke="rgba(255,255,255,0.35)", 

2534 stroke_width="1", 

2535 x=margin_left, 

2536 y=margin_top, 

2537 width=terminal_width, 

2538 height=terminal_height, 

2539 rx=8, 

2540 ) 

2541 

2542 title_color = _theme.foreground_color.hex 

2543 if title: 

2544 chrome += make_tag( 

2545 "text", 

2546 escape_text(title), 

2547 _class=f"{unique_id}-title", 

2548 fill=title_color, 

2549 text_anchor="middle", 

2550 x=terminal_width // 2, 

2551 y=margin_top + char_height + 6, 

2552 ) 

2553 chrome += f""" 

2554 <g transform="translate(26,22)"> 

2555 <circle cx="0" cy="0" r="7" fill="#ff5f57"/> 

2556 <circle cx="22" cy="0" r="7" fill="#febc2e"/> 

2557 <circle cx="44" cy="0" r="7" fill="#28c840"/> 

2558 </g> 

2559 """ 

2560 

2561 svg = code_format.format( 

2562 unique_id=unique_id, 

2563 char_width=char_width, 

2564 char_height=char_height, 

2565 line_height=line_height, 

2566 terminal_width=char_width * width - 1, 

2567 terminal_height=(y + 1) * line_height - 1, 

2568 width=terminal_width + margin_width, 

2569 height=terminal_height + margin_height, 

2570 terminal_x=margin_left + padding_left, 

2571 terminal_y=margin_top + padding_top, 

2572 styles=styles, 

2573 chrome=chrome, 

2574 backgrounds=backgrounds, 

2575 matrix=matrix, 

2576 lines=lines, 

2577 ) 

2578 return svg 

2579 

2580 def save_svg( 

2581 self, 

2582 path: str, 

2583 *, 

2584 title: str = "Rich", 

2585 theme: Optional[TerminalTheme] = None, 

2586 clear: bool = True, 

2587 code_format: str = CONSOLE_SVG_FORMAT, 

2588 font_aspect_ratio: float = 0.61, 

2589 unique_id: Optional[str] = None, 

2590 ) -> None: 

2591 """Generate an SVG file from the console contents (requires record=True in Console constructor). 

2592 

2593 Args: 

2594 path (str): The path to write the SVG to. 

2595 title (str, optional): The title of the tab in the output image 

2596 theme (TerminalTheme, optional): The ``TerminalTheme`` object to use to style the terminal 

2597 clear (bool, optional): Clear record buffer after exporting. Defaults to ``True`` 

2598 code_format (str, optional): Format string used to generate the SVG. Rich will inject a number of variables 

2599 into the string in order to form the final SVG output. The default template used and the variables 

2600 injected by Rich can be found by inspecting the ``console.CONSOLE_SVG_FORMAT`` variable. 

2601 font_aspect_ratio (float, optional): The width to height ratio of the font used in the ``code_format`` 

2602 string. Defaults to 0.61, which is the width to height ratio of Fira Code (the default font). 

2603 If you aren't specifying a different font inside ``code_format``, you probably don't need this. 

2604 unique_id (str, optional): unique id that is used as the prefix for various elements (CSS styles, node 

2605 ids). If not set, this defaults to a computed value based on the recorded content. 

2606 """ 

2607 svg = self.export_svg( 

2608 title=title, 

2609 theme=theme, 

2610 clear=clear, 

2611 code_format=code_format, 

2612 font_aspect_ratio=font_aspect_ratio, 

2613 unique_id=unique_id, 

2614 ) 

2615 with open(path, "w", encoding="utf-8") as write_file: 

2616 write_file.write(svg) 

2617 

2618 

2619def _svg_hash(svg_main_code: str) -> str: 

2620 """Returns a unique hash for the given SVG main code. 

2621 

2622 Args: 

2623 svg_main_code (str): The content we're going to inject in the SVG envelope. 

2624 

2625 Returns: 

2626 str: a hash of the given content 

2627 """ 

2628 return str(zlib.adler32(svg_main_code.encode())) 

2629 

2630 

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

2632 console = Console(record=True) 

2633 

2634 console.log( 

2635 "JSONRPC [i]request[/i]", 

2636 5, 

2637 1.3, 

2638 True, 

2639 False, 

2640 None, 

2641 { 

2642 "jsonrpc": "2.0", 

2643 "method": "subtract", 

2644 "params": {"minuend": 42, "subtrahend": 23}, 

2645 "id": 3, 

2646 }, 

2647 ) 

2648 

2649 console.log("Hello, World!", "{'a': 1}", repr(console)) 

2650 

2651 console.print( 

2652 { 

2653 "name": None, 

2654 "empty": [], 

2655 "quiz": { 

2656 "sport": { 

2657 "answered": True, 

2658 "q1": { 

2659 "question": "Which one is correct team name in NBA?", 

2660 "options": [ 

2661 "New York Bulls", 

2662 "Los Angeles Kings", 

2663 "Golden State Warriors", 

2664 "Huston Rocket", 

2665 ], 

2666 "answer": "Huston Rocket", 

2667 }, 

2668 }, 

2669 "maths": { 

2670 "answered": False, 

2671 "q1": { 

2672 "question": "5 + 7 = ?", 

2673 "options": [10, 11, 12, 13], 

2674 "answer": 12, 

2675 }, 

2676 "q2": { 

2677 "question": "12 - 8 = ?", 

2678 "options": [1, 2, 3, 4], 

2679 "answer": 4, 

2680 }, 

2681 }, 

2682 }, 

2683 } 

2684 )