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
« 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)
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"]:
113 text, style, control = segment
114 _Segment = Segment
116 cell_length = segment.cell_length
117 if cut >= cell_length:
118 return segment, _Segment("", style, control)
120 cell_size = get_character_cell_size
122 pos = int((cut / cell_length) * (len(text) - 1))
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 )
147 raise AssertionError("Will never reach here")
149 def split_cells(self, cut: int) -> Tuple["Segment", "Segment"]:
150 """Split segment in to two segments at the specified column.
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.
155 Returns:
156 Tuple[Segment, Segment]: Two segments.
157 """
158 text, style, control = self
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 )
169 return self._split_cells(self, cut)
171 @classmethod
172 def line(cls) -> "Segment":
173 """Make a new line segment."""
174 return cls("\n")
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.
185 Returns an iterable of segments where the style is replaced by ``style + segment.style + post_style``.
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.
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
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.
223 Args:
224 segments (Iterable[Segment]): An iterable of Segment instances.
225 is_control (bool, optional): is_control flag to match in search.
227 Returns:
228 Iterable[Segment]: And iterable of Segment instances.
230 """
231 if is_control:
232 return filter(attrgetter("control"), segments)
233 else:
234 return filterfalse(attrgetter("control"), segments)
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.
240 Args:
241 segments (Iterable[Segment]): Segments potentially containing line feeds.
243 Yields:
244 Iterable[List[Segment]]: Iterable of segment lists, one per line.
245 """
246 line: List[Segment] = []
247 append = line.append
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
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.
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`.
283 Returns:
284 Iterable[List[Segment]]: An iterable of lines of segments.
285 """
286 line: List[Segment] = []
287 append = line.append
289 adjust_line_length = cls.adjust_line_length
290 new_line_segment = cls("\n")
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)
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).
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.
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]
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
357 @classmethod
358 def get_line_length(cls, line: List["Segment"]) -> int:
359 """Get the length of list of segments.
361 Args:
362 line (List[Segment]): A line encoded as a list of Segments (assumes no '\\\\n' characters),
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)
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.
374 Args:
375 lines (List[List[Segment]]): A list of lines (no '\\\\n' characters).
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))
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).
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.
402 Returns:
403 List[List[Segment]]: New list of lines.
404 """
405 _height = height or len(lines)
407 blank = (
408 [cls(" " * width + "\n", style)] if new_lines else [cls(" " * width, style)]
409 )
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
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).
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.
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
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).
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.
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
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).
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.
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
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.
513 Args:
514 segments (Iterable[Segment]): An iterable of segments.
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
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
536 @classmethod
537 def strip_links(cls, segments: Iterable["Segment"]) -> Iterable["Segment"]:
538 """Remove all links from an iterable of styles.
540 Args:
541 segments (Iterable[Segment]): An iterable segments.
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)
553 @classmethod
554 def strip_styles(cls, segments: Iterable["Segment"]) -> Iterable["Segment"]:
555 """Remove all styles from an iterable of segments.
557 Args:
558 segments (Iterable[Segment]): An iterable segments.
560 Yields:
561 Segment: Segments with styles replace with None
562 """
563 for text, _style, control in segments:
564 yield cls(text, None, control)
566 @classmethod
567 def remove_color(cls, segments: Iterable["Segment"]) -> Iterable["Segment"]:
568 """Remove all color from an iterable of segments.
570 Args:
571 segments (Iterable[Segment]): An iterable segments.
573 Yields:
574 Segment: Segments with colorless style.
575 """
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)
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.
594 Args:
595 cuts (Iterable[int]): Cell positions where to divide.
597 Yields:
598 [Iterable[List[Segment]]]: An iterable of Segments in List.
599 """
600 split_segments: List["Segment"] = []
601 add_segment = split_segments.append
603 iter_cuts = iter(cuts)
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
614 segments_clear = split_segments.clear
615 segments_copy = split_segments.copy
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
627 if end_pos == cut:
628 add_segment(segment)
629 yield segments_copy()
630 segments_clear()
631 pos = end_pos
633 cut = next(iter_cuts, -1)
634 if cut == -1:
635 if split_segments:
636 yield segments_copy()
637 return
639 break
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
649 cut = next(iter_cuts, -1)
650 if cut == -1:
651 if split_segments:
652 yield segments_copy()
653 return
655 yield segments_copy()
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.
662 Args:
663 segments (Iterable[Segment]): An iterable of segments.
664 new_lines (bool, optional): Add new lines between segments. Defaults to False.
665 """
667 def __init__(self, segments: Iterable[Segment], new_lines: bool = False) -> None:
668 self.segments = list(segments)
669 self.new_lines = new_lines
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
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.
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
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
708if __name__ == "__main__": # pragma: no cover
709 from rich.console import Console
710 from rich.syntax import Syntax
711 from rich.text import Text
713 code = """from rich.console import Console
714console = Console()
715text = Text.from_markup("Hello, [bold magenta]World[/]!")
716console.print(text)"""
718 text = Text.from_markup("Hello, [bold magenta]World[/]!")
720 console = Console()
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 )