Source code for cli2.table
"""
cli2 also offers a simple table rendering data that will do it's best to word
wrap cell data so that it fits in the terminal.
This Table module behaves like a list, and is pretty simple. Its purpose is to
tabulate data and it's going to brute force output sizes until it finds a
match.
As such, it's not a good module to display really a lot of data because it
sacrifies performance for human readability.
.. code-block:: python
cli2.Table.factory(['foo', 'bar'], ['much longer', 'test']).print()
.. code-block::
foo bar
much longer test
.. note:: This would look better with :py:mod:`~cli2.color`
.. code-block:: python
cli2.Table.factory(dict(foo=1, bar=2), dict(foo=3, bar=4)).print()
Renders:
.. code-block::
foo bar
=== ===
1 2
3 4
"""
import os
import textwrap
from .colors import colors
[docs]
class Column:
""" Column object """
def __init__(self):
self.maxlength = 1
self.minlength = 1
# size taken by the length of every column
def sumsize(columns):
sumsize = sum([c.maxlength for c in columns])
if len(columns) > 1:
# add one space in-between each column
sumsize += len(columns) - 1
return sumsize
[docs]
class Table(list):
"""
Table object
"""
def __init__(self, *args):
super().__init__(args)
[docs]
@classmethod
def factory(cls, *items):
"""
Instanciate a table with a bunch of items.
:params items: Iterable of lists or dicts or tuples
"""
self = cls()
first = True
kind = None
for item in items:
if not kind:
kind = type(item)
elif kind != type(item):
raise Exception('Data contains different types')
if isinstance(item, (list, tuple)):
self.append([str(item) for item in item])
elif isinstance(item, dict):
if first:
self.append([key for key in item.keys()])
self.append(['=' for key in item.keys()])
first = False
self.append([str(value) for value in item.values()])
return self
[docs]
def calculate_columns(self, termsize):
"""
Calculate columns size based on termsize.
"""
columns = self.columns = []
for row in self:
for colnum, item in enumerate(row):
if len(columns) == colnum:
column = Column()
columns.append(column)
else:
column = columns[colnum]
data = item
if isinstance(data, (list, tuple)):
data = data[1]
else:
data = item
data = str(data)
minlength = max(
[len(word) for word in data.split(' ')]
)
if minlength > column.minlength:
column.minlength = minlength
length = len(data)
if length > column.maxlength:
column.maxlength = length
current = -1
done = set()
while sumsize(columns) > termsize and len(done) < len(columns):
current = current if current >= 0 else len(columns) - 1
if current not in done:
# Let's start shrinking columns from the last
minlength = columns[current].minlength
maxlength = columns[current].maxlength
if maxlength - 1 > minlength:
columns[current].maxlength = maxlength - 1
else:
columns[current].maxlength = minlength
done.add(columns[current])
current -= 1
return columns
[docs]
def print(self, print_function=None, termsize=None):
"""
Print the table.
"""
print_function = print_function or print
if not termsize:
try:
termsize = os.get_terminal_size().columns
except: # noqa
termsize = 80
columns = self.calculate_columns(termsize=termsize)
# separate columns with 2 spaces if possible
if sumsize(columns) + (len(columns) - 1) * 2 <= termsize:
numspaces = 2
else:
numspaces = 1
rows = list(self)
while rows:
row = rows.pop(0)
line = []
leftovers = ['' for c in columns]
for colnum, column in enumerate(columns):
data = row[colnum]
if isinstance(data, (list, tuple)):
color = data[0]
data = data[1]
else:
color = ''
data = row[colnum]
data = str(data)
wrapped = textwrap.wrap(data, column.maxlength)
words = wrapped[0] if wrapped else ''
if words == '=':
words = '=' * column.maxlength
line.append(words)
if colnum + 1 < len(columns):
# add spaces
line[-1] += ' ' * (column.maxlength - len(words))
if len(wrapped) > 1:
leftovers[colnum] = ' '.join(wrapped[1:])
if color:
line[-1] = str(color) + line[-1] + colors.reset
for leftover in leftovers:
if len(leftover):
rows.insert(0, leftovers)
break
print_function((' ' * numspaces).join(line))