Skip to content

Commit 104a68d

Browse files
authored
Merge pull request #15 from python-tableformatter/examples
Examples
2 parents 82f2aab + 34762d0 commit 104a68d

File tree

4 files changed

+441
-15
lines changed

4 files changed

+441
-15
lines changed

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)