This is an automated email from the ASF dual-hosted git repository.

Abacn pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/beam.git


The following commit(s) were added to refs/heads/master by this push:
     new 8a1c19bc9d0 [Python] Convert typing and native generic hints in Watch 
coder inference (#39547)
8a1c19bc9d0 is described below

commit 8a1c19bc9d0e93a020b2286d814419099d022dbf
Author: Elia Liu <[email protected]>
AuthorDate: Thu Jul 30 03:33:26 2026 +1000

    [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.
---
 sdks/python/apache_beam/io/watch.py      | 12 ++++++++++--
 sdks/python/apache_beam/io/watch_test.py | 19 +++++++++++++++++++
 2 files changed, 29 insertions(+), 2 deletions(-)

diff --git a/sdks/python/apache_beam/io/watch.py 
b/sdks/python/apache_beam/io/watch.py
index b0ff2bd6a07..f2eadaf4ace 100644
--- a/sdks/python/apache_beam/io/watch.py
+++ b/sdks/python/apache_beam/io/watch.py
@@ -77,6 +77,7 @@ from apache_beam.runners import sdf_utils
 from apache_beam.transforms import PTransform
 from apache_beam.transforms import core
 from apache_beam.transforms.window import TimestampedValue
+from apache_beam.typehints import native_type_compatibility
 from apache_beam.utils.timestamp import MAX_TIMESTAMP
 from apache_beam.utils.timestamp import Duration
 from apache_beam.utils.timestamp import Timestamp
@@ -642,6 +643,13 @@ def _poll_output_type(poll_fn) -> Any:
   return Any
 
 
+def _coder_for_hint(hint) -> Coder:
+  # typing and native generic hints such as tuple[str, float] must be
+  # converted to Beam typehints, or the registry falls back to pickling.
+  return coders.registry.get_coder(
+      native_type_compatibility.convert_to_beam_type(hint))
+
+
 class Watch(PTransform):
   """Watches a growing set of outputs per input via a periodic poll function.
 
@@ -689,14 +697,14 @@ class Watch(PTransform):
     if output_coder is None and isinstance(self._poll_fn, PollFn):
       output_coder = self._poll_fn.default_output_coder()
     if output_coder is None:
-      output_coder = 
coders.registry.get_coder(_poll_output_type(self._poll_fn))
+      output_coder = _coder_for_hint(_poll_output_type(self._poll_fn))
     if self._output_key_fn is None:
       # The output is its own dedup key, so the key coder is the output coder.
       key_fn = _identity
       key_coder = self._output_key_coder or output_coder
     else:
       key_fn = self._output_key_fn
-      key_coder = self._output_key_coder or coders.registry.get_coder(
+      key_coder = self._output_key_coder or _coder_for_hint(
           _return_type(self._output_key_fn))
     # Dedup hashes the encoded key, so equal keys must encode equally; use the
     # coder's deterministic form and reject coders that have none.
diff --git a/sdks/python/apache_beam/io/watch_test.py 
b/sdks/python/apache_beam/io/watch_test.py
index 8c1f6571da6..472177ceaee 100644
--- a/sdks/python/apache_beam/io/watch_test.py
+++ b/sdks/python/apache_beam/io/watch_test.py
@@ -18,6 +18,7 @@
 """Tests for the Watch transform."""
 
 import collections
+import typing
 import unittest
 
 import apache_beam as beam
@@ -444,6 +445,24 @@ class WatchEndToEndTest(unittest.TestCase):
           | Watch(_complete_poll, poll_interval=Duration(1)))
       self.assertEqual(typehints.Tuple[str, str], output.element_type)
 
+  def test_infers_coder_from_generic_annotations(self):
+    # tuple[str, float] and typing.Tuple[str, float] resolve to a tuple coder,
+    # not the pickling fallback.
+    def native_poll(element) -> PollResult[tuple[str, float]]:
+      return PollResult.complete([(element, 1.0)])
+
+    def typing_poll(
+        element) -> PollResult[typing.Tuple[str, float]]:  # noqa: UP006
+      return PollResult.complete([(element, 1.0)])
+
+    for poll in (native_poll, typing_poll):
+      with self._in_memory_pipeline() as p:
+        output = (
+            p | beam.Create(['k:']) | Watch(poll, poll_interval=Duration(1)))
+        self.assertEqual(
+            typehints.Tuple[str, typehints.Tuple[str, float]],
+            output.element_type)
+
   def test_uses_poll_fn_default_output_coder(self):
     with self._in_memory_pipeline() as p:
       output = (

Reply via email to