python (3.11.7)
       1  import sys
       2  from itertools import chain
       3  from typing import TYPE_CHECKING, Iterable, Optional
       4  
       5  if sys.version_info >= (3, 8):
       6      from typing import Literal
       7  else:
       8      from pip._vendor.typing_extensions import Literal  # pragma: no cover
       9  
      10  from .constrain import Constrain
      11  from .jupyter import JupyterMixin
      12  from .measure import Measurement
      13  from .segment import Segment
      14  from .style import StyleType
      15  
      16  if TYPE_CHECKING:
      17      from .console import Console, ConsoleOptions, RenderableType, RenderResult
      18  
      19  AlignMethod = Literal["left", "center", "right"]
      20  VerticalAlignMethod = Literal["top", "middle", "bottom"]
      21  
      22  
      23  class ESC[4;38;5;81mAlign(ESC[4;38;5;149mJupyterMixin):
      24      """Align a renderable by adding spaces if necessary.
      25  
      26      Args:
      27          renderable (RenderableType): A console renderable.
      28          align (AlignMethod): One of "left", "center", or "right""
      29          style (StyleType, optional): An optional style to apply to the background.
      30          vertical (Optional[VerticalAlginMethod], optional): Optional vertical align, one of "top", "middle", or "bottom". Defaults to None.
      31          pad (bool, optional): Pad the right with spaces. Defaults to True.
      32          width (int, optional): Restrict contents to given width, or None to use default width. Defaults to None.
      33          height (int, optional): Set height of align renderable, or None to fit to contents. Defaults to None.
      34  
      35      Raises:
      36          ValueError: if ``align`` is not one of the expected values.
      37      """
      38  
      39      def __init__(
      40          self,
      41          renderable: "RenderableType",
      42          align: AlignMethod = "left",
      43          style: Optional[StyleType] = None,
      44          *,
      45          vertical: Optional[VerticalAlignMethod] = None,
      46          pad: bool = True,
      47          width: Optional[int] = None,
      48          height: Optional[int] = None,
      49      ) -> None:
      50          if align not in ("left", "center", "right"):
      51              raise ValueError(
      52                  f'invalid value for align, expected "left", "center", or "right" (not {align!r})'
      53              )
      54          if vertical is not None and vertical not in ("top", "middle", "bottom"):
      55              raise ValueError(
      56                  f'invalid value for vertical, expected "top", "middle", or "bottom" (not {vertical!r})'
      57              )
      58          self.renderable = renderable
      59          self.align = align
      60          self.style = style
      61          self.vertical = vertical
      62          self.pad = pad
      63          self.width = width
      64          self.height = height
      65  
      66      def __repr__(self) -> str:
      67          return f"Align({self.renderable!r}, {self.align!r})"
      68  
      69      @classmethod
      70      def left(
      71          cls,
      72          renderable: "RenderableType",
      73          style: Optional[StyleType] = None,
      74          *,
      75          vertical: Optional[VerticalAlignMethod] = None,
      76          pad: bool = True,
      77          width: Optional[int] = None,
      78          height: Optional[int] = None,
      79      ) -> "Align":
      80          """Align a renderable to the left."""
      81          return cls(
      82              renderable,
      83              "left",
      84              style=style,
      85              vertical=vertical,
      86              pad=pad,
      87              width=width,
      88              height=height,
      89          )
      90  
      91      @classmethod
      92      def center(
      93          cls,
      94          renderable: "RenderableType",
      95          style: Optional[StyleType] = None,
      96          *,
      97          vertical: Optional[VerticalAlignMethod] = None,
      98          pad: bool = True,
      99          width: Optional[int] = None,
     100          height: Optional[int] = None,
     101      ) -> "Align":
     102          """Align a renderable to the center."""
     103          return cls(
     104              renderable,
     105              "center",
     106              style=style,
     107              vertical=vertical,
     108              pad=pad,
     109              width=width,
     110              height=height,
     111          )
     112  
     113      @classmethod
     114      def right(
     115          cls,
     116          renderable: "RenderableType",
     117          style: Optional[StyleType] = None,
     118          *,
     119          vertical: Optional[VerticalAlignMethod] = None,
     120          pad: bool = True,
     121          width: Optional[int] = None,
     122          height: Optional[int] = None,
     123      ) -> "Align":
     124          """Align a renderable to the right."""
     125          return cls(
     126              renderable,
     127              "right",
     128              style=style,
     129              vertical=vertical,
     130              pad=pad,
     131              width=width,
     132              height=height,
     133          )
     134  
     135      def __rich_console__(
     136          self, console: "Console", options: "ConsoleOptions"
     137      ) -> "RenderResult":
     138          align = self.align
     139          width = console.measure(self.renderable, options=options).maximum
     140          rendered = console.render(
     141              Constrain(
     142                  self.renderable, width if self.width is None else min(width, self.width)
     143              ),
     144              options.update(height=None),
     145          )
     146          lines = list(Segment.split_lines(rendered))
     147          width, height = Segment.get_shape(lines)
     148          lines = Segment.set_shape(lines, width, height)
     149          new_line = Segment.line()
     150          excess_space = options.max_width - width
     151          style = console.get_style(self.style) if self.style is not None else None
     152  
     153          def generate_segments() -> Iterable[Segment]:
     154              if excess_space <= 0:
     155                  # Exact fit
     156                  for line in lines:
     157                      yield from line
     158                      yield new_line
     159  
     160              elif align == "left":
     161                  # Pad on the right
     162                  pad = Segment(" " * excess_space, style) if self.pad else None
     163                  for line in lines:
     164                      yield from line
     165                      if pad:
     166                          yield pad
     167                      yield new_line
     168  
     169              elif align == "center":
     170                  # Pad left and right
     171                  left = excess_space // 2
     172                  pad = Segment(" " * left, style)
     173                  pad_right = (
     174                      Segment(" " * (excess_space - left), style) if self.pad else None
     175                  )
     176                  for line in lines:
     177                      if left:
     178                          yield pad
     179                      yield from line
     180                      if pad_right:
     181                          yield pad_right
     182                      yield new_line
     183  
     184              elif align == "right":
     185                  # Padding on left
     186                  pad = Segment(" " * excess_space, style)
     187                  for line in lines:
     188                      yield pad
     189                      yield from line
     190                      yield new_line
     191  
     192          blank_line = (
     193              Segment(f"{' ' * (self.width or options.max_width)}\n", style)
     194              if self.pad
     195              else Segment("\n")
     196          )
     197  
     198          def blank_lines(count: int) -> Iterable[Segment]:
     199              if count > 0:
     200                  for _ in range(count):
     201                      yield blank_line
     202  
     203          vertical_height = self.height or options.height
     204          iter_segments: Iterable[Segment]
     205          if self.vertical and vertical_height is not None:
     206              if self.vertical == "top":
     207                  bottom_space = vertical_height - height
     208                  iter_segments = chain(generate_segments(), blank_lines(bottom_space))
     209              elif self.vertical == "middle":
     210                  top_space = (vertical_height - height) // 2
     211                  bottom_space = vertical_height - top_space - height
     212                  iter_segments = chain(
     213                      blank_lines(top_space),
     214                      generate_segments(),
     215                      blank_lines(bottom_space),
     216                  )
     217              else:  #  self.vertical == "bottom":
     218                  top_space = vertical_height - height
     219                  iter_segments = chain(blank_lines(top_space), generate_segments())
     220          else:
     221              iter_segments = generate_segments()
     222          if self.style:
     223              style = console.get_style(self.style)
     224              iter_segments = Segment.apply_style(iter_segments, style)
     225          yield from iter_segments
     226  
     227      def __rich_measure__(
     228          self, console: "Console", options: "ConsoleOptions"
     229      ) -> Measurement:
     230          measurement = Measurement.get(console, options, self.renderable)
     231          return measurement
     232  
     233  
     234  class ESC[4;38;5;81mVerticalCenter(ESC[4;38;5;149mJupyterMixin):
     235      """Vertically aligns a renderable.
     236  
     237      Warn:
     238          This class is deprecated and may be removed in a future version. Use Align class with
     239          `vertical="middle"`.
     240  
     241      Args:
     242          renderable (RenderableType): A renderable object.
     243      """
     244  
     245      def __init__(
     246          self,
     247          renderable: "RenderableType",
     248          style: Optional[StyleType] = None,
     249      ) -> None:
     250          self.renderable = renderable
     251          self.style = style
     252  
     253      def __repr__(self) -> str:
     254          return f"VerticalCenter({self.renderable!r})"
     255  
     256      def __rich_console__(
     257          self, console: "Console", options: "ConsoleOptions"
     258      ) -> "RenderResult":
     259          style = console.get_style(self.style) if self.style is not None else None
     260          lines = console.render_lines(
     261              self.renderable, options.update(height=None), pad=False
     262          )
     263          width, _height = Segment.get_shape(lines)
     264          new_line = Segment.line()
     265          height = options.height or options.size.height
     266          top_space = (height - len(lines)) // 2
     267          bottom_space = height - top_space - len(lines)
     268          blank_line = Segment(f"{' ' * width}", style)
     269  
     270          def blank_lines(count: int) -> Iterable[Segment]:
     271              for _ in range(count):
     272                  yield blank_line
     273                  yield new_line
     274  
     275          if top_space > 0:
     276              yield from blank_lines(top_space)
     277          for line in lines:
     278              yield from line
     279              yield new_line
     280          if bottom_space > 0:
     281              yield from blank_lines(bottom_space)
     282  
     283      def __rich_measure__(
     284          self, console: "Console", options: "ConsoleOptions"
     285      ) -> Measurement:
     286          measurement = Measurement.get(console, options, self.renderable)
     287          return measurement
     288  
     289  
     290  if __name__ == "__main__":  # pragma: no cover
     291      from pip._vendor.rich.console import Console, Group
     292      from pip._vendor.rich.highlighter import ReprHighlighter
     293      from pip._vendor.rich.panel import Panel
     294  
     295      highlighter = ReprHighlighter()
     296      console = Console()
     297  
     298      panel = Panel(
     299          Group(
     300              Align.left(highlighter("align='left'")),
     301              Align.center(highlighter("align='center'")),
     302              Align.right(highlighter("align='right'")),
     303          ),
     304          width=60,
     305          style="on dark_blue",
     306          title="Align",
     307      )
     308  
     309      console.print(
     310          Align.center(panel, vertical="middle", style="on red", height=console.height)
     311      )