diff --git a/CHANGES.rst b/CHANGES.rst index b4ca2792..5b5c85af 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,8 @@ Changes ^^^^^^^^^^^^^^^^^^^ * patch `ResponseParser` * use SPDX license identifier for project metadata +* refactor ``AioResponseParser`` to have a consistent async implementation of the `parse` method. + sync `parse` methods are deprecated and will be removed in a future release.`` 2.21.1 (2025-03-04) ^^^^^^^^^^^^^^^^^^^ diff --git a/aiobotocore/endpoint.py b/aiobotocore/endpoint.py index 2ab4fd8b..1159ddce 100644 --- a/aiobotocore/endpoint.py +++ b/aiobotocore/endpoint.py @@ -1,4 +1,6 @@ import asyncio +import inspect +import warnings from botocore.endpoint import ( DEFAULT_TIMEOUT, @@ -204,15 +206,15 @@ async def _do_get_response(self, request, operation_model, context): ) parser = self._response_parser_factory.create_parser(protocol) - if asyncio.iscoroutinefunction(parser.parse): - parsed_response = await parser.parse( - response_dict, operation_model.output_shape - ) + parsed_response = parser.parse( + response_dict, operation_model.output_shape + ) + if inspect.isawaitable(parsed_response): + parsed_response = await parsed_response else: - parsed_response = parser.parse( - response_dict, operation_model.output_shape + warnings.deprecated( + f"The use of non-async `parse` function on {parser.__class__.__name__} is deprecated." ) - parsed_response.update(customized_response_dict) if http_response.status_code >= 300: @@ -240,10 +242,13 @@ async def _add_modeled_error_fields( if error_shape is None: return - if asyncio.iscoroutinefunction(parser.parse): - modeled_parse = await parser.parse(response_dict, error_shape) + modeled_parse = parser.parse(response_dict, error_shape) + if inspect.isawaitable(modeled_parse): + modeled_parse = await modeled_parse else: - modeled_parse = parser.parse(response_dict, error_shape) + warnings.deprecated( + f"The use of non-async `parse` function on {parser.__class__.__name__} is deprecated." + ) # TODO: avoid naming conflicts with ResponseMetadata and Error parsed_response.update(modeled_parse) diff --git a/aiobotocore/parsers.py b/aiobotocore/parsers.py index 44840220..4181549c 100644 --- a/aiobotocore/parsers.py +++ b/aiobotocore/parsers.py @@ -12,6 +12,7 @@ lowercase_dict, ) +from ._helpers import resolve_awaitable from .eventstream import AioEventStream @@ -31,6 +32,39 @@ def _create_event_stream(self, response, shape): name = response['context'].get('operation_name') return AioEventStream(response['body'], shape, parser, name) + async def parse(self, response, shape): + LOG.debug('Response headers: %s', response['headers']) + LOG.debug('Response body:\n%s', response['body']) + if response['status_code'] >= 301: + if self._is_generic_error_response(response): + parsed = self._do_generic_error_parse(response) + elif self._is_modeled_error_shape(shape): + parsed = self._do_modeled_error_parse(response, shape) + # We don't want to decorate the modeled fields with metadata + return parsed + else: + parsed = self._do_error_parse(response, shape) + else: + parsed = await resolve_awaitable(self._do_parse(response, shape)) + + # We don't want to decorate event stream responses with metadata + if shape and shape.serialization.get('eventstream'): + return parsed + + # Add ResponseMetadata if it doesn't exist and inject the HTTP + # status code and headers from the response. + if isinstance(parsed, dict): + response_metadata = parsed.get('ResponseMetadata', {}) + response_metadata['HTTPStatusCode'] = response['status_code'] + # Ensure that the http header keys are all lower cased. Older + # versions of urllib3 (< 1.11) would unintentionally do this for us + # (see urllib3#633). We need to do this conversion manually now. + headers = response['headers'] + response_metadata['HTTPHeaders'] = lowercase_dict(headers) + parsed['ResponseMetadata'] = response_metadata + self._add_checksum_response_metadata(response, response_metadata) + return parsed + class AioQueryParser(QueryParser, AioResponseParser): pass @@ -66,41 +100,6 @@ async def _handle_event_stream(self, response, shape, event_name): parsed[event_name] = event_stream return parsed - # this is actually from ResponseParser however for now JSONParser is the - # only class that needs this async - async def parse(self, response, shape): - LOG.debug('Response headers: %s', response['headers']) - LOG.debug('Response body:\n%s', response['body']) - if response['status_code'] >= 301: - if self._is_generic_error_response(response): - parsed = self._do_generic_error_parse(response) - elif self._is_modeled_error_shape(shape): - parsed = self._do_modeled_error_parse(response, shape) - # We don't want to decorate the modeled fields with metadata - return parsed - else: - parsed = self._do_error_parse(response, shape) - else: - parsed = await self._do_parse(response, shape) - - # We don't want to decorate event stream responses with metadata - if shape and shape.serialization.get('eventstream'): - return parsed - - # Add ResponseMetadata if it doesn't exist and inject the HTTP - # status code and headers from the response. - if isinstance(parsed, dict): - response_metadata = parsed.get('ResponseMetadata', {}) - response_metadata['HTTPStatusCode'] = response['status_code'] - # Ensure that the http header keys are all lower cased. Older - # versions of urllib3 (< 1.11) would unintentionally do this for us - # (see urllib3#633). We need to do this conversion manually now. - headers = response['headers'] - response_metadata['HTTPHeaders'] = lowercase_dict(headers) - parsed['ResponseMetadata'] = response_metadata - self._add_checksum_response_metadata(response, response_metadata) - return parsed - class AioRestJSONParser(RestJSONParser, AioResponseParser): pass diff --git a/aiobotocore/response.py b/aiobotocore/response.py index 923d3b5c..71a18241 100644 --- a/aiobotocore/response.py +++ b/aiobotocore/response.py @@ -150,10 +150,5 @@ async def get_response(operation_model, http_response): response_dict['body'] = await http_response.content parser = parsers.create_parser(protocol) - if asyncio.iscoroutinefunction(parser.parse): - parsed = await parser.parse( - response_dict, operation_model.output_shape - ) - else: - parsed = parser.parse(response_dict, operation_model.output_shape) + parsed = await parser.parse(response_dict, operation_model.output_shape) return http_response, parsed diff --git a/tests/test_patches.py b/tests/test_patches.py index 739eb506..55f78572 100644 --- a/tests/test_patches.py +++ b/tests/test_patches.py @@ -428,6 +428,7 @@ ResponseParser._create_event_stream: { '0564ba55383a71cc1ba3e5be7110549d7e9992f5', }, + ResponseParser.parse: {'c2153eac3789855f4fc6a816a1f30a6afe0cf969'}, RestXMLParser._create_event_stream: { '0564ba55383a71cc1ba3e5be7110549d7e9992f5' }, @@ -444,8 +445,6 @@ JSONParser._handle_event_stream: { '3cf7bb1ecff0d72bafd7e7fd6625595b4060abd6' }, - # NOTE: if this hits we need to change our ResponseParser impl in JSONParser - JSONParser.parse: {'c2153eac3789855f4fc6a816a1f30a6afe0cf969'}, RestJSONParser._create_event_stream: { '0564ba55383a71cc1ba3e5be7110549d7e9992f5' },