columns.py 7.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187
  1. from collections import defaultdict
  2. from itertools import chain
  3. from operator import itemgetter
  4. from typing import Dict, Iterable, List, Optional, Tuple
  5. from .align import Align, AlignMethod
  6. from .console import Console, ConsoleOptions, RenderableType, RenderResult
  7. from .constrain import Constrain
  8. from .measure import Measurement
  9. from .padding import Padding, PaddingDimensions
  10. from .table import Table
  11. from .text import TextType
  12. from .jupyter import JupyterMixin
  13. class Columns(JupyterMixin):
  14. """Display renderables in neat columns.
  15. Args:
  16. renderables (Iterable[RenderableType]): Any number of Rich renderables (including str).
  17. width (int, optional): The desired width of the columns, or None to auto detect. Defaults to None.
  18. padding (PaddingDimensions, optional): Optional padding around cells. Defaults to (0, 1).
  19. expand (bool, optional): Expand columns to full width. Defaults to False.
  20. equal (bool, optional): Arrange in to equal sized columns. Defaults to False.
  21. column_first (bool, optional): Align items from top to bottom (rather than left to right). Defaults to False.
  22. right_to_left (bool, optional): Start column from right hand side. Defaults to False.
  23. align (str, optional): Align value ("left", "right", or "center") or None for default. Defaults to None.
  24. title (TextType, optional): Optional title for Columns.
  25. """
  26. def __init__(
  27. self,
  28. renderables: Optional[Iterable[RenderableType]] = None,
  29. padding: PaddingDimensions = (0, 1),
  30. *,
  31. width: Optional[int] = None,
  32. expand: bool = False,
  33. equal: bool = False,
  34. column_first: bool = False,
  35. right_to_left: bool = False,
  36. align: Optional[AlignMethod] = None,
  37. title: Optional[TextType] = None,
  38. ) -> None:
  39. self.renderables = list(renderables or [])
  40. self.width = width
  41. self.padding = padding
  42. self.expand = expand
  43. self.equal = equal
  44. self.column_first = column_first
  45. self.right_to_left = right_to_left
  46. self.align: Optional[AlignMethod] = align
  47. self.title = title
  48. def add_renderable(self, renderable: RenderableType) -> None:
  49. """Add a renderable to the columns.
  50. Args:
  51. renderable (RenderableType): Any renderable object.
  52. """
  53. self.renderables.append(renderable)
  54. def __rich_console__(
  55. self, console: Console, options: ConsoleOptions
  56. ) -> RenderResult:
  57. render_str = console.render_str
  58. renderables = [
  59. render_str(renderable) if isinstance(renderable, str) else renderable
  60. for renderable in self.renderables
  61. ]
  62. if not renderables:
  63. return
  64. _top, right, _bottom, left = Padding.unpack(self.padding)
  65. width_padding = max(left, right)
  66. max_width = options.max_width
  67. widths: Dict[int, int] = defaultdict(int)
  68. column_count = len(renderables)
  69. get_measurement = Measurement.get
  70. renderable_widths = [
  71. get_measurement(console, options, renderable).maximum
  72. for renderable in renderables
  73. ]
  74. if self.equal:
  75. renderable_widths = [max(renderable_widths)] * len(renderable_widths)
  76. def iter_renderables(
  77. column_count: int,
  78. ) -> Iterable[Tuple[int, Optional[RenderableType]]]:
  79. item_count = len(renderables)
  80. if self.column_first:
  81. width_renderables = list(zip(renderable_widths, renderables))
  82. column_lengths: List[int] = [item_count // column_count] * column_count
  83. for col_no in range(item_count % column_count):
  84. column_lengths[col_no] += 1
  85. row_count = (item_count + column_count - 1) // column_count
  86. cells = [[-1] * column_count for _ in range(row_count)]
  87. row = col = 0
  88. for index in range(item_count):
  89. cells[row][col] = index
  90. column_lengths[col] -= 1
  91. if column_lengths[col]:
  92. row += 1
  93. else:
  94. col += 1
  95. row = 0
  96. for index in chain.from_iterable(cells):
  97. if index == -1:
  98. break
  99. yield width_renderables[index]
  100. else:
  101. yield from zip(renderable_widths, renderables)
  102. # Pad odd elements with spaces
  103. if item_count % column_count:
  104. for _ in range(column_count - (item_count % column_count)):
  105. yield 0, None
  106. table = Table.grid(padding=self.padding, collapse_padding=True, pad_edge=False)
  107. table.expand = self.expand
  108. table.title = self.title
  109. if self.width is not None:
  110. column_count = (max_width) // (self.width + width_padding)
  111. for _ in range(column_count):
  112. table.add_column(width=self.width)
  113. else:
  114. while column_count > 1:
  115. widths.clear()
  116. column_no = 0
  117. for renderable_width, _ in iter_renderables(column_count):
  118. widths[column_no] = max(widths[column_no], renderable_width)
  119. total_width = sum(widths.values()) + width_padding * (
  120. len(widths) - 1
  121. )
  122. if total_width > max_width:
  123. column_count = len(widths) - 1
  124. break
  125. else:
  126. column_no = (column_no + 1) % column_count
  127. else:
  128. break
  129. get_renderable = itemgetter(1)
  130. _renderables = [
  131. get_renderable(_renderable)
  132. for _renderable in iter_renderables(column_count)
  133. ]
  134. if self.equal:
  135. _renderables = [
  136. None
  137. if renderable is None
  138. else Constrain(renderable, renderable_widths[0])
  139. for renderable in _renderables
  140. ]
  141. if self.align:
  142. align = self.align
  143. _Align = Align
  144. _renderables = [
  145. None if renderable is None else _Align(renderable, align)
  146. for renderable in _renderables
  147. ]
  148. right_to_left = self.right_to_left
  149. add_row = table.add_row
  150. for start in range(0, len(_renderables), column_count):
  151. row = _renderables[start : start + column_count]
  152. if right_to_left:
  153. row = row[::-1]
  154. add_row(*row)
  155. yield table
  156. if __name__ == "__main__": # pragma: no cover
  157. import os
  158. console = Console()
  159. files = [f"{i} {s}" for i, s in enumerate(sorted(os.listdir()))]
  160. columns = Columns(files, padding=(0, 1), expand=False, equal=False)
  161. console.print(columns)
  162. console.rule()
  163. columns.column_first = True
  164. console.print(columns)
  165. columns.right_to_left = True
  166. console.rule()
  167. console.print(columns)