Skip to content

Commit b28a4eb

Browse files
author
Jon Wayne Parrott
authored
Add google.api.core.helpers.grpc_helpers (#4041)
1 parent 3f3cdce commit b28a4eb

File tree

2 files changed

+234
-0
lines changed

2 files changed

+234
-0
lines changed
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
# Copyright 2017 Google Inc.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
"""Helpers for :mod:`grpc`."""
16+
17+
import grpc
18+
import six
19+
20+
from google.api.core import exceptions
21+
22+
23+
# The list of gRPC Callable interfaces that return iterators.
24+
_STREAM_WRAP_CLASSES = (
25+
grpc.UnaryStreamMultiCallable,
26+
grpc.StreamStreamMultiCallable,
27+
)
28+
29+
30+
def _patch_callable_name(callable_):
31+
"""Fix-up gRPC callable attributes.
32+
33+
gRPC callable lack the ``__name__`` attribute which causes
34+
:func:`functools.wraps` to error. This adds the attribute if needed.
35+
"""
36+
if not hasattr(callable_, '__name__'):
37+
callable_.__name__ = callable_.__class__.__name__
38+
39+
40+
def _wrap_unary_errors(callable_):
41+
"""Map errors for Unary-Unary and Stream-Unary gRPC callables."""
42+
_patch_callable_name(callable_)
43+
44+
@six.wraps(callable_)
45+
def error_remapped_callable(*args, **kwargs):
46+
try:
47+
return callable_(*args, **kwargs)
48+
except grpc.RpcError as exc:
49+
six.raise_from(exceptions.from_grpc_error(exc), exc)
50+
51+
return error_remapped_callable
52+
53+
54+
def _wrap_stream_errors(callable_):
55+
"""Wrap errors for Unary-Stream and Stream-Stream gRPC callables.
56+
57+
The callables that return iterators require a bit more logic to re-map
58+
errors when iterating. This wraps both the initial invocation and the
59+
iterator of the return value to re-map errors.
60+
"""
61+
_patch_callable_name(callable_)
62+
63+
@six.wraps(callable_)
64+
def error_remapped_callable(*args, **kwargs):
65+
try:
66+
result = callable_(*args, **kwargs)
67+
# Note: we are patching the private grpc._channel._Rendezvous._next
68+
# method as magic methods (__next__ in this case) can not be
69+
# patched on a per-instance basis (see
70+
# https://docs.python.org/3/reference/datamodel.html
71+
# #special-lookup).
72+
# In an ideal world, gRPC would return a *specific* interface
73+
# from *StreamMultiCallables, but they return a God class that's
74+
# a combination of basically every interface in gRPC making it
75+
# untenable for us to implement a wrapper object using the same
76+
# interface.
77+
result._next = _wrap_unary_errors(result._next)
78+
return result
79+
except grpc.RpcError as exc:
80+
six.raise_from(exceptions.from_grpc_error(exc), exc)
81+
82+
return error_remapped_callable
83+
84+
85+
def wrap_errors(callable_):
86+
"""Wrap a gRPC callable and map :class:`grpc.RpcErrors` to friendly error
87+
classes.
88+
89+
Errors raised by the gRPC callable are mapped to the appropriate
90+
:class:`google.api.core.exceptions.GoogleAPICallError` subclasses.
91+
The original `grpc.RpcError` (which is usually also a `grpc.Call`) is
92+
available from the ``response`` property on the mapped exception. This
93+
is useful for extracting metadata from the original error.
94+
95+
Args:
96+
callable_ (Callable): A gRPC callable.
97+
98+
Returns:
99+
Callable: The wrapped gRPC callable.
100+
"""
101+
if isinstance(callable_, _STREAM_WRAP_CLASSES):
102+
return _wrap_stream_errors(callable_)
103+
else:
104+
return _wrap_unary_errors(callable_)
Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
# Copyright 2017 Google Inc.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
import grpc
16+
import mock
17+
import pytest
18+
19+
from google.api.core import exceptions
20+
from google.api.core.helpers import grpc_helpers
21+
22+
23+
def test__patch_callable_name():
24+
callable = mock.Mock(spec=['__class__'])
25+
callable.__class__ = mock.Mock(spec=['__name__'])
26+
callable.__class__.__name__ = 'TestCallable'
27+
28+
grpc_helpers._patch_callable_name(callable)
29+
30+
assert callable.__name__ == 'TestCallable'
31+
32+
33+
def test__patch_callable_name_no_op():
34+
callable = mock.Mock(spec=['__name__'])
35+
callable.__name__ = 'test_callable'
36+
37+
grpc_helpers._patch_callable_name(callable)
38+
39+
assert callable.__name__ == 'test_callable'
40+
41+
42+
class RpcErrorImpl(grpc.RpcError, grpc.Call):
43+
def __init__(self, code):
44+
super(RpcErrorImpl, self).__init__()
45+
self._code = code
46+
47+
def code(self):
48+
return self._code
49+
50+
def details(self):
51+
return None
52+
53+
54+
def test_wrap_unary_errors():
55+
grpc_error = RpcErrorImpl(grpc.StatusCode.INVALID_ARGUMENT)
56+
callable_ = mock.Mock(spec=['__call__'], side_effect=grpc_error)
57+
58+
wrapped_callable = grpc_helpers._wrap_unary_errors(callable_)
59+
60+
with pytest.raises(exceptions.InvalidArgument) as exc_info:
61+
wrapped_callable(1, 2, three='four')
62+
63+
callable_.assert_called_once_with(1, 2, three='four')
64+
assert exc_info.value.response == grpc_error
65+
66+
67+
def test_wrap_stream_errors_invocation():
68+
grpc_error = RpcErrorImpl(grpc.StatusCode.INVALID_ARGUMENT)
69+
callable_ = mock.Mock(spec=['__call__'], side_effect=grpc_error)
70+
71+
wrapped_callable = grpc_helpers._wrap_stream_errors(callable_)
72+
73+
with pytest.raises(exceptions.InvalidArgument) as exc_info:
74+
wrapped_callable(1, 2, three='four')
75+
76+
callable_.assert_called_once_with(1, 2, three='four')
77+
assert exc_info.value.response == grpc_error
78+
79+
80+
class RpcResponseIteratorImpl(object):
81+
def __init__(self, exception):
82+
self._exception = exception
83+
84+
# Note: This matches grpc._channel._Rendezvous._next which is what is
85+
# patched by _wrap_stream_errors.
86+
def _next(self):
87+
raise self._exception
88+
89+
def __next__(self): # pragma: NO COVER
90+
return self._next()
91+
92+
def next(self): # pragma: NO COVER
93+
return self._next()
94+
95+
96+
def test_wrap_stream_errors_iterator():
97+
grpc_error = RpcErrorImpl(grpc.StatusCode.UNAVAILABLE)
98+
response_iter = RpcResponseIteratorImpl(grpc_error)
99+
callable_ = mock.Mock(spec=['__call__'], return_value=response_iter)
100+
101+
wrapped_callable = grpc_helpers._wrap_stream_errors(callable_)
102+
103+
got_iterator = wrapped_callable(1, 2, three='four')
104+
105+
with pytest.raises(exceptions.ServiceUnavailable) as exc_info:
106+
next(got_iterator)
107+
108+
assert got_iterator == response_iter
109+
callable_.assert_called_once_with(1, 2, three='four')
110+
assert exc_info.value.response == grpc_error
111+
112+
113+
@mock.patch('google.api.core.helpers.grpc_helpers._wrap_unary_errors')
114+
def test_wrap_errors_non_streaming(wrap_unary_errors):
115+
callable_ = mock.create_autospec(grpc.UnaryUnaryMultiCallable)
116+
117+
result = grpc_helpers.wrap_errors(callable_)
118+
119+
assert result == wrap_unary_errors.return_value
120+
wrap_unary_errors.assert_called_once_with(callable_)
121+
122+
123+
@mock.patch('google.api.core.helpers.grpc_helpers._wrap_stream_errors')
124+
def test_wrap_errors_streaming(wrap_stream_errors):
125+
callable_ = mock.create_autospec(grpc.UnaryStreamMultiCallable)
126+
127+
result = grpc_helpers.wrap_errors(callable_)
128+
129+
assert result == wrap_stream_errors.return_value
130+
wrap_stream_errors.assert_called_once_with(callable_)

0 commit comments

Comments
 (0)