@@ -84,25 +84,64 @@ def teardown(self):
8484 return self .dofn .teardown ()
8585
8686
87- class OutputCheckWrapperDoFn (AbstractDoFnWrapper ):
88- """A DoFn that verifies against common errors in the output type."""
89- def __init__ (self , dofn , full_label ):
87+ class RuntimeTypeCheckWrapperDoFn (AbstractDoFnWrapper ):
88+ """A DoFn wrapper performing runtime type-checking of inputs and outputs.
89+
90+ This single wrapper performs the work that was previously split between
91+ two nested wrappers (``TypeCheckWrapperDoFn`` wrapped inside
92+ ``OutputCheckWrapperDoFn``): type-checking of inputs and outputs against
93+ declared type hints, verification against common output errors (such as
94+ returning a plain ``str``, ``bytes`` or ``dict`` from a ``ParDo``), and
95+ labeling of raised ``TypeCheckError`` messages with the offending
96+ transform's full label. Merging the wrappers removes one level of Python
97+ call indirection per processed element when ``--runtime_type_check`` is
98+ enabled.
99+ """
100+ def __init__ (self , dofn , type_hints , full_label ):
90101 super ().__init__ (dofn )
91102 self .full_label = full_label
103+ self ._process_fn = self .dofn ._process_argspec_fn ()
104+ if type_hints .input_types :
105+ input_args , input_kwargs = type_hints .input_types
106+ self ._input_hints = getcallargs_forhints (
107+ self ._process_fn , * input_args , ** input_kwargs )
108+ else :
109+ self ._input_hints = None
110+ # TODO(robertwb): Multi-output.
111+ self ._output_type_hint = type_hints .simple_output_type (full_label )
92112
93113 def wrapper (self , method , args , kwargs ):
94114 try :
95- result = method (* args , ** kwargs )
115+ result = self . _type_check_result ( method (* args , ** kwargs ) )
96116 except TypeCheckError as e :
97- # TODO(BEAM-10710): Remove the 'ParDo' prefix for the label name
98- error_msg = (
99- 'Runtime type violation detected within ParDo(%s): '
100- '%s' % (self .full_label , e ))
101- _ , _ , tb = sys .exc_info ()
102- raise TypeCheckError (error_msg ).with_traceback (tb )
117+ raise self ._add_label (e )
118+ else :
119+ return self ._check_type (result )
120+
121+ def process (self , * args , ** kwargs ):
122+ try :
123+ if self ._input_hints :
124+ actual_inputs = inspect .getcallargs (self ._process_fn , * args , ** kwargs ) # pylint: disable=deprecated-method
125+ for var , hint in self ._input_hints .items ():
126+ if hint is actual_inputs [var ]:
127+ # self parameter
128+ continue
129+ _check_instance_type (hint , actual_inputs [var ], var , True )
130+ result = self ._type_check_result (self .dofn .process (* args , ** kwargs ))
131+ except TypeCheckError as e :
132+ raise self ._add_label (e )
103133 else :
104134 return self ._check_type (result )
105135
136+ def _add_label (self , e ):
137+ """Returns a TypeCheckError labeled with this transform's full label."""
138+ # TODO(BEAM-10710): Remove the 'ParDo' prefix for the label name
139+ error_msg = (
140+ 'Runtime type violation detected within ParDo(%s): '
141+ '%s' % (self .full_label , e ))
142+ _ , _ , tb = sys .exc_info ()
143+ return TypeCheckError (error_msg ).with_traceback (tb )
144+
106145 @staticmethod
107146 def _check_type (output ):
108147 if output is None :
@@ -120,36 +159,6 @@ def _check_type(output):
120159 'iterable. %s was returned instead.' % type (output ))
121160 return output
122161
123-
124- class TypeCheckWrapperDoFn (AbstractDoFnWrapper ):
125- """A wrapper around a DoFn which performs type-checking of input and output.
126- """
127- def __init__ (self , dofn , type_hints , label = None ):
128- super ().__init__ (dofn )
129- self ._process_fn = self .dofn ._process_argspec_fn ()
130- if type_hints .input_types :
131- input_args , input_kwargs = type_hints .input_types
132- self ._input_hints = getcallargs_forhints (
133- self ._process_fn , * input_args , ** input_kwargs )
134- else :
135- self ._input_hints = None
136- # TODO(robertwb): Multi-output.
137- self ._output_type_hint = type_hints .simple_output_type (label )
138-
139- def wrapper (self , method , args , kwargs ):
140- result = method (* args , ** kwargs )
141- return self ._type_check_result (result )
142-
143- def process (self , * args , ** kwargs ):
144- if self ._input_hints :
145- actual_inputs = inspect .getcallargs (self ._process_fn , * args , ** kwargs ) # pylint: disable=deprecated-method
146- for var , hint in self ._input_hints .items ():
147- if hint is actual_inputs [var ]:
148- # self parameter
149- continue
150- _check_instance_type (hint , actual_inputs [var ], var , True )
151- return self ._type_check_result (self .dofn .process (* args , ** kwargs ))
152-
153162 def _type_check_result (self , transform_results ):
154163 if self ._output_type_hint is None or transform_results is None :
155164 return transform_results
@@ -289,11 +298,9 @@ def visit_transform(self, applied_transform):
289298 if isinstance (transform .fn , core .CombineValuesDoFn ):
290299 transform .fn .combinefn = self ._wrapped_fn
291300 else :
292- transform .fn = transform .dofn = OutputCheckWrapperDoFn (
293- TypeCheckWrapperDoFn (
294- transform .fn ,
295- transform .get_type_hints (),
296- applied_transform .full_label ),
301+ transform .fn = transform .dofn = RuntimeTypeCheckWrapperDoFn (
302+ transform .fn ,
303+ transform .get_type_hints (),
297304 applied_transform .full_label )
298305
299306
0 commit comments