You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@beam.apache.org by xq...@apache.org on 2025/10/03 22:38:24 UTC

(beam) branch master updated: fix(sdk:python): Avoid AttributeError for transforms without hints (#36251)

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

xqhu 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 1e973630e67 fix(sdk:python): Avoid AttributeError for transforms without hints (#36251)
1e973630e67 is described below

commit 1e973630e6738a101e3840585d7d2832419018ce
Author: liferoad <hu...@gmail.com>
AuthorDate: Fri Oct 3 18:38:17 2025 -0400

    fix(sdk:python): Avoid AttributeError for transforms without hints (#36251)
    
    * fix(sdk:python): Avoid AttributeError for transforms without hints
    
    The AppliedPTransform initializer would unconditionally attempt to call
    `get_resource_hints()` on a transform object. This could cause an
    AttributeError if a PTransform implementation does not define this method.
    
    This change adds an `hasattr` check to verify the existence of the
    `get_resource_hints` method before calling it, preventing the potential
    crash and making the pipeline construction more robust.
    
    * fix annotations
    
    * fixed more
    
    * fixed more
    
    * more fixes
    
    * one more
---
 sdks/python/apache_beam/pipeline.py | 33 ++++++++++++++++++++++-----------
 1 file changed, 22 insertions(+), 11 deletions(-)

diff --git a/sdks/python/apache_beam/pipeline.py b/sdks/python/apache_beam/pipeline.py
index 02ce12a3bef..0e03c684153 100644
--- a/sdks/python/apache_beam/pipeline.py
+++ b/sdks/python/apache_beam/pipeline.py
@@ -354,7 +354,7 @@ class Pipeline(HasDisplayData):
           if replacement_transform is original_transform_node.transform:
             return
           replacement_transform.side_inputs = tuple(
-              original_transform_node.transform.side_inputs)
+              getattr(original_transform_node.transform, 'side_inputs', ()))
 
           replacement_transform_node = AppliedPTransform(
               original_transform_node.parent,
@@ -1027,7 +1027,9 @@ class Pipeline(HasDisplayData):
         # type: (AppliedPTransform) -> None
         if not transform_node.transform:
           return
-        if transform_node.transform.runner_api_requires_keyed_input():
+        if hasattr(
+            transform_node.transform, 'runner_api_requires_keyed_input'
+        ) and transform_node.transform.runner_api_requires_keyed_input():
           pcoll = transform_node.inputs[0]
           pcoll.element_type = typehints.coerce_to_kv_type(
               pcoll.element_type, transform_node.full_label)
@@ -1046,7 +1048,7 @@ class Pipeline(HasDisplayData):
                 == output.element_type.tuple_types[0]):
               output.requires_deterministic_key_coder = (
                   deterministic_key_coders and transform_node.full_label)
-        for side_input in transform_node.transform.side_inputs:
+        for side_input in getattr(transform_node.transform, 'side_inputs', []):
           if side_input.requires_keyed_input():
             side_input.pvalue.element_type = typehints.coerce_to_kv_type(
                 side_input.pvalue.element_type,
@@ -1240,10 +1242,10 @@ class AppliedPTransform(object):
     # once environment is a first-class citizen in Beam graph and we have
     # access to actual environment, not just an id.
     self.resource_hints = dict(
-        transform.get_resource_hints()) if transform else {
-        }  # type: Dict[str, bytes]
+        transform.get_resource_hints()) if transform and hasattr(
+            transform, 'get_resource_hints') else {}  # type: Dict[str, bytes]
 
-    if transform:
+    if transform and hasattr(transform, 'annotations'):
       annotations = {
           **(annotations or {}), **encode_annotations(transform.annotations())
       }
@@ -1399,8 +1401,11 @@ class AppliedPTransform(object):
       assert not self.main_inputs and not self.side_inputs
       return {}
     else:
-      named_inputs = self.transform._named_inputs(
-          self.main_inputs, self.side_inputs)
+      if hasattr(self.transform, '_named_inputs'):
+        named_inputs = self.transform._named_inputs(
+            self.main_inputs, self.side_inputs)
+      else:
+        named_inputs = {}
       if not self.parts:
         for name, pc_out in self.outputs.items():
           if pc_out.producer is not self and pc_out not in named_inputs.values(
@@ -1414,7 +1419,10 @@ class AppliedPTransform(object):
       assert not self.outputs
       return {}
     else:
-      return self.transform._named_outputs(self.outputs)
+      if hasattr(self.transform, '_named_outputs'):
+        return self.transform._named_outputs(self.outputs)
+      else:
+        return {}
 
   def to_runner_api(self, context):
     # type: (PipelineContext) -> beam_runner_api_pb2.PTransform
@@ -1441,7 +1449,9 @@ class AppliedPTransform(object):
               context,
               has_parts=bool(self.parts),
               named_inputs=self.named_inputs())
-        return transform.to_runner_api(context, has_parts=bool(self.parts))
+        elif hasattr(transform, 'to_runner_api'):
+          return transform.to_runner_api(context, has_parts=bool(self.parts))
+        return None
 
     # Iterate over inputs and outputs by sorted key order, so that ids are
     # consistently generated for multiple runs of the same pipeline.
@@ -1527,7 +1537,8 @@ class AppliedPTransform(object):
         environment_id=None,
         annotations=proto.annotations)
 
-    if result.transform and result.transform.side_inputs:
+    if result.transform and hasattr(
+        result.transform, 'side_inputs') and result.transform.side_inputs:
       for si, pcoll in zip(result.transform.side_inputs, side_inputs):
         si.pvalue = pcoll
       result.side_inputs = tuple(result.transform.side_inputs)