]> jfr.im git - dlqueue.git/blob - venv/lib/python3.11/site-packages/pip/_vendor/rich/segment.py
init: venv aand flask
[dlqueue.git] / venv / lib / python3.11 / site-packages / pip / _vendor / rich / segment.py
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
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
658 class 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
683 class 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
708 if __name__ == "__main__": # pragma: no cover
709 from pip._vendor.rich.console import Console
710 from pip._vendor.rich.syntax import Syntax
711 from pip._vendor.rich.text import Text
712
713 code = """from rich.console import Console
714 console = Console()
715 text = Text.from_markup("Hello, [bold magenta]World[/]!")
716 console.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 )