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
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 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)
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
28if TYPE_CHECKING:
29 from .console import Console, ConsoleOptions, RenderResult
32class ControlType(IntEnum):
33 """Non-printable control codes which typically translate to ANSI codes."""
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
53ControlCode = Union[
54 Tuple[ControlType],
55 Tuple[ControlType, Union[int, str]],
56 Tuple[ControlType, int, int],
57]
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.
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.
70 Attributes:
71 cell_length (int): The cell length of this Segment.
72 """
74 text: str
75 style: Optional[Style] = None
76 control: Optional[Sequence[ControlCode]] = None
78 @property
79 def cell_length(self) -> int:
80 """The number of terminal cells required to display self.text.
82 Returns:
83 int: A number of cells.
84 """
85 text, _style, control = self
86 return 0 if control else cell_len(text)
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
97 def __bool__(self) -> bool:
98 """Check if the segment contains text."""
99 return bool(self.text)
101 @property
102 def is_control(self) -> bool:
103 """Check if the segment contains control codes."""
104 return self.control is not None
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.
111 Note that splitting a double-width character, may result in that character turning
112 into two spaces.
114 Args:
115 segment (Segment): A segment to split.
116 cut (int): A cell position to cut on.
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)
127 cell_size = get_character_cell_size
129 pos = int((cut / cell_length) * len(text))
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
155 def split_cells(self, cut: int) -> Tuple["Segment", "Segment"]:
156 """Split segment in to two segments at the specified column.
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.
161 Args:
162 cut (int): Offset within the segment to cut.
164 Returns:
165 Tuple[Segment, Segment]: Two segments.
166 """
167 text, style, control = self
168 assert cut >= 0
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 )
179 return self._split_cells(self, cut)
181 @classmethod
182 def line(cls) -> "Segment":
183 """Make a new line segment."""
184 return cls("\n")
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.
195 Returns an iterable of segments where the style is replaced by ``style + segment.style + post_style``.
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.
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
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.
233 Args:
234 segments (Iterable[Segment]): An iterable of Segment instances.
235 is_control (bool, optional): is_control flag to match in search.
237 Returns:
238 Iterable[Segment]: And iterable of Segment instances.
240 """
241 if is_control:
242 return filter(attrgetter("control"), segments)
243 else:
244 return filterfalse(attrgetter("control"), segments)
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.
250 Args:
251 segments (Iterable[Segment]): Segments potentially containing line feeds.
253 Yields:
254 Iterable[List[Segment]]: Iterable of segment lists, one per line.
255 """
256 line: List[Segment] = []
257 append = line.append
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
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.
281 Args:
282 segments (Iterable[Segment]): Segments potentially containing line feeds.
284 Yields:
285 Iterable[List[Segment]]: Iterable of segment lists, one per line.
286 """
287 line: List[Segment] = []
288 append = line.append
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)
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.
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`.
324 Returns:
325 Iterable[List[Segment]]: An iterable of lines of segments.
326 """
327 line: List[Segment] = []
328 append = line.append
330 adjust_line_length = cls.adjust_line_length
331 new_line_segment = cls("\n")
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)
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).
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.
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]
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
398 @classmethod
399 def get_line_length(cls, line: List["Segment"]) -> int:
400 """Get the length of list of segments.
402 Args:
403 line (List[Segment]): A line encoded as a list of Segments (assumes no '\\\\n' characters),
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)
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.
415 Args:
416 lines (List[List[Segment]]): A list of lines (no '\\\\n' characters).
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))
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).
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.
443 Returns:
444 List[List[Segment]]: New list of lines.
445 """
446 _height = height or len(lines)
448 blank = (
449 [cls(" " * width + "\n", style)] if new_lines else [cls(" " * width, style)]
450 )
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
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).
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.
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
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).
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.
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
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).
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.
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
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.
554 Args:
555 segments (Iterable[Segment]): An iterable of segments.
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
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
577 @classmethod
578 def strip_links(cls, segments: Iterable["Segment"]) -> Iterable["Segment"]:
579 """Remove all links from an iterable of styles.
581 Args:
582 segments (Iterable[Segment]): An iterable segments.
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)
594 @classmethod
595 def strip_styles(cls, segments: Iterable["Segment"]) -> Iterable["Segment"]:
596 """Remove all styles from an iterable of segments.
598 Args:
599 segments (Iterable[Segment]): An iterable segments.
601 Yields:
602 Segment: Segments with styles replace with None
603 """
604 for text, _style, control in segments:
605 yield cls(text, None, control)
607 @classmethod
608 def remove_color(cls, segments: Iterable["Segment"]) -> Iterable["Segment"]:
609 """Remove all color from an iterable of segments.
611 Args:
612 segments (Iterable[Segment]): An iterable segments.
614 Yields:
615 Segment: Segments with colorless style.
616 """
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)
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.
635 Args:
636 cuts (Iterable[int]): Cell positions where to divide.
638 Yields:
639 [Iterable[List[Segment]]]: An iterable of Segments in List.
640 """
641 split_segments: List["Segment"] = []
642 add_segment = split_segments.append
644 iter_cuts = iter(cuts)
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
655 segments_clear = split_segments.clear
656 segments_copy = split_segments.copy
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
668 if end_pos == cut:
669 add_segment(segment)
670 yield segments_copy()
671 segments_clear()
672 pos = end_pos
674 cut = next(iter_cuts, -1)
675 if cut == -1:
676 if split_segments:
677 yield segments_copy()
678 return
680 break
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
690 cut = next(iter_cuts, -1)
691 if cut == -1:
692 if split_segments:
693 yield segments_copy()
694 return
696 yield segments_copy()
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.
703 Args:
704 segments (Iterable[Segment]): An iterable of segments.
705 new_lines (bool, optional): Add new lines between segments. Defaults to False.
706 """
708 def __init__(self, segments: Iterable[Segment], new_lines: bool = False) -> None:
709 self.segments = list(segments)
710 self.new_lines = new_lines
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
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.
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
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
749if __name__ == "__main__": # pragma: no cover
750 from rich.console import Console
751 from rich.syntax import Syntax
752 from rich.text import Text
754 code = """from rich.console import Console
755console = Console()
756text = Text.from_markup("Hello, [bold magenta]World[/]!")
757console.print(text)"""
759 text = Text.from_markup("Hello, [bold magenta]World[/]!")
761 console = Console()
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 )