Skip to content

Commit 339b85e

Browse files
authored
Make Group work with Iterable and IterableWithSize (#7)
1 parent 29f13e7 commit 339b85e

File tree

5 files changed

+146
-14
lines changed

5 files changed

+146
-14
lines changed

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,3 +130,6 @@ dmypy.json
130130

131131
# Poetry
132132
poetry.lock
133+
134+
# PyCharm
135+
.idea

exasol_udf_mock_python/group.py

Lines changed: 56 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,65 @@
1-
from typing import List, Tuple
1+
import collections.abc
2+
from abc import abstractmethod
3+
from typing import List, Tuple, Iterable, Set, Iterator
24

35

4-
class Group:
6+
class IterableWithSize(collections.abc.Iterable):
7+
@abstractmethod
8+
def __len__(self):
9+
return None
10+
11+
12+
class Group(collections.abc.Iterable):
513
"""
6-
Reperesents a Group (SET Function) or a Batch (Scalar Function) of rows
14+
Represents a Group (SET Function) or a Batch (Scalar Function) of rows
715
"""
816

9-
def __init__(self, rows:List[Tuple]):
10-
self.rows = rows
17+
def __init__(self, rows: Iterable[Tuple]):
18+
self._rows = rows
19+
20+
def __iter__(self):
21+
return iter(self._rows)
22+
23+
def __len__(self):
24+
if isinstance(self._rows, (List, Set, Tuple, IterableWithSize)):
25+
return len(self._rows)
26+
else:
27+
return sum(1 for _ in self._rows)
28+
29+
@property
30+
def rows(self) -> List[Tuple]:
31+
"""
32+
This property transforms the Iterable of rows into a List
33+
None: This can potentially can cause the materialization of a large list.
34+
:return: The rows of this group as a list
35+
"""
36+
return list(iter(self._rows))
1137

1238
def __repr__(self):
1339
return str(self.__class__) + ": " + str(self.__dict__)
1440

15-
def __eq__(self, other):
16-
return self.__dict__ == other.__dict__
41+
def __eq__(self, other: "Group"):
42+
if not isinstance(other, Group):
43+
return False
44+
self_iter = iter(self)
45+
other_iter = iter(other)
46+
return self._compare_iter(self_iter, other_iter)
47+
48+
def _compare_iter(self, self_iter: Iterator[Tuple], other_iter: Iterator[Tuple]):
49+
self_at_end = False
50+
try:
51+
while True:
52+
try:
53+
self_row = next(self_iter)
54+
except StopIteration as e:
55+
self_at_end = True
56+
break
57+
other_row = next(other_iter)
58+
if self_row != other_row:
59+
return False
60+
other_row = next(other_iter)
61+
except StopIteration as e:
62+
if self_at_end:
63+
return True
64+
else:
65+
return False

exasol_udf_mock_python/mock_context.py

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,10 @@ def __init__(self, input_groups: Iterator[Group], metadata: MockMetaData):
1414
self._input_groups = input_groups
1515
self._output_groups = []
1616
self._input_group = None # type: Group
17+
self._output_group_list = None # type: List
1718
self._output_group = None # type: Group
1819
self._iter = None # type: Iterator[Tuple]
20+
self._len = None # type: int
1921
self._metadata = metadata
2022
self._name_position_map = \
2123
{column.name: position
@@ -27,19 +29,25 @@ def _next_group(self):
2729
self._input_group = next(self._input_groups)
2830
except StopIteration as e:
2931
self._data = None
32+
self._output_group_list = None
3033
self._output_group = None
3134
self._input_group = None
3235
self._iter = None
36+
self._len = None
3337
return False
34-
if len(self._input_group.rows) == 0:
38+
self._len = len(self._input_group)
39+
if self._len == 0:
3540
self._data = None
41+
self._output_group_list = None
3642
self._output_group = None
3743
self._input_group = None
3844
self._iter = None
45+
self._len = None
3946
raise RuntimeError("Empty input groups are not allowd")
40-
self._output_group = Group([])
47+
self._output_group_list = []
48+
self._output_group = Group(self._output_group_list)
4149
self._output_groups.append(self._output_group)
42-
self._iter = iter(self._input_group.rows)
50+
self._iter = iter(self._input_group)
4351
self.next()
4452
return True
4553

@@ -80,10 +88,10 @@ def next(self, reset:bool = False):
8088
return False
8189

8290
def size(self):
83-
return len(self._input_group.rows)
91+
return self._len
8492

8593
def reset(self):
86-
self._iter = iter(self._input_group.rows)
94+
self._iter = iter(self._input_group)
8795
self.next()
8896

8997
def emit(self, *args):
@@ -93,7 +101,7 @@ def emit(self, *args):
93101
tuples = [args]
94102
for row in tuples:
95103
self.validate_tuples(row, self._metadata.output_columns)
96-
self._output_group.rows.extend(tuples)
104+
self._output_group_list.extend(tuples)
97105
return
98106

99107
def validate_tuples(self, row: Tuple, columns: List[Column]):

exasol_udf_mock_python/udf_mock_executor.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,10 @@ def _exec_init(self, exa_environment: MockExaEnvironment) -> Dict[str, Any]:
5858
exec(codeObject, exec_globals)
5959
return exec_globals
6060

61-
def run(self, input_groups:Union[Iterator[Group],List[Group]], exa_environment: MockExaEnvironment):
61+
def run(self,
62+
input_groups:Union[Iterator[Group],List[Group]],
63+
exa_environment: MockExaEnvironment)\
64+
->List[Group]:
6265
with self._lock:
6366
if isinstance(input_groups,Iterator):
6467
ctx = MockContext(input_groups, exa_environment.meta)

tests/test_group.py

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
from collections import Iterable
2+
3+
from exasol_udf_mock_python.group import Group, IterableWithSize
4+
5+
6+
def test_groups_are_equal():
7+
group1 = Group([(1,), (2,), (3,)])
8+
group2 = Group([(1,), (2,), (3,)])
9+
assert group1 == group2
10+
11+
12+
def test_group_prefix_equal_but_first_group_is_longer():
13+
group1 = Group([(1,), (2,), (3,), (4,)])
14+
group2 = Group([(1,), (2,), (3,)])
15+
assert group1 != group2
16+
17+
18+
def test_group_prefix_equal_but_second_group_is_longer():
19+
group1 = Group([(1,), (2,), (3,)])
20+
group2 = Group([(1,), (2,), (3,), (4,)])
21+
assert group1 != group2
22+
23+
24+
def test_group_same_length_difference_in_the_middle():
25+
group1 = Group([(1,), (2,), (5,), (4,)])
26+
group2 = Group([(1,), (2,), (3,), (4,)])
27+
assert group1 != group2
28+
29+
30+
def test_group_has_tuple_as_iterable_but_rows_is_list():
31+
group = Group(((1,), (2,), (5,), (4,)))
32+
assert group.rows == [(1,), (2,), (5,), (4,)]
33+
34+
35+
def test_group_len():
36+
group = Group(((1,), (2,), (5,), (4,)))
37+
assert len(group) == 4
38+
39+
40+
def test_group_iter():
41+
group = Group(((1,), (2,), (5,), (4,)))
42+
assert list(iter(group)) == [(1,), (2,), (5,), (4,)]
43+
44+
45+
class MyIterable(Iterable):
46+
def __iter__(self):
47+
return iter([(1,), (2,), (3,)])
48+
49+
50+
def test_group_with_custom_iterable_rows():
51+
group = Group(MyIterable())
52+
assert group.rows == [(1,), (2,), (3,)]
53+
54+
55+
def test_group_with_custom_iterable_len():
56+
group = Group(MyIterable())
57+
assert len(group) == 3
58+
59+
60+
def test_group_with_iterable_with_size_len():
61+
class MyIterableWithSize(IterableWithSize):
62+
def __iter__(self):
63+
raise Exception("The group should use __len__ instead of __iter__ to determine the length")
64+
65+
def __len__(self):
66+
return 3
67+
68+
group = Group(MyIterableWithSize())
69+
assert len(group) == 3

0 commit comments

Comments
 (0)