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

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

407 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 expand (bool, optional): Expand the table to fit the available space if ``True``, otherwise the table width will be auto-calculated. Defaults to False. 

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

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

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

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

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

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

59 row_styles (List[Union, str], optional): Optional list of row styles, if more than one style is given then the styles will alternate. Defaults to None. 

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

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

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

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

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

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

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

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

68 """ 

69 

70 header: "RenderableType" = "" 

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

72 

73 footer: "RenderableType" = "" 

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

75 

76 header_style: StyleType = "" 

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

78 

79 footer_style: StyleType = "" 

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

81 

82 style: StyleType = "" 

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

84 

85 justify: "JustifyMethod" = "left" 

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

87 

88 vertical: "VerticalAlignMethod" = "top" 

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

90 

91 overflow: "OverflowMethod" = "ellipsis" 

92 """str: Overflow method.""" 

93 

94 width: Optional[int] = None 

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

96 

97 min_width: Optional[int] = None 

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

99 

100 max_width: Optional[int] = None 

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

102 

103 ratio: Optional[int] = None 

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

105 

106 no_wrap: bool = False 

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

108 

109 highlight: bool = False 

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

111 

112 _index: int = 0 

113 """Index of column.""" 

114 

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

116 

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

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

119 return replace(self, _cells=[]) 

120 

121 @property 

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

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

124 yield from self._cells 

125 

126 @property 

127 def flexible(self) -> bool: 

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

129 return self.ratio is not None 

130 

131 

132@dataclass 

133class Row: 

134 """Information regarding a row.""" 

135 

136 style: Optional[StyleType] = None 

137 """Style to apply to row.""" 

138 

139 end_section: bool = False 

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

141 

142 

143class _Cell(NamedTuple): 

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

145 

146 style: StyleType 

147 """Style to apply to cell.""" 

148 renderable: "RenderableType" 

149 """Cell renderable.""" 

150 vertical: VerticalAlignMethod 

151 """Cell vertical alignment.""" 

152 

153 

154class Table(JupyterMixin): 

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

156 

157 Args: 

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

184 """ 

185 

186 columns: List[Column] 

187 rows: List[Row] 

188 

189 def __init__( 

190 self, 

191 *headers: Union[Column, str], 

192 title: Optional[TextType] = None, 

193 caption: Optional[TextType] = None, 

194 width: Optional[int] = None, 

195 min_width: Optional[int] = None, 

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

197 safe_box: Optional[bool] = None, 

198 padding: PaddingDimensions = (0, 1), 

199 collapse_padding: bool = False, 

200 pad_edge: bool = True, 

201 expand: bool = False, 

202 show_header: bool = True, 

203 show_footer: bool = False, 

204 show_edge: bool = True, 

205 show_lines: bool = False, 

206 leading: int = 0, 

207 style: StyleType = "none", 

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

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

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

211 border_style: Optional[StyleType] = None, 

212 title_style: Optional[StyleType] = None, 

213 caption_style: Optional[StyleType] = None, 

214 title_justify: "JustifyMethod" = "center", 

215 caption_justify: "JustifyMethod" = "center", 

216 highlight: bool = False, 

217 ) -> None: 

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

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

220 self.title = title 

221 self.caption = caption 

222 self.width = width 

223 self.min_width = min_width 

224 self.box = box 

225 self.safe_box = safe_box 

226 self._padding = Padding.unpack(padding) 

227 self.pad_edge = pad_edge 

228 self._expand = expand 

229 self.show_header = show_header 

230 self.show_footer = show_footer 

231 self.show_edge = show_edge 

232 self.show_lines = show_lines 

233 self.leading = leading 

234 self.collapse_padding = collapse_padding 

235 self.style = style 

236 self.header_style = header_style or "" 

237 self.footer_style = footer_style or "" 

238 self.border_style = border_style 

239 self.title_style = title_style 

240 self.caption_style = caption_style 

241 self.title_justify: "JustifyMethod" = title_justify 

242 self.caption_justify: "JustifyMethod" = caption_justify 

243 self.highlight = highlight 

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

245 append_column = self.columns.append 

246 for header in headers: 

247 if isinstance(header, str): 

248 self.add_column(header=header) 

249 else: 

250 header._index = len(self.columns) 

251 append_column(header) 

252 

253 @classmethod 

254 def grid( 

255 cls, 

256 *headers: Union[Column, str], 

257 padding: PaddingDimensions = 0, 

258 collapse_padding: bool = True, 

259 pad_edge: bool = False, 

260 expand: bool = False, 

261 ) -> "Table": 

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

263 

264 Args: 

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

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

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

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

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

270 

271 Returns: 

272 Table: A table instance. 

273 """ 

274 return cls( 

275 *headers, 

276 box=None, 

277 padding=padding, 

278 collapse_padding=collapse_padding, 

279 show_header=False, 

280 show_footer=False, 

281 show_edge=False, 

282 pad_edge=pad_edge, 

283 expand=expand, 

284 ) 

285 

286 @property 

287 def expand(self) -> bool: 

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

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

290 

291 @expand.setter 

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

293 """Set expand.""" 

294 self._expand = expand 

295 

296 @property 

297 def _extra_width(self) -> int: 

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

299 width = 0 

300 if self.box and self.show_edge: 

301 width += 2 

302 if self.box: 

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

304 return width 

305 

306 @property 

307 def row_count(self) -> int: 

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

309 return len(self.rows) 

310 

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

312 """Get the current row style.""" 

313 style = Style.null() 

314 if self.row_styles: 

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

316 row_style = self.rows[index].style 

317 if row_style is not None: 

318 style += console.get_style(row_style) 

319 return style 

320 

321 def __rich_measure__( 

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

323 ) -> Measurement: 

324 max_width = options.max_width 

325 if self.width is not None: 

326 max_width = self.width 

327 if max_width < 0: 

328 return Measurement(0, 0) 

329 

330 extra_width = self._extra_width 

331 max_width = sum( 

332 self._calculate_column_widths( 

333 console, options.update_width(max_width - extra_width) 

334 ) 

335 ) 

336 _measure_column = self._measure_column 

337 

338 measurements = [ 

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

340 for column in self.columns 

341 ] 

342 minimum_width = ( 

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

344 ) 

345 maximum_width = ( 

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

347 if (self.width is None) 

348 else self.width 

349 ) 

350 measurement = Measurement(minimum_width, maximum_width) 

351 measurement = measurement.clamp(self.min_width) 

352 return measurement 

353 

354 @property 

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

356 """Get cell padding.""" 

357 return self._padding 

358 

359 @padding.setter 

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

361 """Set cell padding.""" 

362 self._padding = Padding.unpack(padding) 

363 return self 

364 

365 def add_column( 

366 self, 

367 header: "RenderableType" = "", 

368 footer: "RenderableType" = "", 

369 *, 

370 header_style: Optional[StyleType] = None, 

371 highlight: Optional[bool] = None, 

372 footer_style: Optional[StyleType] = None, 

373 style: Optional[StyleType] = None, 

374 justify: "JustifyMethod" = "left", 

375 vertical: "VerticalAlignMethod" = "top", 

376 overflow: "OverflowMethod" = "ellipsis", 

377 width: Optional[int] = None, 

378 min_width: Optional[int] = None, 

379 max_width: Optional[int] = None, 

380 ratio: Optional[int] = None, 

381 no_wrap: bool = False, 

382 ) -> None: 

383 """Add a column to the table. 

384 

385 Args: 

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

387 Defaults to "". 

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

389 Defaults to "". 

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

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

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

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

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

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

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

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

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

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

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

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

402 """ 

403 

404 column = Column( 

405 _index=len(self.columns), 

406 header=header, 

407 footer=footer, 

408 header_style=header_style or "", 

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

410 footer_style=footer_style or "", 

411 style=style or "", 

412 justify=justify, 

413 vertical=vertical, 

414 overflow=overflow, 

415 width=width, 

416 min_width=min_width, 

417 max_width=max_width, 

418 ratio=ratio, 

419 no_wrap=no_wrap, 

420 ) 

421 self.columns.append(column) 

422 

423 def add_row( 

424 self, 

425 *renderables: Optional["RenderableType"], 

426 style: Optional[StyleType] = None, 

427 end_section: bool = False, 

428 ) -> None: 

429 """Add a row of renderables. 

430 

431 Args: 

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

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

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

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

436 

437 Raises: 

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

439 """ 

440 

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

442 column._cells.append(renderable) 

443 

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

445 

446 columns = self.columns 

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

448 cell_renderables = [ 

449 *cell_renderables, 

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

451 ] 

452 for index, renderable in enumerate(cell_renderables): 

453 if index == len(columns): 

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

455 for _ in self.rows: 

456 add_cell(column, Text("")) 

457 self.columns.append(column) 

458 else: 

459 column = columns[index] 

460 if renderable is None: 

461 add_cell(column, "") 

462 elif is_renderable(renderable): 

463 add_cell(column, renderable) 

464 else: 

465 raise errors.NotRenderableError( 

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

467 ) 

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

469 

470 def add_section(self) -> None: 

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

472 

473 if self.rows: 

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

475 

476 def __rich_console__( 

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

478 ) -> "RenderResult": 

479 if not self.columns: 

480 yield Segment("\n") 

481 return 

482 

483 max_width = options.max_width 

484 if self.width is not None: 

485 max_width = self.width 

486 

487 extra_width = self._extra_width 

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 get_padding_width = self._get_padding_width 

534 extra_width = self._extra_width 

535 if self.expand: 

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

537 if any(ratios): 

538 fixed_widths = [ 

539 0 if column.flexible else _range.maximum 

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

541 ] 

542 flex_minimum = [ 

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

544 for column in columns 

545 if column.flexible 

546 ] 

547 flexible_width = max_width - sum(fixed_widths) 

548 flex_widths = ratio_distribute(flexible_width, ratios, flex_minimum) 

549 iter_flex_widths = iter(flex_widths) 

550 for index, column in enumerate(columns): 

551 if column.flexible: 

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

553 table_width = sum(widths) 

554 

555 if table_width > max_width: 

556 widths = self._collapse_widths( 

557 widths, 

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

559 max_width, 

560 ) 

561 table_width = sum(widths) 

562 # last resort, reduce columns evenly 

563 if table_width > max_width: 

564 excess_width = table_width - max_width 

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

566 table_width = sum(widths) 

567 

568 width_ranges = [ 

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

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

571 ] 

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

573 

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

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

576 ): 

577 _max_width = ( 

578 max_width 

579 if self.min_width is None 

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

581 ) 

582 pad_widths = ratio_distribute(_max_width - table_width, widths) 

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

584 

585 return widths 

586 

587 @classmethod 

588 def _collapse_widths( 

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

590 ) -> List[int]: 

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

592 

593 Args: 

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

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

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

597 

598 Returns: 

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

600 """ 

601 total_width = sum(widths) 

602 excess_width = total_width - max_width 

603 if any(wrapable): 

604 while total_width and excess_width > 0: 

605 max_column = max( 

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

607 ) 

608 second_max_column = max( 

609 width if allow_wrap and width != max_column else 0 

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

611 ) 

612 column_difference = max_column - second_max_column 

613 ratios = [ 

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

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

616 ] 

617 if not any(ratios) or not column_difference: 

618 break 

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

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

621 

622 total_width = sum(widths) 

623 excess_width = total_width - max_width 

624 return widths 

625 

626 def _get_cells( 

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

628 ) -> Iterable[_Cell]: 

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

630 

631 collapse_padding = self.collapse_padding 

632 pad_edge = self.pad_edge 

633 padding = self.padding 

634 any_padding = any(padding) 

635 

636 first_column = column_index == 0 

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

638 

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

640 

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

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

643 if cached: 

644 return cached 

645 top, right, bottom, left = padding 

646 

647 if collapse_padding: 

648 if not first_column: 

649 left = max(0, left - right) 

650 if not last_row: 

651 bottom = max(0, top - bottom) 

652 

653 if not pad_edge: 

654 if first_column: 

655 left = 0 

656 if last_column: 

657 right = 0 

658 if first_row: 

659 top = 0 

660 if last_row: 

661 bottom = 0 

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

663 _padding_cache[(first_row, last_row)] = _padding 

664 return _padding 

665 

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

667 _append = raw_cells.append 

668 get_style = console.get_style 

669 if self.show_header: 

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

671 column.header_style 

672 ) 

673 _append((header_style, column.header)) 

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

675 for cell in column.cells: 

676 _append((cell_style, cell)) 

677 if self.show_footer: 

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

679 column.footer_style 

680 ) 

681 _append((footer_style, column.footer)) 

682 

683 if any_padding: 

684 _Padding = Padding 

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

686 yield _Cell( 

687 style, 

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

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

690 ) 

691 else: 

692 for style, renderable in raw_cells: 

693 yield _Cell( 

694 style, 

695 renderable, 

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

697 ) 

698 

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

700 """Get extra width from padding.""" 

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

702 if self.collapse_padding: 

703 if column_index > 0: 

704 pad_left = max(0, pad_left - pad_right) 

705 return pad_left + pad_right 

706 

707 def _measure_column( 

708 self, 

709 console: "Console", 

710 options: "ConsoleOptions", 

711 column: Column, 

712 ) -> Measurement: 

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

714 

715 max_width = options.max_width 

716 if max_width < 1: 

717 return Measurement(0, 0) 

718 

719 padding_width = self._get_padding_width(column._index) 

720 

721 if column.width is not None: 

722 # Fixed width column 

723 return Measurement( 

724 column.width + padding_width, column.width + padding_width 

725 ).with_maximum(max_width) 

726 # Flexible column, we need to measure contents 

727 min_widths: List[int] = [] 

728 max_widths: List[int] = [] 

729 append_min = min_widths.append 

730 append_max = max_widths.append 

731 get_render_width = Measurement.get 

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

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

734 append_min(_min) 

735 append_max(_max) 

736 

737 measurement = Measurement( 

738 max(min_widths) if min_widths else 1, 

739 max(max_widths) if max_widths else max_width, 

740 ).with_maximum(max_width) 

741 measurement = measurement.clamp( 

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

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

744 ) 

745 return measurement 

746 

747 def _render( 

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

749 ) -> "RenderResult": 

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

751 

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

753 _column_cells = ( 

754 self._get_cells(console, column_index, column) 

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

756 ) 

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

758 _box = ( 

759 self.box.substitute( 

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

761 ) 

762 if self.box 

763 else None 

764 ) 

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

766 

767 new_line = Segment.line() 

768 

769 columns = self.columns 

770 show_header = self.show_header 

771 show_footer = self.show_footer 

772 show_edge = self.show_edge 

773 show_lines = self.show_lines 

774 leading = self.leading 

775 

776 _Segment = Segment 

777 if _box: 

778 box_segments = [ 

779 ( 

780 _Segment(_box.head_left, border_style), 

781 _Segment(_box.head_right, border_style), 

782 _Segment(_box.head_vertical, border_style), 

783 ), 

784 ( 

785 _Segment(_box.mid_left, border_style), 

786 _Segment(_box.mid_right, border_style), 

787 _Segment(_box.mid_vertical, border_style), 

788 ), 

789 ( 

790 _Segment(_box.foot_left, border_style), 

791 _Segment(_box.foot_right, border_style), 

792 _Segment(_box.foot_vertical, border_style), 

793 ), 

794 ] 

795 if show_edge: 

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

797 yield new_line 

798 else: 

799 box_segments = [] 

800 

801 get_row_style = self.get_row_style 

802 get_style = console.get_style 

803 

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

805 header_row = first and show_header 

806 footer_row = last and show_footer 

807 row = ( 

808 self.rows[index - show_header] 

809 if (not header_row and not footer_row) 

810 else None 

811 ) 

812 max_height = 1 

813 cells: List[List[List[Segment]]] = [] 

814 if header_row or footer_row: 

815 row_style = Style.null() 

816 else: 

817 row_style = get_style( 

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

819 ) 

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

821 render_options = options.update( 

822 width=width, 

823 justify=column.justify, 

824 no_wrap=column.no_wrap, 

825 overflow=column.overflow, 

826 height=None, 

827 highlight=column.highlight, 

828 ) 

829 lines = console.render_lines( 

830 cell.renderable, 

831 render_options, 

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

833 ) 

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

835 cells.append(lines) 

836 

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

838 

839 def align_cell( 

840 cell: List[List[Segment]], 

841 vertical: "VerticalAlignMethod", 

842 width: int, 

843 style: Style, 

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

845 if header_row: 

846 vertical = "bottom" 

847 elif footer_row: 

848 vertical = "top" 

849 

850 if vertical == "top": 

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

852 elif vertical == "middle": 

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

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

855 

856 cells[:] = [ 

857 _Segment.set_shape( 

858 align_cell( 

859 cell, 

860 _cell.vertical, 

861 width, 

862 get_style(_cell.style) + row_style, 

863 ), 

864 width, 

865 max_height, 

866 ) 

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

868 ] 

869 

870 if _box: 

871 if last and show_footer: 

872 yield _Segment( 

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

874 ) 

875 yield new_line 

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

877 

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

879 divider = ( 

880 _divider 

881 if _divider.text.strip() 

882 else _Segment( 

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

884 ) 

885 ) 

886 for line_no in range(max_height): 

887 if show_edge: 

888 yield left 

889 for last_cell, rendered_cell in loop_last(cells): 

890 yield from rendered_cell[line_no] 

891 if not last_cell: 

892 yield divider 

893 if show_edge: 

894 yield right 

895 yield new_line 

896 else: 

897 for line_no in range(max_height): 

898 for rendered_cell in cells: 

899 yield from rendered_cell[line_no] 

900 yield new_line 

901 if _box and first and show_header: 

902 yield _Segment( 

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

904 ) 

905 yield new_line 

906 end_section = row and row.end_section 

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

908 if ( 

909 not last 

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

911 and not (show_header and header_row) 

912 ): 

913 if leading: 

914 yield _Segment( 

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

916 border_style, 

917 ) 

918 else: 

919 yield _Segment( 

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

921 ) 

922 yield new_line 

923 

924 if _box and show_edge: 

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

926 yield new_line 

927 

928 

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

930 from rich.console import Console 

931 from rich.highlighter import ReprHighlighter 

932 

933 from ._timer import timer 

934 

935 with timer("Table render"): 

936 table = Table( 

937 title="Star Wars Movies", 

938 caption="Rich example table", 

939 caption_justify="right", 

940 ) 

941 

942 table.add_column( 

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

944 ) 

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

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

947 

948 table.add_row( 

949 "Dec 20, 2019", 

950 "Star Wars: The Rise of Skywalker", 

951 "$952,110,690", 

952 ) 

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

954 table.add_row( 

955 "Dec 15, 2017", 

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

957 "$1,332,539,889", 

958 style="on black", 

959 end_section=True, 

960 ) 

961 table.add_row( 

962 "Dec 16, 2016", 

963 "Rogue One: A Star Wars Story", 

964 "$1,332,439,889", 

965 ) 

966 

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

968 console.print() 

969 console.rule(highlight(text)) 

970 console.print() 

971 

972 console = Console() 

973 highlight = ReprHighlighter() 

974 header("Example Table") 

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

976 

977 table.expand = True 

978 header("expand=True") 

979 console.print(table) 

980 

981 table.width = 50 

982 header("width=50") 

983 

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

985 

986 table.width = None 

987 table.expand = False 

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

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

990 

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

992 

993 table.width = None 

994 table.expand = False 

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

996 table.leading = 1 

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

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

999 

1000 table.width = None 

1001 table.expand = False 

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

1003 table.show_lines = True 

1004 table.leading = 0 

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

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