python (3.11.7)

(root)/
lib/
python3.11/
site-packages/
pip/
_vendor/
rich/
table.py
       1  from dataclasses import dataclass, field, replace
       2  from typing import (
       3      TYPE_CHECKING,
       4      Dict,
       5      Iterable,
       6      List,
       7      NamedTuple,
       8      Optional,
       9      Sequence,
      10      Tuple,
      11      Union,
      12  )
      13  
      14  from . import box, errors
      15  from ._loop import loop_first_last, loop_last
      16  from ._pick import pick_bool
      17  from ._ratio import ratio_distribute, ratio_reduce
      18  from .align import VerticalAlignMethod
      19  from .jupyter import JupyterMixin
      20  from .measure import Measurement
      21  from .padding import Padding, PaddingDimensions
      22  from .protocol import is_renderable
      23  from .segment import Segment
      24  from .style import Style, StyleType
      25  from .text import Text, TextType
      26  
      27  if TYPE_CHECKING:
      28      from .console import (
      29          Console,
      30          ConsoleOptions,
      31          JustifyMethod,
      32          OverflowMethod,
      33          RenderableType,
      34          RenderResult,
      35      )
      36  
      37  
      38  @dataclass
      39  class ESC[4;38;5;81mColumn:
      40      """Defines a column within a ~Table.
      41  
      42      Args:
      43          title (Union[str, Text], optional): The title of the table rendered at the top. Defaults to None.
      44          caption (Union[str, Text], optional): The table caption rendered below. Defaults to None.
      45          width (int, optional): The width in characters of the table, or ``None`` to automatically fit. Defaults to None.
      46          min_width (Optional[int], optional): The minimum width of the table, or ``None`` for no minimum. Defaults to None.
      47          box (box.Box, optional): One of the constants in box.py used to draw the edges (see :ref:`appendix_box`), or ``None`` for no box lines. Defaults to box.HEAVY_HEAD.
      48          safe_box (Optional[bool], optional): Disable box characters that don't display on windows legacy terminal with *raster* fonts. Defaults to True.
      49          padding (PaddingDimensions, optional): Padding for cells (top, right, bottom, left). Defaults to (0, 1).
      50          collapse_padding (bool, optional): Enable collapsing of padding around cells. Defaults to False.
      51          pad_edge (bool, optional): Enable padding of edge cells. Defaults to True.
      52          expand (bool, optional): Expand the table to fit the available space if ``True``, otherwise the table width will be auto-calculated. Defaults to False.
      53          show_header (bool, optional): Show a header row. Defaults to True.
      54          show_footer (bool, optional): Show a footer row. Defaults to False.
      55          show_edge (bool, optional): Draw a box around the outside of the table. Defaults to True.
      56          show_lines (bool, optional): Draw lines between every row. Defaults to False.
      57          leading (bool, optional): Number of blank lines between rows (precludes ``show_lines``). Defaults to 0.
      58          style (Union[str, Style], optional): Default style for the table. Defaults to "none".
      59          row_styles (List[Union, str], optional): Optional list of row styles, if more than one style is given then the styles will alternate. Defaults to None.
      60          header_style (Union[str, Style], optional): Style of the header. Defaults to "table.header".
      61          footer_style (Union[str, Style], optional): Style of the footer. Defaults to "table.footer".
      62          border_style (Union[str, Style], optional): Style of the border. Defaults to None.
      63          title_style (Union[str, Style], optional): Style of the title. Defaults to None.
      64          caption_style (Union[str, Style], optional): Style of the caption. Defaults to None.
      65          title_justify (str, optional): Justify method for title. Defaults to "center".
      66          caption_justify (str, optional): Justify method for caption. Defaults to "center".
      67          highlight (bool, optional): Highlight cell contents (if str). Defaults to False.
      68      """
      69  
      70      header: "RenderableType" = ""
      71      """RenderableType: Renderable for the header (typically a string)"""
      72  
      73      footer: "RenderableType" = ""
      74      """RenderableType: Renderable for the footer (typically a string)"""
      75  
      76      header_style: StyleType = ""
      77      """StyleType: The style of the header."""
      78  
      79      footer_style: StyleType = ""
      80      """StyleType: The style of the footer."""
      81  
      82      style: StyleType = ""
      83      """StyleType: The style of the column."""
      84  
      85      justify: "JustifyMethod" = "left"
      86      """str: How to justify text within the column ("left", "center", "right", or "full")"""
      87  
      88      vertical: "VerticalAlignMethod" = "top"
      89      """str: How to vertically align content ("top", "middle", or "bottom")"""
      90  
      91      overflow: "OverflowMethod" = "ellipsis"
      92      """str: Overflow method."""
      93  
      94      width: Optional[int] = None
      95      """Optional[int]: Width of the column, or ``None`` (default) to auto calculate width."""
      96  
      97      min_width: Optional[int] = None
      98      """Optional[int]: Minimum width of column, or ``None`` for no minimum. Defaults to None."""
      99  
     100      max_width: Optional[int] = None
     101      """Optional[int]: Maximum width of column, or ``None`` for no maximum. Defaults to None."""
     102  
     103      ratio: Optional[int] = None
     104      """Optional[int]: Ratio to use when calculating column width, or ``None`` (default) to adapt to column contents."""
     105  
     106      no_wrap: bool = False
     107      """bool: Prevent wrapping of text within the column. Defaults to ``False``."""
     108  
     109      _index: int = 0
     110      """Index of column."""
     111  
     112      _cells: List["RenderableType"] = field(default_factory=list)
     113  
     114      def copy(self) -> "Column":
     115          """Return a copy of this Column."""
     116          return replace(self, _cells=[])
     117  
     118      @property
     119      def cells(self) -> Iterable["RenderableType"]:
     120          """Get all cells in the column, not including header."""
     121          yield from self._cells
     122  
     123      @property
     124      def flexible(self) -> bool:
     125          """Check if this column is flexible."""
     126          return self.ratio is not None
     127  
     128  
     129  @dataclass
     130  class ESC[4;38;5;81mRow:
     131      """Information regarding a row."""
     132  
     133      style: Optional[StyleType] = None
     134      """Style to apply to row."""
     135  
     136      end_section: bool = False
     137      """Indicated end of section, which will force a line beneath the row."""
     138  
     139  
     140  class ESC[4;38;5;81m_Cell(ESC[4;38;5;149mNamedTuple):
     141      """A single cell in a table."""
     142  
     143      style: StyleType
     144      """Style to apply to cell."""
     145      renderable: "RenderableType"
     146      """Cell renderable."""
     147      vertical: VerticalAlignMethod
     148      """Cell vertical alignment."""
     149  
     150  
     151  class ESC[4;38;5;81mTable(ESC[4;38;5;149mJupyterMixin):
     152      """A console renderable to draw a table.
     153  
     154      Args:
     155          *headers (Union[Column, str]): Column headers, either as a string, or :class:`~rich.table.Column` instance.
     156          title (Union[str, Text], optional): The title of the table rendered at the top. Defaults to None.
     157          caption (Union[str, Text], optional): The table caption rendered below. Defaults to None.
     158          width (int, optional): The width in characters of the table, or ``None`` to automatically fit. Defaults to None.
     159          min_width (Optional[int], optional): The minimum width of the table, or ``None`` for no minimum. Defaults to None.
     160          box (box.Box, optional): One of the constants in box.py used to draw the edges (see :ref:`appendix_box`), or ``None`` for no box lines. Defaults to box.HEAVY_HEAD.
     161          safe_box (Optional[bool], optional): Disable box characters that don't display on windows legacy terminal with *raster* fonts. Defaults to True.
     162          padding (PaddingDimensions, optional): Padding for cells (top, right, bottom, left). Defaults to (0, 1).
     163          collapse_padding (bool, optional): Enable collapsing of padding around cells. Defaults to False.
     164          pad_edge (bool, optional): Enable padding of edge cells. Defaults to True.
     165          expand (bool, optional): Expand the table to fit the available space if ``True``, otherwise the table width will be auto-calculated. Defaults to False.
     166          show_header (bool, optional): Show a header row. Defaults to True.
     167          show_footer (bool, optional): Show a footer row. Defaults to False.
     168          show_edge (bool, optional): Draw a box around the outside of the table. Defaults to True.
     169          show_lines (bool, optional): Draw lines between every row. Defaults to False.
     170          leading (bool, optional): Number of blank lines between rows (precludes ``show_lines``). Defaults to 0.
     171          style (Union[str, Style], optional): Default style for the table. Defaults to "none".
     172          row_styles (List[Union, str], optional): Optional list of row styles, if more than one style is given then the styles will alternate. Defaults to None.
     173          header_style (Union[str, Style], optional): Style of the header. Defaults to "table.header".
     174          footer_style (Union[str, Style], optional): Style of the footer. Defaults to "table.footer".
     175          border_style (Union[str, Style], optional): Style of the border. Defaults to None.
     176          title_style (Union[str, Style], optional): Style of the title. Defaults to None.
     177          caption_style (Union[str, Style], optional): Style of the caption. Defaults to None.
     178          title_justify (str, optional): Justify method for title. Defaults to "center".
     179          caption_justify (str, optional): Justify method for caption. Defaults to "center".
     180          highlight (bool, optional): Highlight cell contents (if str). Defaults to False.
     181      """
     182  
     183      columns: List[Column]
     184      rows: List[Row]
     185  
     186      def __init__(
     187          self,
     188          *headers: Union[Column, str],
     189          title: Optional[TextType] = None,
     190          caption: Optional[TextType] = None,
     191          width: Optional[int] = None,
     192          min_width: Optional[int] = None,
     193          box: Optional[box.Box] = box.HEAVY_HEAD,
     194          safe_box: Optional[bool] = None,
     195          padding: PaddingDimensions = (0, 1),
     196          collapse_padding: bool = False,
     197          pad_edge: bool = True,
     198          expand: bool = False,
     199          show_header: bool = True,
     200          show_footer: bool = False,
     201          show_edge: bool = True,
     202          show_lines: bool = False,
     203          leading: int = 0,
     204          style: StyleType = "none",
     205          row_styles: Optional[Iterable[StyleType]] = None,
     206          header_style: Optional[StyleType] = "table.header",
     207          footer_style: Optional[StyleType] = "table.footer",
     208          border_style: Optional[StyleType] = None,
     209          title_style: Optional[StyleType] = None,
     210          caption_style: Optional[StyleType] = None,
     211          title_justify: "JustifyMethod" = "center",
     212          caption_justify: "JustifyMethod" = "center",
     213          highlight: bool = False,
     214      ) -> None:
     215  
     216          self.columns: List[Column] = []
     217          self.rows: List[Row] = []
     218          self.title = title
     219          self.caption = caption
     220          self.width = width
     221          self.min_width = min_width
     222          self.box = box
     223          self.safe_box = safe_box
     224          self._padding = Padding.unpack(padding)
     225          self.pad_edge = pad_edge
     226          self._expand = expand
     227          self.show_header = show_header
     228          self.show_footer = show_footer
     229          self.show_edge = show_edge
     230          self.show_lines = show_lines
     231          self.leading = leading
     232          self.collapse_padding = collapse_padding
     233          self.style = style
     234          self.header_style = header_style or ""
     235          self.footer_style = footer_style or ""
     236          self.border_style = border_style
     237          self.title_style = title_style
     238          self.caption_style = caption_style
     239          self.title_justify: "JustifyMethod" = title_justify
     240          self.caption_justify: "JustifyMethod" = caption_justify
     241          self.highlight = highlight
     242          self.row_styles: Sequence[StyleType] = list(row_styles or [])
     243          append_column = self.columns.append
     244          for header in headers:
     245              if isinstance(header, str):
     246                  self.add_column(header=header)
     247              else:
     248                  header._index = len(self.columns)
     249                  append_column(header)
     250  
     251      @classmethod
     252      def grid(
     253          cls,
     254          *headers: Union[Column, str],
     255          padding: PaddingDimensions = 0,
     256          collapse_padding: bool = True,
     257          pad_edge: bool = False,
     258          expand: bool = False,
     259      ) -> "Table":
     260          """Get a table with no lines, headers, or footer.
     261  
     262          Args:
     263              *headers (Union[Column, str]): Column headers, either as a string, or :class:`~rich.table.Column` instance.
     264              padding (PaddingDimensions, optional): Get padding around cells. Defaults to 0.
     265              collapse_padding (bool, optional): Enable collapsing of padding around cells. Defaults to True.
     266              pad_edge (bool, optional): Enable padding around edges of table. Defaults to False.
     267              expand (bool, optional): Expand the table to fit the available space if ``True``, otherwise the table width will be auto-calculated. Defaults to False.
     268  
     269          Returns:
     270              Table: A table instance.
     271          """
     272          return cls(
     273              *headers,
     274              box=None,
     275              padding=padding,
     276              collapse_padding=collapse_padding,
     277              show_header=False,
     278              show_footer=False,
     279              show_edge=False,
     280              pad_edge=pad_edge,
     281              expand=expand,
     282          )
     283  
     284      @property
     285      def expand(self) -> bool:
     286          """Setting a non-None self.width implies expand."""
     287          return self._expand or self.width is not None
     288  
     289      @expand.setter
     290      def expand(self, expand: bool) -> None:
     291          """Set expand."""
     292          self._expand = expand
     293  
     294      @property
     295      def _extra_width(self) -> int:
     296          """Get extra width to add to cell content."""
     297          width = 0
     298          if self.box and self.show_edge:
     299              width += 2
     300          if self.box:
     301              width += len(self.columns) - 1
     302          return width
     303  
     304      @property
     305      def row_count(self) -> int:
     306          """Get the current number of rows."""
     307          return len(self.rows)
     308  
     309      def get_row_style(self, console: "Console", index: int) -> StyleType:
     310          """Get the current row style."""
     311          style = Style.null()
     312          if self.row_styles:
     313              style += console.get_style(self.row_styles[index % len(self.row_styles)])
     314          row_style = self.rows[index].style
     315          if row_style is not None:
     316              style += console.get_style(row_style)
     317          return style
     318  
     319      def __rich_measure__(
     320          self, console: "Console", options: "ConsoleOptions"
     321      ) -> Measurement:
     322          max_width = options.max_width
     323          if self.width is not None:
     324              max_width = self.width
     325          if max_width < 0:
     326              return Measurement(0, 0)
     327  
     328          extra_width = self._extra_width
     329          max_width = sum(
     330              self._calculate_column_widths(
     331                  console, options.update_width(max_width - extra_width)
     332              )
     333          )
     334          _measure_column = self._measure_column
     335  
     336          measurements = [
     337              _measure_column(console, options.update_width(max_width), column)
     338              for column in self.columns
     339          ]
     340          minimum_width = (
     341              sum(measurement.minimum for measurement in measurements) + extra_width
     342          )
     343          maximum_width = (
     344              sum(measurement.maximum for measurement in measurements) + extra_width
     345              if (self.width is None)
     346              else self.width
     347          )
     348          measurement = Measurement(minimum_width, maximum_width)
     349          measurement = measurement.clamp(self.min_width)
     350          return measurement
     351  
     352      @property
     353      def padding(self) -> Tuple[int, int, int, int]:
     354          """Get cell padding."""
     355          return self._padding
     356  
     357      @padding.setter
     358      def padding(self, padding: PaddingDimensions) -> "Table":
     359          """Set cell padding."""
     360          self._padding = Padding.unpack(padding)
     361          return self
     362  
     363      def add_column(
     364          self,
     365          header: "RenderableType" = "",
     366          footer: "RenderableType" = "",
     367          *,
     368          header_style: Optional[StyleType] = None,
     369          footer_style: Optional[StyleType] = None,
     370          style: Optional[StyleType] = None,
     371          justify: "JustifyMethod" = "left",
     372          vertical: "VerticalAlignMethod" = "top",
     373          overflow: "OverflowMethod" = "ellipsis",
     374          width: Optional[int] = None,
     375          min_width: Optional[int] = None,
     376          max_width: Optional[int] = None,
     377          ratio: Optional[int] = None,
     378          no_wrap: bool = False,
     379      ) -> None:
     380          """Add a column to the table.
     381  
     382          Args:
     383              header (RenderableType, optional): Text or renderable for the header.
     384                  Defaults to "".
     385              footer (RenderableType, optional): Text or renderable for the footer.
     386                  Defaults to "".
     387              header_style (Union[str, Style], optional): Style for the header, or None for default. Defaults to None.
     388              footer_style (Union[str, Style], optional): Style for the footer, or None for default. Defaults to None.
     389              style (Union[str, Style], optional): Style for the column cells, or None for default. Defaults to None.
     390              justify (JustifyMethod, optional): Alignment for cells. Defaults to "left".
     391              vertical (VerticalAlignMethod, optional): Vertical alignment, one of "top", "middle", or "bottom". Defaults to "top".
     392              overflow (OverflowMethod): Overflow method: "crop", "fold", "ellipsis". Defaults to "ellipsis".
     393              width (int, optional): Desired width of column in characters, or None to fit to contents. Defaults to None.
     394              min_width (Optional[int], optional): Minimum width of column, or ``None`` for no minimum. Defaults to None.
     395              max_width (Optional[int], optional): Maximum width of column, or ``None`` for no maximum. Defaults to None.
     396              ratio (int, optional): Flexible ratio for the column (requires ``Table.expand`` or ``Table.width``). Defaults to None.
     397              no_wrap (bool, optional): Set to ``True`` to disable wrapping of this column.
     398          """
     399  
     400          column = Column(
     401              _index=len(self.columns),
     402              header=header,
     403              footer=footer,
     404              header_style=header_style or "",
     405              footer_style=footer_style or "",
     406              style=style or "",
     407              justify=justify,
     408              vertical=vertical,
     409              overflow=overflow,
     410              width=width,
     411              min_width=min_width,
     412              max_width=max_width,
     413              ratio=ratio,
     414              no_wrap=no_wrap,
     415          )
     416          self.columns.append(column)
     417  
     418      def add_row(
     419          self,
     420          *renderables: Optional["RenderableType"],
     421          style: Optional[StyleType] = None,
     422          end_section: bool = False,
     423      ) -> None:
     424          """Add a row of renderables.
     425  
     426          Args:
     427              *renderables (None or renderable): Each cell in a row must be a renderable object (including str),
     428                  or ``None`` for a blank cell.
     429              style (StyleType, optional): An optional style to apply to the entire row. Defaults to None.
     430              end_section (bool, optional): End a section and draw a line. Defaults to False.
     431  
     432          Raises:
     433              errors.NotRenderableError: If you add something that can't be rendered.
     434          """
     435  
     436          def add_cell(column: Column, renderable: "RenderableType") -> None:
     437              column._cells.append(renderable)
     438  
     439          cell_renderables: List[Optional["RenderableType"]] = list(renderables)
     440  
     441          columns = self.columns
     442          if len(cell_renderables) < len(columns):
     443              cell_renderables = [
     444                  *cell_renderables,
     445                  *[None] * (len(columns) - len(cell_renderables)),
     446              ]
     447          for index, renderable in enumerate(cell_renderables):
     448              if index == len(columns):
     449                  column = Column(_index=index)
     450                  for _ in self.rows:
     451                      add_cell(column, Text(""))
     452                  self.columns.append(column)
     453              else:
     454                  column = columns[index]
     455              if renderable is None:
     456                  add_cell(column, "")
     457              elif is_renderable(renderable):
     458                  add_cell(column, renderable)
     459              else:
     460                  raise errors.NotRenderableError(
     461                      f"unable to render {type(renderable).__name__}; a string or other renderable object is required"
     462                  )
     463          self.rows.append(Row(style=style, end_section=end_section))
     464  
     465      def add_section(self) -> None:
     466          """Add a new section (draw a line after current row)."""
     467  
     468          if self.rows:
     469              self.rows[-1].end_section = True
     470  
     471      def __rich_console__(
     472          self, console: "Console", options: "ConsoleOptions"
     473      ) -> "RenderResult":
     474  
     475          if not self.columns:
     476              yield Segment("\n")
     477              return
     478  
     479          max_width = options.max_width
     480          if self.width is not None:
     481              max_width = self.width
     482  
     483          extra_width = self._extra_width
     484          widths = self._calculate_column_widths(
     485              console, options.update_width(max_width - extra_width)
     486          )
     487          table_width = sum(widths) + extra_width
     488  
     489          render_options = options.update(
     490              width=table_width, highlight=self.highlight, height=None
     491          )
     492  
     493          def render_annotation(
     494              text: TextType, style: StyleType, justify: "JustifyMethod" = "center"
     495          ) -> "RenderResult":
     496              render_text = (
     497                  console.render_str(text, style=style, highlight=False)
     498                  if isinstance(text, str)
     499                  else text
     500              )
     501              return console.render(
     502                  render_text, options=render_options.update(justify=justify)
     503              )
     504  
     505          if self.title:
     506              yield from render_annotation(
     507                  self.title,
     508                  style=Style.pick_first(self.title_style, "table.title"),
     509                  justify=self.title_justify,
     510              )
     511          yield from self._render(console, render_options, widths)
     512          if self.caption:
     513              yield from render_annotation(
     514                  self.caption,
     515                  style=Style.pick_first(self.caption_style, "table.caption"),
     516                  justify=self.caption_justify,
     517              )
     518  
     519      def _calculate_column_widths(
     520          self, console: "Console", options: "ConsoleOptions"
     521      ) -> List[int]:
     522          """Calculate the widths of each column, including padding, not including borders."""
     523          max_width = options.max_width
     524          columns = self.columns
     525          width_ranges = [
     526              self._measure_column(console, options, column) for column in columns
     527          ]
     528          widths = [_range.maximum or 1 for _range in width_ranges]
     529          get_padding_width = self._get_padding_width
     530          extra_width = self._extra_width
     531          if self.expand:
     532              ratios = [col.ratio or 0 for col in columns if col.flexible]
     533              if any(ratios):
     534                  fixed_widths = [
     535                      0 if column.flexible else _range.maximum
     536                      for _range, column in zip(width_ranges, columns)
     537                  ]
     538                  flex_minimum = [
     539                      (column.width or 1) + get_padding_width(column._index)
     540                      for column in columns
     541                      if column.flexible
     542                  ]
     543                  flexible_width = max_width - sum(fixed_widths)
     544                  flex_widths = ratio_distribute(flexible_width, ratios, flex_minimum)
     545                  iter_flex_widths = iter(flex_widths)
     546                  for index, column in enumerate(columns):
     547                      if column.flexible:
     548                          widths[index] = fixed_widths[index] + next(iter_flex_widths)
     549          table_width = sum(widths)
     550  
     551          if table_width > max_width:
     552              widths = self._collapse_widths(
     553                  widths,
     554                  [(column.width is None and not column.no_wrap) for column in columns],
     555                  max_width,
     556              )
     557              table_width = sum(widths)
     558              # last resort, reduce columns evenly
     559              if table_width > max_width:
     560                  excess_width = table_width - max_width
     561                  widths = ratio_reduce(excess_width, [1] * len(widths), widths, widths)
     562                  table_width = sum(widths)
     563  
     564              width_ranges = [
     565                  self._measure_column(console, options.update_width(width), column)
     566                  for width, column in zip(widths, columns)
     567              ]
     568              widths = [_range.maximum or 0 for _range in width_ranges]
     569  
     570          if (table_width < max_width and self.expand) or (
     571              self.min_width is not None and table_width < (self.min_width - extra_width)
     572          ):
     573              _max_width = (
     574                  max_width
     575                  if self.min_width is None
     576                  else min(self.min_width - extra_width, max_width)
     577              )
     578              pad_widths = ratio_distribute(_max_width - table_width, widths)
     579              widths = [_width + pad for _width, pad in zip(widths, pad_widths)]
     580  
     581          return widths
     582  
     583      @classmethod
     584      def _collapse_widths(
     585          cls, widths: List[int], wrapable: List[bool], max_width: int
     586      ) -> List[int]:
     587          """Reduce widths so that the total is under max_width.
     588  
     589          Args:
     590              widths (List[int]): List of widths.
     591              wrapable (List[bool]): List of booleans that indicate if a column may shrink.
     592              max_width (int): Maximum width to reduce to.
     593  
     594          Returns:
     595              List[int]: A new list of widths.
     596          """
     597          total_width = sum(widths)
     598          excess_width = total_width - max_width
     599          if any(wrapable):
     600              while total_width and excess_width > 0:
     601                  max_column = max(
     602                      width for width, allow_wrap in zip(widths, wrapable) if allow_wrap
     603                  )
     604                  second_max_column = max(
     605                      width if allow_wrap and width != max_column else 0
     606                      for width, allow_wrap in zip(widths, wrapable)
     607                  )
     608                  column_difference = max_column - second_max_column
     609                  ratios = [
     610                      (1 if (width == max_column and allow_wrap) else 0)
     611                      for width, allow_wrap in zip(widths, wrapable)
     612                  ]
     613                  if not any(ratios) or not column_difference:
     614                      break
     615                  max_reduce = [min(excess_width, column_difference)] * len(widths)
     616                  widths = ratio_reduce(excess_width, ratios, max_reduce, widths)
     617  
     618                  total_width = sum(widths)
     619                  excess_width = total_width - max_width
     620          return widths
     621  
     622      def _get_cells(
     623          self, console: "Console", column_index: int, column: Column
     624      ) -> Iterable[_Cell]:
     625          """Get all the cells with padding and optional header."""
     626  
     627          collapse_padding = self.collapse_padding
     628          pad_edge = self.pad_edge
     629          padding = self.padding
     630          any_padding = any(padding)
     631  
     632          first_column = column_index == 0
     633          last_column = column_index == len(self.columns) - 1
     634  
     635          _padding_cache: Dict[Tuple[bool, bool], Tuple[int, int, int, int]] = {}
     636  
     637          def get_padding(first_row: bool, last_row: bool) -> Tuple[int, int, int, int]:
     638              cached = _padding_cache.get((first_row, last_row))
     639              if cached:
     640                  return cached
     641              top, right, bottom, left = padding
     642  
     643              if collapse_padding:
     644                  if not first_column:
     645                      left = max(0, left - right)
     646                  if not last_row:
     647                      bottom = max(0, top - bottom)
     648  
     649              if not pad_edge:
     650                  if first_column:
     651                      left = 0
     652                  if last_column:
     653                      right = 0
     654                  if first_row:
     655                      top = 0
     656                  if last_row:
     657                      bottom = 0
     658              _padding = (top, right, bottom, left)
     659              _padding_cache[(first_row, last_row)] = _padding
     660              return _padding
     661  
     662          raw_cells: List[Tuple[StyleType, "RenderableType"]] = []
     663          _append = raw_cells.append
     664          get_style = console.get_style
     665          if self.show_header:
     666              header_style = get_style(self.header_style or "") + get_style(
     667                  column.header_style
     668              )
     669              _append((header_style, column.header))
     670          cell_style = get_style(column.style or "")
     671          for cell in column.cells:
     672              _append((cell_style, cell))
     673          if self.show_footer:
     674              footer_style = get_style(self.footer_style or "") + get_style(
     675                  column.footer_style
     676              )
     677              _append((footer_style, column.footer))
     678  
     679          if any_padding:
     680              _Padding = Padding
     681              for first, last, (style, renderable) in loop_first_last(raw_cells):
     682                  yield _Cell(
     683                      style,
     684                      _Padding(renderable, get_padding(first, last)),
     685                      getattr(renderable, "vertical", None) or column.vertical,
     686                  )
     687          else:
     688              for (style, renderable) in raw_cells:
     689                  yield _Cell(
     690                      style,
     691                      renderable,
     692                      getattr(renderable, "vertical", None) or column.vertical,
     693                  )
     694  
     695      def _get_padding_width(self, column_index: int) -> int:
     696          """Get extra width from padding."""
     697          _, pad_right, _, pad_left = self.padding
     698          if self.collapse_padding:
     699              if column_index > 0:
     700                  pad_left = max(0, pad_left - pad_right)
     701          return pad_left + pad_right
     702  
     703      def _measure_column(
     704          self,
     705          console: "Console",
     706          options: "ConsoleOptions",
     707          column: Column,
     708      ) -> Measurement:
     709          """Get the minimum and maximum width of the column."""
     710  
     711          max_width = options.max_width
     712          if max_width < 1:
     713              return Measurement(0, 0)
     714  
     715          padding_width = self._get_padding_width(column._index)
     716  
     717          if column.width is not None:
     718              # Fixed width column
     719              return Measurement(
     720                  column.width + padding_width, column.width + padding_width
     721              ).with_maximum(max_width)
     722          # Flexible column, we need to measure contents
     723          min_widths: List[int] = []
     724          max_widths: List[int] = []
     725          append_min = min_widths.append
     726          append_max = max_widths.append
     727          get_render_width = Measurement.get
     728          for cell in self._get_cells(console, column._index, column):
     729              _min, _max = get_render_width(console, options, cell.renderable)
     730              append_min(_min)
     731              append_max(_max)
     732  
     733          measurement = Measurement(
     734              max(min_widths) if min_widths else 1,
     735              max(max_widths) if max_widths else max_width,
     736          ).with_maximum(max_width)
     737          measurement = measurement.clamp(
     738              None if column.min_width is None else column.min_width + padding_width,
     739              None if column.max_width is None else column.max_width + padding_width,
     740          )
     741          return measurement
     742  
     743      def _render(
     744          self, console: "Console", options: "ConsoleOptions", widths: List[int]
     745      ) -> "RenderResult":
     746          table_style = console.get_style(self.style or "")
     747  
     748          border_style = table_style + console.get_style(self.border_style or "")
     749          _column_cells = (
     750              self._get_cells(console, column_index, column)
     751              for column_index, column in enumerate(self.columns)
     752          )
     753          row_cells: List[Tuple[_Cell, ...]] = list(zip(*_column_cells))
     754          _box = (
     755              self.box.substitute(
     756                  options, safe=pick_bool(self.safe_box, console.safe_box)
     757              )
     758              if self.box
     759              else None
     760          )
     761          _box = _box.get_plain_headed_box() if _box and not self.show_header else _box
     762  
     763          new_line = Segment.line()
     764  
     765          columns = self.columns
     766          show_header = self.show_header
     767          show_footer = self.show_footer
     768          show_edge = self.show_edge
     769          show_lines = self.show_lines
     770          leading = self.leading
     771  
     772          _Segment = Segment
     773          if _box:
     774              box_segments = [
     775                  (
     776                      _Segment(_box.head_left, border_style),
     777                      _Segment(_box.head_right, border_style),
     778                      _Segment(_box.head_vertical, border_style),
     779                  ),
     780                  (
     781                      _Segment(_box.foot_left, border_style),
     782                      _Segment(_box.foot_right, border_style),
     783                      _Segment(_box.foot_vertical, border_style),
     784                  ),
     785                  (
     786                      _Segment(_box.mid_left, border_style),
     787                      _Segment(_box.mid_right, border_style),
     788                      _Segment(_box.mid_vertical, border_style),
     789                  ),
     790              ]
     791              if show_edge:
     792                  yield _Segment(_box.get_top(widths), border_style)
     793                  yield new_line
     794          else:
     795              box_segments = []
     796  
     797          get_row_style = self.get_row_style
     798          get_style = console.get_style
     799  
     800          for index, (first, last, row_cell) in enumerate(loop_first_last(row_cells)):
     801              header_row = first and show_header
     802              footer_row = last and show_footer
     803              row = (
     804                  self.rows[index - show_header]
     805                  if (not header_row and not footer_row)
     806                  else None
     807              )
     808              max_height = 1
     809              cells: List[List[List[Segment]]] = []
     810              if header_row or footer_row:
     811                  row_style = Style.null()
     812              else:
     813                  row_style = get_style(
     814                      get_row_style(console, index - 1 if show_header else index)
     815                  )
     816              for width, cell, column in zip(widths, row_cell, columns):
     817                  render_options = options.update(
     818                      width=width,
     819                      justify=column.justify,
     820                      no_wrap=column.no_wrap,
     821                      overflow=column.overflow,
     822                      height=None,
     823                  )
     824                  lines = console.render_lines(
     825                      cell.renderable,
     826                      render_options,
     827                      style=get_style(cell.style) + row_style,
     828                  )
     829                  max_height = max(max_height, len(lines))
     830                  cells.append(lines)
     831  
     832              row_height = max(len(cell) for cell in cells)
     833  
     834              def align_cell(
     835                  cell: List[List[Segment]],
     836                  vertical: "VerticalAlignMethod",
     837                  width: int,
     838                  style: Style,
     839              ) -> List[List[Segment]]:
     840                  if header_row:
     841                      vertical = "bottom"
     842                  elif footer_row:
     843                      vertical = "top"
     844  
     845                  if vertical == "top":
     846                      return _Segment.align_top(cell, width, row_height, style)
     847                  elif vertical == "middle":
     848                      return _Segment.align_middle(cell, width, row_height, style)
     849                  return _Segment.align_bottom(cell, width, row_height, style)
     850  
     851              cells[:] = [
     852                  _Segment.set_shape(
     853                      align_cell(
     854                          cell,
     855                          _cell.vertical,
     856                          width,
     857                          get_style(_cell.style) + row_style,
     858                      ),
     859                      width,
     860                      max_height,
     861                  )
     862                  for width, _cell, cell, column in zip(widths, row_cell, cells, columns)
     863              ]
     864  
     865              if _box:
     866                  if last and show_footer:
     867                      yield _Segment(
     868                          _box.get_row(widths, "foot", edge=show_edge), border_style
     869                      )
     870                      yield new_line
     871                  left, right, _divider = box_segments[0 if first else (2 if last else 1)]
     872  
     873                  # If the column divider is whitespace also style it with the row background
     874                  divider = (
     875                      _divider
     876                      if _divider.text.strip()
     877                      else _Segment(
     878                          _divider.text, row_style.background_style + _divider.style
     879                      )
     880                  )
     881                  for line_no in range(max_height):
     882                      if show_edge:
     883                          yield left
     884                      for last_cell, rendered_cell in loop_last(cells):
     885                          yield from rendered_cell[line_no]
     886                          if not last_cell:
     887                              yield divider
     888                      if show_edge:
     889                          yield right
     890                      yield new_line
     891              else:
     892                  for line_no in range(max_height):
     893                      for rendered_cell in cells:
     894                          yield from rendered_cell[line_no]
     895                      yield new_line
     896              if _box and first and show_header:
     897                  yield _Segment(
     898                      _box.get_row(widths, "head", edge=show_edge), border_style
     899                  )
     900                  yield new_line
     901              end_section = row and row.end_section
     902              if _box and (show_lines or leading or end_section):
     903                  if (
     904                      not last
     905                      and not (show_footer and index >= len(row_cells) - 2)
     906                      and not (show_header and header_row)
     907                  ):
     908                      if leading:
     909                          yield _Segment(
     910                              _box.get_row(widths, "mid", edge=show_edge) * leading,
     911                              border_style,
     912                          )
     913                      else:
     914                          yield _Segment(
     915                              _box.get_row(widths, "row", edge=show_edge), border_style
     916                          )
     917                      yield new_line
     918  
     919          if _box and show_edge:
     920              yield _Segment(_box.get_bottom(widths), border_style)
     921              yield new_line
     922  
     923  
     924  if __name__ == "__main__":  # pragma: no cover
     925      from pip._vendor.rich.console import Console
     926      from pip._vendor.rich.highlighter import ReprHighlighter
     927      from pip._vendor.rich.table import Table as Table
     928  
     929      from ._timer import timer
     930  
     931      with timer("Table render"):
     932          table = Table(
     933              title="Star Wars Movies",
     934              caption="Rich example table",
     935              caption_justify="right",
     936          )
     937  
     938          table.add_column(
     939              "Released", header_style="bright_cyan", style="cyan", no_wrap=True
     940          )
     941          table.add_column("Title", style="magenta")
     942          table.add_column("Box Office", justify="right", style="green")
     943  
     944          table.add_row(
     945              "Dec 20, 2019",
     946              "Star Wars: The Rise of Skywalker",
     947              "$952,110,690",
     948          )
     949          table.add_row("May 25, 2018", "Solo: A Star Wars Story", "$393,151,347")
     950          table.add_row(
     951              "Dec 15, 2017",
     952              "Star Wars Ep. V111: The Last Jedi",
     953              "$1,332,539,889",
     954              style="on black",
     955              end_section=True,
     956          )
     957          table.add_row(
     958              "Dec 16, 2016",
     959              "Rogue One: A Star Wars Story",
     960              "$1,332,439,889",
     961          )
     962  
     963          def header(text: str) -> None:
     964              console.print()
     965              console.rule(highlight(text))
     966              console.print()
     967  
     968          console = Console()
     969          highlight = ReprHighlighter()
     970          header("Example Table")
     971          console.print(table, justify="center")
     972  
     973          table.expand = True
     974          header("expand=True")
     975          console.print(table)
     976  
     977          table.width = 50
     978          header("width=50")
     979  
     980          console.print(table, justify="center")
     981  
     982          table.width = None
     983          table.expand = False
     984          table.row_styles = ["dim", "none"]
     985          header("row_styles=['dim', 'none']")
     986  
     987          console.print(table, justify="center")
     988  
     989          table.width = None
     990          table.expand = False
     991          table.row_styles = ["dim", "none"]
     992          table.leading = 1
     993          header("leading=1, row_styles=['dim', 'none']")
     994          console.print(table, justify="center")
     995  
     996          table.width = None
     997          table.expand = False
     998          table.row_styles = ["dim", "none"]
     999          table.show_lines = True
    1000          table.leading = 0
    1001          header("show_lines=True, row_styles=['dim', 'none']")
    1002          console.print(table, justify="center")