Skip to content

Commit 001b85d

Browse files
committed
Added examples of using field and object formatters for object and tuple based table entries.
Added unit tests of rows options and formatters
1 parent 8d7b357 commit 001b85d

4 files changed

Lines changed: 441 additions & 15 deletions

File tree

examples/formatters.py

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
#!/usr/bin/env python
2+
# coding=utf-8
3+
"""
4+
Demonstration of field and object formatters for both object and tuple based table entries
5+
"""
6+
import tableformatter as tf
7+
8+
9+
class MyRowObject(object):
10+
"""Simple object to demonstrate using a list of objects with TableFormatter"""
11+
def __init__(self, field1: int, field2: int, field3: int, field4: int):
12+
self.field1 = field1
13+
self.field2 = field2
14+
self._field3 = field3
15+
self.field4 = field4
16+
17+
def get_field3(self):
18+
"""Demonstrates accessing object functions"""
19+
return self._field3
20+
21+
22+
def multiply(row_obj: MyRowObject):
23+
"""Demonstrates an object formatter function"""
24+
return str(row_obj.get_field3() * row_obj.field4)
25+
26+
27+
def multiply_tuple(row_obj):
28+
"""Demonstrates an object formatter function"""
29+
return str(row_obj[2] * row_obj[3])
30+
31+
32+
def int2word(num, separator="-"):
33+
"""Demonstrates a field formatter function
34+
From: https://codereview.stackexchange.com/questions/156590/create-the-english-word-for-a-number
35+
"""
36+
ones_and_teens = {0: "Zero", 1: 'One', 2: 'Two', 3: 'Three',
37+
4: 'Four', 5: 'Five', 6: 'Six', 7: 'Seven',
38+
8: 'Eight', 9: 'Nine', 10: 'Ten', 11: 'Eleven',
39+
12: 'Twelve', 13: 'Thirteen', 14: 'Fourteen',
40+
15: 'Fifteen', 16: 'Sixteen', 17: 'Seventeen',
41+
18: 'Eighteen', 19: 'Nineteen'}
42+
twenty2ninety = {2: 'Twenty', 3: 'Thirty', 4: 'Forty', 5: 'Fifty',
43+
6: 'Sixty', 7: 'Seventy', 8: 'Eighty', 9: 'Ninety', 0: ""}
44+
45+
if 0 <= num < 19:
46+
return ones_and_teens[num]
47+
elif 20 <= num <= 99:
48+
tens, below_ten = divmod(num, 10)
49+
if below_ten > 0:
50+
words = twenty2ninety[tens] + separator + \
51+
ones_and_teens[below_ten].lower()
52+
else:
53+
words = twenty2ninety[tens]
54+
return words
55+
56+
elif 100 <= num <= 999:
57+
hundreds, below_hundred = divmod(num, 100)
58+
tens, below_ten = divmod(below_hundred, 10)
59+
if below_hundred == 0:
60+
words = ones_and_teens[hundreds] + separator + "hundred"
61+
elif below_ten == 0:
62+
words = ones_and_teens[hundreds] + separator + \
63+
"hundred" + separator + twenty2ninety[tens].lower()
64+
else:
65+
if tens > 0:
66+
words = ones_and_teens[hundreds] + separator + "hundred" + separator + twenty2ninety[
67+
tens].lower() + separator + ones_and_teens[below_ten].lower()
68+
else:
69+
words = ones_and_teens[
70+
hundreds] + separator + "hundred" + separator + ones_and_teens[below_ten].lower()
71+
return words
72+
73+
else:
74+
print("num out of range")
75+
76+
77+
rows = [MyRowObject(None, None, 17, 4),
78+
MyRowObject('123', '123', 5, 56),
79+
MyRowObject(123, 123, 5, 56),
80+
MyRowObject(12345, 12345, 23, 8),
81+
MyRowObject(12345678, 12345678, 4, 9),
82+
MyRowObject(1234567890, 1234567890, 7, 5),
83+
MyRowObject(1234567890123, 1234567890123, 7, 5)]
84+
85+
columns = (tf.Column('First', width=20, attrib='field1', formatter=tf.FormatBytes(),
86+
cell_halign=tf.ColumnAlignment.AlignRight),
87+
tf.Column('Second', attrib='field2', formatter=tf.FormatCommas(),
88+
cell_halign=tf.ColumnAlignment.AlignRight),
89+
tf.Column('Num 1', width=3, attrib='get_field3'),
90+
tf.Column('Num 2', attrib='field4'),
91+
tf.Column('Multiplied', obj_formatter=multiply))
92+
93+
print("Formatters on object-based row entries")
94+
print(tf.generate_table(rows, columns))
95+
96+
rows = [(None, None, 17, 4, None),
97+
('123', '123', 5, 56, None),
98+
(123, 123, 5, 56, None),
99+
(12345, 12345, 23, 8, None),
100+
(12345678, 12345678, 4, 9, None),
101+
(1234567890, 1234567890, 7, 5, None),
102+
(1234567890123, 1234567890123, 7, 5, None)]
103+
104+
columns = (tf.Column('First', width=20, formatter=tf.FormatBytes(), cell_halign=tf.ColumnAlignment.AlignRight),
105+
tf.Column('Second', formatter=tf.FormatCommas(), cell_halign=tf.ColumnAlignment.AlignRight),
106+
tf.Column('Num 1'),
107+
tf.Column('Num 2', formatter=int2word),
108+
tf.Column('Multiplied', obj_formatter=multiply_tuple))
109+
110+
print("Formatters on tuple-based row entries")
111+
print(tf.generate_table(rows, columns))

tableformatter.py

Lines changed: 28 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -530,7 +530,7 @@ class AlternatingRowGrid(FancyGrid):
530530
531531
This typically looks quite good, but also does a good job of conserving vertical space.
532532
"""
533-
def __init__(self, bg_primary: str=TableColors.BG_RESET, bg_alternate: str=TableColors.BG_COLOR_ROW) -> None:
533+
def __init__(self, bg_primary: str=None, bg_alternate: str=None, bg_reset=None) -> None:
534534
"""Initialize the AlternatingRowGrid with the two alternating colors.
535535
536536
:param bg_primary: string reprsenting the primary background color starting with the 1st row
@@ -542,39 +542,52 @@ def __init__(self, bg_primary: str=TableColors.BG_RESET, bg_alternate: str=Table
542542
self.row_divider_span = ''
543543
self.row_divider_col_divider = ''
544544
self.row_divider_header_col_divider = ''
545-
self.bg_reset = TableColors.BG_RESET
546545
self.bg_primary = bg_primary
547546
self.bg_alt = bg_alternate
547+
self.bg_reset = bg_reset
548548

549549
def border_left_span(self, row_index: Union[int, None]) -> str:
550-
prefix = self.bg_reset + '║'
551-
color = self.bg_reset
550+
bg_reset = self.bg_reset if self.bg_reset is not None else TableColors.BG_RESET
551+
bg_primary = self.bg_primary if self.bg_primary is not None else TableColors.BG_RESET
552+
bg_alt = self.bg_alt if self.bg_alt is not None else TableColors.BG_COLOR_ROW
553+
554+
prefix = bg_reset + '║'
555+
color = bg_reset
552556
if isinstance(row_index, int):
553557
if row_index % 2 == 0:
554-
color = self.bg_primary
558+
color = bg_primary
555559
else:
556-
color = self.bg_alt
560+
color = bg_alt
557561
return prefix + color
558562

559563
def border_right_span(self, row_index: Union[int, None]) -> str:
560-
return self.bg_reset + '║'
564+
bg_reset = self.bg_reset if self.bg_reset is not None else TableColors.BG_RESET
565+
return bg_reset + '║'
561566

562567
def col_divider_span(self, row_index : Union[int, None]) -> str:
563-
color = self.bg_reset
568+
bg_reset = self.bg_reset if self.bg_reset is not None else TableColors.BG_RESET
569+
bg_primary = self.bg_primary if self.bg_primary is not None else TableColors.BG_RESET
570+
bg_alt = self.bg_alt if self.bg_alt is not None else TableColors.BG_COLOR_ROW
571+
572+
color = bg_reset
564573
if isinstance(row_index, int):
565574
if row_index % 2 == 0:
566-
color = self.bg_primary
575+
color = bg_primary
567576
else:
568-
color = self.bg_alt
577+
color = bg_alt
569578
return color + '│'
570579

571580
def header_col_divider_span(self, row_index: Union[int, None]) -> str:
572-
color = self.bg_reset
581+
bg_reset = self.bg_reset if self.bg_reset is not None else TableColors.BG_RESET
582+
bg_primary = self.bg_primary if self.bg_primary is not None else TableColors.BG_RESET
583+
bg_alt = self.bg_alt if self.bg_alt is not None else TableColors.BG_COLOR_ROW
584+
585+
color = bg_reset
573586
if isinstance(row_index, int):
574587
if row_index % 2 == 0:
575-
color = self.bg_primary
588+
color = bg_primary
576589
else:
577-
color = self.bg_alt
590+
color = bg_alt
578591
return color + '║'
579592

580593

@@ -839,7 +852,7 @@ def set_cell_alignment(self,
839852
self._set_column_option(column, TableFormatter.COL_OPT_CELL_HALIGN, horiz_align)
840853
self._set_column_option(column, TableFormatter.COL_OPT_CELL_VALIGN, vert_align)
841854

842-
def generate_table(self, entries: List[Iterable], force_transpose=False):
855+
def generate_table(self, entries: Iterable[Union[Iterable, object]], force_transpose=False):
843856
"""
844857
Generate the table from a list of entries
845858
@@ -1340,7 +1353,7 @@ def __call__(self, byte_size: int):
13401353
elif byte_size > FormatBytes.KB:
13411354
out = decimal_format.format(byte_size / FormatBytes.KB) + " KB"
13421355
else:
1343-
out = decimal_format.format(byte_size) + " B"
1356+
out = decimal_format.format(byte_size) + " B"
13441357

13451358
return out
13461359

tests/test_formatters.py

Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
# coding=utf-8
2+
"""
3+
Unit testing of tableformatter with simple cases
4+
- with a list of tuples as table entries
5+
- using a list of objects for the table entries
6+
"""
7+
import pytest
8+
9+
import tableformatter as tf
10+
11+
# Make the test results reproducible regardless of what color libraries are installed
12+
tf.TableColors.set_color_library('none')
13+
tf.set_default_grid(tf.AlternatingRowGrid('', '', ''))
14+
15+
16+
class MyRowObject(object):
17+
"""Simple object to demonstrate using a list of objects with TableFormatter"""
18+
def __init__(self, field1: int, field2: int, field3: int, field4: int):
19+
self.field1 = field1
20+
self.field2 = field2
21+
self._field3 = field3
22+
self.field4 = field4
23+
24+
def get_field3(self):
25+
"""Demonstrates accessing object functions"""
26+
return self._field3
27+
28+
29+
def multiply(row_obj: MyRowObject):
30+
"""Demonstrates an object formatter function"""
31+
return str(row_obj.get_field3() * row_obj.field4)
32+
33+
34+
def multiply_tuple(row_obj):
35+
"""Demonstrates an object formatter function"""
36+
return str(row_obj[2] * row_obj[3])
37+
38+
39+
def int2word(num, separator="-"):
40+
"""Demonstrates a field formatter function
41+
From: https://codereview.stackexchange.com/questions/156590/create-the-english-word-for-a-number
42+
"""
43+
ones_and_teens = {0: "Zero", 1: 'One', 2: 'Two', 3: 'Three',
44+
4: 'Four', 5: 'Five', 6: 'Six', 7: 'Seven',
45+
8: 'Eight', 9: 'Nine', 10: 'Ten', 11: 'Eleven',
46+
12: 'Twelve', 13: 'Thirteen', 14: 'Fourteen',
47+
15: 'Fifteen', 16: 'Sixteen', 17: 'Seventeen',
48+
18: 'Eighteen', 19: 'Nineteen'}
49+
twenty2ninety = {2: 'Twenty', 3: 'Thirty', 4: 'Forty', 5: 'Fifty',
50+
6: 'Sixty', 7: 'Seventy', 8: 'Eighty', 9: 'Ninety', 0: ""}
51+
52+
if 0 <= num < 19:
53+
return ones_and_teens[num]
54+
elif 20 <= num <= 99:
55+
tens, below_ten = divmod(num, 10)
56+
if below_ten > 0:
57+
words = twenty2ninety[tens] + separator + \
58+
ones_and_teens[below_ten].lower()
59+
else:
60+
words = twenty2ninety[tens]
61+
return words
62+
63+
elif 100 <= num <= 999:
64+
hundreds, below_hundred = divmod(num, 100)
65+
tens, below_ten = divmod(below_hundred, 10)
66+
if below_hundred == 0:
67+
words = ones_and_teens[hundreds] + separator + "hundred"
68+
elif below_ten == 0:
69+
words = ones_and_teens[hundreds] + separator + \
70+
"hundred" + separator + twenty2ninety[tens].lower()
71+
else:
72+
if tens > 0:
73+
words = ones_and_teens[hundreds] + separator + "hundred" + separator + twenty2ninety[
74+
tens].lower() + separator + ones_and_teens[below_ten].lower()
75+
else:
76+
words = ones_and_teens[
77+
hundreds] + separator + "hundred" + separator + ones_and_teens[below_ten].lower()
78+
return words
79+
80+
else:
81+
print("num out of range")
82+
83+
# These tests insert an R and G prefix at the beginning of cells that would
84+
# otherwise have a row color defined. This allows us to test the insertion
85+
# of color escape sequences with no color library installed.
86+
87+
88+
def test_fmt_obj_rows():
89+
expected = '''
90+
╔═══════════╤═══════════════════╤═════╤═══════╤════════════╗
91+
║ │ │ Num │ │ ║
92+
║ First │ Second │ 1 │ Num 2 │ Multiplied ║
93+
╠═══════════╪═══════════════════╪═════╪═══════╪════════════╣
94+
║ │ │ 17 │ 4 │ 68 ║
95+
║ 123.00 B │ 123 │ 5 │ 56 │ 280 ║
96+
║ 123.00 B │ 123 │ 5 │ 56 │ 280 ║
97+
║ 12.06 KB │ 12,345 │ 23 │ 8 │ 184 ║
98+
║ 11.77 MB │ 12,345,678 │ 4 │ 9 │ 36 ║
99+
║ 1.15 GB │ 1,234,567,890 │ 7 │ 5 │ 35 ║
100+
║ 1.12 TB │ 1,234,567,890,123 │ 7 │ 5 │ 35 ║
101+
╚═══════════╧═══════════════════╧═════╧═══════╧════════════╝
102+
'''.lstrip('\n')
103+
rows = [MyRowObject(None, None, 17, 4),
104+
MyRowObject('123', '123', 5, 56),
105+
MyRowObject(123, 123, 5, 56),
106+
MyRowObject(12345, 12345, 23, 8),
107+
MyRowObject(12345678, 12345678, 4, 9),
108+
MyRowObject(1234567890, 1234567890, 7, 5),
109+
MyRowObject(1234567890123, 1234567890123, 7, 5)]
110+
111+
columns = (tf.Column('First', width=20, attrib='field1', formatter=tf.FormatBytes(),
112+
cell_halign=tf.ColumnAlignment.AlignRight),
113+
tf.Column('Second', attrib='field2', formatter=tf.FormatCommas(),
114+
cell_halign=tf.ColumnAlignment.AlignRight),
115+
tf.Column('Num 1', width=3, attrib='get_field3'),
116+
tf.Column('Num 2', attrib='field4'),
117+
tf.Column('Multiplied', obj_formatter=multiply))
118+
table = tf.generate_table(rows, columns)
119+
assert table == expected
120+
121+
122+
def test_fmt_tuple_rows():
123+
expected = '''
124+
╔═══════════╤═══════════════════╤═══════╤═══════════╤════════════╗
125+
║ First │ Second │ Num 1 │ Num 2 │ Multiplied ║
126+
╠═══════════╪═══════════════════╪═══════╪═══════════╪════════════╣
127+
║ │ │ 17 │ Four │ 68 ║
128+
║ 123.00 B │ 123 │ 5 │ Fifty-six │ 280 ║
129+
║ 123.00 B │ 123 │ 5 │ Fifty-six │ 280 ║
130+
║ 12.06 KB │ 12,345 │ 23 │ Eight │ 184 ║
131+
║ 11.77 MB │ 12,345,678 │ 4 │ Nine │ 36 ║
132+
║ 1.15 GB │ 1,234,567,890 │ 7 │ Five │ 35 ║
133+
║ 1.12 TB │ 1,234,567,890,123 │ 7 │ Five │ 35 ║
134+
╚═══════════╧═══════════════════╧═══════╧═══════════╧════════════╝
135+
'''.lstrip('\n')
136+
137+
rows = [(None, None, 17, 4, None),
138+
('123', '123', 5, 56, None),
139+
(123, 123, 5, 56, None),
140+
(12345, 12345, 23, 8, None),
141+
(12345678, 12345678, 4, 9, None),
142+
(1234567890, 1234567890, 7, 5, None),
143+
(1234567890123, 1234567890123, 7, 5, None)]
144+
145+
columns = (tf.Column('First', width=20, formatter=tf.FormatBytes(), cell_halign=tf.ColumnAlignment.AlignRight),
146+
tf.Column('Second', formatter=tf.FormatCommas(), cell_halign=tf.ColumnAlignment.AlignRight),
147+
tf.Column('Num 1'),
148+
tf.Column('Num 2', formatter=int2word),
149+
tf.Column('Multiplied', obj_formatter=multiply_tuple))
150+
151+
table = tf.generate_table(rows, columns)
152+
153+
assert table == expected

0 commit comments

Comments
 (0)