You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

1003 lines
39 KiB

6 months ago
from dataclasses import dataclass, field, replace
from typing import (
TYPE_CHECKING,
Dict,
Iterable,
List,
NamedTuple,
Optional,
Sequence,
Tuple,
Union,
)
from . import box, errors
from ._loop import loop_first_last, loop_last
from ._pick import pick_bool
from ._ratio import ratio_distribute, ratio_reduce
from .align import VerticalAlignMethod
from .jupyter import JupyterMixin
from .measure import Measurement
from .padding import Padding, PaddingDimensions
from .protocol import is_renderable
from .segment import Segment
from .style import Style, StyleType
from .text import Text, TextType
if TYPE_CHECKING:
from .console import (
Console,
ConsoleOptions,
JustifyMethod,
OverflowMethod,
RenderableType,
RenderResult,
)
@dataclass
class Column:
"""Defines a column within a ~Table.
Args:
title (Union[str, Text], optional): The title of the table rendered at the top. Defaults to None.
caption (Union[str, Text], optional): The table caption rendered below. Defaults to None.
width (int, optional): The width in characters of the table, or ``None`` to automatically fit. Defaults to None.
min_width (Optional[int], optional): The minimum width of the table, or ``None`` for no minimum. Defaults to None.
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.
safe_box (Optional[bool], optional): Disable box characters that don't display on windows legacy terminal with *raster* fonts. Defaults to True.
padding (PaddingDimensions, optional): Padding for cells (top, right, bottom, left). Defaults to (0, 1).
collapse_padding (bool, optional): Enable collapsing of padding around cells. Defaults to False.
pad_edge (bool, optional): Enable padding of edge cells. Defaults to True.
expand (bool, optional): Expand the table to fit the available space if ``True``, otherwise the table width will be auto-calculated. Defaults to False.
show_header (bool, optional): Show a header row. Defaults to True.
show_footer (bool, optional): Show a footer row. Defaults to False.
show_edge (bool, optional): Draw a box around the outside of the table. Defaults to True.
show_lines (bool, optional): Draw lines between every row. Defaults to False.
leading (bool, optional): Number of blank lines between rows (precludes ``show_lines``). Defaults to 0.
style (Union[str, Style], optional): Default style for the table. Defaults to "none".
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.
header_style (Union[str, Style], optional): Style of the header. Defaults to "table.header".
footer_style (Union[str, Style], optional): Style of the footer. Defaults to "table.footer".
border_style (Union[str, Style], optional): Style of the border. Defaults to None.
title_style (Union[str, Style], optional): Style of the title. Defaults to None.
caption_style (Union[str, Style], optional): Style of the caption. Defaults to None.
title_justify (str, optional): Justify method for title. Defaults to "center".
caption_justify (str, optional): Justify method for caption. Defaults to "center".
highlight (bool, optional): Highlight cell contents (if str). Defaults to False.
"""
header: "RenderableType" = ""
"""RenderableType: Renderable for the header (typically a string)"""
footer: "RenderableType" = ""
"""RenderableType: Renderable for the footer (typically a string)"""
header_style: StyleType = ""
"""StyleType: The style of the header."""
footer_style: StyleType = ""
"""StyleType: The style of the footer."""
style: StyleType = ""
"""StyleType: The style of the column."""
justify: "JustifyMethod" = "left"
"""str: How to justify text within the column ("left", "center", "right", or "full")"""
vertical: "VerticalAlignMethod" = "top"
"""str: How to vertically align content ("top", "middle", or "bottom")"""
overflow: "OverflowMethod" = "ellipsis"
"""str: Overflow method."""
width: Optional[int] = None
"""Optional[int]: Width of the column, or ``None`` (default) to auto calculate width."""
min_width: Optional[int] = None
"""Optional[int]: Minimum width of column, or ``None`` for no minimum. Defaults to None."""
max_width: Optional[int] = None
"""Optional[int]: Maximum width of column, or ``None`` for no maximum. Defaults to None."""
ratio: Optional[int] = None
"""Optional[int]: Ratio to use when calculating column width, or ``None`` (default) to adapt to column contents."""
no_wrap: bool = False
"""bool: Prevent wrapping of text within the column. Defaults to ``False``."""
_index: int = 0
"""Index of column."""
_cells: List["RenderableType"] = field(default_factory=list)
def copy(self) -> "Column":
"""Return a copy of this Column."""
return replace(self, _cells=[])
@property
def cells(self) -> Iterable["RenderableType"]:
"""Get all cells in the column, not including header."""
yield from self._cells
@property
def flexible(self) -> bool:
"""Check if this column is flexible."""
return self.ratio is not None
@dataclass
class Row:
"""Information regarding a row."""
style: Optional[StyleType] = None
"""Style to apply to row."""
end_section: bool = False
"""Indicated end of section, which will force a line beneath the row."""
class _Cell(NamedTuple):
"""A single cell in a table."""
style: StyleType
"""Style to apply to cell."""
renderable: "RenderableType"
"""Cell renderable."""
vertical: VerticalAlignMethod
"""Cell vertical alignment."""
class Table(JupyterMixin):
"""A console renderable to draw a table.
Args:
*headers (Union[Column, str]): Column headers, either as a string, or :class:`~rich.table.Column` instance.
title (Union[str, Text], optional): The title of the table rendered at the top. Defaults to None.
caption (Union[str, Text], optional): The table caption rendered below. Defaults to None.
width (int, optional): The width in characters of the table, or ``None`` to automatically fit. Defaults to None.
min_width (Optional[int], optional): The minimum width of the table, or ``None`` for no minimum. Defaults to None.
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.
safe_box (Optional[bool], optional): Disable box characters that don't display on windows legacy terminal with *raster* fonts. Defaults to True.
padding (PaddingDimensions, optional): Padding for cells (top, right, bottom, left). Defaults to (0, 1).
collapse_padding (bool, optional): Enable collapsing of padding around cells. Defaults to False.
pad_edge (bool, optional): Enable padding of edge cells. Defaults to True.
expand (bool, optional): Expand the table to fit the available space if ``True``, otherwise the table width will be auto-calculated. Defaults to False.
show_header (bool, optional): Show a header row. Defaults to True.
show_footer (bool, optional): Show a footer row. Defaults to False.
show_edge (bool, optional): Draw a box around the outside of the table. Defaults to True.
show_lines (bool, optional): Draw lines between every row. Defaults to False.
leading (bool, optional): Number of blank lines between rows (precludes ``show_lines``). Defaults to 0.
style (Union[str, Style], optional): Default style for the table. Defaults to "none".
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.
header_style (Union[str, Style], optional): Style of the header. Defaults to "table.header".
footer_style (Union[str, Style], optional): Style of the footer. Defaults to "table.footer".
border_style (Union[str, Style], optional): Style of the border. Defaults to None.
title_style (Union[str, Style], optional): Style of the title. Defaults to None.
caption_style (Union[str, Style], optional): Style of the caption. Defaults to None.
title_justify (str, optional): Justify method for title. Defaults to "center".
caption_justify (str, optional): Justify method for caption. Defaults to "center".
highlight (bool, optional): Highlight cell contents (if str). Defaults to False.
"""
columns: List[Column]
rows: List[Row]
def __init__(
self,
*headers: Union[Column, str],
title: Optional[TextType] = None,
caption: Optional[TextType] = None,
width: Optional[int] = None,
min_width: Optional[int] = None,
box: Optional[box.Box] = box.HEAVY_HEAD,
safe_box: Optional[bool] = None,
padding: PaddingDimensions = (0, 1),
collapse_padding: bool = False,
pad_edge: bool = True,
expand: bool = False,
show_header: bool = True,
show_footer: bool = False,
show_edge: bool = True,
show_lines: bool = False,
leading: int = 0,
style: StyleType = "none",
row_styles: Optional[Iterable[StyleType]] = None,
header_style: Optional[StyleType] = "table.header",
footer_style: Optional[StyleType] = "table.footer",
border_style: Optional[StyleType] = None,
title_style: Optional[StyleType] = None,
caption_style: Optional[StyleType] = None,
title_justify: "JustifyMethod" = "center",
caption_justify: "JustifyMethod" = "center",
highlight: bool = False,
) -> None:
self.columns: List[Column] = []
self.rows: List[Row] = []
self.title = title
self.caption = caption
self.width = width
self.min_width = min_width
self.box = box
self.safe_box = safe_box
self._padding = Padding.unpack(padding)
self.pad_edge = pad_edge
self._expand = expand
self.show_header = show_header
self.show_footer = show_footer
self.show_edge = show_edge
self.show_lines = show_lines
self.leading = leading
self.collapse_padding = collapse_padding
self.style = style
self.header_style = header_style or ""
self.footer_style = footer_style or ""
self.border_style = border_style
self.title_style = title_style
self.caption_style = caption_style
self.title_justify: "JustifyMethod" = title_justify
self.caption_justify: "JustifyMethod" = caption_justify
self.highlight = highlight
self.row_styles: Sequence[StyleType] = list(row_styles or [])
append_column = self.columns.append
for header in headers:
if isinstance(header, str):
self.add_column(header=header)
else:
header._index = len(self.columns)
append_column(header)
@classmethod
def grid(
cls,
*headers: Union[Column, str],
padding: PaddingDimensions = 0,
collapse_padding: bool = True,
pad_edge: bool = False,
expand: bool = False,
) -> "Table":
"""Get a table with no lines, headers, or footer.
Args:
*headers (Union[Column, str]): Column headers, either as a string, or :class:`~rich.table.Column` instance.
padding (PaddingDimensions, optional): Get padding around cells. Defaults to 0.
collapse_padding (bool, optional): Enable collapsing of padding around cells. Defaults to True.
pad_edge (bool, optional): Enable padding around edges of table. Defaults to False.
expand (bool, optional): Expand the table to fit the available space if ``True``, otherwise the table width will be auto-calculated. Defaults to False.
Returns:
Table: A table instance.
"""
return cls(
*headers,
box=None,
padding=padding,
collapse_padding=collapse_padding,
show_header=False,
show_footer=False,
show_edge=False,
pad_edge=pad_edge,
expand=expand,
)
@property
def expand(self) -> bool:
"""Setting a non-None self.width implies expand."""
return self._expand or self.width is not None
@expand.setter
def expand(self, expand: bool) -> None:
"""Set expand."""
self._expand = expand
@property
def _extra_width(self) -> int:
"""Get extra width to add to cell content."""
width = 0
if self.box and self.show_edge:
width += 2
if self.box:
width += len(self.columns) - 1
return width
@property
def row_count(self) -> int:
"""Get the current number of rows."""
return len(self.rows)
def get_row_style(self, console: "Console", index: int) -> StyleType:
"""Get the current row style."""
style = Style.null()
if self.row_styles:
style += console.get_style(self.row_styles[index % len(self.row_styles)])
row_style = self.rows[index].style
if row_style is not None:
style += console.get_style(row_style)
return style
def __rich_measure__(
self, console: "Console", options: "ConsoleOptions"
) -> Measurement:
max_width = options.max_width
if self.width is not None:
max_width = self.width
if max_width < 0:
return Measurement(0, 0)
extra_width = self._extra_width
max_width = sum(
self._calculate_column_widths(
console, options.update_width(max_width - extra_width)
)
)
_measure_column = self._measure_column
measurements = [
_measure_column(console, options.update_width(max_width), column)
for column in self.columns
]
minimum_width = (
sum(measurement.minimum for measurement in measurements) + extra_width
)
maximum_width = (
sum(measurement.maximum for measurement in measurements) + extra_width
if (self.width is None)
else self.width
)
measurement = Measurement(minimum_width, maximum_width)
measurement = measurement.clamp(self.min_width)
return measurement
@property
def padding(self) -> Tuple[int, int, int, int]:
"""Get cell padding."""
return self._padding
@padding.setter
def padding(self, padding: PaddingDimensions) -> "Table":
"""Set cell padding."""
self._padding = Padding.unpack(padding)
return self
def add_column(
self,
header: "RenderableType" = "",
footer: "RenderableType" = "",
*,
header_style: Optional[StyleType] = None,
footer_style: Optional[StyleType] = None,
style: Optional[StyleType] = None,
justify: "JustifyMethod" = "left",
vertical: "VerticalAlignMethod" = "top",
overflow: "OverflowMethod" = "ellipsis",
width: Optional[int] = None,
min_width: Optional[int] = None,
max_width: Optional[int] = None,
ratio: Optional[int] = None,
no_wrap: bool = False,
) -> None:
"""Add a column to the table.
Args:
header (RenderableType, optional): Text or renderable for the header.
Defaults to "".
footer (RenderableType, optional): Text or renderable for the footer.
Defaults to "".
header_style (Union[str, Style], optional): Style for the header, or None for default. Defaults to None.
footer_style (Union[str, Style], optional): Style for the footer, or None for default. Defaults to None.
style (Union[str, Style], optional): Style for the column cells, or None for default. Defaults to None.
justify (JustifyMethod, optional): Alignment for cells. Defaults to "left".
vertical (VerticalAlignMethod, optional): Vertical alignment, one of "top", "middle", or "bottom". Defaults to "top".
overflow (OverflowMethod): Overflow method: "crop", "fold", "ellipsis". Defaults to "ellipsis".
width (int, optional): Desired width of column in characters, or None to fit to contents. Defaults to None.
min_width (Optional[int], optional): Minimum width of column, or ``None`` for no minimum. Defaults to None.
max_width (Optional[int], optional): Maximum width of column, or ``None`` for no maximum. Defaults to None.
ratio (int, optional): Flexible ratio for the column (requires ``Table.expand`` or ``Table.width``). Defaults to None.
no_wrap (bool, optional): Set to ``True`` to disable wrapping of this column.
"""
column = Column(
_index=len(self.columns),
header=header,
footer=footer,
header_style=header_style or "",
footer_style=footer_style or "",
style=style or "",
justify=justify,
vertical=vertical,
overflow=overflow,
width=width,
min_width=min_width,
max_width=max_width,
ratio=ratio,
no_wrap=no_wrap,
)
self.columns.append(column)
def add_row(
self,
*renderables: Optional["RenderableType"],
style: Optional[StyleType] = None,
end_section: bool = False,
) -> None:
"""Add a row of renderables.
Args:
*renderables (None or renderable): Each cell in a row must be a renderable object (including str),
or ``None`` for a blank cell.
style (StyleType, optional): An optional style to apply to the entire row. Defaults to None.
end_section (bool, optional): End a section and draw a line. Defaults to False.
Raises:
errors.NotRenderableError: If you add something that can't be rendered.
"""
def add_cell(column: Column, renderable: "RenderableType") -> None:
column._cells.append(renderable)
cell_renderables: List[Optional["RenderableType"]] = list(renderables)
columns = self.columns
if len(cell_renderables) < len(columns):
cell_renderables = [
*cell_renderables,
*[None] * (len(columns) - len(cell_renderables)),
]
for index, renderable in enumerate(cell_renderables):
if index == len(columns):
column = Column(_index=index)
for _ in self.rows:
add_cell(column, Text(""))
self.columns.append(column)
else:
column = columns[index]
if renderable is None:
add_cell(column, "")
elif is_renderable(renderable):
add_cell(column, renderable)
else:
raise errors.NotRenderableError(
f"unable to render {type(renderable).__name__}; a string or other renderable object is required"
)
self.rows.append(Row(style=style, end_section=end_section))
def add_section(self) -> None:
"""Add a new section (draw a line after current row)."""
if self.rows:
self.rows[-1].end_section = True
def __rich_console__(
self, console: "Console", options: "ConsoleOptions"
) -> "RenderResult":
if not self.columns:
yield Segment("\n")
return
max_width = options.max_width
if self.width is not None:
max_width = self.width
extra_width = self._extra_width
widths = self._calculate_column_widths(
console, options.update_width(max_width - extra_width)
)
table_width = sum(widths) + extra_width
render_options = options.update(
width=table_width, highlight=self.highlight, height=None
)
def render_annotation(
text: TextType, style: StyleType, justify: "JustifyMethod" = "center"
) -> "RenderResult":
render_text = (
console.render_str(text, style=style, highlight=False)
if isinstance(text, str)
else text
)
return console.render(
render_text, options=render_options.update(justify=justify)
)
if self.title:
yield from render_annotation(
self.title,
style=Style.pick_first(self.title_style, "table.title"),
justify=self.title_justify,
)
yield from self._render(console, render_options, widths)
if self.caption:
yield from render_annotation(
self.caption,
style=Style.pick_first(self.caption_style, "table.caption"),
justify=self.caption_justify,
)
def _calculate_column_widths(
self, console: "Console", options: "ConsoleOptions"
) -> List[int]:
"""Calculate the widths of each column, including padding, not including borders."""
max_width = options.max_width
columns = self.columns
width_ranges = [
self._measure_column(console, options, column) for column in columns
]
widths = [_range.maximum or 1 for _range in width_ranges]
get_padding_width = self._get_padding_width
extra_width = self._extra_width
if self.expand:
ratios = [col.ratio or 0 for col in columns if col.flexible]
if any(ratios):
fixed_widths = [
0 if column.flexible else _range.maximum
for _range, column in zip(width_ranges, columns)
]
flex_minimum = [
(column.width or 1) + get_padding_width(column._index)
for column in columns
if column.flexible
]
flexible_width = max_width - sum(fixed_widths)
flex_widths = ratio_distribute(flexible_width, ratios, flex_minimum)
iter_flex_widths = iter(flex_widths)
for index, column in enumerate(columns):
if column.flexible:
widths[index] = fixed_widths[index] + next(iter_flex_widths)
table_width = sum(widths)
if table_width > max_width:
widths = self._collapse_widths(
widths,
[(column.width is None and not column.no_wrap) for column in columns],
max_width,
)
table_width = sum(widths)
# last resort, reduce columns evenly
if table_width > max_width:
excess_width = table_width - max_width
widths = ratio_reduce(excess_width, [1] * len(widths), widths, widths)
table_width = sum(widths)
width_ranges = [
self._measure_column(console, options.update_width(width), column)
for width, column in zip(widths, columns)
]
widths = [_range.maximum or 0 for _range in width_ranges]
if (table_width < max_width and self.expand) or (
self.min_width is not None and table_width < (self.min_width - extra_width)
):
_max_width = (
max_width
if self.min_width is None
else min(self.min_width - extra_width, max_width)
)
pad_widths = ratio_distribute(_max_width - table_width, widths)
widths = [_width + pad for _width, pad in zip(widths, pad_widths)]
return widths
@classmethod
def _collapse_widths(
cls, widths: List[int], wrapable: List[bool], max_width: int
) -> List[int]:
"""Reduce widths so that the total is under max_width.
Args:
widths (List[int]): List of widths.
wrapable (List[bool]): List of booleans that indicate if a column may shrink.
max_width (int): Maximum width to reduce to.
Returns:
List[int]: A new list of widths.
"""
total_width = sum(widths)
excess_width = total_width - max_width
if any(wrapable):
while total_width and excess_width > 0:
max_column = max(
width for width, allow_wrap in zip(widths, wrapable) if allow_wrap
)
second_max_column = max(
width if allow_wrap and width != max_column else 0
for width, allow_wrap in zip(widths, wrapable)
)
column_difference = max_column - second_max_column
ratios = [
(1 if (width == max_column and allow_wrap) else 0)
for width, allow_wrap in zip(widths, wrapable)
]
if not any(ratios) or not column_difference:
break
max_reduce = [min(excess_width, column_difference)] * len(widths)
widths = ratio_reduce(excess_width, ratios, max_reduce, widths)
total_width = sum(widths)
excess_width = total_width - max_width
return widths
def _get_cells(
self, console: "Console", column_index: int, column: Column
) -> Iterable[_Cell]:
"""Get all the cells with padding and optional header."""
collapse_padding = self.collapse_padding
pad_edge = self.pad_edge
padding = self.padding
any_padding = any(padding)
first_column = column_index == 0
last_column = column_index == len(self.columns) - 1
_padding_cache: Dict[Tuple[bool, bool], Tuple[int, int, int, int]] = {}
def get_padding(first_row: bool, last_row: bool) -> Tuple[int, int, int, int]:
cached = _padding_cache.get((first_row, last_row))
if cached:
return cached
top, right, bottom, left = padding
if collapse_padding:
if not first_column:
left = max(0, left - right)
if not last_row:
bottom = max(0, top - bottom)
if not pad_edge:
if first_column:
left = 0
if last_column:
right = 0
if first_row:
top = 0
if last_row:
bottom = 0
_padding = (top, right, bottom, left)
_padding_cache[(first_row, last_row)] = _padding
return _padding
raw_cells: List[Tuple[StyleType, "RenderableType"]] = []
_append = raw_cells.append
get_style = console.get_style
if self.show_header:
header_style = get_style(self.header_style or "") + get_style(
column.header_style
)
_append((header_style, column.header))
cell_style = get_style(column.style or "")
for cell in column.cells:
_append((cell_style, cell))
if self.show_footer:
footer_style = get_style(self.footer_style or "") + get_style(
column.footer_style
)
_append((footer_style, column.footer))
if any_padding:
_Padding = Padding
for first, last, (style, renderable) in loop_first_last(raw_cells):
yield _Cell(
style,
_Padding(renderable, get_padding(first, last)),
getattr(renderable, "vertical", None) or column.vertical,
)
else:
for (style, renderable) in raw_cells:
yield _Cell(
style,
renderable,
getattr(renderable, "vertical", None) or column.vertical,
)
def _get_padding_width(self, column_index: int) -> int:
"""Get extra width from padding."""
_, pad_right, _, pad_left = self.padding
if self.collapse_padding:
if column_index > 0:
pad_left = max(0, pad_left - pad_right)
return pad_left + pad_right
def _measure_column(
self,
console: "Console",
options: "ConsoleOptions",
column: Column,
) -> Measurement:
"""Get the minimum and maximum width of the column."""
max_width = options.max_width
if max_width < 1:
return Measurement(0, 0)
padding_width = self._get_padding_width(column._index)
if column.width is not None:
# Fixed width column
return Measurement(
column.width + padding_width, column.width + padding_width
).with_maximum(max_width)
# Flexible column, we need to measure contents
min_widths: List[int] = []
max_widths: List[int] = []
append_min = min_widths.append
append_max = max_widths.append
get_render_width = Measurement.get
for cell in self._get_cells(console, column._index, column):
_min, _max = get_render_width(console, options, cell.renderable)
append_min(_min)
append_max(_max)
measurement = Measurement(
max(min_widths) if min_widths else 1,
max(max_widths) if max_widths else max_width,
).with_maximum(max_width)
measurement = measurement.clamp(
None if column.min_width is None else column.min_width + padding_width,
None if column.max_width is None else column.max_width + padding_width,
)
return measurement
def _render(
self, console: "Console", options: "ConsoleOptions", widths: List[int]
) -> "RenderResult":
table_style = console.get_style(self.style or "")
border_style = table_style + console.get_style(self.border_style or "")
_column_cells = (
self._get_cells(console, column_index, column)
for column_index, column in enumerate(self.columns)
)
row_cells: List[Tuple[_Cell, ...]] = list(zip(*_column_cells))
_box = (
self.box.substitute(
options, safe=pick_bool(self.safe_box, console.safe_box)
)
if self.box
else None
)
_box = _box.get_plain_headed_box() if _box and not self.show_header else _box
new_line = Segment.line()
columns = self.columns
show_header = self.show_header
show_footer = self.show_footer
show_edge = self.show_edge
show_lines = self.show_lines
leading = self.leading
_Segment = Segment
if _box:
box_segments = [
(
_Segment(_box.head_left, border_style),
_Segment(_box.head_right, border_style),
_Segment(_box.head_vertical, border_style),
),
(
_Segment(_box.foot_left, border_style),
_Segment(_box.foot_right, border_style),
_Segment(_box.foot_vertical, border_style),
),
(
_Segment(_box.mid_left, border_style),
_Segment(_box.mid_right, border_style),
_Segment(_box.mid_vertical, border_style),
),
]
if show_edge:
yield _Segment(_box.get_top(widths), border_style)
yield new_line
else:
box_segments = []
get_row_style = self.get_row_style
get_style = console.get_style
for index, (first, last, row_cell) in enumerate(loop_first_last(row_cells)):
header_row = first and show_header
footer_row = last and show_footer
row = (
self.rows[index - show_header]
if (not header_row and not footer_row)
else None
)
max_height = 1
cells: List[List[List[Segment]]] = []
if header_row or footer_row:
row_style = Style.null()
else:
row_style = get_style(
get_row_style(console, index - 1 if show_header else index)
)
for width, cell, column in zip(widths, row_cell, columns):
render_options = options.update(
width=width,
justify=column.justify,
no_wrap=column.no_wrap,
overflow=column.overflow,
height=None,
)
lines = console.render_lines(
cell.renderable,
render_options,
style=get_style(cell.style) + row_style,
)
max_height = max(max_height, len(lines))
cells.append(lines)
row_height = max(len(cell) for cell in cells)
def align_cell(
cell: List[List[Segment]],
vertical: "VerticalAlignMethod",
width: int,
style: Style,
) -> List[List[Segment]]:
if header_row:
vertical = "bottom"
elif footer_row:
vertical = "top"
if vertical == "top":
return _Segment.align_top(cell, width, row_height, style)
elif vertical == "middle":
return _Segment.align_middle(cell, width, row_height, style)
return _Segment.align_bottom(cell, width, row_height, style)
cells[:] = [
_Segment.set_shape(
align_cell(
cell,
_cell.vertical,
width,
get_style(_cell.style) + row_style,
),
width,
max_height,
)
for width, _cell, cell, column in zip(widths, row_cell, cells, columns)
]
if _box:
if last and show_footer:
yield _Segment(
_box.get_row(widths, "foot", edge=show_edge), border_style
)
yield new_line
left, right, _divider = box_segments[0 if first else (2 if last else 1)]
# If the column divider is whitespace also style it with the row background
divider = (
_divider
if _divider.text.strip()
else _Segment(
_divider.text, row_style.background_style + _divider.style
)
)
for line_no in range(max_height):
if show_edge:
yield left
for last_cell, rendered_cell in loop_last(cells):
yield from rendered_cell[line_no]
if not last_cell:
yield divider
if show_edge:
yield right
yield new_line
else:
for line_no in range(max_height):
for rendered_cell in cells:
yield from rendered_cell[line_no]
yield new_line
if _box and first and show_header:
yield _Segment(
_box.get_row(widths, "head", edge=show_edge), border_style
)
yield new_line
end_section = row and row.end_section
if _box and (show_lines or leading or end_section):
if (
not last
and not (show_footer and index >= len(row_cells) - 2)
and not (show_header and header_row)
):
if leading:
yield _Segment(
_box.get_row(widths, "mid", edge=show_edge) * leading,
border_style,
)
else:
yield _Segment(
_box.get_row(widths, "row", edge=show_edge), border_style
)
yield new_line
if _box and show_edge:
yield _Segment(_box.get_bottom(widths), border_style)
yield new_line
if __name__ == "__main__": # pragma: no cover
from pip._vendor.rich.console import Console
from pip._vendor.rich.highlighter import ReprHighlighter
from pip._vendor.rich.table import Table as Table
from ._timer import timer
with timer("Table render"):
table = Table(
title="Star Wars Movies",
caption="Rich example table",
caption_justify="right",
)
table.add_column(
"Released", header_style="bright_cyan", style="cyan", no_wrap=True
)
table.add_column("Title", style="magenta")
table.add_column("Box Office", justify="right", style="green")
table.add_row(
"Dec 20, 2019",
"Star Wars: The Rise of Skywalker",
"$952,110,690",
)
table.add_row("May 25, 2018", "Solo: A Star Wars Story", "$393,151,347")
table.add_row(
"Dec 15, 2017",
"Star Wars Ep. V111: The Last Jedi",
"$1,332,539,889",
style="on black",
end_section=True,
)
table.add_row(
"Dec 16, 2016",
"Rogue One: A Star Wars Story",
"$1,332,439,889",
)
def header(text: str) -> None:
console.print()
console.rule(highlight(text))
console.print()
console = Console()
highlight = ReprHighlighter()
header("Example Table")
console.print(table, justify="center")
table.expand = True
header("expand=True")
console.print(table)
table.width = 50
header("width=50")
console.print(table, justify="center")
table.width = None
table.expand = False
table.row_styles = ["dim", "none"]
header("row_styles=['dim', 'none']")
console.print(table, justify="center")
table.width = None
table.expand = False
table.row_styles = ["dim", "none"]
table.leading = 1
header("leading=1, row_styles=['dim', 'none']")
console.print(table, justify="center")
table.width = None
table.expand = False
table.row_styles = ["dim", "none"]
table.show_lines = True
table.leading = 0
header("show_lines=True, row_styles=['dim', 'none']")
console.print(table, justify="center")