Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.8/site-packages/rich/table.py: 20%
388 statements
« prev ^ index » next coverage.py v7.0.1, created at 2022-12-25 06:11 +0000
« prev ^ index » next coverage.py v7.0.1, created at 2022-12-25 06:11 +0000
1from dataclasses import dataclass, field, replace
2from typing import (
3 TYPE_CHECKING,
4 Dict,
5 Iterable,
6 List,
7 NamedTuple,
8 Optional,
9 Sequence,
10 Tuple,
11 Union,
12)
14from . import box, errors
15from ._loop import loop_first_last, loop_last
16from ._pick import pick_bool
17from ._ratio import ratio_distribute, ratio_reduce
18from .align import VerticalAlignMethod
19from .jupyter import JupyterMixin
20from .measure import Measurement
21from .padding import Padding, PaddingDimensions
22from .protocol import is_renderable
23from .segment import Segment
24from .style import Style, StyleType
25from .text import Text, TextType
27if TYPE_CHECKING:
28 from .console import (
29 Console,
30 ConsoleOptions,
31 JustifyMethod,
32 OverflowMethod,
33 RenderableType,
34 RenderResult,
35 )
38@dataclass
39class Column:
40 """Defines a column within a ~Table.
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 """
70 header: "RenderableType" = ""
71 """RenderableType: Renderable for the header (typically a string)"""
73 footer: "RenderableType" = ""
74 """RenderableType: Renderable for the footer (typically a string)"""
76 header_style: StyleType = ""
77 """StyleType: The style of the header."""
79 footer_style: StyleType = ""
80 """StyleType: The style of the footer."""
82 style: StyleType = ""
83 """StyleType: The style of the column."""
85 justify: "JustifyMethod" = "left"
86 """str: How to justify text within the column ("left", "center", "right", or "full")"""
88 vertical: "VerticalAlignMethod" = "top"
89 """str: How to vertically align content ("top", "middle", or "bottom")"""
91 overflow: "OverflowMethod" = "ellipsis"
92 """str: Overflow method."""
94 width: Optional[int] = None
95 """Optional[int]: Width of the column, or ``None`` (default) to auto calculate width."""
97 min_width: Optional[int] = None
98 """Optional[int]: Minimum width of column, or ``None`` for no minimum. Defaults to None."""
100 max_width: Optional[int] = None
101 """Optional[int]: Maximum width of column, or ``None`` for no maximum. Defaults to None."""
103 ratio: Optional[int] = None
104 """Optional[int]: Ratio to use when calculating column width, or ``None`` (default) to adapt to column contents."""
106 no_wrap: bool = False
107 """bool: Prevent wrapping of text within the column. Defaults to ``False``."""
109 _index: int = 0
110 """Index of column."""
112 _cells: List["RenderableType"] = field(default_factory=list)
114 def copy(self) -> "Column":
115 """Return a copy of this Column."""
116 return replace(self, _cells=[])
118 @property
119 def cells(self) -> Iterable["RenderableType"]:
120 """Get all cells in the column, not including header."""
121 yield from self._cells
123 @property
124 def flexible(self) -> bool:
125 """Check if this column is flexible."""
126 return self.ratio is not None
129@dataclass
130class Row:
131 """Information regarding a row."""
133 style: Optional[StyleType] = None
134 """Style to apply to row."""
136 end_section: bool = False
137 """Indicated end of section, which will force a line beneath the row."""
140class _Cell(NamedTuple):
141 """A single cell in a table."""
143 style: StyleType
144 """Style to apply to cell."""
145 renderable: "RenderableType"
146 """Cell renderable."""
147 vertical: VerticalAlignMethod
148 """Cell vertical alignment."""
151class Table(JupyterMixin):
152 """A console renderable to draw a table.
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 """
183 columns: List[Column]
184 rows: List[Row]
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:
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)
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.
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.
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 )
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
289 @expand.setter
290 def expand(self, expand: bool) -> None:
291 """Set expand."""
292 self._expand = expand
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
304 @property
305 def row_count(self) -> int:
306 """Get the current number of rows."""
307 return len(self.rows)
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
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)
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
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
352 @property
353 def padding(self) -> Tuple[int, int, int, int]:
354 """Get cell padding."""
355 return self._padding
357 @padding.setter
358 def padding(self, padding: PaddingDimensions) -> "Table":
359 """Set cell padding."""
360 self._padding = Padding.unpack(padding)
361 return self
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.
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 """
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)
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.
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.
432 Raises:
433 errors.NotRenderableError: If you add something that can't be rendered.
434 """
436 def add_cell(column: Column, renderable: "RenderableType") -> None:
437 column._cells.append(renderable)
439 cell_renderables: List[Optional["RenderableType"]] = list(renderables)
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))
465 def add_section(self) -> None:
466 """Add a new section (draw a line after current row)."""
468 if self.rows:
469 self.rows[-1].end_section = True
471 def __rich_console__(
472 self, console: "Console", options: "ConsoleOptions"
473 ) -> "RenderResult":
475 if not self.columns:
476 yield Segment("\n")
477 return
479 max_width = options.max_width
480 if self.width is not None:
481 max_width = self.width
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
489 render_options = options.update(
490 width=table_width, highlight=self.highlight, height=None
491 )
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 )
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 )
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)
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)
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]
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)]
581 return widths
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.
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.
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)
618 total_width = sum(widths)
619 excess_width = total_width - max_width
620 return widths
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."""
627 collapse_padding = self.collapse_padding
628 pad_edge = self.pad_edge
629 padding = self.padding
630 any_padding = any(padding)
632 first_column = column_index == 0
633 last_column = column_index == len(self.columns) - 1
635 _padding_cache: Dict[Tuple[bool, bool], Tuple[int, int, int, int]] = {}
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
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)
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
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))
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 )
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
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."""
711 max_width = options.max_width
712 if max_width < 1:
713 return Measurement(0, 0)
715 padding_width = self._get_padding_width(column._index)
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)
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
743 def _render(
744 self, console: "Console", options: "ConsoleOptions", widths: List[int]
745 ) -> "RenderResult":
746 table_style = console.get_style(self.style or "")
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
763 new_line = Segment.line()
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
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 = []
797 get_row_style = self.get_row_style
798 get_style = console.get_style
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)
832 row_height = max(len(cell) for cell in cells)
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"
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)
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 ]
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)]
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
919 if _box and show_edge:
920 yield _Segment(_box.get_bottom(widths), border_style)
921 yield new_line
924if __name__ == "__main__": # pragma: no cover
925 from rich.console import Console
926 from rich.highlighter import ReprHighlighter
927 from rich.table import Table as Table
929 from ._timer import timer
931 with timer("Table render"):
932 table = Table(
933 title="Star Wars Movies",
934 caption="Rich example table",
935 caption_justify="right",
936 )
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")
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 )
963 def header(text: str) -> None:
964 console.print()
965 console.rule(highlight(text))
966 console.print()
968 console = Console()
969 highlight = ReprHighlighter()
970 header("Example Table")
971 console.print(table, justify="center")
973 table.expand = True
974 header("expand=True")
975 console.print(table)
977 table.width = 50
978 header("width=50")
980 console.print(table, justify="center")
982 table.width = None
983 table.expand = False
984 table.row_styles = ["dim", "none"]
985 header("row_styles=['dim', 'none']")
987 console.print(table, justify="center")
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")
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")