Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/rich/table.py: 78%

Shortcuts on this page

r m x   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

412 statements  

1from dataclasses import dataclass, field, replace 

2from typing import ( 

3 TYPE_CHECKING, 

4 Dict, 

5 Iterable, 

6 List, 

7 NamedTuple, 

8 Optional, 

9 Sequence, 

10 Tuple, 

11 Union, 

12) 

13 

14from . import box, errors 

15from ._loop import loop_first_last, loop_last 

16from ._pick import pick_bool 

17from ._ratio import ratio_distribute, ratio_reduce 

18from .align import VerticalAlignMethod 

19from .jupyter import JupyterMixin 

20from .measure import Measurement 

21from .padding import Padding, PaddingDimensions 

22from .protocol import is_renderable 

23from .segment import Segment 

24from .style import Style, StyleType 

25from .text import Text, TextType 

26 

27if TYPE_CHECKING: 

28 from .console import ( 

29 Console, 

30 ConsoleOptions, 

31 JustifyMethod, 

32 OverflowMethod, 

33 RenderableType, 

34 RenderResult, 

35 ) 

36 

37 

38@dataclass 

39class Column: 

40 """Defines a column within a ~Table. 

41 

42 Args: 

43 title (Union[str, Text], optional): The title of the table rendered at the top. Defaults to None. 

44 caption (Union[str, Text], optional): The table caption rendered below. Defaults to None. 

45 width (int, optional): The width in characters of the table, or ``None`` to automatically fit. Defaults to None. 

46 min_width (Optional[int], optional): The minimum width of the table, or ``None`` for no minimum. Defaults to None. 

47 box (box.Box, optional): One of the constants in box.py used to draw the edges (see :ref:`appendix_box`), or ``None`` for no box lines. Defaults to box.HEAVY_HEAD. 

48 safe_box (Optional[bool], optional): Disable box characters that don't display on windows legacy terminal with *raster* fonts. Defaults to True. 

49 padding (PaddingDimensions, optional): Padding for cells (top, right, bottom, left). Defaults to (0, 1). 

50 collapse_padding (bool, optional): Enable collapsing of padding around cells. Defaults to False. 

51 pad_edge (bool, optional): Enable padding of edge cells. Defaults to True. 

52 show_header (bool, optional): Show a header row. Defaults to True. 

53 show_footer (bool, optional): Show a footer row. Defaults to False. 

54 show_edge (bool, optional): Draw a box around the outside of the table. Defaults to True. 

55 show_lines (bool, optional): Draw lines between every row. Defaults to False. 

56 leading (int, optional): Number of blank lines between rows (precludes ``show_lines``). Defaults to 0. 

57 style (Union[str, Style], optional): Default style for the table. Defaults to "none". 

58 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. 

59 header_style (Union[str, Style], optional): Style of the header. Defaults to "table.header". 

60 footer_style (Union[str, Style], optional): Style of the footer. Defaults to "table.footer". 

61 border_style (Union[str, Style], optional): Style of the border. Defaults to None. 

62 title_style (Union[str, Style], optional): Style of the title. Defaults to None. 

63 caption_style (Union[str, Style], optional): Style of the caption. Defaults to None. 

64 title_justify (str, optional): Justify method for title. Defaults to "center". 

65 caption_justify (str, optional): Justify method for caption. Defaults to "center". 

66 highlight (bool, optional): Highlight cell contents (if str). Defaults to False. 

67 """ 

68 

69 header: "RenderableType" = "" 

70 """RenderableType: Renderable for the header (typically a string)""" 

71 

72 footer: "RenderableType" = "" 

73 """RenderableType: Renderable for the footer (typically a string)""" 

74 

75 header_style: StyleType = "" 

76 """StyleType: The style of the header.""" 

77 

78 footer_style: StyleType = "" 

79 """StyleType: The style of the footer.""" 

80 

81 style: StyleType = "" 

82 """StyleType: The style of the column.""" 

83 

84 justify: "JustifyMethod" = "left" 

85 """str: How to justify text within the column ("left", "center", "right", or "full")""" 

86 

87 vertical: "VerticalAlignMethod" = "top" 

88 """str: How to vertically align content ("top", "middle", or "bottom")""" 

89 

90 overflow: "OverflowMethod" = "ellipsis" 

91 """str: Overflow method.""" 

92 

93 width: Optional[int] = None 

94 """Optional[int]: Width of the column, or ``None`` (default) to auto calculate width.""" 

95 

96 min_width: Optional[int] = None 

97 """Optional[int]: Minimum width of column, or ``None`` for no minimum. Defaults to None.""" 

98 

99 max_width: Optional[int] = None 

100 """Optional[int]: Maximum width of column, or ``None`` for no maximum. Defaults to None.""" 

101 

102 ratio: Optional[int] = None 

103 """Optional[int]: Ratio to use when calculating column width, or ``None`` (default) to adapt to column contents.""" 

104 

105 no_wrap: bool = False 

106 """bool: Prevent wrapping of text within the column. Defaults to ``False``.""" 

107 

108 highlight: bool = False 

109 """bool: Apply highlighter to column. Defaults to ``False``.""" 

110 

111 _index: int = 0 

112 """Index of column.""" 

113 

114 _cells: List["RenderableType"] = field(default_factory=list) 

115 

116 def copy(self) -> "Column": 

117 """Return a copy of this Column.""" 

118 return replace(self, _cells=[]) 

119 

120 @property 

121 def cells(self) -> Iterable["RenderableType"]: 

122 """Get all cells in the column, not including header.""" 

123 yield from self._cells 

124 

125 @property 

126 def flexible(self) -> bool: 

127 """Check if this column is flexible.""" 

128 return self.ratio is not None 

129 

130 

131@dataclass 

132class Row: 

133 """Information regarding a row.""" 

134 

135 style: Optional[StyleType] = None 

136 """Style to apply to row.""" 

137 

138 end_section: bool = False 

139 """Indicated end of section, which will force a line beneath the row.""" 

140 

141 

142class _Cell(NamedTuple): 

143 """A single cell in a table.""" 

144 

145 style: StyleType 

146 """Style to apply to cell.""" 

147 renderable: "RenderableType" 

148 """Cell renderable.""" 

149 vertical: VerticalAlignMethod 

150 """Cell vertical alignment.""" 

151 

152 

153class Table(JupyterMixin): 

154 """A console renderable to draw a table. 

155 

156 Args: 

157 *headers (Union[Column, str]): Column headers, either as a string, or :class:`~rich.table.Column` instance. 

158 title (Union[str, Text], optional): The title of the table rendered at the top. Defaults to None. 

159 caption (Union[str, Text], optional): The table caption rendered below. Defaults to None. 

160 width (int, optional): The width in characters of the table, or ``None`` to automatically fit. Defaults to None. 

161 min_width (Optional[int], optional): The minimum width of the table, or ``None`` for no minimum. Defaults to None. 

162 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. 

163 safe_box (Optional[bool], optional): Disable box characters that don't display on windows legacy terminal with *raster* fonts. Defaults to True. 

164 padding (PaddingDimensions, optional): Padding for cells (top, right, bottom, left). Defaults to (0, 1). 

165 collapse_padding (bool, optional): Enable collapsing of padding around cells. Defaults to False. 

166 pad_edge (bool, optional): Enable padding of edge cells. Defaults to True. 

167 expand (bool, optional): Expand the table to fit the available space if ``True``, otherwise the table width will be auto-calculated. Defaults to False. 

168 show_header (bool, optional): Show a header row. Defaults to True. 

169 show_footer (bool, optional): Show a footer row. Defaults to False. 

170 show_edge (bool, optional): Draw a box around the outside of the table. Defaults to True. 

171 show_lines (bool, optional): Draw lines between every row. Defaults to False. 

172 leading (int, optional): Number of blank lines between rows (precludes ``show_lines``). Defaults to 0. 

173 style (Union[str, Style], optional): Default style for the table. Defaults to "none". 

174 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. 

175 header_style (Union[str, Style], optional): Style of the header. Defaults to "table.header". 

176 footer_style (Union[str, Style], optional): Style of the footer. Defaults to "table.footer". 

177 border_style (Union[str, Style], optional): Style of the border. Defaults to None. 

178 title_style (Union[str, Style], optional): Style of the title. Defaults to None. 

179 caption_style (Union[str, Style], optional): Style of the caption. Defaults to None. 

180 title_justify (str, optional): Justify method for title. Defaults to "center". 

181 caption_justify (str, optional): Justify method for caption. Defaults to "center". 

182 highlight (bool, optional): Highlight cell contents (if str). Defaults to False. 

183 """ 

184 

185 columns: List[Column] 

186 rows: List[Row] 

187 

188 def __init__( 

189 self, 

190 *headers: Union[Column, str], 

191 title: Optional[TextType] = None, 

192 caption: Optional[TextType] = None, 

193 width: Optional[int] = None, 

194 min_width: Optional[int] = None, 

195 box: Optional[box.Box] = box.HEAVY_HEAD, 

196 safe_box: Optional[bool] = None, 

197 padding: PaddingDimensions = (0, 1), 

198 collapse_padding: bool = False, 

199 pad_edge: bool = True, 

200 expand: bool = False, 

201 show_header: bool = True, 

202 show_footer: bool = False, 

203 show_edge: bool = True, 

204 show_lines: bool = False, 

205 leading: int = 0, 

206 style: StyleType = "none", 

207 row_styles: Optional[Iterable[StyleType]] = None, 

208 header_style: Optional[StyleType] = "table.header", 

209 footer_style: Optional[StyleType] = "table.footer", 

210 border_style: Optional[StyleType] = None, 

211 title_style: Optional[StyleType] = None, 

212 caption_style: Optional[StyleType] = None, 

213 title_justify: "JustifyMethod" = "center", 

214 caption_justify: "JustifyMethod" = "center", 

215 highlight: bool = False, 

216 ) -> None: 

217 self.columns: List[Column] = [] 

218 self.rows: List[Row] = [] 

219 self.title = title 

220 self.caption = caption 

221 self.width = width 

222 self.min_width = min_width 

223 self.box = box 

224 self.safe_box = safe_box 

225 self._padding = Padding.unpack(padding) 

226 self.pad_edge = pad_edge 

227 self._expand = expand 

228 self.show_header = show_header 

229 self.show_footer = show_footer 

230 self.show_edge = show_edge 

231 self.show_lines = show_lines 

232 self.leading = leading 

233 self.collapse_padding = collapse_padding 

234 self.style = style 

235 self.header_style = header_style or "" 

236 self.footer_style = footer_style or "" 

237 self.border_style = border_style 

238 self.title_style = title_style 

239 self.caption_style = caption_style 

240 self.title_justify: "JustifyMethod" = title_justify 

241 self.caption_justify: "JustifyMethod" = caption_justify 

242 self.highlight = highlight 

243 self.row_styles: Sequence[StyleType] = list(row_styles or []) 

244 append_column = self.columns.append 

245 for header in headers: 

246 if isinstance(header, str): 

247 self.add_column(header=header) 

248 else: 

249 header._index = len(self.columns) 

250 append_column(header) 

251 

252 @classmethod 

253 def grid( 

254 cls, 

255 *headers: Union[Column, str], 

256 padding: PaddingDimensions = 0, 

257 collapse_padding: bool = True, 

258 pad_edge: bool = False, 

259 expand: bool = False, 

260 ) -> "Table": 

261 """Get a table with no lines, headers, or footer. 

262 

263 Args: 

264 *headers (Union[Column, str]): Column headers, either as a string, or :class:`~rich.table.Column` instance. 

265 padding (PaddingDimensions, optional): Get padding around cells. Defaults to 0. 

266 collapse_padding (bool, optional): Enable collapsing of padding around cells. Defaults to True. 

267 pad_edge (bool, optional): Enable padding around edges of table. Defaults to False. 

268 expand (bool, optional): Expand the table to fit the available space if ``True``, otherwise the table width will be auto-calculated. Defaults to False. 

269 

270 Returns: 

271 Table: A table instance. 

272 """ 

273 return cls( 

274 *headers, 

275 box=None, 

276 padding=padding, 

277 collapse_padding=collapse_padding, 

278 show_header=False, 

279 show_footer=False, 

280 show_edge=False, 

281 pad_edge=pad_edge, 

282 expand=expand, 

283 ) 

284 

285 @property 

286 def expand(self) -> bool: 

287 """Setting a non-None self.width implies expand.""" 

288 return self._expand or self.width is not None 

289 

290 @expand.setter 

291 def expand(self, expand: bool) -> None: 

292 """Set expand.""" 

293 self._expand = expand 

294 

295 @property 

296 def _extra_width(self) -> int: 

297 """Get extra width to add to cell content.""" 

298 width = 0 

299 if self.box and self.show_edge: 

300 width += 2 

301 if self.box: 

302 width += len(self.columns) - 1 

303 return width 

304 

305 @property 

306 def row_count(self) -> int: 

307 """Get the current number of rows.""" 

308 return len(self.rows) 

309 

310 def get_row_style(self, console: "Console", index: int) -> StyleType: 

311 """Get the current row style.""" 

312 style = Style.null() 

313 if self.row_styles: 

314 style += console.get_style(self.row_styles[index % len(self.row_styles)]) 

315 row_style = self.rows[index].style 

316 if row_style is not None: 

317 style += console.get_style(row_style) 

318 return style 

319 

320 def __rich_measure__( 

321 self, console: "Console", options: "ConsoleOptions" 

322 ) -> Measurement: 

323 max_width = options.max_width 

324 if self.width is not None: 

325 max_width = self.width 

326 if max_width < 0: 

327 return Measurement(0, 0) 

328 

329 extra_width = self._extra_width 

330 max_width = sum( 

331 self._calculate_column_widths( 

332 console, options.update_width(max_width - extra_width) 

333 ) 

334 ) 

335 _measure_column = self._measure_column 

336 

337 measurements = [ 

338 _measure_column(console, options.update_width(max_width), column) 

339 for column in self.columns 

340 ] 

341 minimum_width = ( 

342 sum(measurement.minimum for measurement in measurements) + extra_width 

343 ) 

344 maximum_width = ( 

345 sum(measurement.maximum for measurement in measurements) + extra_width 

346 if (self.width is None) 

347 else self.width 

348 ) 

349 measurement = Measurement(minimum_width, maximum_width) 

350 measurement = measurement.clamp(self.min_width) 

351 return measurement 

352 

353 @property 

354 def padding(self) -> Tuple[int, int, int, int]: 

355 """Get cell padding.""" 

356 return self._padding 

357 

358 @padding.setter 

359 def padding(self, padding: PaddingDimensions) -> "Table": 

360 """Set cell padding.""" 

361 self._padding = Padding.unpack(padding) 

362 return self 

363 

364 def add_column( 

365 self, 

366 header: "RenderableType" = "", 

367 footer: "RenderableType" = "", 

368 *, 

369 header_style: Optional[StyleType] = None, 

370 highlight: Optional[bool] = None, 

371 footer_style: Optional[StyleType] = None, 

372 style: Optional[StyleType] = None, 

373 justify: "JustifyMethod" = "left", 

374 vertical: "VerticalAlignMethod" = "top", 

375 overflow: "OverflowMethod" = "ellipsis", 

376 width: Optional[int] = None, 

377 min_width: Optional[int] = None, 

378 max_width: Optional[int] = None, 

379 ratio: Optional[int] = None, 

380 no_wrap: bool = False, 

381 ) -> None: 

382 """Add a column to the table. 

383 

384 Args: 

385 header (RenderableType, optional): Text or renderable for the header. 

386 Defaults to "". 

387 footer (RenderableType, optional): Text or renderable for the footer. 

388 Defaults to "". 

389 header_style (Union[str, Style], optional): Style for the header, or None for default. Defaults to None. 

390 highlight (bool, optional): Whether to highlight the text. The default of None uses the value of the table (self) object. 

391 footer_style (Union[str, Style], optional): Style for the footer, or None for default. Defaults to None. 

392 style (Union[str, Style], optional): Style for the column cells, or None for default. Defaults to None. 

393 justify (JustifyMethod, optional): Alignment for cells. Defaults to "left". 

394 vertical (VerticalAlignMethod, optional): Vertical alignment, one of "top", "middle", or "bottom". Defaults to "top". 

395 overflow (OverflowMethod): Overflow method: "crop", "fold", "ellipsis". Defaults to "ellipsis". 

396 width (int, optional): Desired width of column in characters, or None to fit to contents. Defaults to None. 

397 min_width (Optional[int], optional): Minimum width of column, or ``None`` for no minimum. Defaults to None. 

398 max_width (Optional[int], optional): Maximum width of column, or ``None`` for no maximum. Defaults to None. 

399 ratio (int, optional): Flexible ratio for the column (requires ``Table.expand`` or ``Table.width``). Defaults to None. 

400 no_wrap (bool, optional): Set to ``True`` to disable wrapping of this column. 

401 """ 

402 

403 column = Column( 

404 _index=len(self.columns), 

405 header=header, 

406 footer=footer, 

407 header_style=header_style or "", 

408 highlight=highlight if highlight is not None else self.highlight, 

409 footer_style=footer_style or "", 

410 style=style or "", 

411 justify=justify, 

412 vertical=vertical, 

413 overflow=overflow, 

414 width=width, 

415 min_width=min_width, 

416 max_width=max_width, 

417 ratio=ratio, 

418 no_wrap=no_wrap, 

419 ) 

420 self.columns.append(column) 

421 

422 def add_row( 

423 self, 

424 *renderables: Optional["RenderableType"], 

425 style: Optional[StyleType] = None, 

426 end_section: bool = False, 

427 ) -> None: 

428 """Add a row of renderables. 

429 

430 Args: 

431 *renderables (None or renderable): Each cell in a row must be a renderable object (including str), 

432 or ``None`` for a blank cell. 

433 style (StyleType, optional): An optional style to apply to the entire row. Defaults to None. 

434 end_section (bool, optional): End a section and draw a line. Defaults to False. 

435 

436 Raises: 

437 errors.NotRenderableError: If you add something that can't be rendered. 

438 """ 

439 

440 def add_cell(column: Column, renderable: "RenderableType") -> None: 

441 column._cells.append(renderable) 

442 

443 cell_renderables: List[Optional["RenderableType"]] = list(renderables) 

444 

445 columns = self.columns 

446 if len(cell_renderables) < len(columns): 

447 cell_renderables = [ 

448 *cell_renderables, 

449 *[None] * (len(columns) - len(cell_renderables)), 

450 ] 

451 for index, renderable in enumerate(cell_renderables): 

452 if index == len(columns): 

453 column = Column(_index=index, highlight=self.highlight) 

454 for _ in self.rows: 

455 add_cell(column, Text("")) 

456 self.columns.append(column) 

457 else: 

458 column = columns[index] 

459 if renderable is None: 

460 add_cell(column, "") 

461 elif is_renderable(renderable): 

462 add_cell(column, renderable) 

463 else: 

464 raise errors.NotRenderableError( 

465 f"unable to render {type(renderable).__name__}; a string or other renderable object is required" 

466 ) 

467 self.rows.append(Row(style=style, end_section=end_section)) 

468 

469 def add_section(self) -> None: 

470 """Add a new section (draw a line after current row).""" 

471 

472 if self.rows: 

473 self.rows[-1].end_section = True 

474 

475 def __rich_console__( 

476 self, console: "Console", options: "ConsoleOptions" 

477 ) -> "RenderResult": 

478 if not self.columns: 

479 yield Segment("\n") 

480 return 

481 

482 max_width = options.max_width 

483 if self.width is not None: 

484 max_width = self.width 

485 

486 extra_width = self._extra_width 

487 

488 widths = self._calculate_column_widths( 

489 console, options.update_width(max_width - extra_width) 

490 ) 

491 table_width = sum(widths) + extra_width 

492 

493 render_options = options.update( 

494 width=table_width, highlight=self.highlight, height=None 

495 ) 

496 

497 def render_annotation( 

498 text: TextType, style: StyleType, justify: "JustifyMethod" = "center" 

499 ) -> "RenderResult": 

500 render_text = ( 

501 console.render_str(text, style=style, highlight=False) 

502 if isinstance(text, str) 

503 else text 

504 ) 

505 return console.render( 

506 render_text, options=render_options.update(justify=justify) 

507 ) 

508 

509 if self.title: 

510 yield from render_annotation( 

511 self.title, 

512 style=Style.pick_first(self.title_style, "table.title"), 

513 justify=self.title_justify, 

514 ) 

515 yield from self._render(console, render_options, widths) 

516 if self.caption: 

517 yield from render_annotation( 

518 self.caption, 

519 style=Style.pick_first(self.caption_style, "table.caption"), 

520 justify=self.caption_justify, 

521 ) 

522 

523 def _calculate_column_widths( 

524 self, console: "Console", options: "ConsoleOptions" 

525 ) -> List[int]: 

526 """Calculate the widths of each column, including padding, not including borders.""" 

527 max_width = options.max_width 

528 columns = self.columns 

529 width_ranges = [ 

530 self._measure_column(console, options, column) for column in columns 

531 ] 

532 widths = [_range.maximum or 1 for _range in width_ranges] 

533 

534 get_padding_width = self._get_padding_width 

535 extra_width = self._extra_width 

536 if self.expand: 

537 ratios = [col.ratio or 0 for col in columns if col.flexible] 

538 if any(ratios): 

539 fixed_widths = [ 

540 0 if column.flexible else _range.maximum 

541 for _range, column in zip(width_ranges, columns) 

542 ] 

543 flex_minimum = [ 

544 (column.width or 1) + get_padding_width(column._index) 

545 for column in columns 

546 if column.flexible 

547 ] 

548 flexible_width = max_width - sum(fixed_widths) 

549 flex_widths = ratio_distribute(flexible_width, ratios, flex_minimum) 

550 iter_flex_widths = iter(flex_widths) 

551 for index, column in enumerate(columns): 

552 if column.flexible: 

553 widths[index] = fixed_widths[index] + next(iter_flex_widths) 

554 table_width = sum(widths) 

555 

556 if table_width > max_width: 

557 widths = self._collapse_widths( 

558 widths, 

559 [(column.width is None and not column.no_wrap) for column in columns], 

560 max_width, 

561 ) 

562 table_width = sum(widths) 

563 # last resort, reduce columns evenly 

564 if table_width > max_width: 

565 excess_width = table_width - max_width 

566 widths = ratio_reduce(excess_width, [1] * len(widths), widths, widths) 

567 table_width = sum(widths) 

568 

569 width_ranges = [ 

570 self._measure_column(console, options.update_width(width), column) 

571 for width, column in zip(widths, columns) 

572 ] 

573 widths = [_range.maximum or 0 for _range in width_ranges] 

574 

575 if (table_width < max_width and self.expand) or ( 

576 self.min_width is not None and table_width < (self.min_width - extra_width) 

577 ): 

578 _max_width = ( 

579 max_width 

580 if self.min_width is None 

581 else min(self.min_width - extra_width, max_width) 

582 ) 

583 pad_widths = ratio_distribute(_max_width - table_width, widths) 

584 widths = [_width + pad for _width, pad in zip(widths, pad_widths)] 

585 

586 return widths 

587 

588 @classmethod 

589 def _collapse_widths( 

590 cls, widths: List[int], wrapable: List[bool], max_width: int 

591 ) -> List[int]: 

592 """Reduce widths so that the total is under max_width. 

593 

594 Args: 

595 widths (List[int]): List of widths. 

596 wrapable (List[bool]): List of booleans that indicate if a column may shrink. 

597 max_width (int): Maximum width to reduce to. 

598 

599 Returns: 

600 List[int]: A new list of widths. 

601 """ 

602 total_width = sum(widths) 

603 excess_width = total_width - max_width 

604 if any(wrapable): 

605 while total_width and excess_width > 0: 

606 max_column = max( 

607 width for width, allow_wrap in zip(widths, wrapable) if allow_wrap 

608 ) 

609 second_max_column = max( 

610 width if allow_wrap and width != max_column else 0 

611 for width, allow_wrap in zip(widths, wrapable) 

612 ) 

613 column_difference = max_column - second_max_column 

614 ratios = [ 

615 (1 if (width == max_column and allow_wrap) else 0) 

616 for width, allow_wrap in zip(widths, wrapable) 

617 ] 

618 if not any(ratios) or not column_difference: 

619 break 

620 max_reduce = [min(excess_width, column_difference)] * len(widths) 

621 widths = ratio_reduce(excess_width, ratios, max_reduce, widths) 

622 

623 total_width = sum(widths) 

624 excess_width = total_width - max_width 

625 return widths 

626 

627 def _get_cells( 

628 self, console: "Console", column_index: int, column: Column 

629 ) -> Iterable[_Cell]: 

630 """Get all the cells with padding and optional header.""" 

631 

632 collapse_padding = self.collapse_padding 

633 pad_edge = self.pad_edge 

634 padding = self.padding 

635 any_padding = any(padding) 

636 

637 first_column = column_index == 0 

638 last_column = column_index == len(self.columns) - 1 

639 

640 _padding_cache: Dict[Tuple[bool, bool], Tuple[int, int, int, int]] = {} 

641 

642 def get_padding(first_row: bool, last_row: bool) -> Tuple[int, int, int, int]: 

643 cached = _padding_cache.get((first_row, last_row)) 

644 if cached: 

645 return cached 

646 top, right, bottom, left = padding 

647 

648 if collapse_padding: 

649 if not first_column: 

650 left = max(0, left - right) 

651 if not last_row: 

652 bottom = max(0, top - bottom) 

653 

654 if not pad_edge: 

655 if first_column: 

656 left = 0 

657 if last_column: 

658 right = 0 

659 if first_row: 

660 top = 0 

661 if last_row: 

662 bottom = 0 

663 _padding = (top, right, bottom, left) 

664 _padding_cache[(first_row, last_row)] = _padding 

665 return _padding 

666 

667 raw_cells: List[Tuple[StyleType, "RenderableType"]] = [] 

668 _append = raw_cells.append 

669 get_style = console.get_style 

670 if self.show_header: 

671 header_style = get_style(self.header_style or "") + get_style( 

672 column.header_style 

673 ) 

674 _append((header_style, column.header)) 

675 cell_style = get_style(column.style or "") 

676 for cell in column.cells: 

677 _append((cell_style, cell)) 

678 if self.show_footer: 

679 footer_style = get_style(self.footer_style or "") + get_style( 

680 column.footer_style 

681 ) 

682 _append((footer_style, column.footer)) 

683 

684 if any_padding: 

685 _Padding = Padding 

686 for first, last, (style, renderable) in loop_first_last(raw_cells): 

687 yield _Cell( 

688 style, 

689 _Padding(renderable, get_padding(first, last)), 

690 getattr(renderable, "vertical", None) or column.vertical, 

691 ) 

692 else: 

693 for style, renderable in raw_cells: 

694 yield _Cell( 

695 style, 

696 renderable, 

697 getattr(renderable, "vertical", None) or column.vertical, 

698 ) 

699 

700 def _get_padding_width(self, column_index: int) -> int: 

701 """Get extra width from padding.""" 

702 _, pad_right, _, pad_left = self.padding 

703 

704 if self.collapse_padding: 

705 pad_left = 0 

706 pad_right = abs(pad_left - pad_right) 

707 

708 if not self.pad_edge: 

709 if column_index == 0: 

710 pad_left = 0 

711 if column_index == len(self.columns) - 1: 

712 pad_right = 0 

713 

714 return pad_left + pad_right 

715 

716 def _measure_column( 

717 self, 

718 console: "Console", 

719 options: "ConsoleOptions", 

720 column: Column, 

721 ) -> Measurement: 

722 """Get the minimum and maximum width of the column.""" 

723 

724 max_width = options.max_width 

725 if max_width < 1: 

726 return Measurement(0, 0) 

727 

728 padding_width = self._get_padding_width(column._index) 

729 if column.width is not None: 

730 # Fixed width column 

731 return Measurement( 

732 column.width + padding_width, column.width + padding_width 

733 ).with_maximum(max_width) 

734 # Flexible column, we need to measure contents 

735 min_widths: List[int] = [] 

736 max_widths: List[int] = [] 

737 append_min = min_widths.append 

738 append_max = max_widths.append 

739 get_render_width = Measurement.get 

740 for cell in self._get_cells(console, column._index, column): 

741 _min, _max = get_render_width(console, options, cell.renderable) 

742 append_min(_min) 

743 append_max(_max) 

744 

745 measurement = Measurement( 

746 max(min_widths) if min_widths else 1, 

747 max(max_widths) if max_widths else max_width, 

748 ).with_maximum(max_width) 

749 measurement = measurement.clamp( 

750 None if column.min_width is None else column.min_width + padding_width, 

751 None if column.max_width is None else column.max_width + padding_width, 

752 ) 

753 return measurement 

754 

755 def _render( 

756 self, console: "Console", options: "ConsoleOptions", widths: List[int] 

757 ) -> "RenderResult": 

758 table_style = console.get_style(self.style or "") 

759 

760 border_style = table_style + console.get_style(self.border_style or "") 

761 _column_cells = ( 

762 self._get_cells(console, column_index, column) 

763 for column_index, column in enumerate(self.columns) 

764 ) 

765 

766 row_cells: List[Tuple[_Cell, ...]] = list(zip(*_column_cells)) 

767 _box = ( 

768 self.box.substitute( 

769 options, safe=pick_bool(self.safe_box, console.safe_box) 

770 ) 

771 if self.box 

772 else None 

773 ) 

774 _box = _box.get_plain_headed_box() if _box and not self.show_header else _box 

775 

776 new_line = Segment.line() 

777 

778 columns = self.columns 

779 show_header = self.show_header 

780 show_footer = self.show_footer 

781 show_edge = self.show_edge 

782 show_lines = self.show_lines 

783 leading = self.leading 

784 

785 _Segment = Segment 

786 if _box: 

787 box_segments = [ 

788 ( 

789 _Segment(_box.head_left, border_style), 

790 _Segment(_box.head_right, border_style), 

791 _Segment(_box.head_vertical, border_style), 

792 ), 

793 ( 

794 _Segment(_box.mid_left, border_style), 

795 _Segment(_box.mid_right, border_style), 

796 _Segment(_box.mid_vertical, border_style), 

797 ), 

798 ( 

799 _Segment(_box.foot_left, border_style), 

800 _Segment(_box.foot_right, border_style), 

801 _Segment(_box.foot_vertical, border_style), 

802 ), 

803 ] 

804 if show_edge: 

805 yield _Segment(_box.get_top(widths), border_style) 

806 yield new_line 

807 else: 

808 box_segments = [] 

809 

810 get_row_style = self.get_row_style 

811 get_style = console.get_style 

812 

813 for index, (first, last, row_cell) in enumerate(loop_first_last(row_cells)): 

814 header_row = first and show_header 

815 footer_row = last and show_footer 

816 row = ( 

817 self.rows[index - show_header] 

818 if (not header_row and not footer_row) 

819 else None 

820 ) 

821 max_height = 1 

822 cells: List[List[List[Segment]]] = [] 

823 if header_row or footer_row: 

824 row_style = Style.null() 

825 else: 

826 row_style = get_style( 

827 get_row_style(console, index - 1 if show_header else index) 

828 ) 

829 for width, cell, column in zip(widths, row_cell, columns): 

830 render_options = options.update( 

831 width=width, 

832 justify=column.justify, 

833 no_wrap=column.no_wrap, 

834 overflow=column.overflow, 

835 height=None, 

836 highlight=column.highlight, 

837 ) 

838 lines = console.render_lines( 

839 cell.renderable, 

840 render_options, 

841 style=get_style(cell.style) + row_style, 

842 ) 

843 max_height = max(max_height, len(lines)) 

844 cells.append(lines) 

845 

846 row_height = max(len(cell) for cell in cells) 

847 

848 def align_cell( 

849 cell: List[List[Segment]], 

850 vertical: "VerticalAlignMethod", 

851 width: int, 

852 style: Style, 

853 ) -> List[List[Segment]]: 

854 if header_row: 

855 vertical = "bottom" 

856 elif footer_row: 

857 vertical = "top" 

858 

859 if vertical == "top": 

860 return _Segment.align_top(cell, width, row_height, style) 

861 elif vertical == "middle": 

862 return _Segment.align_middle(cell, width, row_height, style) 

863 return _Segment.align_bottom(cell, width, row_height, style) 

864 

865 cells[:] = [ 

866 _Segment.set_shape( 

867 align_cell( 

868 cell, 

869 _cell.vertical, 

870 width, 

871 get_style(_cell.style) + row_style, 

872 ), 

873 width, 

874 max_height, 

875 ) 

876 for width, _cell, cell, column in zip(widths, row_cell, cells, columns) 

877 ] 

878 

879 if _box: 

880 if last and show_footer: 

881 yield _Segment( 

882 _box.get_row(widths, "foot", edge=show_edge), border_style 

883 ) 

884 yield new_line 

885 left, right, _divider = box_segments[0 if first else (2 if last else 1)] 

886 

887 # If the column divider is whitespace also style it with the row background 

888 divider = ( 

889 _divider 

890 if _divider.text.strip() 

891 else _Segment( 

892 _divider.text, row_style.background_style + _divider.style 

893 ) 

894 ) 

895 for line_no in range(max_height): 

896 if show_edge: 

897 yield left 

898 for last_cell, rendered_cell in loop_last(cells): 

899 yield from rendered_cell[line_no] 

900 if not last_cell: 

901 yield divider 

902 if show_edge: 

903 yield right 

904 yield new_line 

905 else: 

906 for line_no in range(max_height): 

907 for rendered_cell in cells: 

908 yield from rendered_cell[line_no] 

909 yield new_line 

910 if _box and first and show_header: 

911 yield _Segment( 

912 _box.get_row(widths, "head", edge=show_edge), border_style 

913 ) 

914 yield new_line 

915 end_section = row and row.end_section 

916 if _box and (show_lines or leading or end_section): 

917 if ( 

918 not last 

919 and not (show_footer and index >= len(row_cells) - 2) 

920 and not (show_header and header_row) 

921 ): 

922 if leading: 

923 yield _Segment( 

924 _box.get_row(widths, "mid", edge=show_edge) * leading, 

925 border_style, 

926 ) 

927 else: 

928 yield _Segment( 

929 _box.get_row(widths, "row", edge=show_edge), border_style 

930 ) 

931 yield new_line 

932 

933 if _box and show_edge: 

934 yield _Segment(_box.get_bottom(widths), border_style) 

935 yield new_line 

936 

937 

938if __name__ == "__main__": # pragma: no cover 

939 from rich.console import Console 

940 from rich.highlighter import ReprHighlighter 

941 

942 from ._timer import timer 

943 

944 with timer("Table render"): 

945 table = Table( 

946 title="Star Wars Movies", 

947 caption="Rich example table", 

948 caption_justify="right", 

949 ) 

950 

951 table.add_column( 

952 "Released", header_style="bright_cyan", style="cyan", no_wrap=True 

953 ) 

954 table.add_column("Title", style="magenta") 

955 table.add_column("Box Office", justify="right", style="green") 

956 

957 table.add_row( 

958 "Dec 20, 2019", 

959 "Star Wars: The Rise of Skywalker", 

960 "$952,110,690", 

961 ) 

962 table.add_row("May 25, 2018", "Solo: A Star Wars Story", "$393,151,347") 

963 table.add_row( 

964 "Dec 15, 2017", 

965 "Star Wars Ep. V111: The Last Jedi", 

966 "$1,332,539,889", 

967 style="on black", 

968 end_section=True, 

969 ) 

970 table.add_row( 

971 "Dec 16, 2016", 

972 "Rogue One: A Star Wars Story", 

973 "$1,332,439,889", 

974 ) 

975 

976 def header(text: str) -> None: 

977 console.print() 

978 console.rule(highlight(text)) 

979 console.print() 

980 

981 console = Console() 

982 highlight = ReprHighlighter() 

983 header("Example Table") 

984 console.print(table, justify="center") 

985 

986 table.expand = True 

987 header("expand=True") 

988 console.print(table) 

989 

990 table.width = 50 

991 header("width=50") 

992 

993 console.print(table, justify="center") 

994 

995 table.width = None 

996 table.expand = False 

997 table.row_styles = ["dim", "none"] 

998 header("row_styles=['dim', 'none']") 

999 

1000 console.print(table, justify="center") 

1001 

1002 table.width = None 

1003 table.expand = False 

1004 table.row_styles = ["dim", "none"] 

1005 table.leading = 1 

1006 header("leading=1, row_styles=['dim', 'none']") 

1007 console.print(table, justify="center") 

1008 

1009 table.width = None 

1010 table.expand = False 

1011 table.row_styles = ["dim", "none"] 

1012 table.show_lines = True 

1013 table.leading = 0 

1014 header("show_lines=True, row_styles=['dim', 'none']") 

1015 console.print(table, justify="center")