python (3.11.7)

(root)/
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 ESC[4;38;5;81mControlType(ESC[4;38;5;149mIntEnum):
      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 ESC[4;38;5;81mSegment(ESC[4;38;5;149mNamedTuple):
      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 ESC[4;38;5;81mSegments:
     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 ESC[4;38;5;81mSegmentLines:
     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      )