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
a9fe3d1
710f645
811bae7
0902702
0754825
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,68 @@ 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_owner = process_fn.__self__ | ||||||
| else: | ||||||
| self._process_fn = process_fn | ||||||
| self._process_fn_owner = None | ||||||
|
|
||||||
| 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_owner is not None: | ||||||
| actual_inputs = inspect.getcallargs( # pylint: disable=deprecated-method | ||||||
| self._process_fn, self._process_fn_owner, *args, **kwargs) | ||||||
| else: | ||||||
| actual_inputs = inspect.getcallargs( # pylint: disable=deprecated-method | ||||||
| self._process_fn, *args, **kwargs) | ||||||
|
|
||||||
| 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) | ||||||
|
|
||||||
|
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: When 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. I tested this change with the regression test I just pushed. It fails because the |
||||||
| 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 +163,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 +302,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.
This block (94-99) binds arguments differently from master: it passes the DoFn where master passed the method's own object. Not sure of all the cases this affects, but for example it differs for beam.Map(obj.method) with a method whose first parameter is *args
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.
Hello, I updated the wrapper to preserve
process_fn.__self__and pass the original bound-method owner toinspect.getcallargs, rather than passing the DoFn wrapper. I also added a regression test covering a bound method with*args.