@@ -84,25 +84,81 @@ 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+ # Note that a *bound* process method must not be cached on the instance:
104+ # an attribute holding a bound method is visible to the stateful DoFn
105+ # reflection in userstate.get_dofn_specs, and a cached copy can diverge
106+ # from self.dofn.process across (de)serialization, in which case stateful
107+ # DoFn validation would see duplicate StateSpecs/TimerSpecs. Caching the
108+ # underlying (unbound) function is safe: plain functions stored on an
109+ # instance are not bound methods, so DoFn reflection ignores them.
110+ process_fn = dofn ._process_argspec_fn ()
111+ if hasattr (process_fn , '__func__' ):
112+ self ._process_fn = process_fn .__func__
113+ self ._process_fn_needs_self = True
114+ else :
115+ self ._process_fn = process_fn
116+ self ._process_fn_needs_self = False
117+ if type_hints .input_types :
118+ input_args , input_kwargs = type_hints .input_types
119+ self ._input_hints = getcallargs_forhints (
120+ process_fn , * input_args , ** input_kwargs )
121+ else :
122+ self ._input_hints = None
123+ # TODO(robertwb): Multi-output.
124+ self ._output_type_hint = type_hints .simple_output_type (full_label )
92125
93126 def wrapper (self , method , args , kwargs ):
94127 try :
95- result = method (* args , ** kwargs )
128+ result = self . _type_check_result ( method (* args , ** kwargs ) )
96129 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 )
130+ raise self ._add_label (e )
131+ else :
132+ return self ._check_type (result )
133+
134+ def process (self , * args , ** kwargs ):
135+ try :
136+ if self ._input_hints :
137+ if self ._process_fn_needs_self :
138+ actual_inputs = inspect .getcallargs (
139+ self ._process_fn , self .dofn , * args , ** kwargs ) # pylint: disable=deprecated-method
140+ else :
141+ actual_inputs = inspect .getcallargs (self ._process_fn , * args , ** kwargs ) # pylint: disable=deprecated-method
142+ for var , hint in self ._input_hints .items ():
143+ if hint is actual_inputs [var ]:
144+ # self parameter
145+ continue
146+ _check_instance_type (hint , actual_inputs [var ], var , True )
147+ result = self ._type_check_result (self .dofn .process (* args , ** kwargs ))
148+ except TypeCheckError as e :
149+ raise self ._add_label (e )
103150 else :
104151 return self ._check_type (result )
105152
153+ def _add_label (self , e ):
154+ """Returns a TypeCheckError labeled with this transform's full label."""
155+ # TODO(BEAM-10710): Remove the 'ParDo' prefix for the label name
156+ error_msg = (
157+ 'Runtime type violation detected within ParDo(%s): '
158+ '%s' % (self .full_label , e ))
159+ _ , _ , tb = sys .exc_info ()
160+ return TypeCheckError (error_msg ).with_traceback (tb )
161+
106162 @staticmethod
107163 def _check_type (output ):
108164 if output is None :
@@ -120,36 +176,6 @@ def _check_type(output):
120176 'iterable. %s was returned instead.' % type (output ))
121177 return output
122178
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-
153179 def _type_check_result (self , transform_results ):
154180 if self ._output_type_hint is None or transform_results is None :
155181 return transform_results
@@ -289,11 +315,9 @@ def visit_transform(self, applied_transform):
289315 if isinstance (transform .fn , core .CombineValuesDoFn ):
290316 transform .fn .combinefn = self ._wrapped_fn
291317 else :
292- transform .fn = transform .dofn = OutputCheckWrapperDoFn (
293- TypeCheckWrapperDoFn (
294- transform .fn ,
295- transform .get_type_hints (),
296- applied_transform .full_label ),
318+ transform .fn = transform .dofn = RuntimeTypeCheckWrapperDoFn (
319+ transform .fn ,
320+ transform .get_type_hints (),
297321 applied_transform .full_label )
298322
299323
0 commit comments