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

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

331 statements  

1from enum import IntEnum 

2from functools import lru_cache 

3from itertools import filterfalse 

4from operator import attrgetter 

5from typing import ( 

6 TYPE_CHECKING, 

7 Dict, 

8 Iterable, 

9 List, 

10 NamedTuple, 

11 Optional, 

12 Sequence, 

13 Tuple, 

14 Type, 

15 Union, 

16) 

17 

18from .cells import ( 

19 _is_single_cell_widths, 

20 cached_cell_len, 

21 cell_len, 

22 get_character_cell_size, 

23 set_cell_size, 

24) 

25from .repr import Result, rich_repr 

26from .style import Style 

27 

28if TYPE_CHECKING: 

29 from .console import Console, ConsoleOptions, RenderResult 

30 

31 

32class ControlType(IntEnum): 

33 """Non-printable control codes which typically translate to ANSI codes.""" 

34 

35 BELL = 1 

36 CARRIAGE_RETURN = 2 

37 HOME = 3 

38 CLEAR = 4 

39 SHOW_CURSOR = 5 

40 HIDE_CURSOR = 6 

41 ENABLE_ALT_SCREEN = 7 

42 DISABLE_ALT_SCREEN = 8 

43 CURSOR_UP = 9 

44 CURSOR_DOWN = 10 

45 CURSOR_FORWARD = 11 

46 CURSOR_BACKWARD = 12 

47 CURSOR_MOVE_TO_COLUMN = 13 

48 CURSOR_MOVE_TO = 14 

49 ERASE_IN_LINE = 15 

50 SET_WINDOW_TITLE = 16 

51 

52 

53ControlCode = Union[ 

54 Tuple[ControlType], 

55 Tuple[ControlType, Union[int, str]], 

56 Tuple[ControlType, int, int], 

57] 

58 

59 

60@rich_repr() 

61class Segment(NamedTuple): 

62 """A piece of text with associated style. Segments are produced by the Console render process and 

63 are ultimately converted in to strings to be written to the terminal. 

64 

65 Args: 

66 text (str): A piece of text. 

67 style (:class:`~rich.style.Style`, optional): An optional style to apply to the text. 

68 control (Tuple[ControlCode], optional): Optional sequence of control codes. 

69 

70 Attributes: 

71 cell_length (int): The cell length of this Segment. 

72 """ 

73 

74 text: str 

75 style: Optional[Style] = None 

76 control: Optional[Sequence[ControlCode]] = None 

77 

78 @property 

79 def cell_length(self) -> int: 

80 """The number of terminal cells required to display self.text. 

81 

82 Returns: 

83 int: A number of cells. 

84 """ 

85 text, _style, control = self 

86 return 0 if control else cell_len(text) 

87 

88 def __rich_repr__(self) -> Result: 

89 yield self.text 

90 if self.control is None: 

91 if self.style is not None: 

92 yield self.style 

93 else: 

94 yield self.style 

95 yield self.control 

96 

97 def __bool__(self) -> bool: 

98 """Check if the segment contains text.""" 

99 return bool(self.text) 

100 

101 @property 

102 def is_control(self) -> bool: 

103 """Check if the segment contains control codes.""" 

104 return self.control is not None 

105 

106 @classmethod 

107 @lru_cache(1024 * 16) 

108 def _split_cells(cls, segment: "Segment", cut: int) -> Tuple["Segment", "Segment"]: 

109 """Split a segment in to two at a given cell position. 

110 

111 Note that splitting a double-width character, may result in that character turning 

112 into two spaces. 

113 

114 Args: 

115 segment (Segment): A segment to split. 

116 cut (int): A cell position to cut on. 

117 

118 Returns: 

119 A tuple of two segments. 

120 """ 

121 text, style, control = segment 

122 _Segment = Segment 

123 cell_length = segment.cell_length 

124 if cut >= cell_length: 

125 return segment, _Segment("", style, control) 

126 

127 cell_size = get_character_cell_size 

128 

129 pos = int((cut / cell_length) * len(text)) 

130 

131 while True: 

132 before = text[:pos] 

133 cell_pos = cell_len(before) 

134 out_by = cell_pos - cut 

135 if not out_by: 

136 return ( 

137 _Segment(before, style, control), 

138 _Segment(text[pos:], style, control), 

139 ) 

140 if out_by == -1 and cell_size(text[pos]) == 2: 

141 return ( 

142 _Segment(text[:pos] + " ", style, control), 

143 _Segment(" " + text[pos + 1 :], style, control), 

144 ) 

145 if out_by == +1 and cell_size(text[pos - 1]) == 2: 

146 return ( 

147 _Segment(text[: pos - 1] + " ", style, control), 

148 _Segment(" " + text[pos:], style, control), 

149 ) 

150 if cell_pos < cut: 

151 pos += 1 

152 else: 

153 pos -= 1 

154 

155 def split_cells(self, cut: int) -> Tuple["Segment", "Segment"]: 

156 """Split segment in to two segments at the specified column. 

157 

158 If the cut point falls in the middle of a 2-cell wide character then it is replaced 

159 by two spaces, to preserve the display width of the parent segment. 

160 

161 Args: 

162 cut (int): Offset within the segment to cut. 

163 

164 Returns: 

165 Tuple[Segment, Segment]: Two segments. 

166 """ 

167 text, style, control = self 

168 assert cut >= 0 

169 

170 if _is_single_cell_widths(text): 

171 # Fast path with all 1 cell characters 

172 if cut >= len(text): 

173 return self, Segment("", style, control) 

174 return ( 

175 Segment(text[:cut], style, control), 

176 Segment(text[cut:], style, control), 

177 ) 

178 

179 return self._split_cells(self, cut) 

180 

181 @classmethod 

182 def line(cls) -> "Segment": 

183 """Make a new line segment.""" 

184 return cls("\n") 

185 

186 @classmethod 

187 def apply_style( 

188 cls, 

189 segments: Iterable["Segment"], 

190 style: Optional[Style] = None, 

191 post_style: Optional[Style] = None, 

192 ) -> Iterable["Segment"]: 

193 """Apply style(s) to an iterable of segments. 

194 

195 Returns an iterable of segments where the style is replaced by ``style + segment.style + post_style``. 

196 

197 Args: 

198 segments (Iterable[Segment]): Segments to process. 

199 style (Style, optional): Base style. Defaults to None. 

200 post_style (Style, optional): Style to apply on top of segment style. Defaults to None. 

201 

202 Returns: 

203 Iterable[Segments]: A new iterable of segments (possibly the same iterable). 

204 """ 

205 result_segments = segments 

206 if style: 

207 apply = style.__add__ 

208 result_segments = ( 

209 cls(text, None if control else apply(_style), control) 

210 for text, _style, control in result_segments 

211 ) 

212 if post_style: 

213 result_segments = ( 

214 cls( 

215 text, 

216 ( 

217 None 

218 if control 

219 else (_style + post_style if _style else post_style) 

220 ), 

221 control, 

222 ) 

223 for text, _style, control in result_segments 

224 ) 

225 return result_segments 

226 

227 @classmethod 

228 def filter_control( 

229 cls, segments: Iterable["Segment"], is_control: bool = False 

230 ) -> Iterable["Segment"]: 

231 """Filter segments by ``is_control`` attribute. 

232 

233 Args: 

234 segments (Iterable[Segment]): An iterable of Segment instances. 

235 is_control (bool, optional): is_control flag to match in search. 

236 

237 Returns: 

238 Iterable[Segment]: And iterable of Segment instances. 

239 

240 """ 

241 if is_control: 

242 return filter(attrgetter("control"), segments) 

243 else: 

244 return filterfalse(attrgetter("control"), segments) 

245 

246 @classmethod 

247 def split_lines(cls, segments: Iterable["Segment"]) -> Iterable[List["Segment"]]: 

248 """Split a sequence of segments in to a list of lines. 

249 

250 Args: 

251 segments (Iterable[Segment]): Segments potentially containing line feeds. 

252 

253 Yields: 

254 Iterable[List[Segment]]: Iterable of segment lists, one per line. 

255 """ 

256 line: List[Segment] = [] 

257 append = line.append 

258 

259 for segment in segments: 

260 if "\n" in segment.text and not segment.control: 

261 text, style, _ = segment 

262 while text: 

263 _text, new_line, text = text.partition("\n") 

264 if _text: 

265 append(cls(_text, style)) 

266 if new_line: 

267 yield line 

268 line = [] 

269 append = line.append 

270 else: 

271 append(segment) 

272 if line: 

273 yield line 

274 

275 @classmethod 

276 def split_lines_terminator( 

277 cls, segments: Iterable["Segment"] 

278 ) -> Iterable[Tuple[List["Segment"], bool]]: 

279 """Split a sequence of segments in to a list of lines and a boolean to indicate if there was a new line. 

280 

281 Args: 

282 segments (Iterable[Segment]): Segments potentially containing line feeds. 

283 

284 Yields: 

285 Iterable[List[Segment]]: Iterable of segment lists, one per line. 

286 """ 

287 line: List[Segment] = [] 

288 append = line.append 

289 

290 for segment in segments: 

291 if "\n" in segment.text and not segment.control: 

292 text, style, _ = segment 

293 while text: 

294 _text, new_line, text = text.partition("\n") 

295 if _text: 

296 append(cls(_text, style)) 

297 if new_line: 

298 yield (line, True) 

299 line = [] 

300 append = line.append 

301 else: 

302 append(segment) 

303 if line: 

304 yield (line, False) 

305 

306 @classmethod 

307 def split_and_crop_lines( 

308 cls, 

309 segments: Iterable["Segment"], 

310 length: int, 

311 style: Optional[Style] = None, 

312 pad: bool = True, 

313 include_new_lines: bool = True, 

314 ) -> Iterable[List["Segment"]]: 

315 """Split segments in to lines, and crop lines greater than a given length. 

316 

317 Args: 

318 segments (Iterable[Segment]): An iterable of segments, probably 

319 generated from console.render. 

320 length (int): Desired line length. 

321 style (Style, optional): Style to use for any padding. 

322 pad (bool): Enable padding of lines that are less than `length`. 

323 

324 Returns: 

325 Iterable[List[Segment]]: An iterable of lines of segments. 

326 """ 

327 line: List[Segment] = [] 

328 append = line.append 

329 

330 adjust_line_length = cls.adjust_line_length 

331 new_line_segment = cls("\n") 

332 

333 for segment in segments: 

334 if "\n" in segment.text and not segment.control: 

335 text, segment_style, _ = segment 

336 while text: 

337 _text, new_line, text = text.partition("\n") 

338 if _text: 

339 append(cls(_text, segment_style)) 

340 if new_line: 

341 cropped_line = adjust_line_length( 

342 line, length, style=style, pad=pad 

343 ) 

344 if include_new_lines: 

345 cropped_line.append(new_line_segment) 

346 yield cropped_line 

347 line.clear() 

348 else: 

349 append(segment) 

350 if line: 

351 yield adjust_line_length(line, length, style=style, pad=pad) 

352 

353 @classmethod 

354 def adjust_line_length( 

355 cls, 

356 line: List["Segment"], 

357 length: int, 

358 style: Optional[Style] = None, 

359 pad: bool = True, 

360 ) -> List["Segment"]: 

361 """Adjust a line to a given width (cropping or padding as required). 

362 

363 Args: 

364 segments (Iterable[Segment]): A list of segments in a single line. 

365 length (int): The desired width of the line. 

366 style (Style, optional): The style of padding if used (space on the end). Defaults to None. 

367 pad (bool, optional): Pad lines with spaces if they are shorter than `length`. Defaults to True. 

368 

369 Returns: 

370 List[Segment]: A line of segments with the desired length. 

371 """ 

372 line_length = sum(segment.cell_length for segment in line) 

373 new_line: List[Segment] 

374 

375 if line_length < length: 

376 if pad: 

377 new_line = line + [cls(" " * (length - line_length), style)] 

378 else: 

379 new_line = line[:] 

380 elif line_length > length: 

381 new_line = [] 

382 append = new_line.append 

383 line_length = 0 

384 for segment in line: 

385 segment_length = segment.cell_length 

386 if line_length + segment_length < length or segment.control: 

387 append(segment) 

388 line_length += segment_length 

389 else: 

390 text, segment_style, _ = segment 

391 text = set_cell_size(text, length - line_length) 

392 append(cls(text, segment_style)) 

393 break 

394 else: 

395 new_line = line[:] 

396 return new_line 

397 

398 @classmethod 

399 def get_line_length(cls, line: List["Segment"]) -> int: 

400 """Get the length of list of segments. 

401 

402 Args: 

403 line (List[Segment]): A line encoded as a list of Segments (assumes no '\\\\n' characters), 

404 

405 Returns: 

406 int: The length of the line. 

407 """ 

408 _cell_len = cell_len 

409 return sum(_cell_len(text) for text, style, control in line if not control) 

410 

411 @classmethod 

412 def get_shape(cls, lines: List[List["Segment"]]) -> Tuple[int, int]: 

413 """Get the shape (enclosing rectangle) of a list of lines. 

414 

415 Args: 

416 lines (List[List[Segment]]): A list of lines (no '\\\\n' characters). 

417 

418 Returns: 

419 Tuple[int, int]: Width and height in characters. 

420 """ 

421 get_line_length = cls.get_line_length 

422 max_width = max(get_line_length(line) for line in lines) if lines else 0 

423 return (max_width, len(lines)) 

424 

425 @classmethod 

426 def set_shape( 

427 cls, 

428 lines: List[List["Segment"]], 

429 width: int, 

430 height: Optional[int] = None, 

431 style: Optional[Style] = None, 

432 new_lines: bool = False, 

433 ) -> List[List["Segment"]]: 

434 """Set the shape of a list of lines (enclosing rectangle). 

435 

436 Args: 

437 lines (List[List[Segment]]): A list of lines. 

438 width (int): Desired width. 

439 height (int, optional): Desired height or None for no change. 

440 style (Style, optional): Style of any padding added. 

441 new_lines (bool, optional): Padded lines should include "\n". Defaults to False. 

442 

443 Returns: 

444 List[List[Segment]]: New list of lines. 

445 """ 

446 _height = height or len(lines) 

447 

448 blank = ( 

449 [cls(" " * width + "\n", style)] if new_lines else [cls(" " * width, style)] 

450 ) 

451 

452 adjust_line_length = cls.adjust_line_length 

453 shaped_lines = lines[:_height] 

454 shaped_lines[:] = [ 

455 adjust_line_length(line, width, style=style) for line in lines 

456 ] 

457 if len(shaped_lines) < _height: 

458 shaped_lines.extend([blank] * (_height - len(shaped_lines))) 

459 return shaped_lines 

460 

461 @classmethod 

462 def align_top( 

463 cls: Type["Segment"], 

464 lines: List[List["Segment"]], 

465 width: int, 

466 height: int, 

467 style: Style, 

468 new_lines: bool = False, 

469 ) -> List[List["Segment"]]: 

470 """Aligns lines to top (adds extra lines to bottom as required). 

471 

472 Args: 

473 lines (List[List[Segment]]): A list of lines. 

474 width (int): Desired width. 

475 height (int, optional): Desired height or None for no change. 

476 style (Style): Style of any padding added. 

477 new_lines (bool, optional): Padded lines should include "\n". Defaults to False. 

478 

479 Returns: 

480 List[List[Segment]]: New list of lines. 

481 """ 

482 extra_lines = height - len(lines) 

483 if not extra_lines: 

484 return lines[:] 

485 lines = lines[:height] 

486 blank = cls(" " * width + "\n", style) if new_lines else cls(" " * width, style) 

487 lines = lines + [[blank]] * extra_lines 

488 return lines 

489 

490 @classmethod 

491 def align_bottom( 

492 cls: Type["Segment"], 

493 lines: List[List["Segment"]], 

494 width: int, 

495 height: int, 

496 style: Style, 

497 new_lines: bool = False, 

498 ) -> List[List["Segment"]]: 

499 """Aligns render to bottom (adds extra lines above as required). 

500 

501 Args: 

502 lines (List[List[Segment]]): A list of lines. 

503 width (int): Desired width. 

504 height (int, optional): Desired height or None for no change. 

505 style (Style): Style of any padding added. Defaults to None. 

506 new_lines (bool, optional): Padded lines should include "\n". Defaults to False. 

507 

508 Returns: 

509 List[List[Segment]]: New list of lines. 

510 """ 

511 extra_lines = height - len(lines) 

512 if not extra_lines: 

513 return lines[:] 

514 lines = lines[:height] 

515 blank = cls(" " * width + "\n", style) if new_lines else cls(" " * width, style) 

516 lines = [[blank]] * extra_lines + lines 

517 return lines 

518 

519 @classmethod 

520 def align_middle( 

521 cls: Type["Segment"], 

522 lines: List[List["Segment"]], 

523 width: int, 

524 height: int, 

525 style: Style, 

526 new_lines: bool = False, 

527 ) -> List[List["Segment"]]: 

528 """Aligns lines to middle (adds extra lines to above and below as required). 

529 

530 Args: 

531 lines (List[List[Segment]]): A list of lines. 

532 width (int): Desired width. 

533 height (int, optional): Desired height or None for no change. 

534 style (Style): Style of any padding added. 

535 new_lines (bool, optional): Padded lines should include "\n". Defaults to False. 

536 

537 Returns: 

538 List[List[Segment]]: New list of lines. 

539 """ 

540 extra_lines = height - len(lines) 

541 if not extra_lines: 

542 return lines[:] 

543 lines = lines[:height] 

544 blank = cls(" " * width + "\n", style) if new_lines else cls(" " * width, style) 

545 top_lines = extra_lines // 2 

546 bottom_lines = extra_lines - top_lines 

547 lines = [[blank]] * top_lines + lines + [[blank]] * bottom_lines 

548 return lines 

549 

550 @classmethod 

551 def simplify(cls, segments: Iterable["Segment"]) -> Iterable["Segment"]: 

552 """Simplify an iterable of segments by combining contiguous segments with the same style. 

553 

554 Args: 

555 segments (Iterable[Segment]): An iterable of segments. 

556 

557 Returns: 

558 Iterable[Segment]: A possibly smaller iterable of segments that will render the same way. 

559 """ 

560 iter_segments = iter(segments) 

561 try: 

562 last_segment = next(iter_segments) 

563 except StopIteration: 

564 return 

565 

566 _Segment = Segment 

567 for segment in iter_segments: 

568 if last_segment.style == segment.style and not segment.control: 

569 last_segment = _Segment( 

570 last_segment.text + segment.text, last_segment.style 

571 ) 

572 else: 

573 yield last_segment 

574 last_segment = segment 

575 yield last_segment 

576 

577 @classmethod 

578 def strip_links(cls, segments: Iterable["Segment"]) -> Iterable["Segment"]: 

579 """Remove all links from an iterable of styles. 

580 

581 Args: 

582 segments (Iterable[Segment]): An iterable segments. 

583 

584 Yields: 

585 Segment: Segments with link removed. 

586 """ 

587 for segment in segments: 

588 if segment.control or segment.style is None: 

589 yield segment 

590 else: 

591 text, style, _control = segment 

592 yield cls(text, style.update_link(None) if style else None) 

593 

594 @classmethod 

595 def strip_styles(cls, segments: Iterable["Segment"]) -> Iterable["Segment"]: 

596 """Remove all styles from an iterable of segments. 

597 

598 Args: 

599 segments (Iterable[Segment]): An iterable segments. 

600 

601 Yields: 

602 Segment: Segments with styles replace with None 

603 """ 

604 for text, _style, control in segments: 

605 yield cls(text, None, control) 

606 

607 @classmethod 

608 def remove_color(cls, segments: Iterable["Segment"]) -> Iterable["Segment"]: 

609 """Remove all color from an iterable of segments. 

610 

611 Args: 

612 segments (Iterable[Segment]): An iterable segments. 

613 

614 Yields: 

615 Segment: Segments with colorless style. 

616 """ 

617 

618 cache: Dict[Style, Style] = {} 

619 for text, style, control in segments: 

620 if style: 

621 colorless_style = cache.get(style) 

622 if colorless_style is None: 

623 colorless_style = style.without_color 

624 cache[style] = colorless_style 

625 yield cls(text, colorless_style, control) 

626 else: 

627 yield cls(text, None, control) 

628 

629 @classmethod 

630 def divide( 

631 cls, segments: Iterable["Segment"], cuts: Iterable[int] 

632 ) -> Iterable[List["Segment"]]: 

633 """Divides an iterable of segments in to portions. 

634 

635 Args: 

636 cuts (Iterable[int]): Cell positions where to divide. 

637 

638 Yields: 

639 [Iterable[List[Segment]]]: An iterable of Segments in List. 

640 """ 

641 split_segments: List["Segment"] = [] 

642 add_segment = split_segments.append 

643 

644 iter_cuts = iter(cuts) 

645 

646 while True: 

647 cut = next(iter_cuts, -1) 

648 if cut == -1: 

649 return 

650 if cut != 0: 

651 break 

652 yield [] 

653 pos = 0 

654 

655 segments_clear = split_segments.clear 

656 segments_copy = split_segments.copy 

657 

658 _cell_len = cached_cell_len 

659 for segment in segments: 

660 text, _style, control = segment 

661 while text: 

662 end_pos = pos if control else pos + _cell_len(text) 

663 if end_pos < cut: 

664 add_segment(segment) 

665 pos = end_pos 

666 break 

667 

668 if end_pos == cut: 

669 add_segment(segment) 

670 yield segments_copy() 

671 segments_clear() 

672 pos = end_pos 

673 

674 cut = next(iter_cuts, -1) 

675 if cut == -1: 

676 if split_segments: 

677 yield segments_copy() 

678 return 

679 

680 break 

681 

682 else: 

683 before, segment = segment.split_cells(cut - pos) 

684 text, _style, control = segment 

685 add_segment(before) 

686 yield segments_copy() 

687 segments_clear() 

688 pos = cut 

689 

690 cut = next(iter_cuts, -1) 

691 if cut == -1: 

692 if split_segments: 

693 yield segments_copy() 

694 return 

695 

696 yield segments_copy() 

697 

698 

699class Segments: 

700 """A simple renderable to render an iterable of segments. This class may be useful if 

701 you want to print segments outside of a __rich_console__ method. 

702 

703 Args: 

704 segments (Iterable[Segment]): An iterable of segments. 

705 new_lines (bool, optional): Add new lines between segments. Defaults to False. 

706 """ 

707 

708 def __init__(self, segments: Iterable[Segment], new_lines: bool = False) -> None: 

709 self.segments = list(segments) 

710 self.new_lines = new_lines 

711 

712 def __rich_console__( 

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

714 ) -> "RenderResult": 

715 if self.new_lines: 

716 line = Segment.line() 

717 for segment in self.segments: 

718 yield segment 

719 yield line 

720 else: 

721 yield from self.segments 

722 

723 

724class SegmentLines: 

725 def __init__(self, lines: Iterable[List[Segment]], new_lines: bool = False) -> None: 

726 """A simple renderable containing a number of lines of segments. May be used as an intermediate 

727 in rendering process. 

728 

729 Args: 

730 lines (Iterable[List[Segment]]): Lists of segments forming lines. 

731 new_lines (bool, optional): Insert new lines after each line. Defaults to False. 

732 """ 

733 self.lines = list(lines) 

734 self.new_lines = new_lines 

735 

736 def __rich_console__( 

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

738 ) -> "RenderResult": 

739 if self.new_lines: 

740 new_line = Segment.line() 

741 for line in self.lines: 

742 yield from line 

743 yield new_line 

744 else: 

745 for line in self.lines: 

746 yield from line 

747 

748 

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

750 from rich.console import Console 

751 from rich.syntax import Syntax 

752 from rich.text import Text 

753 

754 code = """from rich.console import Console 

755console = Console() 

756text = Text.from_markup("Hello, [bold magenta]World[/]!") 

757console.print(text)""" 

758 

759 text = Text.from_markup("Hello, [bold magenta]World[/]!") 

760 

761 console = Console() 

762 

763 console.rule("rich.Segment") 

764 console.print( 

765 "A Segment is the last step in the Rich render process before generating text with ANSI codes." 

766 ) 

767 console.print("\nConsider the following code:\n") 

768 console.print(Syntax(code, "python", line_numbers=True)) 

769 console.print() 

770 console.print( 

771 "When you call [b]print()[/b], Rich [i]renders[/i] the object in to the following:\n" 

772 ) 

773 fragments = list(console.render(text)) 

774 console.print(fragments) 

775 console.print() 

776 console.print("The Segments are then processed to produce the following output:\n") 

777 console.print(text) 

778 console.print( 

779 "\nYou will only need to know this if you are implementing your own Rich renderables." 

780 )