Skip to content

Commit 6aa5459

Browse files
committed
Normalize types in dataclass field type resolving
Add a pipeline option to allow fallback to Any
1 parent bec756f commit 6aa5459

6 files changed

Lines changed: 64 additions & 4 deletions

File tree

CHANGES.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,9 @@
7474
## Breaking Changes
7575

7676
* X behavior was changed ([#X](https://github.com/apache/beam/issues/X)).
77+
* (Python) Typehints of dataclass fields are honored during type inferences. To restore the behavior of fallback-to-any,
78+
use pipeline option `--exclude_infer_dataclass_field_type` ([#38797](https://github.com/apache/beam/issues/38797)).
79+
However fixing forward is recommended.
7780

7881
## Deprecations
7982

sdks/python/apache_beam/options/pipeline_options.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -888,6 +888,15 @@ def _add_argparse_args(cls, parser):
888888
default=False,
889889
action='store_true',
890890
help='Disable the use of beartype for type checking.')
891+
parser.add_argument(
892+
'--exclude_infer_dataclass_field_type',
893+
default=False,
894+
action='store_true',
895+
help='Exclude certain typehint inference involving dataclass fields '
896+
'and resolve to Any (as in beam<=2.74.0). NOTE: this option is '
897+
'for backward compatibility only and the exclusion senario is subject '
898+
'to change, until removing in a future version. For details see: '
899+
'https://beam.apache.org/releases/pydoc/current/apache_beam.typehints.trivial_inference.html#apache_beam.typehints.trivial_inference.resolve_dataclass_field_type') # pylint: disable=line-too-long
891900
parser.add_argument(
892901
'--runtime_type_check',
893902
default=False,

sdks/python/apache_beam/typehints/opcodes.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@
4242
from apache_beam.typehints.trivial_inference import Const
4343
from apache_beam.typehints.trivial_inference import element_type
4444
from apache_beam.typehints.trivial_inference import key_value_types
45+
from apache_beam.typehints.trivial_inference import resolve_dataclass_field_type
4546
from apache_beam.typehints.trivial_inference import union
4647
from apache_beam.typehints.typehints import Any
4748
from apache_beam.typehints.typehints import Dict
@@ -451,8 +452,9 @@ def _getattr(o, name):
451452
elif inspect.isclass(o) and dataclasses.is_dataclass(o):
452453
field = o.__dataclass_fields__.get(name)
453454
if field is not None:
454-
return field.type
455+
return resolve_dataclass_field_type(field.type)
455456
return Any
457+
456458
else:
457459
return Any
458460

sdks/python/apache_beam/typehints/trivial_inference.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -774,3 +774,33 @@ def infer_return_type_func(f, input_types, debug=False, depth=0):
774774
if debug:
775775
print(f, id(f), input_types, '->', result)
776776
return result
777+
778+
779+
def resolve_dataclass_field_type(x):
780+
"""
781+
Resolve a type to Beam typehint under global pipeline option context.
782+
783+
Since Beam 2.75.0, typehints of dataclass fields are honored during type
784+
inferences. However, in case of breakage (possible scenarios include
785+
incorrect typehints; non-deterministic or nullable types disallowed by
786+
consumer transform but check disabled by Any; tests rely on Any),
787+
--exclude_infer_dataclass_field_type option to instruct falling back to Any.
788+
Fields of builtin primitives are always respected.
789+
"""
790+
from apache_beam.options.pipeline_options_context import get_pipeline_options
791+
options = get_pipeline_options()
792+
if options:
793+
from apache_beam.options.pipeline_options import TypeOptions
794+
try:
795+
disabled = options.view_as(TypeOptions).exclude_infer_dataclass_field_type
796+
except AttributeError:
797+
print(options.view_as(TypeOptions)._visible_option_list())
798+
raise
799+
else:
800+
disabled = False
801+
802+
if not disabled:
803+
return typehints.normalize(x)
804+
if x in (bool, bytes, complex, float, int, str):
805+
return x
806+
return Any

sdks/python/apache_beam/typehints/trivial_inference_test.py

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@
2424
import unittest
2525

2626
import apache_beam as beam
27+
from apache_beam.options.pipeline_options import PipelineOptions
28+
from apache_beam.options.pipeline_options_context import scoped_pipeline_options
2729
from apache_beam.typehints import row_type
2830
from apache_beam.typehints import trivial_inference
2931
from apache_beam.typehints import typehints
@@ -489,15 +491,28 @@ def testPyCallable(self):
489491
[int])
490492

491493
def testDataClassFields(self):
494+
@dataclasses.dataclass
495+
class BaseClass:
496+
pass
497+
492498
@dataclasses.dataclass
493499
class MyDataClass:
494500
id: int
495501
name: str
502+
tags: list[str]
503+
custom: BaseClass
496504

497505
self.assertReturnType(
498-
typehints.Tuple[int, str],
499-
python_callable.PythonCallableWithSource("lambda x: (x.id, x.name)"),
500-
[MyDataClass])
506+
typehints.Tuple[int, str, typehints.List[str], BaseClass],
507+
python_callable.PythonCallableWithSource(
508+
"lambda x: (x.id, x.name, x.tags, x.custom)"), [MyDataClass])
509+
510+
options = PipelineOptions(['--exclude_infer_dataclass_field_type'])
511+
with scoped_pipeline_options(options):
512+
self.assertReturnType(
513+
typehints.Tuple[int, str, typehints.Any, typehints.Any],
514+
python_callable.PythonCallableWithSource(
515+
"lambda x: (x.id, x.name, x.tags, x.custom)"), [MyDataClass])
501516

502517

503518
if __name__ == '__main__':

sdks/python/apache_beam/typehints/typehints.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1453,6 +1453,7 @@ def __getitem__(self, type_params):
14531453

14541454

14551455
def normalize(x, none_as_type=False):
1456+
"""Normalize a type to Beam typehint."""
14561457
# None is inconsistantly used for Any, unknown, or NoneType.
14571458

14581459
# Avoid circular imports

0 commit comments

Comments
 (0)