table.py 36 KB

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