table.py 39 KB

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