Skip to content

Test regression when grpcio is installed but grpcio-status is not #301

Closed
@mgorny

Description

@mgorny

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 --version 3.8.12, 3.9.7
  • pip version: pip --version n/a
  • google-api-core version: pip show google-api-core 2.2.0, 2.2.1, git master (d2a729e)

Steps to reproduce

  1. git clone https://github.com/googleapis/python-api-core
  2. cd python-api-core
  3. python3.9 -m venv .venv
  4. . .venv/bin/activate
  5. pip install . grpcio pytest mock proto-plus pytest-asyncio
  6. pytest

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'

Metadata

Metadata

Labels

priority: p2Moderately-important priority. Fix may not be included in next release.type: bugError or flaw in code with unintended results or allowing sub-optimal usage patterns.

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions