Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,7 @@ htmlcov
# mypy optional static type checker
.mypy_cache
*~

# Pipenv (this is a library, not an application)
.venv
Pipfile.lock
14 changes: 14 additions & 0 deletions Pipfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
[[source]]
name = "pypi"
url = "https://pypi.org/simple"
verify_ssl = true

[packages]
wcwidth = "*"

[dev-packages]
tableformatter = {editable = true,path = "."}
flake8 = "*"
invoke = "*"
pytest = "*"
pytest-cov = "*"
2 changes: 1 addition & 1 deletion examples/color.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,5 +29,5 @@

columns = ('Col1', 'Col2', 'Col3', 'Col4')

print("Table with colorful alternting rows")
print("Table with colorful alternating rows")
print(generate_table(rows, columns, grid_style=tf.AlternatingRowGrid(BACK_GREEN, BACK_BLUE)))
61 changes: 61 additions & 0 deletions examples/simple_dict.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
#!/usr/bin/env python
# coding=utf-8
"""
Simple demonstration of TableFormatter with a list of dicts for the table entries.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adding unit tests related to the two things that got fixed and the new feature would be highly desirable.

This approach requires providing the dictionary key to query
for each cell (via attrib='attrib_name').
"""
from tableformatter import generate_table, FancyGrid, SparseGrid, Column


class MyRowObject(object):
"""Simple object to demonstrate using a list of objects with TableFormatter"""
def __init__(self, field1: str, field2: str, field3: str, field4: str):
self.field1 = field1
self.field2 = field2
self._field3 = field3
self.field4 = field4

def get_field3(self):
"""Demonstrates accessing object functions"""
return self._field3

KEY1 = "Key1"
KEY2 = "Key2"
KEY3 = "Key3"
KEY4 = "Key4"

rows = [{KEY1:'A1', KEY2:'A2', KEY3:'A3', KEY4:'A4'},
{KEY1:'B1', KEY2:'B2\nB2\nB2', KEY3:'B3', KEY4:'B4'},
{KEY1:'C1', KEY2:'C2', KEY3:'C3', KEY4:'C4'},
{KEY1:'D1', KEY2:'D2', KEY3:'D3', KEY4:'D4'}]


columns = (Column('Col1', attrib=KEY1),
Column('Col2', attrib=KEY2),
Column('Col3', attrib=KEY3),
Column('Col4', attrib=KEY4))

print("Table with header, AlteratingRowGrid:")
print(generate_table(rows, columns))


print("Table with header, transposed, AlteratingRowGrid:")
print(generate_table(rows, columns, transpose=True))


print("Table with header, transposed, FancyGrid:")
print(generate_table(rows, columns, grid_style=FancyGrid(), transpose=True))

print("Table with header, transposed, SparseGrid:")
print(generate_table(rows, columns, grid_style=SparseGrid(), transpose=True))


columns2 = (Column('Col1', attrib=KEY3),
Column('Col2', attrib=KEY2),
Column('Col3', attrib=KEY1),
Column('Col4', attrib=KEY4))


print("Table with header, Columns rearranged")
print(generate_table(rows, columns2))
112 changes: 77 additions & 35 deletions tableformatter.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
from typing import Collection
except ImportError:
from typing import Container, Generic, Sized, TypeVar

# Python 3.5
# noinspection PyAbstractClass
class Collection(Generic[TypeVar('T_co', covariant=True)], Container, Sized, Iterable):
Expand Down Expand Up @@ -124,6 +125,45 @@ def _split(self, text):
chunks = [c for c in chunks if c]
return chunks

def _handle_long_word(self, reversed_chunks, cur_line, cur_len, width):
"""_handle_long_word(chunks : [string],
cur_line : [string],
cur_len : int, width : int)

Handle a chunk of text (most likely a word, not whitespace) that
is too long to fit in any line.
"""
# Figure out when indent is larger than the specified width, and make
# sure at least one character is stripped off on every pass
if width < 1:
space_left = 1
else:
space_left = width - cur_len

# If we're allowed to break long words, then do so: put as much
# of the next chunk onto the current line as will fit.
if self.break_long_words:
shard_length = space_left
shard = reversed_chunks[-1][:shard_length]
while _wcswidth(shard) > space_left and shard_length > 0:
shard_length -= 1
shard = reversed_chunks[-1][:shard_length]
if shard_length > 0:
cur_line.append(shard)
reversed_chunks[-1] = reversed_chunks[-1][shard_length:]

# Otherwise, we have to preserve the long word intact. Only add
# it to the current line if there's nothing already there --
# that minimizes how much we violate the width constraint.
elif not cur_line:
cur_line.append(reversed_chunks.pop())

# If we're not allowed to break long words, and there's already
# text on the current line, do nothing. Next time through the
# main loop of _wrap_chunks(), we'll wind up here again, but
# cur_len will be zero, so the next line will be entirely
# devoted to the long word that we can't handle right now.

def _wrap_chunks(self, chunks):
"""_wrap_chunks(chunks : [string]) -> [string]

Expand Down Expand Up @@ -174,12 +214,12 @@ def _wrap_chunks(self, chunks):
del chunks[-1]

while chunks:
l = _wcswidth(chunks[-1])
length = _wcswidth(chunks[-1])

# Can at least squeeze this chunk onto the current line.
if cur_len + l <= width:
if cur_len + length <= width:
cur_line.append(chunks.pop())
cur_len += l
cur_len += length

# Nope, this line is full.
else:
Expand All @@ -197,19 +237,15 @@ def _wrap_chunks(self, chunks):
del cur_line[-1]

if cur_line:
if (self.max_lines is None or
len(lines) + 1 < self.max_lines or
(not chunks or
self.drop_whitespace and
len(chunks) == 1 and
not chunks[0].strip()) and cur_len <= width):
if (self.max_lines is None or len(lines) + 1 < self.max_lines
or (not chunks or self.drop_whitespace and len(chunks) == 1 and not chunks[0].strip())
and cur_len <= width):
# Convert current line back to a string and store it in
# list of all lines (return value).
lines.append(indent + ''.join(cur_line))
else:
while cur_line:
if (cur_line[-1].strip() and
cur_len + _wcswidth(self.placeholder) <= width):
if cur_line[-1].strip() and cur_len + _wcswidth(self.placeholder) <= width:
cur_line.append(self.placeholder)
lines.append(indent + ''.join(cur_line))
break
Expand All @@ -218,8 +254,7 @@ def _wrap_chunks(self, chunks):
else:
if lines:
prev_line = lines[-1].rstrip()
if (_wcswidth(prev_line) + _wcswidth(self.placeholder) <=
self.width):
if _wcswidth(prev_line) + _wcswidth(self.placeholder) <= self.width:
lines[-1] = prev_line + self.placeholder
break
lines.append(indent + self.placeholder.lstrip())
Expand All @@ -233,7 +268,7 @@ def _translate_tabs(text: str) -> str:
tabpos = text.find('\t')
while tabpos >= 0:
before_text = text[:tabpos]
after_text = text[tabpos+1:]
after_text = text[tabpos + 1:]
before_width = _wcswidth(before_text)
tab_pad = TAB_WIDTH - (before_width % TAB_WIDTH)
text = before_text + '{: <{width}}'.format('', width=tab_pad) + after_text
Expand All @@ -259,7 +294,7 @@ class TableColors(object):
TEXT_COLOR_GREEN = fg(119)
TEXT_COLOR_BLUE = fg(27)
BG_COLOR_ROW = bg(234)
BG_RESET = bg(0)
BG_RESET = attr('reset') # docs say bg(0) should do this but it doesn't work right
BOLD = attr('bold')
RESET = attr('reset')
except ImportError:
Expand Down Expand Up @@ -298,7 +333,7 @@ def set_color_library(cls, library_name: str) -> None:
cls.TEXT_COLOR_GREEN = fg(119)
cls.TEXT_COLOR_BLUE = fg(27)
cls.BG_COLOR_ROW = bg(234)
cls.BG_RESET = bg(0)
cls.BG_RESET = attr('reset') # docs say bg(0) should do this but it doesn't work right
cls.BOLD = attr('bold')
cls.RESET = attr('reset')
elif library_name == 'colorama':
Expand Down Expand Up @@ -351,25 +386,26 @@ def _pad_columns(text: str, pad_char: str, align: Union[ColumnAlignment, str], w
"""Returns a string padded out to the specified width"""
text = _translate_tabs(text)
display_width = _printable_width(text)
diff = width - display_width
if display_width >= width:
return text

if align in (ColumnAlignment.AlignLeft, ColumnAlignment.AlignLeft.format_string()):
out_text = text
out_text += '{:{pad}<{width}}'.format('', pad=pad_char, width=width-display_width)
out_text += '{:{pad}<{width}}'.format('', pad=pad_char, width=diff)
elif align in (ColumnAlignment.AlignRight, ColumnAlignment.AlignRight.format_string()):
out_text = '{:{pad}<{width}}'.format('', pad=pad_char, width=width-display_width)
out_text = '{:{pad}<{width}}'.format('', pad=pad_char, width=diff)
out_text += text
elif align in (ColumnAlignment.AlignCenter, ColumnAlignment.AlignCenter.format_string()):
lead_pad = int((width - display_width) / 2)
tail_pad = width - display_width - lead_pad
lead_pad = diff // 2
tail_pad = diff - lead_pad

out_text = '{:{pad}<{width}}'.format('', pad=pad_char, width=lead_pad)
out_text += text
out_text += '{:{pad}<{width}}'.format('', pad=pad_char, width=tail_pad)
else:
out_text = text
out_text += '{:{pad}<{width}}'.format('', pad=pad_char, width=width-display_width)
out_text += '{:{pad}<{width}}'.format('', pad=pad_char, width=diff)

return out_text

Expand Down Expand Up @@ -565,7 +601,7 @@ def border_right_span(self, row_index: Union[int, None]) -> str:
bg_reset = self.bg_reset if self.bg_reset is not None else TableColors.BG_RESET
return bg_reset + '║'

def col_divider_span(self, row_index : Union[int, None]) -> str:
def col_divider_span(self, row_index: Union[int, None]) -> str:
bg_reset = self.bg_reset if self.bg_reset is not None else TableColors.BG_RESET
bg_primary = self.bg_primary if self.bg_primary is not None else TableColors.BG_RESET
bg_alt = self.bg_alt if self.bg_alt is not None else TableColors.BG_COLOR_ROW
Expand Down Expand Up @@ -948,24 +984,30 @@ def generate_table(self, entries: Iterable[Union[Iterable, object]], force_trans
entry_opts = dict()
if use_attribs:
# if use_attribs is set, the entries can optionally be a tuple with (object, options)
try:
iter(entry)
except TypeError:
# not iterable, so we just use the object directly
if isinstance(entry, dict):
entry_obj = entry
if self._row_tagger is not None:
entry_opts = self._row_tagger(entry_obj)
else:
entry_obj = entry[0]
if self._row_tagger is not None:
entry_opts = self._row_tagger(entry_obj)
if len(entry) == 2 and isinstance(entry[1], dict):
entry_opts.update(entry[1])
try:
iter(entry)
except TypeError:
# not iterable, so we just use the object directly
entry_obj = entry
if self._row_tagger is not None:
entry_opts = self._row_tagger(entry_obj)
else:
entry_obj = entry[0]
if self._row_tagger is not None:
entry_opts = self._row_tagger(entry_obj)
if len(entry) == 2 and isinstance(entry[1], dict):
entry_opts.update(entry[1])

for column_index, attrib_name in enumerate(self._column_attribs):
field_obj = None
if isinstance(attrib_name, str) and hasattr(entry_obj, attrib_name):
field_obj = getattr(entry_obj, attrib_name, '')
if isinstance(attrib_name, str):
if hasattr(entry_obj, attrib_name):
field_obj = getattr(entry_obj, attrib_name, '')
elif isinstance(entry_obj, dict) and attrib_name in entry_obj:
field_obj = entry_obj[attrib_name]
# if the object attribute is callable, go ahead and call it and get the result
if callable(field_obj):
field_obj = field_obj()
Expand Down
7 changes: 7 additions & 0 deletions tasks.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
#
# coding=utf-8
# flake8: noqa E302
"""Development related tasks to be run with 'invoke'.

Make sure you satisfy the following Python module requirements if you are trying to publish a release to PyPI:
Expand Down Expand Up @@ -164,3 +165,9 @@ def pypi_test(context):
context.run('twine upload --repository-url https://test.pypi.org/legacy/ dist/*')
namespace.add_task(pypi_test)

# Flake8 - linter and tool for style guide enforcement and linting
@invoke.task
def flake8(context):
"Run flake8 linter and tool for style guide enforcement"
context.run("flake8 --ignore=E252,W503 --max-complexity=26 --max-line-length=127 --show-source --statistics --exclude=.git,__pycache__,.tox,.eggs,*.egg,.venv,.idea,.pytest_cache,.vscode,build,dist,htmlcov")
namespace.add_task(flake8)