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

314 statements  

« prev     ^ index     » next       coverage.py v7.2.7, created at 2023-06-07 06:35 +0000

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 

113 text, style, control = segment 

114 _Segment = Segment 

115 

116 cell_length = segment.cell_length 

117 if cut >= cell_length: 

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

119 

120 cell_size = get_character_cell_size 

121 

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

123 

124 before = text[:pos] 

125 cell_pos = cell_len(before) 

126 if cell_pos == cut: 

127 return ( 

128 _Segment(before, style, control), 

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

130 ) 

131 while pos < len(text): 

132 char = text[pos] 

133 pos += 1 

134 cell_pos += cell_size(char) 

135 before = text[:pos] 

136 if cell_pos == cut: 

137 return ( 

138 _Segment(before, style, control), 

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

140 ) 

141 if cell_pos > cut: 

142 return ( 

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

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

145 ) 

146 

147 raise AssertionError("Will never reach here") 

148 

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

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

151 

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

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

154 

155 Returns: 

156 Tuple[Segment, Segment]: Two segments. 

157 """ 

158 text, style, control = self 

159 

160 if _is_single_cell_widths(text): 

161 # Fast path with all 1 cell characters 

162 if cut >= len(text): 

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

164 return ( 

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

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

167 ) 

168 

169 return self._split_cells(self, cut) 

170 

171 @classmethod 

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

173 """Make a new line segment.""" 

174 return cls("\n") 

175 

176 @classmethod 

177 def apply_style( 

178 cls, 

179 segments: Iterable["Segment"], 

180 style: Optional[Style] = None, 

181 post_style: Optional[Style] = None, 

182 ) -> Iterable["Segment"]: 

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

184 

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

186 

187 Args: 

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

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

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

191 

192 Returns: 

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

194 """ 

195 result_segments = segments 

196 if style: 

197 apply = style.__add__ 

198 result_segments = ( 

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

200 for text, _style, control in result_segments 

201 ) 

202 if post_style: 

203 result_segments = ( 

204 cls( 

205 text, 

206 ( 

207 None 

208 if control 

209 else (_style + post_style if _style else post_style) 

210 ), 

211 control, 

212 ) 

213 for text, _style, control in result_segments 

214 ) 

215 return result_segments 

216 

217 @classmethod 

218 def filter_control( 

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

220 ) -> Iterable["Segment"]: 

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

222 

223 Args: 

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

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

226 

227 Returns: 

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

229 

230 """ 

231 if is_control: 

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

233 else: 

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

235 

236 @classmethod 

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

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

239 

240 Args: 

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

242 

243 Yields: 

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

245 """ 

246 line: List[Segment] = [] 

247 append = line.append 

248 

249 for segment in segments: 

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

251 text, style, _ = segment 

252 while text: 

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

254 if _text: 

255 append(cls(_text, style)) 

256 if new_line: 

257 yield line 

258 line = [] 

259 append = line.append 

260 else: 

261 append(segment) 

262 if line: 

263 yield line 

264 

265 @classmethod 

266 def split_and_crop_lines( 

267 cls, 

268 segments: Iterable["Segment"], 

269 length: int, 

270 style: Optional[Style] = None, 

271 pad: bool = True, 

272 include_new_lines: bool = True, 

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

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

275 

276 Args: 

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

278 generated from console.render. 

279 length (int): Desired line length. 

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

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

282 

283 Returns: 

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

285 """ 

286 line: List[Segment] = [] 

287 append = line.append 

288 

289 adjust_line_length = cls.adjust_line_length 

290 new_line_segment = cls("\n") 

291 

292 for segment in segments: 

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

294 text, segment_style, _ = segment 

295 while text: 

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

297 if _text: 

298 append(cls(_text, segment_style)) 

299 if new_line: 

300 cropped_line = adjust_line_length( 

301 line, length, style=style, pad=pad 

302 ) 

303 if include_new_lines: 

304 cropped_line.append(new_line_segment) 

305 yield cropped_line 

306 line.clear() 

307 else: 

308 append(segment) 

309 if line: 

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

311 

312 @classmethod 

313 def adjust_line_length( 

314 cls, 

315 line: List["Segment"], 

316 length: int, 

317 style: Optional[Style] = None, 

318 pad: bool = True, 

319 ) -> List["Segment"]: 

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

321 

322 Args: 

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

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

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

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

327 

328 Returns: 

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

330 """ 

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

332 new_line: List[Segment] 

333 

334 if line_length < length: 

335 if pad: 

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

337 else: 

338 new_line = line[:] 

339 elif line_length > length: 

340 new_line = [] 

341 append = new_line.append 

342 line_length = 0 

343 for segment in line: 

344 segment_length = segment.cell_length 

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

346 append(segment) 

347 line_length += segment_length 

348 else: 

349 text, segment_style, _ = segment 

350 text = set_cell_size(text, length - line_length) 

351 append(cls(text, segment_style)) 

352 break 

353 else: 

354 new_line = line[:] 

355 return new_line 

356 

357 @classmethod 

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

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

360 

361 Args: 

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

363 

364 Returns: 

365 int: The length of the line. 

366 """ 

367 _cell_len = cell_len 

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

369 

370 @classmethod 

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

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

373 

374 Args: 

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

376 

377 Returns: 

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

379 """ 

380 get_line_length = cls.get_line_length 

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

382 return (max_width, len(lines)) 

383 

384 @classmethod 

385 def set_shape( 

386 cls, 

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

388 width: int, 

389 height: Optional[int] = None, 

390 style: Optional[Style] = None, 

391 new_lines: bool = False, 

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

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

394 

395 Args: 

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

397 width (int): Desired width. 

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

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

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

401 

402 Returns: 

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

404 """ 

405 _height = height or len(lines) 

406 

407 blank = ( 

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

409 ) 

410 

411 adjust_line_length = cls.adjust_line_length 

412 shaped_lines = lines[:_height] 

413 shaped_lines[:] = [ 

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

415 ] 

416 if len(shaped_lines) < _height: 

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

418 return shaped_lines 

419 

420 @classmethod 

421 def align_top( 

422 cls: Type["Segment"], 

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

424 width: int, 

425 height: int, 

426 style: Style, 

427 new_lines: bool = False, 

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

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

430 

431 Args: 

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

433 width (int): Desired width. 

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

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

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

437 

438 Returns: 

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

440 """ 

441 extra_lines = height - len(lines) 

442 if not extra_lines: 

443 return lines[:] 

444 lines = lines[:height] 

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

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

447 return lines 

448 

449 @classmethod 

450 def align_bottom( 

451 cls: Type["Segment"], 

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

453 width: int, 

454 height: int, 

455 style: Style, 

456 new_lines: bool = False, 

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

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

459 

460 Args: 

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

462 width (int): Desired width. 

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

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

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

466 

467 Returns: 

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

469 """ 

470 extra_lines = height - len(lines) 

471 if not extra_lines: 

472 return lines[:] 

473 lines = lines[:height] 

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

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

476 return lines 

477 

478 @classmethod 

479 def align_middle( 

480 cls: Type["Segment"], 

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

482 width: int, 

483 height: int, 

484 style: Style, 

485 new_lines: bool = False, 

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

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

488 

489 Args: 

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

491 width (int): Desired width. 

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

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

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

495 

496 Returns: 

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

498 """ 

499 extra_lines = height - len(lines) 

500 if not extra_lines: 

501 return lines[:] 

502 lines = lines[:height] 

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

504 top_lines = extra_lines // 2 

505 bottom_lines = extra_lines - top_lines 

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

507 return lines 

508 

509 @classmethod 

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

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

512 

513 Args: 

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

515 

516 Returns: 

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

518 """ 

519 iter_segments = iter(segments) 

520 try: 

521 last_segment = next(iter_segments) 

522 except StopIteration: 

523 return 

524 

525 _Segment = Segment 

526 for segment in iter_segments: 

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

528 last_segment = _Segment( 

529 last_segment.text + segment.text, last_segment.style 

530 ) 

531 else: 

532 yield last_segment 

533 last_segment = segment 

534 yield last_segment 

535 

536 @classmethod 

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

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

539 

540 Args: 

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

542 

543 Yields: 

544 Segment: Segments with link removed. 

545 """ 

546 for segment in segments: 

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

548 yield segment 

549 else: 

550 text, style, _control = segment 

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

552 

553 @classmethod 

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

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

556 

557 Args: 

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

559 

560 Yields: 

561 Segment: Segments with styles replace with None 

562 """ 

563 for text, _style, control in segments: 

564 yield cls(text, None, control) 

565 

566 @classmethod 

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

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

569 

570 Args: 

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

572 

573 Yields: 

574 Segment: Segments with colorless style. 

575 """ 

576 

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

578 for text, style, control in segments: 

579 if style: 

580 colorless_style = cache.get(style) 

581 if colorless_style is None: 

582 colorless_style = style.without_color 

583 cache[style] = colorless_style 

584 yield cls(text, colorless_style, control) 

585 else: 

586 yield cls(text, None, control) 

587 

588 @classmethod 

589 def divide( 

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

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

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

593 

594 Args: 

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

596 

597 Yields: 

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

599 """ 

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

601 add_segment = split_segments.append 

602 

603 iter_cuts = iter(cuts) 

604 

605 while True: 

606 cut = next(iter_cuts, -1) 

607 if cut == -1: 

608 return [] 

609 if cut != 0: 

610 break 

611 yield [] 

612 pos = 0 

613 

614 segments_clear = split_segments.clear 

615 segments_copy = split_segments.copy 

616 

617 _cell_len = cached_cell_len 

618 for segment in segments: 

619 text, _style, control = segment 

620 while text: 

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

622 if end_pos < cut: 

623 add_segment(segment) 

624 pos = end_pos 

625 break 

626 

627 if end_pos == cut: 

628 add_segment(segment) 

629 yield segments_copy() 

630 segments_clear() 

631 pos = end_pos 

632 

633 cut = next(iter_cuts, -1) 

634 if cut == -1: 

635 if split_segments: 

636 yield segments_copy() 

637 return 

638 

639 break 

640 

641 else: 

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

643 text, _style, control = segment 

644 add_segment(before) 

645 yield segments_copy() 

646 segments_clear() 

647 pos = cut 

648 

649 cut = next(iter_cuts, -1) 

650 if cut == -1: 

651 if split_segments: 

652 yield segments_copy() 

653 return 

654 

655 yield segments_copy() 

656 

657 

658class Segments: 

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

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

661 

662 Args: 

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

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

665 """ 

666 

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

668 self.segments = list(segments) 

669 self.new_lines = new_lines 

670 

671 def __rich_console__( 

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

673 ) -> "RenderResult": 

674 if self.new_lines: 

675 line = Segment.line() 

676 for segment in self.segments: 

677 yield segment 

678 yield line 

679 else: 

680 yield from self.segments 

681 

682 

683class SegmentLines: 

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

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

686 in rendering process. 

687 

688 Args: 

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

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

691 """ 

692 self.lines = list(lines) 

693 self.new_lines = new_lines 

694 

695 def __rich_console__( 

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

697 ) -> "RenderResult": 

698 if self.new_lines: 

699 new_line = Segment.line() 

700 for line in self.lines: 

701 yield from line 

702 yield new_line 

703 else: 

704 for line in self.lines: 

705 yield from line 

706 

707 

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

709 from rich.console import Console 

710 from rich.syntax import Syntax 

711 from rich.text import Text 

712 

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

714console = Console() 

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

716console.print(text)""" 

717 

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

719 

720 console = Console() 

721 

722 console.rule("rich.Segment") 

723 console.print( 

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

725 ) 

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

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

728 console.print() 

729 console.print( 

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

731 ) 

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

733 console.print(fragments) 

734 console.print() 

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

736 console.print(text) 

737 console.print( 

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

739 )