|
38 | 38 | from apache_beam.testing.test_pipeline import TestPipeline |
39 | 39 | from apache_beam.testing.util import assert_that |
40 | 40 | from apache_beam.testing.util import equal_to |
| 41 | +from apache_beam.transforms import combiners |
| 42 | +from apache_beam.transforms import userstate |
41 | 43 | from apache_beam.typehints import decorators |
42 | 44 | from apache_beam.typehints import typecheck |
43 | 45 | from apache_beam.typehints import with_input_types |
@@ -348,5 +350,52 @@ def test_wrapper_preserves_results(self): |
348 | 350 | self.assertEqual(list(dofn.process(1)), [2]) |
349 | 351 |
|
350 | 352 |
|
| 353 | +def _make_stateful_dofn(): |
| 354 | + # Defined inside a function so that the class is serialized by value |
| 355 | + # (like the DoFn created by GroupIntoBatches), which is the case where a |
| 356 | + # cached bound method diverges from the reconstructed class's process. |
| 357 | + count_state = userstate.CombiningValueStateSpec( |
| 358 | + 'count', combiners.CountCombineFn()) |
| 359 | + |
| 360 | + class _CountingStatefulDoFn(beam.DoFn): |
| 361 | + def process(self, element, count=beam.DoFn.StateParam(count_state)): |
| 362 | + count.add(1) |
| 363 | + yield element[1] |
| 364 | + |
| 365 | + return _CountingStatefulDoFn() |
| 366 | + |
| 367 | + |
| 368 | +class RuntimeTypeCheckStatefulDoFnTest(unittest.TestCase): |
| 369 | + """Regression tests for runtime_type_check with stateful DoFns. |
| 370 | +
|
| 371 | + The type-check wrapper must not expose duplicate StateSpecs/TimerSpecs to |
| 372 | + stateful DoFn validation, including after the runner serializes and |
| 373 | + reconstructs the wrapped DoFn. |
| 374 | + """ |
| 375 | + def test_wrapper_does_not_cache_bound_process(self): |
| 376 | + dofn = _make_stateful_dofn() |
| 377 | + wrapper = typecheck.RuntimeTypeCheckWrapperDoFn( |
| 378 | + dofn, dofn.get_type_hints(), 'Step') |
| 379 | + # A bound method cached in the instance __dict__ is visible to |
| 380 | + # userstate.get_dofn_specs and can diverge from self.dofn.process |
| 381 | + # across (de)serialization. |
| 382 | + for attr, value in wrapper.__dict__.items(): |
| 383 | + self.assertFalse( |
| 384 | + hasattr(value, '__self__'), |
| 385 | + 'wrapper caches bound method %r, which breaks stateful DoFn ' |
| 386 | + 'validation' % attr) |
| 387 | + userstate.validate_stateful_dofn(wrapper) |
| 388 | + |
| 389 | + def test_stateful_dofn_with_runtime_type_check(self): |
| 390 | + options = PipelineOptions() |
| 391 | + options.view_as(TypeOptions).runtime_type_check = True |
| 392 | + with TestPipeline(options=options) as p: |
| 393 | + result = ( |
| 394 | + p |
| 395 | + | beam.Create([('k', 1), ('k', 2), ('k', 3)]) |
| 396 | + | beam.ParDo(_make_stateful_dofn())) |
| 397 | + assert_that(result, equal_to([1, 2, 3])) |
| 398 | + |
| 399 | + |
351 | 400 | if __name__ == '__main__': |
352 | 401 | unittest.main() |
0 commit comments