1717from unittest import mock
1818import os
1919import pytest
20+ import pytest_asyncio
2021
2122from typing import Sequence , Tuple
2223
6061 except :
6162 HAS_ASYNC_REST_IDENTITY_TRANSPORT = False
6263
64+ _GRPC_VERSION = grpc .__version__
65+
6366 # TODO: use async auth anon credentials by default once the minimum version of google-auth is upgraded.
6467 # See related issue: https://github.com/googleapis/gapic-generator-python/issues/2107.
6568 def async_anonymous_credentials ():
@@ -79,7 +82,7 @@ def async_anonymous_credentials():
7982 def event_loop ():
8083 return asyncio .get_event_loop ()
8184
82- @pytest .fixture (params = ["grpc_asyncio" , "rest_asyncio" ])
85+ @pytest_asyncio .fixture (params = ["grpc_asyncio" , "rest_asyncio" ])
8386 def async_echo (use_mtls , request , event_loop ):
8487 transport = request .param
8588 if transport == "rest_asyncio" and not HAS_ASYNC_REST_ECHO_TRANSPORT :
@@ -94,7 +97,7 @@ def async_echo(use_mtls, request, event_loop):
9497 credentials = async_anonymous_credentials (),
9598 )
9699
97- @pytest .fixture (params = ["grpc_asyncio" , "rest_asyncio" ])
100+ @pytest_asyncio .fixture (params = ["grpc_asyncio" , "rest_asyncio" ])
98101 def async_identity (use_mtls , request , event_loop ):
99102 transport = request .param
100103 if transport == "rest_asyncio" and not HAS_ASYNC_REST_IDENTITY_TRANSPORT :
@@ -310,9 +313,18 @@ def __init__(self, key, value):
310313
311314 def _add_request_metadata (self , client_call_details ):
312315 if client_call_details .metadata is not None :
316+ # https://grpc.github.io/grpc/python/glossary.html#term-metadata.
317+ # For sync, `ClientCallDetails.metadata` is a list.
318+ # Whereas for async, `ClientCallDetails.metadata` is a mapping.
319+ # https://grpc.github.io/grpc/python/grpc_asyncio.html#grpc.aio.Metadata
313320 client_call_details .metadata .append ((self ._key , self ._value ))
314321 self .request_metadata = client_call_details .metadata
315322
323+ def _read_response_metadata_stream (self ):
324+ # Access the metadata via the original stream object
325+ if hasattr (self , "_original_stream" ):
326+ self .response_metadata = self ._original_stream .trailing_metadata ()
327+
316328 def intercept_unary_unary (self , continuation , client_call_details , request ):
317329 self ._add_request_metadata (client_call_details )
318330 response = continuation (client_call_details , request )
@@ -323,6 +335,7 @@ def intercept_unary_unary(self, continuation, client_call_details, request):
323335 def intercept_unary_stream (self , continuation , client_call_details , request ):
324336 self ._add_request_metadata (client_call_details )
325337 response_it = continuation (client_call_details , request )
338+ self ._original_stream = response_it
326339 return response_it
327340
328341 def intercept_stream_unary (
@@ -337,6 +350,7 @@ def intercept_stream_stream(
337350 ):
338351 self ._add_request_metadata (client_call_details )
339352 response_it = continuation (client_call_details , request_iterator )
353+ self ._original_stream = response_it
340354 return response_it
341355
342356
@@ -354,8 +368,21 @@ def __init__(self, key, value):
354368
355369 async def _add_request_metadata (self , client_call_details ):
356370 if client_call_details .metadata is not None :
357- client_call_details .metadata .append ((self ._key , self ._value ))
358- self .request_metadata = client_call_details .metadata
371+ # As of gRPC 1.75.0 and newer,
372+ # https://grpc.github.io/grpc/python/grpc_asyncio.html#grpc.aio.Metadata
373+ # Note that for async, `ClientCallDetails.metadata` is a mapping.
374+ # Whereas for sync, `ClientCallDetails.metadata` is a list.
375+ # https://grpc.github.io/grpc/python/glossary.html#term-metadata.
376+ # Prior to gRPC 1.75.0, `ClientCallDetails.metadata` is a list
377+ # for both sync and async.
378+ grpc_major , grpc_minor = [
379+ int (part ) for part in _GRPC_VERSION .split ("." )[0 :2 ]
380+ ]
381+ if grpc_major == 1 and grpc_minor < 75 :
382+ client_call_details .metadata .append ((self ._key , self ._value ))
383+ else :
384+ client_call_details .metadata [self ._key ] = self ._value
385+ self .request_metadata = list (client_call_details .metadata )
359386
360387 async def intercept_unary_unary (self , continuation , client_call_details , request ):
361388 await self ._add_request_metadata (client_call_details )
@@ -407,8 +434,8 @@ def intercepted_echo_grpc(use_mtls):
407434 return EchoClient (transport = transport ), interceptor
408435
409436
410- @pytest .fixture
411- def intercepted_echo_grpc_async ():
437+ @pytest_asyncio .fixture
438+ async def intercepted_echo_grpc_async ():
412439 # The interceptor adds 'showcase-trailer' client metadata. Showcase server
413440 # echoes any metadata with key 'showcase-trailer', so the same metadata
414441 # should appear as trailing metadata in the response.
0 commit comments