Repository navigation
Test regression when grpcio is installed but grpcio-status is not #301
Description
CC @atulep
The following commit causes a regression in the test suite:
commit ef6f0fcfdfe771172056e35e3c990998b3b00416
Author: Aza Tulepbergenov <aza.tulepber@gmail.com>
Date: 2021-10-19 20:17:49 +0200
feat: add 'GoogleAPICallError.error_details' property (#286)
Based on 'google.rpc.status.details'.
Prior to this commit, the package handled both missing and present grpc consistently. However, the following condition in exceptions.py:
try:
import grpc
from grpc_status import rpc_status
except ImportError: # pragma: NO COVER
grpc = None
rpc_status = None
means that if grpc_status module is not installed, the exception module behaves as if grpc wasn't present even though other modules (notably tests) behave as if it was present. As a result, people having one but not the other module installed get a bunch of test failures because grpc-based tests are run but e.g. the exception module's from_grpc_error() function fails since grpc is forced to None.
This is especially problematic since grpcio-status is a new optional dependency, and since it's not packaged on Gentoo, we'd effectively have to force people to uninstall grpcio.
Environment details
- OS type and version: Gentoo Linux
- Python version:
python --version3.8.12, 3.9.7 - pip version:
pip --versionn/a google-api-coreversion:pip show google-api-core2.2.0, 2.2.1, git master (d2a729e)
Steps to reproduce
git clone https://github.com/googleapis/python-api-corecd python-api-corepython3.9 -m venv .venv. .venv/bin/activatepip install . grpcio pytest mock proto-plus pytest-asynciopytest
Stack trace
Example test failure:
_______________________________________________________ test_wrap_unary_errors ________________________________________________________
self = <google.api_core.grpc_helpers_async._WrappedUnaryUnaryCall object at 0x7fe0e3de3790>
def __await__(self):
try:
> response = yield from self._call.__await__()
.venv/lib/python3.9/site-packages/google/api_core/grpc_helpers_async.py:84:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
_mock_self = <AsyncMock id='140603872279952'>, args = (1, 2), kwargs = {'three': 'four'}, self = <AsyncMock id='140603872279952'>
_call = call(1, 2, three='four'), effect = RpcErrorImpl()
async def _execute_mock_call(_mock_self, *args, **kwargs):
self = _mock_self
# This is nearly just like super(), except for special handling
# of coroutines
_call = _Call((args, kwargs), two=True)
self.await_count += 1
self.await_args = _call
self.await_args_list.append(_call)
effect = self.side_effect
if effect is not None:
if _is_exception(effect):
> raise effect
E tests.asyncio.test_grpc_helpers_async.RpcErrorImpl
.venv/lib/python3.9/site-packages/mock/mock.py:2169: RpcErrorImpl
During handling of the above exception, another exception occurred:
@pytest.mark.asyncio
async def test_wrap_unary_errors():
grpc_error = RpcErrorImpl(grpc.StatusCode.INVALID_ARGUMENT)
callable_ = mock.AsyncMock(spec=["__call__"], side_effect=grpc_error)
wrapped_callable = grpc_helpers_async._wrap_unary_errors(callable_)
with pytest.raises(exceptions.InvalidArgument) as exc_info:
> await wrapped_callable(1, 2, three="four")
tests/asyncio/test_grpc_helpers_async.py:57:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
.venv/lib/python3.9/site-packages/google/api_core/grpc_helpers_async.py:87: in __await__
raise exceptions.from_grpc_error(rpc_error) from rpc_error
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
rpc_exc = RpcErrorImpl()
def from_grpc_error(rpc_exc):
"""Create a :class:`GoogleAPICallError` from a :class:`grpc.RpcError`.
Args:
rpc_exc (grpc.RpcError): The gRPC error.
Returns:
GoogleAPICallError: An instance of the appropriate subclass of
:class:`GoogleAPICallError`.
"""
# NOTE(lidiz) All gRPC error shares the parent class grpc.RpcError.
# However, check for grpc.RpcError breaks backward compatibility.
> if isinstance(rpc_exc, grpc.Call) or _is_informative_grpc_error(rpc_exc):
E AttributeError: 'NoneType' object has no attribute 'Call'
.venv/lib/python3.9/site-packages/google/api_core/exceptions.py:532: AttributeError
======================================================= short test summary info =======================================================
FAILED tests/asyncio/test_grpc_helpers_async.py::test_wrap_unary_errors - AttributeError: 'NoneType' object has no attribute 'Call'
Activity
busunkim96 commentedon Oct 29, 2021
Hi @mgorny,
Thanks for opening an issue. The intent was that
grpcio-statuswould always be installed next togrpcio(see how the extra is written insetup.py) so I don't think we considered this particular scenario.python-api-core/setup.py
Lines 38 to 42 in d2a729e
That said, I don't think separating the imports into two try/except blocks will make the code terribly more complicated.
Could you go into more details on this? Is it difficult to add
grpcio-status?mgorny commentedon Oct 29, 2021
The point is more like there's no clean way of expressing "if you have grpcio installed, install grpcio-status as well" from the point of python-api-core. We could technically make grpcio always install grpcio-status but if that was the right thing to do, why are they separate packages in the first place?
That said, I'm not maintaining grpcio in Gentoo and i have no interest in it. From my perspective, it's just an optional dependency that now started causing trouble.
liu-du commentedon Jan 31, 2022
Experiencing the same issue indirectly with google pubsub client, the stacktrace is:
ddoskind commentedon Mar 25, 2022
Thanks for taking a look at this!
I've noticed that while the fix in #355 is helpful in figuring out what's going on, consumer libraries (such as
google-ads-pythonin our specific case) will still crash with anAttributeErrorwhenever a gRPC error occurs.Could the relevant code here:
python-api-core/google/api_core/exceptions.py
Lines 600 to 611 in 112049e
grpc.Callactually exists, falling through to just returning an "generic"GoogleAPICallErrorif that's not the case?