Skip to content

Commit 33a6f5e

Browse files
authored
Merge pull request #25 from python-tableformatter/data_types
Improved handling of common data types and documented supported data types
2 parents 2b2c9d8 + 41bb4da commit 33a6f5e

File tree

5 files changed

+245
-1
lines changed

5 files changed

+245
-1
lines changed

README.md

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,23 @@ print(tf.generate_table(rows))
6969

7070
*NOTE: Rendering of tables looks much better in Python than it appears in this Markdown file.*
7171

72+
See the [simple_text.py](https://github.com/python-tableformatter/tableformatter/blob/master/examples/simple_text.py) and
73+
[simple_object.py](https://github.com/python-tableformatter/tableformatter/blob/master/examples/simple_object.py) examples
74+
for more basic usage.
75+
76+
## Supported Data Types
77+
The following tabular data types are supported:
78+
* list of lists or another iterable of iterables
79+
* two-dimensional NumPy arrays
80+
* NumPy record arrays (names as columns)
81+
* pandas.DataFrame
82+
* list or another iterable of arbitrary non-iterable objects (column specifier required)
83+
* list or another iterable of dicts (dict keys iterated through as rows where each key must be a hashable iterable)
84+
* dict of iterables (keys as columns)
85+
86+
See the [data_types.py](https://github.com/python-tableformatter/tableformatter/blob/master/examples/data_types.py)
87+
example for more info.
88+
7289

7390
## Column Headers
7491
The second argument to ``generate_table`` named ``columns`` is optional and defines a list of column headers to be used.
@@ -88,7 +105,6 @@ print(tf.generate_table(rows, cols))
88105
╚══════╧══════╧══════╧══════╝
89106
```
90107

91-
92108
## Grid Style
93109
The third argument to ``generated`` table named ``grid_style`` is optional and specifies how the table lines are drawn.
94110

examples/data_types.py

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
#!/usr/bin/env python
2+
# coding=utf-8
3+
"""
4+
tableformatter supports and has been tested with the following tabular data types:
5+
* list of lists or another iterable of iterables
6+
* two-dimensional NumPy arrays
7+
* NumPy record arrays (names as columns)
8+
* pandas.DataFrame
9+
* list or another iterable of arbitrary non-iterable objects (column specifier required)
10+
* list or another iterable of dicts (dict keys iterated through as rows where each key must be a hashable iterable)
11+
* dict of iterables (keys as columns)
12+
13+
This example demonstrates tableformatter working with these data types in the simplest possible manner.
14+
"""
15+
from collections import OrderedDict
16+
import numpy as np
17+
import pandas as pd
18+
import tableformatter as tf
19+
20+
iteralbe_of_iterables = [[1, 2, 3, 4],
21+
[5, 6, 7, 8]]
22+
print("Data type: iterable of iterables")
23+
print(iteralbe_of_iterables)
24+
print(tf.generate_table(iteralbe_of_iterables))
25+
26+
np_2d_array = np.array([[1, 2, 3, 4],
27+
[5, 6, 7, 8]])
28+
print("Data type: NumPy 2D array")
29+
print(np_2d_array)
30+
print(tf.generate_table(np_2d_array))
31+
32+
np_rec_array = np.rec.array([(1, 2., 'Hello'),
33+
(2, 3., "World")],
34+
dtype=[('foo', 'i4'), ('bar', 'f4'), ('baz', 'U10')])
35+
print("Data type: Numpy record array")
36+
print(np_rec_array)
37+
print(tf.generate_table(np_rec_array))
38+
39+
d = {'col1': [1, 5], 'col2': [2, 6], 'col3': [3, 7], 'col4': [4, 8]}
40+
od = OrderedDict(sorted(d.items(), key=lambda t: t[0]))
41+
pandas_dataframe = pd.DataFrame(data=od)
42+
print("Data type: Pandas DataFrame")
43+
print(pandas_dataframe)
44+
print(tf.generate_table(pandas_dataframe))
45+
46+
d1 = {1: 'a', 2: 'b', 3: 'c', 4: 'd'}
47+
d2 = {5: 'e', 6: 'f', 7: 'g', 8: 'h'}
48+
iterable_of_dicts = [ OrderedDict(sorted(d1.items(), key=lambda t: t[0])),
49+
OrderedDict(sorted(d2.items(), key=lambda t: t[0]))]
50+
print("Data type: iterable of dicts (dict keys iterated through as column values)")
51+
print(iterable_of_dicts)
52+
print(tf.generate_table(iterable_of_dicts))
53+
54+
dict_of_iterables = od
55+
print("Data type: dict of iterables (dict keys iterated through as rows where each key must be a hashable iterable)")
56+
print(dict_of_iterables)
57+
print(tf.generate_table(dict_of_iterables))
58+
59+
60+
class MyRowObject(object):
61+
"""Simple object to demonstrate using a list of non-iterable objects with TableFormatter"""
62+
def __init__(self, field1: int, field2: int, field3: int, field4: int):
63+
self.field1 = field1
64+
self.field2 = field2
65+
self._field3 = field3
66+
self.field4 = field4
67+
68+
def get_field3(self):
69+
"""Demonstrates accessing object functions"""
70+
return self._field3
71+
72+
73+
rows = [MyRowObject(1, 2, 3, 4),
74+
MyRowObject(5, 6, 7, 8)]
75+
columns = (tf.Column('Col1', attrib='field1'),
76+
tf.Column('Col2', attrib='field2'),
77+
tf.Column('Col3', attrib='get_field3'),
78+
tf.Column('Col4', attrib='field4'))
79+
print("Data type: iterable of arbitrary non-iterable objects")
80+
print(rows)
81+
print(tf.generate_table(rows, columns))

tableformatter.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
"""
55
import abc
66
import enum
7+
import itertools
78
import re
89
import textwrap as textw
910
from typing import List, Iterable, Optional, Tuple, Union, Callable
@@ -615,6 +616,33 @@ def generate_table(rows: Iterable[Union[Iterable, object]],
615616
:param row_tagger: decorator function to apply per-row options
616617
:return: formatted string containing the table
617618
"""
619+
# If a dictionary is passed in, then treat keys as column headers and values as column values
620+
if isinstance(rows, dict):
621+
if not columns:
622+
columns = rows.keys()
623+
rows = list(itertools.zip_longest(*rows.values())) # columns have to be transposed
624+
625+
# Extract column headers if this is a NumPy record array and columns weren't specified
626+
if not columns:
627+
try:
628+
import numpy as np
629+
except ImportError:
630+
pass
631+
else:
632+
if isinstance(rows, np.recarray):
633+
columns = rows.dtype.names
634+
635+
# Deal with Pandas DataFrames not being iterable in a sane way
636+
try:
637+
import pandas as pd
638+
except ImportError:
639+
pass
640+
else:
641+
if isinstance(rows, pd.DataFrame):
642+
if not columns:
643+
columns = rows.columns
644+
rows = rows.values
645+
618646
show_headers = True
619647
use_attrib = False
620648
if isinstance(columns, Collection) and len(columns) > 0:

tests/test_data_types.py

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
# coding=utf-8
2+
"""
3+
Unit testing the variety of data types supported by tableformatter.
4+
"""
5+
from collections import OrderedDict
6+
import tableformatter as tf
7+
8+
9+
EXPECTED_BASIC = '''
10+
╔═══╤═══╤═══╤═══╗
11+
║ 1 │ 2 │ 3 │ 4 ║
12+
║ 5 │ 6 │ 7 │ 8 ║
13+
╚═══╧═══╧═══╧═══╝
14+
'''.lstrip('\n')
15+
16+
EXPECTED_WITH_HEADERS = '''
17+
╔══════╤══════╤══════╤══════╗
18+
║ col1 │ col2 │ col3 │ col4 ║
19+
╠══════╪══════╪══════╪══════╣
20+
║ 1 │ 2 │ 3 │ 4 ║
21+
║ 5 │ 6 │ 7 │ 8 ║
22+
╚══════╧══════╧══════╧══════╝
23+
'''.lstrip('\n')
24+
25+
iteralbe_of_iterables = [[1, 2, 3, 4],
26+
[5, 6, 7, 8]]
27+
d = {'col1': [1, 5], 'col2': [2, 6], 'col3': [3, 7], 'col4': [4, 8]}
28+
od = OrderedDict(sorted(d.items(), key=lambda t: t[0]))
29+
30+
31+
def test_iterable_of_iterables():
32+
table = tf.generate_table(iteralbe_of_iterables)
33+
assert table == EXPECTED_BASIC
34+
35+
36+
def test_iterable_of_dicts():
37+
d1 = {1: 'a', 2: 'b', 3: 'c', 4: 'd'}
38+
d2 = {5: 'e', 6: 'f', 7: 'g', 8: 'h'}
39+
iterable_of_dicts = [OrderedDict(sorted(d1.items(), key=lambda t: t[0])),
40+
OrderedDict(sorted(d2.items(), key=lambda t: t[0]))]
41+
table = tf.generate_table(iterable_of_dicts)
42+
assert table == EXPECTED_BASIC
43+
44+
45+
def test_dict_of_iterables():
46+
table = tf.generate_table(od)
47+
assert table == EXPECTED_WITH_HEADERS
48+
49+
50+
class MyRowObject(object):
51+
"""Simple object to demonstrate using a list of non-iterable objects with TableFormatter"""
52+
def __init__(self, field1: int, field2: int, field3: int, field4: int):
53+
self.field1 = field1
54+
self.field2 = field2
55+
self._field3 = field3
56+
self.field4 = field4
57+
58+
def get_field3(self):
59+
"""Demonstrates accessing object functions"""
60+
return self._field3
61+
62+
63+
def test_iterable_of_non_iterable_objects():
64+
rows = [MyRowObject(1, 2, 3, 4),
65+
MyRowObject(5, 6, 7, 8)]
66+
columns = (tf.Column('col1', attrib='field1'),
67+
tf.Column('col2', attrib='field2'),
68+
tf.Column('col3', attrib='get_field3'),
69+
tf.Column('col4', attrib='field4'))
70+
table = tf.generate_table(rows, columns)
71+
assert table == EXPECTED_WITH_HEADERS
72+
73+
74+
try:
75+
import numpy as np
76+
except ImportError:
77+
pass
78+
else:
79+
np_2d_array = np.array(iteralbe_of_iterables)
80+
81+
def test_numpy_2d_array():
82+
table = tf.generate_table(np_2d_array)
83+
assert table == EXPECTED_BASIC
84+
85+
def test_numpy_record_array():
86+
np_rec_array = np.rec.array([(1, 2., 'Hello'),
87+
(2, 3., "World")],
88+
dtype=[('foo', 'i4'), ('bar', 'f4'), ('baz', 'U10')])
89+
table = tf.generate_table(np_rec_array)
90+
expected = '''
91+
╔═════╤═════╤═══════╗
92+
║ foo │ bar │ baz ║
93+
╠═════╪═════╪═══════╣
94+
║ 1 │ 2.0 │ Hello ║
95+
║ 2 │ 3.0 │ World ║
96+
╚═════╧═════╧═══════╝
97+
'''.lstrip('\n')
98+
assert table == expected
99+
100+
try:
101+
import pandas as pd
102+
except ImportError:
103+
pass
104+
else:
105+
df = pd.DataFrame(data=od)
106+
107+
def test_pandas_dataframe():
108+
table = tf.generate_table(df)
109+
assert table == EXPECTED_WITH_HEADERS

tox.ini

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ commands =
2121
[testenv:py35]
2222
deps =
2323
codecov
24+
numpy
25+
pandas
2426
pytest
2527
pytest-cov
2628
commands =
@@ -29,12 +31,16 @@ commands =
2931

3032
[testenv:py35-win]
3133
deps =
34+
numpy
35+
pandas
3236
pytest
3337
commands = py.test -v
3438

3539
[testenv:py36]
3640
deps =
3741
codecov
42+
numpy
43+
pandas
3844
pytest
3945
pytest-cov
4046
commands =
@@ -44,6 +50,8 @@ commands =
4450
[testenv:py36-win]
4551
deps =
4652
codecov
53+
numpy
54+
pandas
4755
pytest
4856
pytest-cov
4957
commands =
@@ -52,6 +60,8 @@ commands =
5260

5361
[testenv:py37]
5462
deps =
63+
numpy
64+
pandas
5565
pytest
5666
commands = py.test -v
5767

0 commit comments

Comments
 (0)