|
|
|
from collections import defaultdict
|
|
|
|
from itertools import chain
|
|
|
|
from operator import itemgetter
|
|
|
|
from typing import Dict, Iterable, List, Optional, Tuple
|
|
|
|
|
|
|
|
from .align import Align, AlignMethod
|
|
|
|
from .console import Console, ConsoleOptions, RenderableType, RenderResult
|
|
|
|
from .constrain import Constrain
|
|
|
|
from .measure import Measurement
|
|
|
|
from .padding import Padding, PaddingDimensions
|
|
|
|
from .table import Table
|
|
|
|
from .text import TextType
|
|
|
|
from .jupyter import JupyterMixin
|
|
|
|
|
|
|
|
|
|
|
|
class Columns(JupyterMixin):
|
|
|
|
"""Display renderables in neat columns.
|
|
|
|
|
|
|
|
Args:
|
|
|
|
renderables (Iterable[RenderableType]): Any number of Rich renderables (including str).
|
|
|
|
width (int, optional): The desired width of the columns, or None to auto detect. Defaults to None.
|
|
|
|
padding (PaddingDimensions, optional): Optional padding around cells. Defaults to (0, 1).
|
|
|
|
expand (bool, optional): Expand columns to full width. Defaults to False.
|
|
|
|
equal (bool, optional): Arrange in to equal sized columns. Defaults to False.
|
|
|
|
column_first (bool, optional): Align items from top to bottom (rather than left to right). Defaults to False.
|
|
|
|
right_to_left (bool, optional): Start column from right hand side. Defaults to False.
|
|
|
|
align (str, optional): Align value ("left", "right", or "center") or None for default. Defaults to None.
|
|
|
|
title (TextType, optional): Optional title for Columns.
|
|
|
|
"""
|
|
|
|
|
|
|
|
def __init__(
|
|
|
|
self,
|
|
|
|
renderables: Optional[Iterable[RenderableType]] = None,
|
|
|
|
padding: PaddingDimensions = (0, 1),
|
|
|
|
*,
|
|
|
|
width: Optional[int] = None,
|
|
|
|
expand: bool = False,
|
|
|
|
equal: bool = False,
|
|
|
|
column_first: bool = False,
|
|
|
|
right_to_left: bool = False,
|
|
|
|
align: Optional[AlignMethod] = None,
|
|
|
|
title: Optional[TextType] = None,
|
|
|
|
) -> None:
|
|
|
|
self.renderables = list(renderables or [])
|
|
|
|
self.width = width
|
|
|
|
self.padding = padding
|
|
|
|
self.expand = expand
|
|
|
|
self.equal = equal
|
|
|
|
self.column_first = column_first
|
|
|
|
self.right_to_left = right_to_left
|
|
|
|
self.align: Optional[AlignMethod] = align
|
|
|
|
self.title = title
|
|
|
|
|
|
|
|
def add_renderable(self, renderable: RenderableType) -> None:
|
|
|
|
"""Add a renderable to the columns.
|
|
|
|
|
|
|
|
Args:
|
|
|
|
renderable (RenderableType): Any renderable object.
|
|
|
|
"""
|
|
|
|
self.renderables.append(renderable)
|
|
|
|
|
|
|
|
def __rich_console__(
|
|
|
|
self, console: Console, options: ConsoleOptions
|
|
|
|
) -> RenderResult:
|
|
|
|
render_str = console.render_str
|
|
|
|
renderables = [
|
|
|
|
render_str(renderable) if isinstance(renderable, str) else renderable
|
|
|
|
for renderable in self.renderables
|
|
|
|
]
|
|
|
|
if not renderables:
|
|
|
|
return
|
|
|
|
_top, right, _bottom, left = Padding.unpack(self.padding)
|
|
|
|
width_padding = max(left, right)
|
|
|
|
max_width = options.max_width
|
|
|
|
widths: Dict[int, int] = defaultdict(int)
|
|
|
|
column_count = len(renderables)
|
|
|
|
|
|
|
|
get_measurement = Measurement.get
|
|
|
|
renderable_widths = [
|
|
|
|
get_measurement(console, options, renderable).maximum
|
|
|
|
for renderable in renderables
|
|
|
|
]
|
|
|
|
if self.equal:
|
|
|
|
renderable_widths = [max(renderable_widths)] * len(renderable_widths)
|
|
|
|
|
|
|
|
def iter_renderables(
|
|
|
|
column_count: int,
|
|
|
|
) -> Iterable[Tuple[int, Optional[RenderableType]]]:
|
|
|
|
item_count = len(renderables)
|
|
|
|
if self.column_first:
|
|
|
|
width_renderables = list(zip(renderable_widths, renderables))
|
|
|
|
|
|
|
|
column_lengths: List[int] = [item_count // column_count] * column_count
|
|
|
|
for col_no in range(item_count % column_count):
|
|
|
|
column_lengths[col_no] += 1
|
|
|
|
|
|
|
|
row_count = (item_count + column_count - 1) // column_count
|
|
|
|
cells = [[-1] * column_count for _ in range(row_count)]
|
|
|
|
row = col = 0
|
|
|
|
for index in range(item_count):
|
|
|
|
cells[row][col] = index
|
|
|
|
column_lengths[col] -= 1
|
|
|
|
if column_lengths[col]:
|
|
|
|
row += 1
|
|
|
|
else:
|
|
|
|
col += 1
|
|
|
|
row = 0
|
|
|
|
for index in chain.from_iterable(cells):
|
|
|
|
if index == -1:
|
|
|
|
break
|
|
|
|
yield width_renderables[index]
|
|
|
|
else:
|
|
|
|
yield from zip(renderable_widths, renderables)
|
|
|
|
# Pad odd elements with spaces
|
|
|
|
if item_count % column_count:
|
|
|
|
for _ in range(column_count - (item_count % column_count)):
|
|
|
|
yield 0, None
|
|
|
|
|
|
|
|
table = Table.grid(padding=self.padding, collapse_padding=True, pad_edge=False)
|
|
|
|
table.expand = self.expand
|
|
|
|
table.title = self.title
|
|
|
|
|
|
|
|
if self.width is not None:
|
|
|
|
column_count = (max_width) // (self.width + width_padding)
|
|
|
|
for _ in range(column_count):
|
|
|
|
table.add_column(width=self.width)
|
|
|
|
else:
|
|
|
|
while column_count > 1:
|
|
|
|
widths.clear()
|
|
|
|
column_no = 0
|
|
|
|
for renderable_width, _ in iter_renderables(column_count):
|
|
|
|
widths[column_no] = max(widths[column_no], renderable_width)
|
|
|
|
total_width = sum(widths.values()) + width_padding * (
|
|
|
|
len(widths) - 1
|
|
|
|
)
|
|
|
|
if total_width > max_width:
|
|
|
|
column_count = len(widths) - 1
|
|
|
|
break
|
|
|
|
else:
|
|
|
|
column_no = (column_no + 1) % column_count
|
|
|
|
else:
|
|
|
|
break
|
|
|
|
|
|
|
|
get_renderable = itemgetter(1)
|
|
|
|
_renderables = [
|
|
|
|
get_renderable(_renderable)
|
|
|
|
for _renderable in iter_renderables(column_count)
|
|
|
|
]
|
|
|
|
if self.equal:
|
|
|
|
_renderables = [
|
|
|
|
None
|
|
|
|
if renderable is None
|
|
|
|
else Constrain(renderable, renderable_widths[0])
|
|
|
|
for renderable in _renderables
|
|
|
|
]
|
|
|
|
if self.align:
|
|
|
|
align = self.align
|
|
|
|
_Align = Align
|
|
|
|
_renderables = [
|
|
|
|
None if renderable is None else _Align(renderable, align)
|
|
|
|
for renderable in _renderables
|
|
|
|
]
|
|
|
|
|
|
|
|
right_to_left = self.right_to_left
|
|
|
|
add_row = table.add_row
|
|
|
|
for start in range(0, len(_renderables), column_count):
|
|
|
|
row = _renderables[start : start + column_count]
|
|
|
|
if right_to_left:
|
|
|
|
row = row[::-1]
|
|
|
|
add_row(*row)
|
|
|
|
yield table
|
|
|
|
|
|
|
|
|
|
|
|
if __name__ == "__main__": # pragma: no cover
|
|
|
|
import os
|
|
|
|
|
|
|
|
console = Console()
|
|
|
|
|
|
|
|
files = [f"{i} {s}" for i, s in enumerate(sorted(os.listdir()))]
|
|
|
|
columns = Columns(files, padding=(0, 1), expand=False, equal=False)
|
|
|
|
console.print(columns)
|
|
|
|
console.rule()
|
|
|
|
columns.column_first = True
|
|
|
|
console.print(columns)
|
|
|
|
columns.right_to_left = True
|
|
|
|
console.rule()
|
|
|
|
console.print(columns)
|