Skip to content

Commit d66743b

Browse files
authored
Fix 'UnaryUnaryCall' object has no attribute 'exception' error in async requests (#1061)
1 parent cfdea2b commit d66743b

4 files changed

Lines changed: 76 additions & 51 deletions

File tree

google/ads/googleads/interceptors/exception_interceptor.py

Lines changed: 24 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -159,8 +159,8 @@ def __await__(self):
159159
return response
160160
else:
161161
return util.convert_proto_plus_to_protobuf(response)
162-
except grpc.RpcError:
163-
yield from self._interceptor._handle_grpc_failure_async(self._call).__await__()
162+
except grpc.RpcError as exception:
163+
yield from self._interceptor._handle_grpc_failure_async(self._call, exception).__await__()
164164
raise
165165

166166
def cancel(self):
@@ -209,8 +209,8 @@ async def _wrapped_aiter():
209209
yield response
210210
else:
211211
yield util.convert_proto_plus_to_protobuf(response)
212-
except grpc.RpcError:
213-
await self._interceptor._handle_grpc_failure_async(self._call)
212+
except grpc.RpcError as exception:
213+
await self._interceptor._handle_grpc_failure_async(self._call, exception)
214214
raise
215215
return _wrapped_aiter()
216216

@@ -252,19 +252,30 @@ async def read(self):
252252
return response
253253
else:
254254
return util.convert_proto_plus_to_protobuf(response)
255-
except grpc.RpcError:
256-
await self._interceptor._handle_grpc_failure_async(self._call)
255+
except grpc.RpcError as exception:
256+
await self._interceptor._handle_grpc_failure_async(self._call, exception)
257257
raise
258258

259-
class _AsyncExceptionInterceptor(
260-
ExceptionInterceptor,
261-
):
259+
class _AsyncExceptionInterceptor(Interceptor, grpc.aio.UnaryUnaryClientInterceptor, grpc.aio.UnaryStreamClientInterceptor,):
262260
"""An interceptor that wraps rpc exceptions."""
263261

264-
async def _handle_grpc_failure_async(self, response: grpc.aio.Call):
262+
def __init__(self, api_version: str, use_proto_plus: bool = False):
263+
"""Initializes the ExceptionInterceptor.
264+
265+
Args:
266+
api_version: a str of the API version of the request.
267+
use_proto_plus: a boolean of whether returned messages should be
268+
proto_plus or protobuf.
269+
"""
270+
super().__init__(api_version)
271+
self._api_version = api_version
272+
self._use_proto_plus = use_proto_plus
273+
274+
async def _handle_grpc_failure_async(self, response: grpc.aio.Call, exception: grpc.RpcError):
265275
"""Async version of _handle_grpc_failure."""
266-
status_code = response.code()
267-
response_exception = response.exception()
276+
status_code = await response.code()
277+
278+
response_exception = exception
268279

269280
# We need to access _RETRY_STATUS_CODES from interceptor module?
270281
# It's imported in interceptor.py but not exposed in ExceptionInterceptor?
@@ -300,7 +311,7 @@ async def _handle_grpc_failure_async(self, response: grpc.aio.Call):
300311
raise response_exception
301312

302313
# If we got here, maybe no exception? But we only call this on error.
303-
raise response.exception()
314+
raise response_exception
304315

305316
async def intercept_unary_unary(
306317
self,

google/ads/googleads/interceptors/logging_interceptor.py

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -468,10 +468,13 @@ async def _log_request_async(
468468
# Since this is called in on_done, it is done.
469469

470470
try:
471-
# This might raise if cancelled?
472-
exception = response.exception()
473-
except Exception:
474-
exception = None
471+
if hasattr(response, "exception"):
472+
exception = response.code()
473+
else:
474+
await response.code()
475+
exception = None
476+
except Exception as ex:
477+
exception = ex
475478

476479
if exception:
477480
# We need to adapt exception logging for async exception?

tests/interceptors/exception_interceptor_test.py

Lines changed: 23 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -361,7 +361,7 @@ async def test_handle_grpc_failure(self):
361361
mock_error_message = _MOCK_FAILURE_VALUE
362362

363363
class MockRpcErrorResponse(grpc.RpcError):
364-
def code(self):
364+
async def code(self):
365365
return grpc.StatusCode.INVALID_ARGUMENT
366366

367367
async def trailing_metadata(self):
@@ -373,13 +373,16 @@ def exception(self):
373373
interceptor = self._create_test_interceptor()
374374

375375
with self.assertRaises(GoogleAdsException):
376-
await interceptor._handle_grpc_failure_async(MockRpcErrorResponse())
376+
error_response = MockRpcErrorResponse()
377+
await interceptor._handle_grpc_failure_async(
378+
error_response, error_response
379+
)
377380

378381
async def test_handle_grpc_failure_retryable(self):
379382
"""Raises retryable exceptions as-is."""
380383

381384
class MockRpcErrorResponse(grpc.RpcError):
382-
def code(self):
385+
async def code(self):
383386
return grpc.StatusCode.INTERNAL
384387

385388
def exception(self):
@@ -388,13 +391,16 @@ def exception(self):
388391
interceptor = self._create_test_interceptor()
389392

390393
with self.assertRaises(MockRpcErrorResponse):
391-
await interceptor._handle_grpc_failure_async(MockRpcErrorResponse())
394+
error_response = MockRpcErrorResponse()
395+
await interceptor._handle_grpc_failure_async(
396+
error_response, error_response
397+
)
392398

393399
async def test_handle_grpc_failure_not_google_ads_failure(self):
394400
"""Raises as-is non-retryable non-GoogleAdsFailure exceptions."""
395401

396402
class MockRpcErrorResponse(grpc.RpcError):
397-
def code(self):
403+
async def code(self):
398404
return grpc.StatusCode.INVALID_ARGUMENT
399405

400406
async def trailing_metadata(self):
@@ -406,7 +412,10 @@ def exception(self):
406412
interceptor = self._create_test_interceptor()
407413

408414
with self.assertRaises(MockRpcErrorResponse):
409-
await interceptor._handle_grpc_failure_async(MockRpcErrorResponse())
415+
error_response = MockRpcErrorResponse()
416+
await interceptor._handle_grpc_failure_async(
417+
error_response, error_response
418+
)
410419

411420
async def test_intercept_unary_unary_response_is_exception(self):
412421
"""If response.exception() is not None exception is handled."""
@@ -439,7 +448,7 @@ async def mock_continuation(client_call_details, request):
439448
except grpc.RpcError:
440449
pass
441450

442-
mock_handle.assert_called_once_with(mock_call)
451+
mock_handle.assert_called_once_with(mock_call, mock_exception)
443452

444453
async def test_intercept_unary_stream_response_is_exception(self):
445454
"""Ensure errors raised from response iteration are handled/wrapped."""
@@ -470,21 +479,15 @@ async def mock_continuation(client_call_details, request):
470479
mock_continuation, mock_client_call_details, mock_request
471480
)
472481

473-
# Ensure the returned value is a wrapped response object.
474482
self.assertIsInstance(response, _AsyncUnaryStreamCallWrapper)
475483

476-
# Initiate an iteration of the wrapped response object
477484
try:
478485
async for _ in response:
479-
# This loop body should not be entered because the exception
480-
# is raised on the first attempt to get an item.
481486
pass
482487
except grpc.RpcError:
483488
pass
484489

485-
# Check that the error handler method on the interceptor instance
486-
# was called as a result of the iteration.
487-
mock_handle.assert_called_once_with(mock_call)
490+
mock_handle.assert_called_once_with(mock_call, mock_exception)
488491

489492
async def test_intercept_unary_unary_response_is_successful(self):
490493
"""If response.exception() is None response is returned."""
@@ -625,7 +628,9 @@ async def mock_continuation(client_call_details, request):
625628
found = True
626629
break # We only need the first item
627630

628-
self.assertTrue(found, "Iterator should have yielded at least one message")
631+
self.assertTrue(
632+
found, "Iterator should have yielded at least one message"
633+
)
629634
self.assertIsInstance(message, proto.Message)
630635

631636
async def test_intercept_unary_stream_protobuf_proto(self):
@@ -661,5 +666,7 @@ async def mock_continuation(client_call_details, request):
661666
found = True
662667
break # We only need the first item
663668

664-
self.assertTrue(found, "Iterator should have yielded at least one message")
669+
self.assertTrue(
670+
found, "Iterator should have yielded at least one message"
671+
)
665672
self.assertIsInstance(message, ProtobufMessageType)

tests/interceptors/logging_interceptor_test.py

Lines changed: 22 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@
1313
# limitations under the License.
1414
"""Tests for the Logging gRPC Interceptor."""
1515

16-
1716
from importlib import import_module
1817
import json
1918
import logging
@@ -32,7 +31,6 @@
3231
import google.ads.googleads.interceptors.logging_interceptor as interceptor_module
3332
from google.ads.googleads import util
3433

35-
3634
default_version = Client._DEFAULT_VERSION
3735
module_prefix = f"google.ads.googleads.{default_version}"
3836

@@ -60,7 +58,6 @@
6058
)
6159

6260

63-
6461
class AwaitableMagicMock(mock.MagicMock):
6562
def __await__(self):
6663
return self._await_impl().__await__()
@@ -937,7 +934,9 @@ def _create_test_interceptor(
937934
if not version:
938935
version = default_version
939936

940-
return interceptor_module._AsyncLoggingInterceptor(logger, version, endpoint)
937+
return interceptor_module._AsyncLoggingInterceptor(
938+
logger, version, endpoint
939+
)
941940

942941
def _get_mock_client_call_details(self):
943942
mock_client_call_details = mock.Mock()
@@ -970,39 +969,41 @@ def _get_mock_exception(self):
970969
def _get_mock_response(self, failed=False, streaming=False):
971970
mock_response = AwaitableMagicMock()
972971

973-
# Async trailing_metadata
974972
async def mock_trailing_metadata():
975973
return self._MOCK_TRAILING_METADATA
974+
976975
mock_response.trailing_metadata = mock_trailing_metadata
977976

978-
# Sync exception
979-
def mock_exception_fn():
980-
if failed:
977+
if failed:
978+
979+
def mock_exception_fn():
981980
return self._get_mock_exception()
982-
return None
983-
mock_response.exception = mock_exception_fn
984981

985-
# Async await for UnaryUnary
982+
mock_response.exception = mock_exception_fn
983+
mock_response.code = lambda: self._get_mock_exception()
984+
else:
985+
del mock_response.exception
986+
987+
async def mock_code():
988+
return 0
989+
990+
mock_response.code = mock_code
991+
986992
async def get_result():
987993
if streaming:
988994
return self._MOCK_STREAM
989995
return self._MOCK_RESPONSE_MSG
990996

991997
mock_response._await_impl = get_result
992998

993-
# For streaming, we might need 'read' attribute to distinguish
994999
if streaming:
9951000
mock_response.read = mock.AsyncMock(return_value=self._MOCK_STREAM)
9961001
else:
9971002
del mock_response.read
9981003

9991004
del mock_response.result
10001005

1001-
# Sync add_done_callback
10021006
def mock_add_done_callback(fn):
1003-
# In async interceptor, this is called to register the logging task.
1004-
# We want to execute it immediately or schedule it.
1005-
# Since fn expects a future (the call), and mock_response is the call.
10061007
fn(mock_response)
10071008

10081009
mock_response.add_done_callback = mock_add_done_callback
@@ -1013,6 +1014,7 @@ def _get_mock_continuation_fn(self, fail=False):
10131014
async def mock_continuation_fn(*args):
10141015
mock_response = self._get_mock_response(fail)
10151016
return mock_response
1017+
10161018
return mock_continuation_fn
10171019

10181020
async def test_intercept_unary_unary_unconfigured(self):
@@ -1058,7 +1060,9 @@ async def test_intercept_unary_unary_successful_request(self):
10581060
mock_request = self._get_mock_request()
10591061

10601062
# We need to get the response to assert against it
1061-
mock_response = await mock_continuation_fn(mock_client_call_details, mock_request)
1063+
mock_response = await mock_continuation_fn(
1064+
mock_client_call_details, mock_request
1065+
)
10621066
mock_trailing_metadata = await mock_response.trailing_metadata()
10631067

10641068
with (
@@ -1156,7 +1160,7 @@ async def mock_continuation_fn(*args):
11561160
initial_metadata,
11571161
mock_request,
11581162
trailing_metadata,
1159-
None, # Result is None for stream in async interceptor
1163+
None, # Result is None for stream in async interceptor
11601164
)
11611165
)
11621166

0 commit comments

Comments
 (0)