table.py 39 KB

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