codekingpro/portable-devtools
114k
1from dataclasses import dataclass, field, replace2from typing import (3 TYPE_CHECKING,4 Dict,5 Iterable,6 List,7 NamedTuple,8 Optional,9 Sequence,10 Tuple,11 Union,12)13 14from . import box, errors15from ._loop import loop_first_last, loop_last16from ._pick import pick_bool17from ._ratio import ratio_distribute, ratio_reduce18from .align import VerticalAlignMethod19from .jupyter import JupyterMixin20from .measure import Measurement21from .padding import Padding, PaddingDimensions22from .protocol import is_renderable23from .segment import Segment24from .style import Style, StyleType25from .text import Text, TextType26 27if TYPE_CHECKING:28 from .console import (29 Console,30 ConsoleOptions,31 JustifyMethod,32 OverflowMethod,33 RenderableType,34 RenderResult,35 )36 37 38@dataclass39class Column: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] = None95 """Optional[int]: Width of the column, or ``None`` (default) to auto calculate width."""96 97 min_width: Optional[int] = None98 """Optional[int]: Minimum width of column, or ``None`` for no minimum. Defaults to None."""99 100 max_width: Optional[int] = None101 """Optional[int]: Maximum width of column, or ``None`` for no maximum. Defaults to None."""102 103 ratio: Optional[int] = None104 """Optional[int]: Ratio to use when calculating column width, or ``None`` (default) to adapt to column contents."""105 106 no_wrap: bool = False107 """bool: Prevent wrapping of text within the column. Defaults to ``False``."""108 109 _index: int = 0110 """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 @property119 def cells(self) -> Iterable["RenderableType"]:120 """Get all cells in the column, not including header."""121 yield from self._cells122 123 @property124 def flexible(self) -> bool:125 """Check if this column is flexible."""126 return self.ratio is not None127 128 129@dataclass130class Row:131 """Information regarding a row."""132 133 style: Optional[StyleType] = None134 """Style to apply to row."""135 136 end_section: bool = False137 """Indicated end of section, which will force a line beneath the row."""138 139 140class _Cell(NamedTuple):141 """A single cell in a table."""142 143 style: StyleType144 """Style to apply to cell."""145 renderable: "RenderableType"146 """Cell renderable."""147 vertical: VerticalAlignMethod148 """Cell vertical alignment."""149 150 151class Table(JupyterMixin):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 self.columns: List[Column] = []216 self.rows: List[Row] = []217 self.title = title218 self.caption = caption219 self.width = width220 self.min_width = min_width221 self.box = box222 self.safe_box = safe_box223 self._padding = Padding.unpack(padding)224 self.pad_edge = pad_edge225 self._expand = expand226 self.show_header = show_header227 self.show_footer = show_footer228 self.show_edge = show_edge229 self.show_lines = show_lines230 self.leading = leading231 self.collapse_padding = collapse_padding232 self.style = style233 self.header_style = header_style or ""234 self.footer_style = footer_style or ""235 self.border_style = border_style236 self.title_style = title_style237 self.caption_style = caption_style238 self.title_justify: "JustifyMethod" = title_justify239 self.caption_justify: "JustifyMethod" = caption_justify240 self.highlight = highlight241 self.row_styles: Sequence[StyleType] = list(row_styles or [])242 append_column = self.columns.append243 for header in headers:244 if isinstance(header, str):245 self.add_column(header=header)246 else:247 header._index = len(self.columns)248 append_column(header)249 250 @classmethod251 def grid(252 cls,253 *headers: Union[Column, str],254 padding: PaddingDimensions = 0,255 collapse_padding: bool = True,256 pad_edge: bool = False,257 expand: bool = False,258 ) -> "Table":259 """Get a table with no lines, headers, or footer.260 261 Args:262 *headers (Union[Column, str]): Column headers, either as a string, or :class:`~rich.table.Column` instance.263 padding (PaddingDimensions, optional): Get padding around cells. Defaults to 0.264 collapse_padding (bool, optional): Enable collapsing of padding around cells. Defaults to True.265 pad_edge (bool, optional): Enable padding around edges of table. Defaults to False.266 expand (bool, optional): Expand the table to fit the available space if ``True``, otherwise the table width will be auto-calculated. Defaults to False.267 268 Returns:269 Table: A table instance.270 """271 return cls(272 *headers,273 box=None,274 padding=padding,275 collapse_padding=collapse_padding,276 show_header=False,277 show_footer=False,278 show_edge=False,279 pad_edge=pad_edge,280 expand=expand,281 )282 283 @property284 def expand(self) -> bool:285 """Setting a non-None self.width implies expand."""286 return self._expand or self.width is not None287 288 @expand.setter289 def expand(self, expand: bool) -> None:290 """Set expand."""291 self._expand = expand292 293 @property294 def _extra_width(self) -> int:295 """Get extra width to add to cell content."""296 width = 0297 if self.box and self.show_edge:298 width += 2299 if self.box:300 width += len(self.columns) - 1301 return width302 303 @property304 def row_count(self) -> int:305 """Get the current number of rows."""306 return len(self.rows)307 308 def get_row_style(self, console: "Console", index: int) -> StyleType:309 """Get the current row style."""310 style = Style.null()311 if self.row_styles:312 style += console.get_style(self.row_styles[index % len(self.row_styles)])313 row_style = self.rows[index].style314 if row_style is not None:315 style += console.get_style(row_style)316 return style317 318 def __rich_measure__(319 self, console: "Console", options: "ConsoleOptions"320 ) -> Measurement:321 max_width = options.max_width322 if self.width is not None:323 max_width = self.width324 if max_width < 0:325 return Measurement(0, 0)326 327 extra_width = self._extra_width328 max_width = sum(329 self._calculate_column_widths(330 console, options.update_width(max_width - extra_width)331 )332 )333 _measure_column = self._measure_column334 335 measurements = [336 _measure_column(console, options.update_width(max_width), column)337 for column in self.columns338 ]339 minimum_width = (340 sum(measurement.minimum for measurement in measurements) + extra_width341 )342 maximum_width = (343 sum(measurement.maximum for measurement in measurements) + extra_width344 if (self.width is None)345 else self.width346 )347 measurement = Measurement(minimum_width, maximum_width)348 measurement = measurement.clamp(self.min_width)349 return measurement350 351 @property352 def padding(self) -> Tuple[int, int, int, int]:353 """Get cell padding."""354 return self._padding355 356 @padding.setter357 def padding(self, padding: PaddingDimensions) -> "Table":358 """Set cell padding."""359 self._padding = Padding.unpack(padding)360 return self361 362 def add_column(363 self,364 header: "RenderableType" = "",365 footer: "RenderableType" = "",366 *,367 header_style: Optional[StyleType] = None,368 footer_style: Optional[StyleType] = None,369 style: Optional[StyleType] = None,370 justify: "JustifyMethod" = "left",371 vertical: "VerticalAlignMethod" = "top",372 overflow: "OverflowMethod" = "ellipsis",373 width: Optional[int] = None,374 min_width: Optional[int] = None,375 max_width: Optional[int] = None,376 ratio: Optional[int] = None,377 no_wrap: bool = False,378 ) -> None:379 """Add a column to the table.380 381 Args:382 header (RenderableType, optional): Text or renderable for the header.383 Defaults to "".384 footer (RenderableType, optional): Text or renderable for the footer.385 Defaults to "".386 header_style (Union[str, Style], optional): Style for the header, or None for default. Defaults to None.387 footer_style (Union[str, Style], optional): Style for the footer, or None for default. Defaults to None.388 style (Union[str, Style], optional): Style for the column cells, or None for default. Defaults to None.389 justify (JustifyMethod, optional): Alignment for cells. Defaults to "left".390 vertical (VerticalAlignMethod, optional): Vertical alignment, one of "top", "middle", or "bottom". Defaults to "top".391 overflow (OverflowMethod): Overflow method: "crop", "fold", "ellipsis". Defaults to "ellipsis".392 width (int, optional): Desired width of column in characters, or None to fit to contents. Defaults to None.393 min_width (Optional[int], optional): Minimum width of column, or ``None`` for no minimum. Defaults to None.394 max_width (Optional[int], optional): Maximum width of column, or ``None`` for no maximum. Defaults to None.395 ratio (int, optional): Flexible ratio for the column (requires ``Table.expand`` or ``Table.width``). Defaults to None.396 no_wrap (bool, optional): Set to ``True`` to disable wrapping of this column.397 """398 399 column = Column(400 _index=len(self.columns),401 header=header,402 footer=footer,403 header_style=header_style or "",404 footer_style=footer_style or "",405 style=style or "",406 justify=justify,407 vertical=vertical,408 overflow=overflow,409 width=width,410 min_width=min_width,411 max_width=max_width,412 ratio=ratio,413 no_wrap=no_wrap,414 )415 self.columns.append(column)416 417 def add_row(418 self,419 *renderables: Optional["RenderableType"],420 style: Optional[StyleType] = None,421 end_section: bool = False,422 ) -> None:423 """Add a row of renderables.424 425 Args:426 *renderables (None or renderable): Each cell in a row must be a renderable object (including str),427 or ``None`` for a blank cell.428 style (StyleType, optional): An optional style to apply to the entire row. Defaults to None.429 end_section (bool, optional): End a section and draw a line. Defaults to False.430 431 Raises:432 errors.NotRenderableError: If you add something that can't be rendered.433 """434 435 def add_cell(column: Column, renderable: "RenderableType") -> None:436 column._cells.append(renderable)437 438 cell_renderables: List[Optional["RenderableType"]] = list(renderables)439 440 columns = self.columns441 if len(cell_renderables) < len(columns):442 cell_renderables = [443 *cell_renderables,444 *[None] * (len(columns) - len(cell_renderables)),445 ]446 for index, renderable in enumerate(cell_renderables):447 if index == len(columns):448 column = Column(_index=index)449 for _ in self.rows:450 add_cell(column, Text(""))451 self.columns.append(column)452 else:453 column = columns[index]454 if renderable is None:455 add_cell(column, "")456 elif is_renderable(renderable):457 add_cell(column, renderable)458 else:459 raise errors.NotRenderableError(460 f"unable to render {type(renderable).__name__}; a string or other renderable object is required"461 )462 self.rows.append(Row(style=style, end_section=end_section))463 464 def add_section(self) -> None:465 """Add a new section (draw a line after current row)."""466 467 if self.rows:468 self.rows[-1].end_section = True469 470 def __rich_console__(471 self, console: "Console", options: "ConsoleOptions"472 ) -> "RenderResult":473 if not self.columns:474 yield Segment("\n")475 return476 477 max_width = options.max_width478 if self.width is not None:479 max_width = self.width480 481 extra_width = self._extra_width482 widths = self._calculate_column_widths(483 console, options.update_width(max_width - extra_width)484 )485 table_width = sum(widths) + extra_width486 487 render_options = options.update(488 width=table_width, highlight=self.highlight, height=None489 )490 491 def render_annotation(492 text: TextType, style: StyleType, justify: "JustifyMethod" = "center"493 ) -> "RenderResult":494 render_text = (495 console.render_str(text, style=style, highlight=False)496 if isinstance(text, str)497 else text498 )499 return console.render(500 render_text, options=render_options.update(justify=justify)501 )502 503 if self.title:504 yield from render_annotation(505 self.title,506 style=Style.pick_first(self.title_style, "table.title"),507 justify=self.title_justify,508 )509 yield from self._render(console, render_options, widths)510 if self.caption:511 yield from render_annotation(512 self.caption,513 style=Style.pick_first(self.caption_style, "table.caption"),514 justify=self.caption_justify,515 )516 517 def _calculate_column_widths(518 self, console: "Console", options: "ConsoleOptions"519 ) -> List[int]:520 """Calculate the widths of each column, including padding, not including borders."""521 max_width = options.max_width522 columns = self.columns523 width_ranges = [524 self._measure_column(console, options, column) for column in columns525 ]526 widths = [_range.maximum or 1 for _range in width_ranges]527 get_padding_width = self._get_padding_width528 extra_width = self._extra_width529 if self.expand:530 ratios = [col.ratio or 0 for col in columns if col.flexible]531 if any(ratios):532 fixed_widths = [533 0 if column.flexible else _range.maximum534 for _range, column in zip(width_ranges, columns)535 ]536 flex_minimum = [537 (column.width or 1) + get_padding_width(column._index)538 for column in columns539 if column.flexible540 ]541 flexible_width = max_width - sum(fixed_widths)542 flex_widths = ratio_distribute(flexible_width, ratios, flex_minimum)543 iter_flex_widths = iter(flex_widths)544 for index, column in enumerate(columns):545 if column.flexible:546 widths[index] = fixed_widths[index] + next(iter_flex_widths)547 table_width = sum(widths)548 549 if table_width > max_width:550 widths = self._collapse_widths(551 widths,552 [(column.width is None and not column.no_wrap) for column in columns],553 max_width,554 )555 table_width = sum(widths)556 # last resort, reduce columns evenly557 if table_width > max_width:558 excess_width = table_width - max_width559 widths = ratio_reduce(excess_width, [1] * len(widths), widths, widths)560 table_width = sum(widths)561 562 width_ranges = [563 self._measure_column(console, options.update_width(width), column)564 for width, column in zip(widths, columns)565 ]566 widths = [_range.maximum or 0 for _range in width_ranges]567 568 if (table_width < max_width and self.expand) or (569 self.min_width is not None and table_width < (self.min_width - extra_width)570 ):571 _max_width = (572 max_width573 if self.min_width is None574 else min(self.min_width - extra_width, max_width)575 )576 pad_widths = ratio_distribute(_max_width - table_width, widths)577 widths = [_width + pad for _width, pad in zip(widths, pad_widths)]578 579 return widths580 581 @classmethod582 def _collapse_widths(583 cls, widths: List[int], wrapable: List[bool], max_width: int584 ) -> List[int]:585 """Reduce widths so that the total is under max_width.586 587 Args:588 widths (List[int]): List of widths.589 wrapable (List[bool]): List of booleans that indicate if a column may shrink.590 max_width (int): Maximum width to reduce to.591 592 Returns:593 List[int]: A new list of widths.594 """595 total_width = sum(widths)596 excess_width = total_width - max_width597 if any(wrapable):598 while total_width and excess_width > 0:599 max_column = max(600 width for width, allow_wrap in zip(widths, wrapable) if allow_wrap601 )602 second_max_column = max(603 width if allow_wrap and width != max_column else 0604 for width, allow_wrap in zip(widths, wrapable)605 )606 column_difference = max_column - second_max_column607 ratios = [608 (1 if (width == max_column and allow_wrap) else 0)609 for width, allow_wrap in zip(widths, wrapable)610 ]611 if not any(ratios) or not column_difference:612 break613 max_reduce = [min(excess_width, column_difference)] * len(widths)614 widths = ratio_reduce(excess_width, ratios, max_reduce, widths)615 616 total_width = sum(widths)617 excess_width = total_width - max_width618 return widths619 620 def _get_cells(621 self, console: "Console", column_index: int, column: Column622 ) -> Iterable[_Cell]:623 """Get all the cells with padding and optional header."""624 625 collapse_padding = self.collapse_padding626 pad_edge = self.pad_edge627 padding = self.padding628 any_padding = any(padding)629 630 first_column = column_index == 0631 last_column = column_index == len(self.columns) - 1632 633 _padding_cache: Dict[Tuple[bool, bool], Tuple[int, int, int, int]] = {}634 635 def get_padding(first_row: bool, last_row: bool) -> Tuple[int, int, int, int]:636 cached = _padding_cache.get((first_row, last_row))637 if cached:638 return cached639 top, right, bottom, left = padding640 641 if collapse_padding:642 if not first_column:643 left = max(0, left - right)644 if not last_row:645 bottom = max(0, top - bottom)646 647 if not pad_edge:648 if first_column:649 left = 0650 if last_column:651 right = 0652 if first_row:653 top = 0654 if last_row:655 bottom = 0656 _padding = (top, right, bottom, left)657 _padding_cache[(first_row, last_row)] = _padding658 return _padding659 660 raw_cells: List[Tuple[StyleType, "RenderableType"]] = []661 _append = raw_cells.append662 get_style = console.get_style663 if self.show_header:664 header_style = get_style(self.header_style or "") + get_style(665 column.header_style666 )667 _append((header_style, column.header))668 cell_style = get_style(column.style or "")669 for cell in column.cells:670 _append((cell_style, cell))671 if self.show_footer:672 footer_style = get_style(self.footer_style or "") + get_style(673 column.footer_style674 )675 _append((footer_style, column.footer))676 677 if any_padding:678 _Padding = Padding679 for first, last, (style, renderable) in loop_first_last(raw_cells):680 yield _Cell(681 style,682 _Padding(renderable, get_padding(first, last)),683 getattr(renderable, "vertical", None) or column.vertical,684 )685 else:686 for style, renderable in raw_cells:687 yield _Cell(688 style,689 renderable,690 getattr(renderable, "vertical", None) or column.vertical,691 )692 693 def _get_padding_width(self, column_index: int) -> int:694 """Get extra width from padding."""695 _, pad_right, _, pad_left = self.padding696 if self.collapse_padding:697 if column_index > 0:698 pad_left = max(0, pad_left - pad_right)699 return pad_left + pad_right700 701 def _measure_column(702 self,703 console: "Console",704 options: "ConsoleOptions",705 column: Column,706 ) -> Measurement:707 """Get the minimum and maximum width of the column."""708 709 max_width = options.max_width710 if max_width < 1:711 return Measurement(0, 0)712 713 padding_width = self._get_padding_width(column._index)714 715 if column.width is not None:716 # Fixed width column717 return Measurement(718 column.width + padding_width, column.width + padding_width719 ).with_maximum(max_width)720 # Flexible column, we need to measure contents721 min_widths: List[int] = []722 max_widths: List[int] = []723 append_min = min_widths.append724 append_max = max_widths.append725 get_render_width = Measurement.get726 for cell in self._get_cells(console, column._index, column):727 _min, _max = get_render_width(console, options, cell.renderable)728 append_min(_min)729 append_max(_max)730 731 measurement = Measurement(732 max(min_widths) if min_widths else 1,733 max(max_widths) if max_widths else max_width,734 ).with_maximum(max_width)735 measurement = measurement.clamp(736 None if column.min_width is None else column.min_width + padding_width,737 None if column.max_width is None else column.max_width + padding_width,738 )739 return measurement740 741 def _render(742 self, console: "Console", options: "ConsoleOptions", widths: List[int]743 ) -> "RenderResult":744 table_style = console.get_style(self.style or "")745 746 border_style = table_style + console.get_style(self.border_style or "")747 _column_cells = (748 self._get_cells(console, column_index, column)749 for column_index, column in enumerate(self.columns)750 )751 row_cells: List[Tuple[_Cell, ...]] = list(zip(*_column_cells))752 _box = (753 self.box.substitute(754 options, safe=pick_bool(self.safe_box, console.safe_box)755 )756 if self.box757 else None758 )759 _box = _box.get_plain_headed_box() if _box and not self.show_header else _box760 761 new_line = Segment.line()762 763 columns = self.columns764 show_header = self.show_header765 show_footer = self.show_footer766 show_edge = self.show_edge767 show_lines = self.show_lines768 leading = self.leading769 770 _Segment = Segment771 if _box:772 box_segments = [773 (774 _Segment(_box.head_left, border_style),775 _Segment(_box.head_right, border_style),776 _Segment(_box.head_vertical, border_style),777 ),778 (779 _Segment(_box.foot_left, border_style),780 _Segment(_box.foot_right, border_style),781 _Segment(_box.foot_vertical, border_style),782 ),783 (784 _Segment(_box.mid_left, border_style),785 _Segment(_box.mid_right, border_style),786 _Segment(_box.mid_vertical, border_style),787 ),788 ]789 if show_edge:790 yield _Segment(_box.get_top(widths), border_style)791 yield new_line792 else:793 box_segments = []794 795 get_row_style = self.get_row_style796 get_style = console.get_style797 798 for index, (first, last, row_cell) in enumerate(loop_first_last(row_cells)):799 header_row = first and show_header800 footer_row = last and show_footer801 row = (802 self.rows[index - show_header]803 if (not header_row and not footer_row)804 else None805 )806 max_height = 1807 cells: List[List[List[Segment]]] = []808 if header_row or footer_row:809 row_style = Style.null()810 else:811 row_style = get_style(812 get_row_style(console, index - 1 if show_header else index)813 )814 for width, cell, column in zip(widths, row_cell, columns):815 render_options = options.update(816 width=width,817 justify=column.justify,818 no_wrap=column.no_wrap,819 overflow=column.overflow,820 height=None,821 )822 lines = console.render_lines(823 cell.renderable,824 render_options,825 style=get_style(cell.style) + row_style,826 )827 max_height = max(max_height, len(lines))828 cells.append(lines)829 830 row_height = max(len(cell) for cell in cells)831 832 def align_cell(833 cell: List[List[Segment]],834 vertical: "VerticalAlignMethod",835 width: int,836 style: Style,837 ) -> List[List[Segment]]:838 if header_row:839 vertical = "bottom"840 elif footer_row:841 vertical = "top"842 843 if vertical == "top":844 return _Segment.align_top(cell, width, row_height, style)845 elif vertical == "middle":846 return _Segment.align_middle(cell, width, row_height, style)847 return _Segment.align_bottom(cell, width, row_height, style)848 849 cells[:] = [850 _Segment.set_shape(851 align_cell(852 cell,853 _cell.vertical,854 width,855 get_style(_cell.style) + row_style,856 ),857 width,858 max_height,859 )860 for width, _cell, cell, column in zip(widths, row_cell, cells, columns)861 ]862 863 if _box:864 if last and show_footer:865 yield _Segment(866 _box.get_row(widths, "foot", edge=show_edge), border_style867 )868 yield new_line869 left, right, _divider = box_segments[0 if first else (2 if last else 1)]870 871 # If the column divider is whitespace also style it with the row background872 divider = (873 _divider874 if _divider.text.strip()875 else _Segment(876 _divider.text, row_style.background_style + _divider.style877 )878 )879 for line_no in range(max_height):880 if show_edge:881 yield left882 for last_cell, rendered_cell in loop_last(cells):883 yield from rendered_cell[line_no]884 if not last_cell:885 yield divider886 if show_edge:887 yield right888 yield new_line889 else:890 for line_no in range(max_height):891 for rendered_cell in cells:892 yield from rendered_cell[line_no]893 yield new_line894 if _box and first and show_header:895 yield _Segment(896 _box.get_row(widths, "head", edge=show_edge), border_style897 )898 yield new_line899 end_section = row and row.end_section900 if _box and (show_lines or leading or end_section):901 if (902 not last903 and not (show_footer and index >= len(row_cells) - 2)904 and not (show_header and header_row)905 ):906 if leading:907 yield _Segment(908 _box.get_row(widths, "mid", edge=show_edge) * leading,909 border_style,910 )911 else:912 yield _Segment(913 _box.get_row(widths, "row", edge=show_edge), border_style914 )915 yield new_line916 917 if _box and show_edge:918 yield _Segment(_box.get_bottom(widths), border_style)919 yield new_line920 921 922if __name__ == "__main__": # pragma: no cover923 from rich.console import Console924 from rich.highlighter import ReprHighlighter925 from rich.table import Table as Table926 927 from ._timer import timer928 929 with timer("Table render"):930 table = Table(931 title="Star Wars Movies",932 caption="Rich example table",933 caption_justify="right",934 )935 936 table.add_column(937 "Released", header_style="bright_cyan", style="cyan", no_wrap=True938 )939 table.add_column("Title", style="magenta")940 table.add_column("Box Office", justify="right", style="green")941 942 table.add_row(943 "Dec 20, 2019",944 "Star Wars: The Rise of Skywalker",945 "$952,110,690",946 )947 table.add_row("May 25, 2018", "Solo: A Star Wars Story", "$393,151,347")948 table.add_row(949 "Dec 15, 2017",950 "Star Wars Ep. V111: The Last Jedi",951 "$1,332,539,889",952 style="on black",953 end_section=True,954 )955 table.add_row(956 "Dec 16, 2016",957 "Rogue One: A Star Wars Story",958 "$1,332,439,889",959 )960 961 def header(text: str) -> None:962 console.print()963 console.rule(highlight(text))964 console.print()965 966 console = Console()967 highlight = ReprHighlighter()968 header("Example Table")969 console.print(table, justify="center")970 971 table.expand = True972 header("expand=True")973 console.print(table)974 975 table.width = 50976 header("width=50")977 978 console.print(table, justify="center")979 980 table.width = None981 table.expand = False982 table.row_styles = ["dim", "none"]983 header("row_styles=['dim', 'none']")984 985 console.print(table, justify="center")986 987 table.width = None988 table.expand = False989 table.row_styles = ["dim", "none"]990 table.leading = 1991 header("leading=1, row_styles=['dim', 'none']")992 console.print(table, justify="center")993 994 table.width = None995 table.expand = False996 table.row_styles = ["dim", "none"]997 table.show_lines = True998 table.leading = 0999 header("show_lines=True, row_styles=['dim', 'none']")1000 console.print(table, justify="center")1001 