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

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

314 statements  

1from enum import IntEnum 

2from functools import lru_cache 

3from itertools import filterfalse 

4from logging import getLogger 

5from operator import attrgetter 

6from typing import ( 

7 TYPE_CHECKING, 

8 Dict, 

9 Iterable, 

10 List, 

11 NamedTuple, 

12 Optional, 

13 Sequence, 

14 Tuple, 

15 Type, 

16 Union, 

17) 

18 

19from .cells import ( 

20 _is_single_cell_widths, 

21 cached_cell_len, 

22 cell_len, 

23 get_character_cell_size, 

24 set_cell_size, 

25) 

26from .repr import Result, rich_repr 

27from .style import Style 

28 

29if TYPE_CHECKING: 

30 from .console import Console, ConsoleOptions, RenderResult 

31 

32log = getLogger("rich") 

33 

34 

35class ControlType(IntEnum): 

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

37 

38 BELL = 1 

39 CARRIAGE_RETURN = 2 

40 HOME = 3 

41 CLEAR = 4 

42 SHOW_CURSOR = 5 

43 HIDE_CURSOR = 6 

44 ENABLE_ALT_SCREEN = 7 

45 DISABLE_ALT_SCREEN = 8 

46 CURSOR_UP = 9 

47 CURSOR_DOWN = 10 

48 CURSOR_FORWARD = 11 

49 CURSOR_BACKWARD = 12 

50 CURSOR_MOVE_TO_COLUMN = 13 

51 CURSOR_MOVE_TO = 14 

52 ERASE_IN_LINE = 15 

53 SET_WINDOW_TITLE = 16 

54 

55 

56ControlCode = Union[ 

57 Tuple[ControlType], 

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

59 Tuple[ControlType, int, int], 

60] 

61 

62 

63@rich_repr() 

64class Segment(NamedTuple): 

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

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

67 

68 Args: 

69 text (str): A piece of text. 

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

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

72 

73 Attributes: 

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

75 """ 

76 

77 text: str 

78 style: Optional[Style] = None 

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

80 

81 @property 

82 def cell_length(self) -> int: 

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

84 

85 Returns: 

86 int: A number of cells. 

87 """ 

88 text, _style, control = self 

89 return 0 if control else cell_len(text) 

90 

91 def __rich_repr__(self) -> Result: 

92 yield self.text 

93 if self.control is None: 

94 if self.style is not None: 

95 yield self.style 

96 else: 

97 yield self.style 

98 yield self.control 

99 

100 def __bool__(self) -> bool: 

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

102 return bool(self.text) 

103 

104 @property 

105 def is_control(self) -> bool: 

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

107 return self.control is not None 

108 

109 @classmethod 

110 @lru_cache(1024 * 16) 

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

112 text, style, control = segment 

113 _Segment = Segment 

114 

115 cell_length = segment.cell_length 

116 if cut >= cell_length: 

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

118 

119 cell_size = get_character_cell_size 

120 

121 pos = int((cut / cell_length) * (len(text) - 1)) 

122 

123 before = text[:pos] 

124 cell_pos = cell_len(before) 

125 if cell_pos == cut: 

126 return ( 

127 _Segment(before, style, control), 

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

129 ) 

130 while pos < len(text): 

131 char = text[pos] 

132 pos += 1 

133 cell_pos += cell_size(char) 

134 before = text[:pos] 

135 if cell_pos == cut: 

136 return ( 

137 _Segment(before, style, control), 

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

139 ) 

140 if cell_pos > cut: 

141 return ( 

142 _Segment(before[: pos - 1] + " ", style, control), 

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

144 ) 

145 

146 raise AssertionError("Will never reach here") 

147 

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

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

150 

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

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

153 

154 Returns: 

155 Tuple[Segment, Segment]: Two segments. 

156 """ 

157 text, style, control = self 

158 

159 if _is_single_cell_widths(text): 

160 # Fast path with all 1 cell characters 

161 if cut >= len(text): 

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

163 return ( 

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

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

166 ) 

167 

168 return self._split_cells(self, cut) 

169 

170 @classmethod 

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

172 """Make a new line segment.""" 

173 return cls("\n") 

174 

175 @classmethod 

176 def apply_style( 

177 cls, 

178 segments: Iterable["Segment"], 

179 style: Optional[Style] = None, 

180 post_style: Optional[Style] = None, 

181 ) -> Iterable["Segment"]: 

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

183 

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

185 

186 Args: 

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

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

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

190 

191 Returns: 

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

193 """ 

194 result_segments = segments 

195 if style: 

196 apply = style.__add__ 

197 result_segments = ( 

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

199 for text, _style, control in result_segments 

200 ) 

201 if post_style: 

202 result_segments = ( 

203 cls( 

204 text, 

205 ( 

206 None 

207 if control 

208 else (_style + post_style if _style else post_style) 

209 ), 

210 control, 

211 ) 

212 for text, _style, control in result_segments 

213 ) 

214 return result_segments 

215 

216 @classmethod 

217 def filter_control( 

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

219 ) -> Iterable["Segment"]: 

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

221 

222 Args: 

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

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

225 

226 Returns: 

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

228 

229 """ 

230 if is_control: 

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

232 else: 

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

234 

235 @classmethod 

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

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

238 

239 Args: 

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

241 

242 Yields: 

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

244 """ 

245 line: List[Segment] = [] 

246 append = line.append 

247 

248 for segment in segments: 

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

250 text, style, _ = segment 

251 while text: 

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

253 if _text: 

254 append(cls(_text, style)) 

255 if new_line: 

256 yield line 

257 line = [] 

258 append = line.append 

259 else: 

260 append(segment) 

261 if line: 

262 yield line 

263 

264 @classmethod 

265 def split_and_crop_lines( 

266 cls, 

267 segments: Iterable["Segment"], 

268 length: int, 

269 style: Optional[Style] = None, 

270 pad: bool = True, 

271 include_new_lines: bool = True, 

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

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

274 

275 Args: 

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

277 generated from console.render. 

278 length (int): Desired line length. 

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

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

281 

282 Returns: 

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

284 """ 

285 line: List[Segment] = [] 

286 append = line.append 

287 

288 adjust_line_length = cls.adjust_line_length 

289 new_line_segment = cls("\n") 

290 

291 for segment in segments: 

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

293 text, segment_style, _ = segment 

294 while text: 

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

296 if _text: 

297 append(cls(_text, segment_style)) 

298 if new_line: 

299 cropped_line = adjust_line_length( 

300 line, length, style=style, pad=pad 

301 ) 

302 if include_new_lines: 

303 cropped_line.append(new_line_segment) 

304 yield cropped_line 

305 line.clear() 

306 else: 

307 append(segment) 

308 if line: 

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

310 

311 @classmethod 

312 def adjust_line_length( 

313 cls, 

314 line: List["Segment"], 

315 length: int, 

316 style: Optional[Style] = None, 

317 pad: bool = True, 

318 ) -> List["Segment"]: 

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

320 

321 Args: 

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

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

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

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

326 

327 Returns: 

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

329 """ 

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

331 new_line: List[Segment] 

332 

333 if line_length < length: 

334 if pad: 

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

336 else: 

337 new_line = line[:] 

338 elif line_length > length: 

339 new_line = [] 

340 append = new_line.append 

341 line_length = 0 

342 for segment in line: 

343 segment_length = segment.cell_length 

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

345 append(segment) 

346 line_length += segment_length 

347 else: 

348 text, segment_style, _ = segment 

349 text = set_cell_size(text, length - line_length) 

350 append(cls(text, segment_style)) 

351 break 

352 else: 

353 new_line = line[:] 

354 return new_line 

355 

356 @classmethod 

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

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

359 

360 Args: 

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

362 

363 Returns: 

364 int: The length of the line. 

365 """ 

366 _cell_len = cell_len 

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

368 

369 @classmethod 

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

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

372 

373 Args: 

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

375 

376 Returns: 

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

378 """ 

379 get_line_length = cls.get_line_length 

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

381 return (max_width, len(lines)) 

382 

383 @classmethod 

384 def set_shape( 

385 cls, 

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

387 width: int, 

388 height: Optional[int] = None, 

389 style: Optional[Style] = None, 

390 new_lines: bool = False, 

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

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

393 

394 Args: 

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

396 width (int): Desired width. 

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

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

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

400 

401 Returns: 

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

403 """ 

404 _height = height or len(lines) 

405 

406 blank = ( 

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

408 ) 

409 

410 adjust_line_length = cls.adjust_line_length 

411 shaped_lines = lines[:_height] 

412 shaped_lines[:] = [ 

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

414 ] 

415 if len(shaped_lines) < _height: 

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

417 return shaped_lines 

418 

419 @classmethod 

420 def align_top( 

421 cls: Type["Segment"], 

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

423 width: int, 

424 height: int, 

425 style: Style, 

426 new_lines: bool = False, 

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

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

429 

430 Args: 

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

432 width (int): Desired width. 

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

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

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

436 

437 Returns: 

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

439 """ 

440 extra_lines = height - len(lines) 

441 if not extra_lines: 

442 return lines[:] 

443 lines = lines[:height] 

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

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

446 return lines 

447 

448 @classmethod 

449 def align_bottom( 

450 cls: Type["Segment"], 

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

452 width: int, 

453 height: int, 

454 style: Style, 

455 new_lines: bool = False, 

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

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

458 

459 Args: 

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

461 width (int): Desired width. 

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

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

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

465 

466 Returns: 

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

468 """ 

469 extra_lines = height - len(lines) 

470 if not extra_lines: 

471 return lines[:] 

472 lines = lines[:height] 

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

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

475 return lines 

476 

477 @classmethod 

478 def align_middle( 

479 cls: Type["Segment"], 

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

481 width: int, 

482 height: int, 

483 style: Style, 

484 new_lines: bool = False, 

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

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

487 

488 Args: 

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

490 width (int): Desired width. 

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

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

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

494 

495 Returns: 

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

497 """ 

498 extra_lines = height - len(lines) 

499 if not extra_lines: 

500 return lines[:] 

501 lines = lines[:height] 

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

503 top_lines = extra_lines // 2 

504 bottom_lines = extra_lines - top_lines 

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

506 return lines 

507 

508 @classmethod 

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

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

511 

512 Args: 

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

514 

515 Returns: 

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

517 """ 

518 iter_segments = iter(segments) 

519 try: 

520 last_segment = next(iter_segments) 

521 except StopIteration: 

522 return 

523 

524 _Segment = Segment 

525 for segment in iter_segments: 

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

527 last_segment = _Segment( 

528 last_segment.text + segment.text, last_segment.style 

529 ) 

530 else: 

531 yield last_segment 

532 last_segment = segment 

533 yield last_segment 

534 

535 @classmethod 

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

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

538 

539 Args: 

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

541 

542 Yields: 

543 Segment: Segments with link removed. 

544 """ 

545 for segment in segments: 

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

547 yield segment 

548 else: 

549 text, style, _control = segment 

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

551 

552 @classmethod 

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

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

555 

556 Args: 

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

558 

559 Yields: 

560 Segment: Segments with styles replace with None 

561 """ 

562 for text, _style, control in segments: 

563 yield cls(text, None, control) 

564 

565 @classmethod 

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

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

568 

569 Args: 

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

571 

572 Yields: 

573 Segment: Segments with colorless style. 

574 """ 

575 

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

577 for text, style, control in segments: 

578 if style: 

579 colorless_style = cache.get(style) 

580 if colorless_style is None: 

581 colorless_style = style.without_color 

582 cache[style] = colorless_style 

583 yield cls(text, colorless_style, control) 

584 else: 

585 yield cls(text, None, control) 

586 

587 @classmethod 

588 def divide( 

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

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

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

592 

593 Args: 

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

595 

596 Yields: 

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

598 """ 

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

600 add_segment = split_segments.append 

601 

602 iter_cuts = iter(cuts) 

603 

604 while True: 

605 cut = next(iter_cuts, -1) 

606 if cut == -1: 

607 return [] 

608 if cut != 0: 

609 break 

610 yield [] 

611 pos = 0 

612 

613 segments_clear = split_segments.clear 

614 segments_copy = split_segments.copy 

615 

616 _cell_len = cached_cell_len 

617 for segment in segments: 

618 text, _style, control = segment 

619 while text: 

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

621 if end_pos < cut: 

622 add_segment(segment) 

623 pos = end_pos 

624 break 

625 

626 if end_pos == cut: 

627 add_segment(segment) 

628 yield segments_copy() 

629 segments_clear() 

630 pos = end_pos 

631 

632 cut = next(iter_cuts, -1) 

633 if cut == -1: 

634 if split_segments: 

635 yield segments_copy() 

636 return 

637 

638 break 

639 

640 else: 

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

642 text, _style, control = segment 

643 add_segment(before) 

644 yield segments_copy() 

645 segments_clear() 

646 pos = cut 

647 

648 cut = next(iter_cuts, -1) 

649 if cut == -1: 

650 if split_segments: 

651 yield segments_copy() 

652 return 

653 

654 yield segments_copy() 

655 

656 

657class Segments: 

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

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

660 

661 Args: 

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

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

664 """ 

665 

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

667 self.segments = list(segments) 

668 self.new_lines = new_lines 

669 

670 def __rich_console__( 

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

672 ) -> "RenderResult": 

673 if self.new_lines: 

674 line = Segment.line() 

675 for segment in self.segments: 

676 yield segment 

677 yield line 

678 else: 

679 yield from self.segments 

680 

681 

682class SegmentLines: 

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

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

685 in rendering process. 

686 

687 Args: 

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

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

690 """ 

691 self.lines = list(lines) 

692 self.new_lines = new_lines 

693 

694 def __rich_console__( 

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

696 ) -> "RenderResult": 

697 if self.new_lines: 

698 new_line = Segment.line() 

699 for line in self.lines: 

700 yield from line 

701 yield new_line 

702 else: 

703 for line in self.lines: 

704 yield from line 

705 

706 

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

708 from rich.console import Console 

709 from rich.syntax import Syntax 

710 from rich.text import Text 

711 

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

713console = Console() 

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

715console.print(text)""" 

716 

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

718 

719 console = Console() 

720 

721 console.rule("rich.Segment") 

722 console.print( 

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

724 ) 

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

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

727 console.print() 

728 console.print( 

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

730 ) 

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

732 console.print(fragments) 

733 console.print() 

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

735 console.print(text) 

736 console.print( 

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

738 )