Skip to content

Commit ffd26a9

Browse files
committed
Additional validation to prevent non-indexable iterables row objects from crashing
1 parent 17ada41 commit ffd26a9

File tree

2 files changed

+55
-6
lines changed

2 files changed

+55
-6
lines changed

tableformatter.py

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
import itertools
88
import re
99
import textwrap as textw
10-
from typing import List, Iterable, Optional, Tuple, Union, Callable
10+
from typing import List, Iterable, Optional, Tuple, Union, Callable, Sequence
1111

1212
from wcwidth import wcswidth
1313

@@ -18,9 +18,10 @@
1818
except ImportError:
1919
from typing import Container, Generic, Sized, TypeVar
2020

21+
T_co = TypeVar('T_co', covariant=True)
2122
# Python 3.5
2223
# noinspection PyAbstractClass
23-
class Collection(Generic[TypeVar('T_co', covariant=True)], Container, Sized, Iterable):
24+
class Collection(Generic[T_co], Container, Sized, Iterable):
2425
"""hack to enable Collection typing"""
2526
__slots__ = ()
2627

@@ -997,13 +998,18 @@ def generate_table(self, entries: Iterable[Union[Iterable, object]], force_trans
997998
else:
998999
# check if this is a tuple containing a dictionary of decorated values. If so, the row object
9991000
# is the first element a the decorated values is the second element.
1000-
if len(entry) == 2 and isinstance(entry[1], dict):
1001-
entry_obj = entry[0]
1002-
else:
1001+
is_tagged = False
1002+
try:
1003+
if isinstance(entry, Sequence) and len(entry) == 2 and isinstance(entry[1], dict):
1004+
entry_obj = entry[0]
1005+
is_tagged = True
1006+
else:
1007+
entry_obj = entry
1008+
except KeyError:
10031009
entry_obj = entry
10041010
if self._row_tagger is not None:
10051011
entry_opts = self._row_tagger(entry_obj)
1006-
if len(entry) == 2 and isinstance(entry[1], dict):
1012+
if is_tagged:
10071013
entry_opts.update(entry[1])
10081014

10091015
for column_index, attrib_name in enumerate(self._column_attribs):

tests/test_data_types.py

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
Unit testing the variety of data types supported by tableformatter.
44
"""
55
from collections import OrderedDict
6+
from typing import Iterator
7+
68
import tableformatter as tf
79

810

@@ -71,6 +73,47 @@ def test_iterable_of_non_iterable_objects():
7173
assert table == EXPECTED_WITH_HEADERS
7274

7375

76+
class NonIndexableRowObject:
77+
def __init__(self, field1: int, field2: int, field3: int, field4: int):
78+
self.field1 = field1
79+
self.field2 = field2
80+
self._field3 = field3
81+
self.field4 = field4
82+
83+
@property
84+
def field3(self) -> int:
85+
return self._field3
86+
87+
def __getitem__(self, key: str) -> int:
88+
if key == 'field1':
89+
return self.field1
90+
elif key == 'field2':
91+
return self.field2
92+
elif key == 'field3':
93+
return self.field3
94+
elif key == 'field4':
95+
return self.field4
96+
else:
97+
raise KeyError('not a valid field')
98+
99+
def __len__(self) -> int:
100+
return 2
101+
102+
def __iter__(self) -> Iterator[int]:
103+
return iter((self.field1, self.field2, self._field3, self.field4))
104+
105+
106+
def test_iterable_of_non_indexable_iterables():
107+
rows = [NonIndexableRowObject(1, 2, 3, 4),
108+
NonIndexableRowObject(5, 6, 7, 8)]
109+
columns = (tf.Column('col1', attrib='field1'),
110+
tf.Column('col2', attrib='field2'),
111+
tf.Column('col3', attrib='field3'),
112+
tf.Column('col4', attrib='field4'))
113+
table = tf.generate_table(rows, columns)
114+
assert table == EXPECTED_WITH_HEADERS
115+
116+
74117
try:
75118
import numpy as np
76119
except ImportError:

0 commit comments

Comments
 (0)