Skip to content

Commit 863823e

Browse files
authored
Evaluate namedTuples as equivalent to rows (#35218)
* Evaluate namedTuples as equivalent to rows * lint
1 parent 7b235f8 commit 863823e

2 files changed

Lines changed: 85 additions & 1 deletion

File tree

sdks/python/apache_beam/testing/util.py

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@
5050
'matches_all',
5151
# open_shards is internal and has no backwards compatibility guarantees.
5252
'open_shards',
53+
'row_namedtuple_equals_fn',
5354
'TestWindowedValue',
5455
]
5556

@@ -167,7 +168,7 @@ def _equal(actual, equals_fn=equals_fn):
167168
# collection. It can also raise false negatives for types that don't have
168169
# a deterministic sort order, like pyarrow Tables as of 0.14.1
169170
if not equals_fn:
170-
equals_fn = lambda e, a: e == a
171+
equals_fn = row_namedtuple_equals_fn
171172
try:
172173
sorted_expected = sorted(expected)
173174
sorted_actual = sorted(actual)
@@ -202,6 +203,33 @@ def _equal(actual, equals_fn=equals_fn):
202203
return _equal
203204

204205

206+
def row_namedtuple_equals_fn(expected, actual, fallback_equals_fn=None):
207+
"""
208+
equals_fn which can be used by equal_to which treats Rows and
209+
NamedTuples as equivalent types. This can be useful since Beam converts
210+
Rows to NamedTuples when they are sent across portability layers, so a Row
211+
may be converted to a NamedTuple automatically by Beam.
212+
"""
213+
if fallback_equals_fn is None:
214+
fallback_equals_fn = lambda e, a: e == a
215+
if type(expected) is not pvalue.Row and not _is_named_tuple(expected):
216+
return fallback_equals_fn(expected, actual)
217+
if type(actual) is not pvalue.Row and not _is_named_tuple(actual):
218+
return fallback_equals_fn(expected, actual)
219+
220+
expected_dict = expected._asdict()
221+
actual_dict = actual._asdict()
222+
if len(expected_dict) != len(actual_dict):
223+
return False
224+
for k, v in expected_dict.items():
225+
if k not in actual_dict:
226+
return False
227+
if not row_namedtuple_equals_fn(v, actual_dict[k]):
228+
return False
229+
230+
return True
231+
232+
205233
def matches_all(expected):
206234
"""Matcher used by assert_that to check a set of matchers.
207235
@@ -386,5 +414,11 @@ def _sort_lists(result):
386414
return result
387415

388416

417+
def _is_named_tuple(obj) -> bool:
418+
return (
419+
isinstance(obj, tuple) and hasattr(obj, '_asdict') and
420+
hasattr(obj, '_fields'))
421+
422+
389423
# A utility transform that recursively sorts lists for easier testing.
390424
SortLists = Map(_sort_lists)

sdks/python/apache_beam/testing/util_test.py

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
# pytype: skip-file
2121

2222
import unittest
23+
from typing import NamedTuple
2324

2425
import apache_beam as beam
2526
from apache_beam import Create
@@ -32,6 +33,7 @@
3233
from apache_beam.testing.util import equal_to_per_window
3334
from apache_beam.testing.util import is_empty
3435
from apache_beam.testing.util import is_not_empty
36+
from apache_beam.testing.util import row_namedtuple_equals_fn
3537
from apache_beam.transforms import trigger
3638
from apache_beam.transforms import window
3739
from apache_beam.transforms.window import FixedWindows
@@ -254,6 +256,54 @@ def test_equal_to_per_window_fail_unexpected_element(self):
254256
equal_to_per_window(expected),
255257
reify_windows=True)
256258

259+
def test_row_namedtuple_equals(self):
260+
class RowTuple(NamedTuple):
261+
a: str
262+
b: int
263+
264+
self.assertTrue(
265+
row_namedtuple_equals_fn(
266+
beam.Row(a='123', b=456), beam.Row(a='123', b=456)))
267+
self.assertTrue(
268+
row_namedtuple_equals_fn(
269+
beam.Row(a='123', b=456), RowTuple(a='123', b=456)))
270+
self.assertTrue(
271+
row_namedtuple_equals_fn(
272+
RowTuple(a='123', b=456), RowTuple(a='123', b=456)))
273+
self.assertTrue(
274+
row_namedtuple_equals_fn(
275+
RowTuple(a='123', b=456), beam.Row(a='123', b=456)))
276+
self.assertTrue(row_namedtuple_equals_fn('foo', 'foo'))
277+
self.assertFalse(
278+
row_namedtuple_equals_fn(
279+
beam.Row(a='123', b=456), beam.Row(a='123', b=4567)))
280+
self.assertFalse(
281+
row_namedtuple_equals_fn(
282+
beam.Row(a='123', b=456), beam.Row(a='123', b=456, c='a')))
283+
self.assertFalse(
284+
row_namedtuple_equals_fn(
285+
beam.Row(a='123', b=456), RowTuple(a='123', b=4567)))
286+
self.assertFalse(
287+
row_namedtuple_equals_fn(
288+
beam.Row(a='123', b=456, c='foo'), RowTuple(a='123', b=4567)))
289+
self.assertFalse(
290+
row_namedtuple_equals_fn(beam.Row(a='123'), RowTuple(a='123', b=4567)))
291+
self.assertFalse(row_namedtuple_equals_fn(beam.Row(a='123'), '123'))
292+
self.assertFalse(row_namedtuple_equals_fn('123', RowTuple(a='123', b=456)))
293+
294+
class NestedNamedTuple(NamedTuple):
295+
a: str
296+
b: RowTuple
297+
298+
self.assertTrue(
299+
row_namedtuple_equals_fn(
300+
beam.Row(a='foo', b=beam.Row(a='123', b=456)),
301+
NestedNamedTuple(a='foo', b=RowTuple(a='123', b=456))))
302+
self.assertTrue(
303+
row_namedtuple_equals_fn(
304+
beam.Row(a='foo', b=beam.Row(a='123', b=456)),
305+
beam.Row(a='foo', b=RowTuple(a='123', b=456))))
306+
257307

258308
if __name__ == '__main__':
259309
unittest.main()

0 commit comments

Comments
 (0)