Repository navigation
[Python] Merge runtime type-check wrappers #40501
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -84,25 +84,67 @@ def teardown(self): | |||||
| return self.dofn.teardown() | ||||||
|
|
||||||
|
|
||||||
| class OutputCheckWrapperDoFn(AbstractDoFnWrapper): | ||||||
| """A DoFn that verifies against common errors in the output type.""" | ||||||
| def __init__(self, dofn, full_label): | ||||||
| class RuntimeTypeCheckWrapperDoFn(AbstractDoFnWrapper): | ||||||
| """A wrapper around a DoFn which performs type-checking of input and output.""" | ||||||
| def __init__(self, dofn, type_hints, full_label): | ||||||
| super().__init__(dofn) | ||||||
| self.full_label = full_label | ||||||
|
|
||||||
| process_fn = self.dofn._process_argspec_fn() | ||||||
| if hasattr(process_fn, '__func__'): | ||||||
| self._process_fn = process_fn.__func__ | ||||||
| self._process_fn_needs_self = True | ||||||
| else: | ||||||
| self._process_fn = process_fn | ||||||
| self._process_fn_needs_self = False | ||||||
|
|
||||||
| if type_hints.input_types: | ||||||
| input_args, input_kwargs = type_hints.input_types | ||||||
| self._input_hints = getcallargs_forhints( | ||||||
| process_fn, *input_args, **input_kwargs) | ||||||
| else: | ||||||
| self._input_hints = None | ||||||
|
|
||||||
| # TODO(robertwb): Multi-output. | ||||||
| self._output_type_hint = type_hints.simple_output_type(full_label) | ||||||
|
|
||||||
| def wrapper(self, method, args, kwargs): | ||||||
| try: | ||||||
| result = method(*args, **kwargs) | ||||||
| result = self._type_check_result(method(*args, **kwargs)) | ||||||
| except TypeCheckError as e: | ||||||
| # TODO(BEAM-10710): Remove the 'ParDo' prefix for the label name | ||||||
| error_msg = ( | ||||||
| 'Runtime type violation detected within ParDo(%s): ' | ||||||
| '%s' % (self.full_label, e)) | ||||||
| _, _, tb = sys.exc_info() | ||||||
| raise TypeCheckError(error_msg).with_traceback(tb) | ||||||
| raise self._add_label(e) | ||||||
| else: | ||||||
| return self._check_type(result) | ||||||
|
|
||||||
| def process(self, *args, **kwargs): | ||||||
| try: | ||||||
| if self._input_hints: | ||||||
| if self._process_fn_needs_self: | ||||||
| actual_inputs = inspect.getcallargs( | ||||||
| self._process_fn, self.dofn, *args, **kwargs) # pylint: disable=deprecated-method | ||||||
| else: | ||||||
| actual_inputs = inspect.getcallargs(self._process_fn, *args, **kwargs) # pylint: disable=deprecated-method | ||||||
|
|
||||||
| for var, hint in self._input_hints.items(): | ||||||
| if hint is actual_inputs[var]: | ||||||
| # self parameter | ||||||
| continue | ||||||
| _check_instance_type(hint, actual_inputs[var], var, True) | ||||||
|
|
||||||
| result = self._type_check_result(self.dofn.process(*args, **kwargs)) | ||||||
| except TypeCheckError as e: | ||||||
| raise self._add_label(e) | ||||||
| else: | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟠 High: Pair with moving
Suggested change
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The same issue applies here: exceptions raised in |
||||||
| return self._check_type(result) | ||||||
|
|
||||||
| def _add_label(self, e): | ||||||
| # TODO(BEAM-10710): Remove the 'ParDo' prefix for the label name. | ||||||
| error_msg = ( | ||||||
| 'Runtime type violation detected within ParDo(%s): ' | ||||||
| '%s' % (self.full_label, e)) | ||||||
| _, _, tb = sys.exc_info() | ||||||
| return TypeCheckError(error_msg).with_traceback(tb) | ||||||
|
|
||||||
| @staticmethod | ||||||
| def _check_type(output): | ||||||
| if output is None: | ||||||
|
|
@@ -120,36 +162,6 @@ def _check_type(output): | |||||
| 'iterable. %s was returned instead.' % type(output)) | ||||||
| return output | ||||||
|
|
||||||
|
|
||||||
| class TypeCheckWrapperDoFn(AbstractDoFnWrapper): | ||||||
| """A wrapper around a DoFn which performs type-checking of input and output. | ||||||
| """ | ||||||
| def __init__(self, dofn, type_hints, label=None): | ||||||
| super().__init__(dofn) | ||||||
| self._process_fn = self.dofn._process_argspec_fn() | ||||||
| if type_hints.input_types: | ||||||
| input_args, input_kwargs = type_hints.input_types | ||||||
| self._input_hints = getcallargs_forhints( | ||||||
| self._process_fn, *input_args, **input_kwargs) | ||||||
| else: | ||||||
| self._input_hints = None | ||||||
| # TODO(robertwb): Multi-output. | ||||||
| self._output_type_hint = type_hints.simple_output_type(label) | ||||||
|
|
||||||
| def wrapper(self, method, args, kwargs): | ||||||
| result = method(*args, **kwargs) | ||||||
| return self._type_check_result(result) | ||||||
|
|
||||||
| def process(self, *args, **kwargs): | ||||||
| if self._input_hints: | ||||||
| actual_inputs = inspect.getcallargs(self._process_fn, *args, **kwargs) # pylint: disable=deprecated-method | ||||||
| for var, hint in self._input_hints.items(): | ||||||
| if hint is actual_inputs[var]: | ||||||
| # self parameter | ||||||
| continue | ||||||
| _check_instance_type(hint, actual_inputs[var], var, True) | ||||||
| return self._type_check_result(self.dofn.process(*args, **kwargs)) | ||||||
|
|
||||||
| def _type_check_result(self, transform_results): | ||||||
| if self._output_type_hint is None or transform_results is None: | ||||||
| return transform_results | ||||||
|
|
@@ -289,11 +301,9 @@ def visit_transform(self, applied_transform): | |||||
| if isinstance(transform.fn, core.CombineValuesDoFn): | ||||||
| transform.fn.combinefn = self._wrapped_fn | ||||||
| else: | ||||||
| transform.fn = transform.dofn = OutputCheckWrapperDoFn( | ||||||
| TypeCheckWrapperDoFn( | ||||||
| transform.fn, | ||||||
| transform.get_type_hints(), | ||||||
| applied_transform.full_label), | ||||||
| transform.fn = transform.dofn = RuntimeTypeCheckWrapperDoFn( | ||||||
| transform.fn, | ||||||
| transform.get_type_hints(), | ||||||
| applied_transform.full_label) | ||||||
|
|
||||||
|
|
||||||
|
|
||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🟠 High:
self._type_check_result(...)is called inside thetry ... except TypeCheckError as e: raise self._add_label(e)block in bothprocess()(line 133) andwrapper()(line 114), whiletype_check_output()inside_type_check_result()(lines 174-177) also catchesTypeCheckErrorand raisesself._add_label(e).When
process()orwrapper()returns a non-generator iterable (e.g., alistortuple, which includesbeam.Mapand list-returningFlatMap/ParDotransforms),for o in transform_results: type_check_output(o)runs synchronously inside_type_check_result. Any outputTypeCheckErroris wrapped byself._add_label(e)insidetype_check_outputand then caught and wrapped byself._add_label(e)a second time inprocess()/wrapper(), producing a duplicated prefix:Runtime type violation detected within ParDo(<label>): Runtime type violation detected within ParDo(<label>): ...Moving
self._type_check_result(result)into theelse:branch (in bothprocessandwrapper) ensuresself._add_label(e)is applied only once for both generator and non-generator outputs.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I tested this change with the regression test I just pushed. It fails because the
TypeCheckErrorraised inelsebypasses the precedingexcept, so theParDolabel is lost. I'll keep_type_check_result()insidetry.