Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/rich/segment.py: 36%
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
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
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)
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
29if TYPE_CHECKING:
30 from .console import Console, ConsoleOptions, RenderResult
32log = getLogger("rich")
35class ControlType(IntEnum):
36 """Non-printable control codes which typically translate to ANSI codes."""
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
56ControlCode = Union[
57 Tuple[ControlType],
58 Tuple[ControlType, Union[int, str]],
59 Tuple[ControlType, int, int],
60]
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.
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.
73 Attributes:
74 cell_length (int): The cell length of this Segment.
75 """
77 text: str
78 style: Optional[Style] = None
79 control: Optional[Sequence[ControlCode]] = None
81 @property
82 def cell_length(self) -> int:
83 """The number of terminal cells required to display self.text.
85 Returns:
86 int: A number of cells.
87 """
88 text, _style, control = self
89 return 0 if control else cell_len(text)
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
100 def __bool__(self) -> bool:
101 """Check if the segment contains text."""
102 return bool(self.text)
104 @property
105 def is_control(self) -> bool:
106 """Check if the segment contains control codes."""
107 return self.control is not None
109 @classmethod
110 @lru_cache(1024 * 16)
111 def _split_cells(cls, segment: "Segment", cut: int) -> Tuple["Segment", "Segment"]:
112 """Split a segment in to two at a given cell position.
114 Note that splitting a double-width character, may result in that character turning
115 into two spaces.
117 Args:
118 segment (Segment): A segment to split.
119 cut (int): A cell position to cut on.
121 Returns:
122 A tuple of two segments.
123 """
124 text, style, control = segment
125 _Segment = Segment
126 cell_length = segment.cell_length
127 if cut >= cell_length:
128 return segment, _Segment("", style, control)
130 cell_size = get_character_cell_size
132 pos = int((cut / cell_length) * len(text))
134 while True:
135 before = text[:pos]
136 cell_pos = cell_len(before)
137 out_by = cell_pos - cut
138 if not out_by:
139 return (
140 _Segment(before, style, control),
141 _Segment(text[pos:], style, control),
142 )
143 if out_by == -1 and cell_size(text[pos]) == 2:
144 return (
145 _Segment(text[:pos] + " ", style, control),
146 _Segment(" " + text[pos + 1 :], style, control),
147 )
148 if out_by == +1 and cell_size(text[pos - 1]) == 2:
149 return (
150 _Segment(text[: pos - 1] + " ", style, control),
151 _Segment(" " + text[pos:], style, control),
152 )
153 if cell_pos < cut:
154 pos += 1
155 else:
156 pos -= 1
158 def split_cells(self, cut: int) -> Tuple["Segment", "Segment"]:
159 """Split segment in to two segments at the specified column.
161 If the cut point falls in the middle of a 2-cell wide character then it is replaced
162 by two spaces, to preserve the display width of the parent segment.
164 Args:
165 cut (int): Offset within the segment to cut.
167 Returns:
168 Tuple[Segment, Segment]: Two segments.
169 """
170 text, style, control = self
171 assert cut >= 0
173 if _is_single_cell_widths(text):
174 # Fast path with all 1 cell characters
175 if cut >= len(text):
176 return self, Segment("", style, control)
177 return (
178 Segment(text[:cut], style, control),
179 Segment(text[cut:], style, control),
180 )
182 return self._split_cells(self, cut)
184 @classmethod
185 def line(cls) -> "Segment":
186 """Make a new line segment."""
187 return cls("\n")
189 @classmethod
190 def apply_style(
191 cls,
192 segments: Iterable["Segment"],
193 style: Optional[Style] = None,
194 post_style: Optional[Style] = None,
195 ) -> Iterable["Segment"]:
196 """Apply style(s) to an iterable of segments.
198 Returns an iterable of segments where the style is replaced by ``style + segment.style + post_style``.
200 Args:
201 segments (Iterable[Segment]): Segments to process.
202 style (Style, optional): Base style. Defaults to None.
203 post_style (Style, optional): Style to apply on top of segment style. Defaults to None.
205 Returns:
206 Iterable[Segments]: A new iterable of segments (possibly the same iterable).
207 """
208 result_segments = segments
209 if style:
210 apply = style.__add__
211 result_segments = (
212 cls(text, None if control else apply(_style), control)
213 for text, _style, control in result_segments
214 )
215 if post_style:
216 result_segments = (
217 cls(
218 text,
219 (
220 None
221 if control
222 else (_style + post_style if _style else post_style)
223 ),
224 control,
225 )
226 for text, _style, control in result_segments
227 )
228 return result_segments
230 @classmethod
231 def filter_control(
232 cls, segments: Iterable["Segment"], is_control: bool = False
233 ) -> Iterable["Segment"]:
234 """Filter segments by ``is_control`` attribute.
236 Args:
237 segments (Iterable[Segment]): An iterable of Segment instances.
238 is_control (bool, optional): is_control flag to match in search.
240 Returns:
241 Iterable[Segment]: And iterable of Segment instances.
243 """
244 if is_control:
245 return filter(attrgetter("control"), segments)
246 else:
247 return filterfalse(attrgetter("control"), segments)
249 @classmethod
250 def split_lines(cls, segments: Iterable["Segment"]) -> Iterable[List["Segment"]]:
251 """Split a sequence of segments in to a list of lines.
253 Args:
254 segments (Iterable[Segment]): Segments potentially containing line feeds.
256 Yields:
257 Iterable[List[Segment]]: Iterable of segment lists, one per line.
258 """
259 line: List[Segment] = []
260 append = line.append
262 for segment in segments:
263 if "\n" in segment.text and not segment.control:
264 text, style, _ = segment
265 while text:
266 _text, new_line, text = text.partition("\n")
267 if _text:
268 append(cls(_text, style))
269 if new_line:
270 yield line
271 line = []
272 append = line.append
273 else:
274 append(segment)
275 if line:
276 yield line
278 @classmethod
279 def split_lines_terminator(
280 cls, segments: Iterable["Segment"]
281 ) -> Iterable[Tuple[List["Segment"], bool]]:
282 """Split a sequence of segments in to a list of lines and a boolean to indicate if there was a new line.
284 Args:
285 segments (Iterable[Segment]): Segments potentially containing line feeds.
287 Yields:
288 Iterable[List[Segment]]: Iterable of segment lists, one per line.
289 """
290 line: List[Segment] = []
291 append = line.append
293 for segment in segments:
294 if "\n" in segment.text and not segment.control:
295 text, style, _ = segment
296 while text:
297 _text, new_line, text = text.partition("\n")
298 if _text:
299 append(cls(_text, style))
300 if new_line:
301 yield (line, True)
302 line = []
303 append = line.append
304 else:
305 append(segment)
306 if line:
307 yield (line, False)
309 @classmethod
310 def split_and_crop_lines(
311 cls,
312 segments: Iterable["Segment"],
313 length: int,
314 style: Optional[Style] = None,
315 pad: bool = True,
316 include_new_lines: bool = True,
317 ) -> Iterable[List["Segment"]]:
318 """Split segments in to lines, and crop lines greater than a given length.
320 Args:
321 segments (Iterable[Segment]): An iterable of segments, probably
322 generated from console.render.
323 length (int): Desired line length.
324 style (Style, optional): Style to use for any padding.
325 pad (bool): Enable padding of lines that are less than `length`.
327 Returns:
328 Iterable[List[Segment]]: An iterable of lines of segments.
329 """
330 line: List[Segment] = []
331 append = line.append
333 adjust_line_length = cls.adjust_line_length
334 new_line_segment = cls("\n")
336 for segment in segments:
337 if "\n" in segment.text and not segment.control:
338 text, segment_style, _ = segment
339 while text:
340 _text, new_line, text = text.partition("\n")
341 if _text:
342 append(cls(_text, segment_style))
343 if new_line:
344 cropped_line = adjust_line_length(
345 line, length, style=style, pad=pad
346 )
347 if include_new_lines:
348 cropped_line.append(new_line_segment)
349 yield cropped_line
350 line.clear()
351 else:
352 append(segment)
353 if line:
354 yield adjust_line_length(line, length, style=style, pad=pad)
356 @classmethod
357 def adjust_line_length(
358 cls,
359 line: List["Segment"],
360 length: int,
361 style: Optional[Style] = None,
362 pad: bool = True,
363 ) -> List["Segment"]:
364 """Adjust a line to a given width (cropping or padding as required).
366 Args:
367 segments (Iterable[Segment]): A list of segments in a single line.
368 length (int): The desired width of the line.
369 style (Style, optional): The style of padding if used (space on the end). Defaults to None.
370 pad (bool, optional): Pad lines with spaces if they are shorter than `length`. Defaults to True.
372 Returns:
373 List[Segment]: A line of segments with the desired length.
374 """
375 line_length = sum(segment.cell_length for segment in line)
376 new_line: List[Segment]
378 if line_length < length:
379 if pad:
380 new_line = line + [cls(" " * (length - line_length), style)]
381 else:
382 new_line = line[:]
383 elif line_length > length:
384 new_line = []
385 append = new_line.append
386 line_length = 0
387 for segment in line:
388 segment_length = segment.cell_length
389 if line_length + segment_length < length or segment.control:
390 append(segment)
391 line_length += segment_length
392 else:
393 text, segment_style, _ = segment
394 text = set_cell_size(text, length - line_length)
395 append(cls(text, segment_style))
396 break
397 else:
398 new_line = line[:]
399 return new_line
401 @classmethod
402 def get_line_length(cls, line: List["Segment"]) -> int:
403 """Get the length of list of segments.
405 Args:
406 line (List[Segment]): A line encoded as a list of Segments (assumes no '\\\\n' characters),
408 Returns:
409 int: The length of the line.
410 """
411 _cell_len = cell_len
412 return sum(_cell_len(text) for text, style, control in line if not control)
414 @classmethod
415 def get_shape(cls, lines: List[List["Segment"]]) -> Tuple[int, int]:
416 """Get the shape (enclosing rectangle) of a list of lines.
418 Args:
419 lines (List[List[Segment]]): A list of lines (no '\\\\n' characters).
421 Returns:
422 Tuple[int, int]: Width and height in characters.
423 """
424 get_line_length = cls.get_line_length
425 max_width = max(get_line_length(line) for line in lines) if lines else 0
426 return (max_width, len(lines))
428 @classmethod
429 def set_shape(
430 cls,
431 lines: List[List["Segment"]],
432 width: int,
433 height: Optional[int] = None,
434 style: Optional[Style] = None,
435 new_lines: bool = False,
436 ) -> List[List["Segment"]]:
437 """Set the shape of a list of lines (enclosing rectangle).
439 Args:
440 lines (List[List[Segment]]): A list of lines.
441 width (int): Desired width.
442 height (int, optional): Desired height or None for no change.
443 style (Style, optional): Style of any padding added.
444 new_lines (bool, optional): Padded lines should include "\n". Defaults to False.
446 Returns:
447 List[List[Segment]]: New list of lines.
448 """
449 _height = height or len(lines)
451 blank = (
452 [cls(" " * width + "\n", style)] if new_lines else [cls(" " * width, style)]
453 )
455 adjust_line_length = cls.adjust_line_length
456 shaped_lines = lines[:_height]
457 shaped_lines[:] = [
458 adjust_line_length(line, width, style=style) for line in lines
459 ]
460 if len(shaped_lines) < _height:
461 shaped_lines.extend([blank] * (_height - len(shaped_lines)))
462 return shaped_lines
464 @classmethod
465 def align_top(
466 cls: Type["Segment"],
467 lines: List[List["Segment"]],
468 width: int,
469 height: int,
470 style: Style,
471 new_lines: bool = False,
472 ) -> List[List["Segment"]]:
473 """Aligns lines to top (adds extra lines to bottom as required).
475 Args:
476 lines (List[List[Segment]]): A list of lines.
477 width (int): Desired width.
478 height (int, optional): Desired height or None for no change.
479 style (Style): Style of any padding added.
480 new_lines (bool, optional): Padded lines should include "\n". Defaults to False.
482 Returns:
483 List[List[Segment]]: New list of lines.
484 """
485 extra_lines = height - len(lines)
486 if not extra_lines:
487 return lines[:]
488 lines = lines[:height]
489 blank = cls(" " * width + "\n", style) if new_lines else cls(" " * width, style)
490 lines = lines + [[blank]] * extra_lines
491 return lines
493 @classmethod
494 def align_bottom(
495 cls: Type["Segment"],
496 lines: List[List["Segment"]],
497 width: int,
498 height: int,
499 style: Style,
500 new_lines: bool = False,
501 ) -> List[List["Segment"]]:
502 """Aligns render to bottom (adds extra lines above as required).
504 Args:
505 lines (List[List[Segment]]): A list of lines.
506 width (int): Desired width.
507 height (int, optional): Desired height or None for no change.
508 style (Style): Style of any padding added. Defaults to None.
509 new_lines (bool, optional): Padded lines should include "\n". Defaults to False.
511 Returns:
512 List[List[Segment]]: New list of lines.
513 """
514 extra_lines = height - len(lines)
515 if not extra_lines:
516 return lines[:]
517 lines = lines[:height]
518 blank = cls(" " * width + "\n", style) if new_lines else cls(" " * width, style)
519 lines = [[blank]] * extra_lines + lines
520 return lines
522 @classmethod
523 def align_middle(
524 cls: Type["Segment"],
525 lines: List[List["Segment"]],
526 width: int,
527 height: int,
528 style: Style,
529 new_lines: bool = False,
530 ) -> List[List["Segment"]]:
531 """Aligns lines to middle (adds extra lines to above and below as required).
533 Args:
534 lines (List[List[Segment]]): A list of lines.
535 width (int): Desired width.
536 height (int, optional): Desired height or None for no change.
537 style (Style): Style of any padding added.
538 new_lines (bool, optional): Padded lines should include "\n". Defaults to False.
540 Returns:
541 List[List[Segment]]: New list of lines.
542 """
543 extra_lines = height - len(lines)
544 if not extra_lines:
545 return lines[:]
546 lines = lines[:height]
547 blank = cls(" " * width + "\n", style) if new_lines else cls(" " * width, style)
548 top_lines = extra_lines // 2
549 bottom_lines = extra_lines - top_lines
550 lines = [[blank]] * top_lines + lines + [[blank]] * bottom_lines
551 return lines
553 @classmethod
554 def simplify(cls, segments: Iterable["Segment"]) -> Iterable["Segment"]:
555 """Simplify an iterable of segments by combining contiguous segments with the same style.
557 Args:
558 segments (Iterable[Segment]): An iterable of segments.
560 Returns:
561 Iterable[Segment]: A possibly smaller iterable of segments that will render the same way.
562 """
563 iter_segments = iter(segments)
564 try:
565 last_segment = next(iter_segments)
566 except StopIteration:
567 return
569 _Segment = Segment
570 for segment in iter_segments:
571 if last_segment.style == segment.style and not segment.control:
572 last_segment = _Segment(
573 last_segment.text + segment.text, last_segment.style
574 )
575 else:
576 yield last_segment
577 last_segment = segment
578 yield last_segment
580 @classmethod
581 def strip_links(cls, segments: Iterable["Segment"]) -> Iterable["Segment"]:
582 """Remove all links from an iterable of styles.
584 Args:
585 segments (Iterable[Segment]): An iterable segments.
587 Yields:
588 Segment: Segments with link removed.
589 """
590 for segment in segments:
591 if segment.control or segment.style is None:
592 yield segment
593 else:
594 text, style, _control = segment
595 yield cls(text, style.update_link(None) if style else None)
597 @classmethod
598 def strip_styles(cls, segments: Iterable["Segment"]) -> Iterable["Segment"]:
599 """Remove all styles from an iterable of segments.
601 Args:
602 segments (Iterable[Segment]): An iterable segments.
604 Yields:
605 Segment: Segments with styles replace with None
606 """
607 for text, _style, control in segments:
608 yield cls(text, None, control)
610 @classmethod
611 def remove_color(cls, segments: Iterable["Segment"]) -> Iterable["Segment"]:
612 """Remove all color from an iterable of segments.
614 Args:
615 segments (Iterable[Segment]): An iterable segments.
617 Yields:
618 Segment: Segments with colorless style.
619 """
621 cache: Dict[Style, Style] = {}
622 for text, style, control in segments:
623 if style:
624 colorless_style = cache.get(style)
625 if colorless_style is None:
626 colorless_style = style.without_color
627 cache[style] = colorless_style
628 yield cls(text, colorless_style, control)
629 else:
630 yield cls(text, None, control)
632 @classmethod
633 def divide(
634 cls, segments: Iterable["Segment"], cuts: Iterable[int]
635 ) -> Iterable[List["Segment"]]:
636 """Divides an iterable of segments in to portions.
638 Args:
639 cuts (Iterable[int]): Cell positions where to divide.
641 Yields:
642 [Iterable[List[Segment]]]: An iterable of Segments in List.
643 """
644 split_segments: List["Segment"] = []
645 add_segment = split_segments.append
647 iter_cuts = iter(cuts)
649 while True:
650 cut = next(iter_cuts, -1)
651 if cut == -1:
652 return
653 if cut != 0:
654 break
655 yield []
656 pos = 0
658 segments_clear = split_segments.clear
659 segments_copy = split_segments.copy
661 _cell_len = cached_cell_len
662 for segment in segments:
663 text, _style, control = segment
664 while text:
665 end_pos = pos if control else pos + _cell_len(text)
666 if end_pos < cut:
667 add_segment(segment)
668 pos = end_pos
669 break
671 if end_pos == cut:
672 add_segment(segment)
673 yield segments_copy()
674 segments_clear()
675 pos = end_pos
677 cut = next(iter_cuts, -1)
678 if cut == -1:
679 if split_segments:
680 yield segments_copy()
681 return
683 break
685 else:
686 before, segment = segment.split_cells(cut - pos)
687 text, _style, control = segment
688 add_segment(before)
689 yield segments_copy()
690 segments_clear()
691 pos = cut
693 cut = next(iter_cuts, -1)
694 if cut == -1:
695 if split_segments:
696 yield segments_copy()
697 return
699 yield segments_copy()
702class Segments:
703 """A simple renderable to render an iterable of segments. This class may be useful if
704 you want to print segments outside of a __rich_console__ method.
706 Args:
707 segments (Iterable[Segment]): An iterable of segments.
708 new_lines (bool, optional): Add new lines between segments. Defaults to False.
709 """
711 def __init__(self, segments: Iterable[Segment], new_lines: bool = False) -> None:
712 self.segments = list(segments)
713 self.new_lines = new_lines
715 def __rich_console__(
716 self, console: "Console", options: "ConsoleOptions"
717 ) -> "RenderResult":
718 if self.new_lines:
719 line = Segment.line()
720 for segment in self.segments:
721 yield segment
722 yield line
723 else:
724 yield from self.segments
727class SegmentLines:
728 def __init__(self, lines: Iterable[List[Segment]], new_lines: bool = False) -> None:
729 """A simple renderable containing a number of lines of segments. May be used as an intermediate
730 in rendering process.
732 Args:
733 lines (Iterable[List[Segment]]): Lists of segments forming lines.
734 new_lines (bool, optional): Insert new lines after each line. Defaults to False.
735 """
736 self.lines = list(lines)
737 self.new_lines = new_lines
739 def __rich_console__(
740 self, console: "Console", options: "ConsoleOptions"
741 ) -> "RenderResult":
742 if self.new_lines:
743 new_line = Segment.line()
744 for line in self.lines:
745 yield from line
746 yield new_line
747 else:
748 for line in self.lines:
749 yield from line
752if __name__ == "__main__": # pragma: no cover
753 from rich.console import Console
754 from rich.syntax import Syntax
755 from rich.text import Text
757 code = """from rich.console import Console
758console = Console()
759text = Text.from_markup("Hello, [bold magenta]World[/]!")
760console.print(text)"""
762 text = Text.from_markup("Hello, [bold magenta]World[/]!")
764 console = Console()
766 console.rule("rich.Segment")
767 console.print(
768 "A Segment is the last step in the Rich render process before generating text with ANSI codes."
769 )
770 console.print("\nConsider the following code:\n")
771 console.print(Syntax(code, "python", line_numbers=True))
772 console.print()
773 console.print(
774 "When you call [b]print()[/b], Rich [i]renders[/i] the object in to the following:\n"
775 )
776 fragments = list(console.render(text))
777 console.print(fragments)
778 console.print()
779 console.print("The Segments are then processed to produce the following output:\n")
780 console.print(text)
781 console.print(
782 "\nYou will only need to know this if you are implementing your own Rich renderables."
783 )