Skip to content

Commit e08937c

Browse files
authored
Merge pull request #34 from python-tableformatter/dict_rows
Dict rows
2 parents 4a047d2 + e3e4104 commit e08937c

File tree

6 files changed

+164
-36
lines changed

6 files changed

+164
-36
lines changed

.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,3 +21,7 @@ htmlcov
2121
# mypy optional static type checker
2222
.mypy_cache
2323
*~
24+
25+
# Pipenv (this is a library, not an application)
26+
.venv
27+
Pipfile.lock

Pipfile

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
[[source]]
2+
name = "pypi"
3+
url = "https://pypi.org/simple"
4+
verify_ssl = true
5+
6+
[packages]
7+
wcwidth = "*"
8+
9+
[dev-packages]
10+
tableformatter = {editable = true,path = "."}
11+
flake8 = "*"
12+
invoke = "*"
13+
pytest = "*"
14+
pytest-cov = "*"

examples/color.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,5 +29,5 @@
2929

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

32-
print("Table with colorful alternting rows")
32+
print("Table with colorful alternating rows")
3333
print(generate_table(rows, columns, grid_style=tf.AlternatingRowGrid(BACK_GREEN, BACK_BLUE)))

examples/simple_dict.py

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
#!/usr/bin/env python
2+
# coding=utf-8
3+
"""
4+
Simple demonstration of TableFormatter with a list of dicts for the table entries.
5+
This approach requires providing the dictionary key to query
6+
for each cell (via attrib='attrib_name').
7+
"""
8+
from tableformatter import generate_table, FancyGrid, SparseGrid, Column
9+
10+
11+
class MyRowObject(object):
12+
"""Simple object to demonstrate using a list of objects with TableFormatter"""
13+
def __init__(self, field1: str, field2: str, field3: str, field4: str):
14+
self.field1 = field1
15+
self.field2 = field2
16+
self._field3 = field3
17+
self.field4 = field4
18+
19+
def get_field3(self):
20+
"""Demonstrates accessing object functions"""
21+
return self._field3
22+
23+
KEY1 = "Key1"
24+
KEY2 = "Key2"
25+
KEY3 = "Key3"
26+
KEY4 = "Key4"
27+
28+
rows = [{KEY1:'A1', KEY2:'A2', KEY3:'A3', KEY4:'A4'},
29+
{KEY1:'B1', KEY2:'B2\nB2\nB2', KEY3:'B3', KEY4:'B4'},
30+
{KEY1:'C1', KEY2:'C2', KEY3:'C3', KEY4:'C4'},
31+
{KEY1:'D1', KEY2:'D2', KEY3:'D3', KEY4:'D4'}]
32+
33+
34+
columns = (Column('Col1', attrib=KEY1),
35+
Column('Col2', attrib=KEY2),
36+
Column('Col3', attrib=KEY3),
37+
Column('Col4', attrib=KEY4))
38+
39+
print("Table with header, AlteratingRowGrid:")
40+
print(generate_table(rows, columns))
41+
42+
43+
print("Table with header, transposed, AlteratingRowGrid:")
44+
print(generate_table(rows, columns, transpose=True))
45+
46+
47+
print("Table with header, transposed, FancyGrid:")
48+
print(generate_table(rows, columns, grid_style=FancyGrid(), transpose=True))
49+
50+
print("Table with header, transposed, SparseGrid:")
51+
print(generate_table(rows, columns, grid_style=SparseGrid(), transpose=True))
52+
53+
54+
columns2 = (Column('Col1', attrib=KEY3),
55+
Column('Col2', attrib=KEY2),
56+
Column('Col3', attrib=KEY1),
57+
Column('Col4', attrib=KEY4))
58+
59+
60+
print("Table with header, Columns rearranged")
61+
print(generate_table(rows, columns2))

tableformatter.py

Lines changed: 77 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
from typing import Collection
1818
except ImportError:
1919
from typing import Container, Generic, Sized, TypeVar
20+
2021
# Python 3.5
2122
# noinspection PyAbstractClass
2223
class Collection(Generic[TypeVar('T_co', covariant=True)], Container, Sized, Iterable):
@@ -124,6 +125,45 @@ def _split(self, text):
124125
chunks = [c for c in chunks if c]
125126
return chunks
126127

128+
def _handle_long_word(self, reversed_chunks, cur_line, cur_len, width):
129+
"""_handle_long_word(chunks : [string],
130+
cur_line : [string],
131+
cur_len : int, width : int)
132+
133+
Handle a chunk of text (most likely a word, not whitespace) that
134+
is too long to fit in any line.
135+
"""
136+
# Figure out when indent is larger than the specified width, and make
137+
# sure at least one character is stripped off on every pass
138+
if width < 1:
139+
space_left = 1
140+
else:
141+
space_left = width - cur_len
142+
143+
# If we're allowed to break long words, then do so: put as much
144+
# of the next chunk onto the current line as will fit.
145+
if self.break_long_words:
146+
shard_length = space_left
147+
shard = reversed_chunks[-1][:shard_length]
148+
while _wcswidth(shard) > space_left and shard_length > 0:
149+
shard_length -= 1
150+
shard = reversed_chunks[-1][:shard_length]
151+
if shard_length > 0:
152+
cur_line.append(shard)
153+
reversed_chunks[-1] = reversed_chunks[-1][shard_length:]
154+
155+
# Otherwise, we have to preserve the long word intact. Only add
156+
# it to the current line if there's nothing already there --
157+
# that minimizes how much we violate the width constraint.
158+
elif not cur_line:
159+
cur_line.append(reversed_chunks.pop())
160+
161+
# If we're not allowed to break long words, and there's already
162+
# text on the current line, do nothing. Next time through the
163+
# main loop of _wrap_chunks(), we'll wind up here again, but
164+
# cur_len will be zero, so the next line will be entirely
165+
# devoted to the long word that we can't handle right now.
166+
127167
def _wrap_chunks(self, chunks):
128168
"""_wrap_chunks(chunks : [string]) -> [string]
129169
@@ -174,12 +214,12 @@ def _wrap_chunks(self, chunks):
174214
del chunks[-1]
175215

176216
while chunks:
177-
l = _wcswidth(chunks[-1])
217+
length = _wcswidth(chunks[-1])
178218

179219
# Can at least squeeze this chunk onto the current line.
180-
if cur_len + l <= width:
220+
if cur_len + length <= width:
181221
cur_line.append(chunks.pop())
182-
cur_len += l
222+
cur_len += length
183223

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

199239
if cur_line:
200-
if (self.max_lines is None or
201-
len(lines) + 1 < self.max_lines or
202-
(not chunks or
203-
self.drop_whitespace and
204-
len(chunks) == 1 and
205-
not chunks[0].strip()) and cur_len <= width):
240+
if (self.max_lines is None or len(lines) + 1 < self.max_lines
241+
or (not chunks or self.drop_whitespace and len(chunks) == 1 and not chunks[0].strip())
242+
and cur_len <= width):
206243
# Convert current line back to a string and store it in
207244
# list of all lines (return value).
208245
lines.append(indent + ''.join(cur_line))
209246
else:
210247
while cur_line:
211-
if (cur_line[-1].strip() and
212-
cur_len + _wcswidth(self.placeholder) <= width):
248+
if cur_line[-1].strip() and cur_len + _wcswidth(self.placeholder) <= width:
213249
cur_line.append(self.placeholder)
214250
lines.append(indent + ''.join(cur_line))
215251
break
@@ -218,8 +254,7 @@ def _wrap_chunks(self, chunks):
218254
else:
219255
if lines:
220256
prev_line = lines[-1].rstrip()
221-
if (_wcswidth(prev_line) + _wcswidth(self.placeholder) <=
222-
self.width):
257+
if _wcswidth(prev_line) + _wcswidth(self.placeholder) <= self.width:
223258
lines[-1] = prev_line + self.placeholder
224259
break
225260
lines.append(indent + self.placeholder.lstrip())
@@ -233,7 +268,7 @@ def _translate_tabs(text: str) -> str:
233268
tabpos = text.find('\t')
234269
while tabpos >= 0:
235270
before_text = text[:tabpos]
236-
after_text = text[tabpos+1:]
271+
after_text = text[tabpos + 1:]
237272
before_width = _wcswidth(before_text)
238273
tab_pad = TAB_WIDTH - (before_width % TAB_WIDTH)
239274
text = before_text + '{: <{width}}'.format('', width=tab_pad) + after_text
@@ -259,7 +294,7 @@ class TableColors(object):
259294
TEXT_COLOR_GREEN = fg(119)
260295
TEXT_COLOR_BLUE = fg(27)
261296
BG_COLOR_ROW = bg(234)
262-
BG_RESET = bg(0)
297+
BG_RESET = attr('reset') # docs say bg(0) should do this but it doesn't work right
263298
BOLD = attr('bold')
264299
RESET = attr('reset')
265300
except ImportError:
@@ -298,7 +333,7 @@ def set_color_library(cls, library_name: str) -> None:
298333
cls.TEXT_COLOR_GREEN = fg(119)
299334
cls.TEXT_COLOR_BLUE = fg(27)
300335
cls.BG_COLOR_ROW = bg(234)
301-
cls.BG_RESET = bg(0)
336+
cls.BG_RESET = attr('reset') # docs say bg(0) should do this but it doesn't work right
302337
cls.BOLD = attr('bold')
303338
cls.RESET = attr('reset')
304339
elif library_name == 'colorama':
@@ -351,25 +386,26 @@ def _pad_columns(text: str, pad_char: str, align: Union[ColumnAlignment, str], w
351386
"""Returns a string padded out to the specified width"""
352387
text = _translate_tabs(text)
353388
display_width = _printable_width(text)
389+
diff = width - display_width
354390
if display_width >= width:
355391
return text
356392

357393
if align in (ColumnAlignment.AlignLeft, ColumnAlignment.AlignLeft.format_string()):
358394
out_text = text
359-
out_text += '{:{pad}<{width}}'.format('', pad=pad_char, width=width-display_width)
395+
out_text += '{:{pad}<{width}}'.format('', pad=pad_char, width=diff)
360396
elif align in (ColumnAlignment.AlignRight, ColumnAlignment.AlignRight.format_string()):
361-
out_text = '{:{pad}<{width}}'.format('', pad=pad_char, width=width-display_width)
397+
out_text = '{:{pad}<{width}}'.format('', pad=pad_char, width=diff)
362398
out_text += text
363399
elif align in (ColumnAlignment.AlignCenter, ColumnAlignment.AlignCenter.format_string()):
364-
lead_pad = int((width - display_width) / 2)
365-
tail_pad = width - display_width - lead_pad
400+
lead_pad = diff // 2
401+
tail_pad = diff - lead_pad
366402

367403
out_text = '{:{pad}<{width}}'.format('', pad=pad_char, width=lead_pad)
368404
out_text += text
369405
out_text += '{:{pad}<{width}}'.format('', pad=pad_char, width=tail_pad)
370406
else:
371407
out_text = text
372-
out_text += '{:{pad}<{width}}'.format('', pad=pad_char, width=width-display_width)
408+
out_text += '{:{pad}<{width}}'.format('', pad=pad_char, width=diff)
373409

374410
return out_text
375411

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

568-
def col_divider_span(self, row_index : Union[int, None]) -> str:
604+
def col_divider_span(self, row_index: Union[int, None]) -> str:
569605
bg_reset = self.bg_reset if self.bg_reset is not None else TableColors.BG_RESET
570606
bg_primary = self.bg_primary if self.bg_primary is not None else TableColors.BG_RESET
571607
bg_alt = self.bg_alt if self.bg_alt is not None else TableColors.BG_COLOR_ROW
@@ -948,24 +984,30 @@ def generate_table(self, entries: Iterable[Union[Iterable, object]], force_trans
948984
entry_opts = dict()
949985
if use_attribs:
950986
# if use_attribs is set, the entries can optionally be a tuple with (object, options)
951-
try:
952-
iter(entry)
953-
except TypeError:
954-
# not iterable, so we just use the object directly
987+
if isinstance(entry, dict):
955988
entry_obj = entry
956-
if self._row_tagger is not None:
957-
entry_opts = self._row_tagger(entry_obj)
958989
else:
959-
entry_obj = entry[0]
960-
if self._row_tagger is not None:
961-
entry_opts = self._row_tagger(entry_obj)
962-
if len(entry) == 2 and isinstance(entry[1], dict):
963-
entry_opts.update(entry[1])
990+
try:
991+
iter(entry)
992+
except TypeError:
993+
# not iterable, so we just use the object directly
994+
entry_obj = entry
995+
if self._row_tagger is not None:
996+
entry_opts = self._row_tagger(entry_obj)
997+
else:
998+
entry_obj = entry[0]
999+
if self._row_tagger is not None:
1000+
entry_opts = self._row_tagger(entry_obj)
1001+
if len(entry) == 2 and isinstance(entry[1], dict):
1002+
entry_opts.update(entry[1])
9641003

9651004
for column_index, attrib_name in enumerate(self._column_attribs):
9661005
field_obj = None
967-
if isinstance(attrib_name, str) and hasattr(entry_obj, attrib_name):
968-
field_obj = getattr(entry_obj, attrib_name, '')
1006+
if isinstance(attrib_name, str):
1007+
if hasattr(entry_obj, attrib_name):
1008+
field_obj = getattr(entry_obj, attrib_name, '')
1009+
elif isinstance(entry_obj, dict) and attrib_name in entry_obj:
1010+
field_obj = entry_obj[attrib_name]
9691011
# if the object attribute is callable, go ahead and call it and get the result
9701012
if callable(field_obj):
9711013
field_obj = field_obj()

tasks.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
#
22
# coding=utf-8
3+
# flake8: noqa E302
34
"""Development related tasks to be run with 'invoke'.
45
56
Make sure you satisfy the following Python module requirements if you are trying to publish a release to PyPI:
@@ -164,3 +165,9 @@ def pypi_test(context):
164165
context.run('twine upload --repository-url https://test.pypi.org/legacy/ dist/*')
165166
namespace.add_task(pypi_test)
166167

168+
# Flake8 - linter and tool for style guide enforcement and linting
169+
@invoke.task
170+
def flake8(context):
171+
"Run flake8 linter and tool for style guide enforcement"
172+
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")
173+
namespace.add_task(flake8)

0 commit comments

Comments
 (0)