Skip to content

Commit 8a1c19b

Browse files
authored
[Python] Convert typing and native generic hints in Watch coder inference (#39547)
registry.get_coder receives typing and native generic annotations such as tuple[str, float] unconverted and falls back to pickling. Watch now converts hints with convert_to_beam_type before the registry lookup, so annotated poll functions and key functions infer real structured coders and fully typed elements.
1 parent 08dc50a commit 8a1c19b

2 files changed

Lines changed: 29 additions & 2 deletions

File tree

sdks/python/apache_beam/io/watch.py

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@ def poll(prefix) -> PollResult[str]:
7777
from apache_beam.transforms import PTransform
7878
from apache_beam.transforms import core
7979
from apache_beam.transforms.window import TimestampedValue
80+
from apache_beam.typehints import native_type_compatibility
8081
from apache_beam.utils.timestamp import MAX_TIMESTAMP
8182
from apache_beam.utils.timestamp import Duration
8283
from apache_beam.utils.timestamp import Timestamp
@@ -642,6 +643,13 @@ def _poll_output_type(poll_fn) -> Any:
642643
return Any
643644

644645

646+
def _coder_for_hint(hint) -> Coder:
647+
# typing and native generic hints such as tuple[str, float] must be
648+
# converted to Beam typehints, or the registry falls back to pickling.
649+
return coders.registry.get_coder(
650+
native_type_compatibility.convert_to_beam_type(hint))
651+
652+
645653
class Watch(PTransform):
646654
"""Watches a growing set of outputs per input via a periodic poll function.
647655
@@ -689,14 +697,14 @@ def expand(self, pcoll):
689697
if output_coder is None and isinstance(self._poll_fn, PollFn):
690698
output_coder = self._poll_fn.default_output_coder()
691699
if output_coder is None:
692-
output_coder = coders.registry.get_coder(_poll_output_type(self._poll_fn))
700+
output_coder = _coder_for_hint(_poll_output_type(self._poll_fn))
693701
if self._output_key_fn is None:
694702
# The output is its own dedup key, so the key coder is the output coder.
695703
key_fn = _identity
696704
key_coder = self._output_key_coder or output_coder
697705
else:
698706
key_fn = self._output_key_fn
699-
key_coder = self._output_key_coder or coders.registry.get_coder(
707+
key_coder = self._output_key_coder or _coder_for_hint(
700708
_return_type(self._output_key_fn))
701709
# Dedup hashes the encoded key, so equal keys must encode equally; use the
702710
# coder's deterministic form and reject coders that have none.

sdks/python/apache_beam/io/watch_test.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
"""Tests for the Watch transform."""
1919

2020
import collections
21+
import typing
2122
import unittest
2223

2324
import apache_beam as beam
@@ -444,6 +445,24 @@ def test_infers_output_coder_from_return_annotation(self):
444445
| Watch(_complete_poll, poll_interval=Duration(1)))
445446
self.assertEqual(typehints.Tuple[str, str], output.element_type)
446447

448+
def test_infers_coder_from_generic_annotations(self):
449+
# tuple[str, float] and typing.Tuple[str, float] resolve to a tuple coder,
450+
# not the pickling fallback.
451+
def native_poll(element) -> PollResult[tuple[str, float]]:
452+
return PollResult.complete([(element, 1.0)])
453+
454+
def typing_poll(
455+
element) -> PollResult[typing.Tuple[str, float]]: # noqa: UP006
456+
return PollResult.complete([(element, 1.0)])
457+
458+
for poll in (native_poll, typing_poll):
459+
with self._in_memory_pipeline() as p:
460+
output = (
461+
p | beam.Create(['k:']) | Watch(poll, poll_interval=Duration(1)))
462+
self.assertEqual(
463+
typehints.Tuple[str, typehints.Tuple[str, float]],
464+
output.element_type)
465+
447466
def test_uses_poll_fn_default_output_coder(self):
448467
with self._in_memory_pipeline() as p:
449468
output = (

0 commit comments

Comments
 (0)