Coverage for /run/media/veganeco/water/status600.com/pypi/reptilian_climates/modules_series_2/ships/fields/gardens_pip/rich/segment.py: 27%

1 from enum import IntEnum

2 from functools import lru_cache

3 from itertools import filterfalse

4 from logging import getLogger

5 from operator import attrgetter

6 from 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

19 from .cells import (

20 _is_single_cell_widths,

21 cached_cell_len,

22 cell_len,

23 get_character_cell_size,

24 set_cell_size,

25)

26 from .repr import Result, rich_repr

27 from .style import Style

28

29 if TYPE_CHECKING:

30 from .console import Console, ConsoleOptions, RenderResult

31

32 log = getLogger("rich")

33

34

35 class 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

56 ControlCode = Union[

57 Tuple[ControlType],

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

59 Tuple[ControlType, int, int],

60]

61

62

63@rich_repr()

64 class 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

657 class 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

682 class 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

707 if __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

713 console = Console()

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

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