diff --git a/botocore/handlers.py b/botocore/handlers.py
index dcebda2405..fe6b3c252e 100644
--- a/botocore/handlers.py
+++ b/botocore/handlers.py
@@ -152,7 +152,7 @@ def check_for_200_error(
if (
http_response is None
or operation_model.has_streaming_output
- or not _has_modeled_body(operation_model)
+ or not operation_model.has_modeled_body_output
):
# A None response can happen if an exception is raised while
# trying to retrieve the response. See Endpoint._get_response().
@@ -190,16 +190,6 @@ def _looks_like_special_case_error(http_response):
return False
-def _has_modeled_body(operation_model):
- if output_shape := operation_model.output_shape:
- for member_shape in output_shape.members.values():
- if not member_shape.serialization.get('location'):
- # If any member is not bound to a location,
- # we can expect a body
- return True
- return False
-
-
def set_operation_specific_signer(context, signing_name, **kwargs):
"""Choose the operation-specific signer.
diff --git a/botocore/model.py b/botocore/model.py
index 677266c8d2..f4070fb6ee 100644
--- a/botocore/model.py
+++ b/botocore/model.py
@@ -707,6 +707,26 @@ def _get_streaming_body(self, shape):
return payload_shape
return None
+ @CachedProperty
+ def has_modeled_body_input(self):
+ return self._has_modeled_body(self.input_shape)
+
+ @CachedProperty
+ def has_modeled_body_output(self):
+ return self._has_modeled_body(self.output_shape)
+
+ def _has_modeled_body(self, shape):
+ """
+ Determines if an operation has a modeled body.
+ If any member is not bound to a location, we can expect a body.
+ """
+ if shape is None:
+ return False
+ for member_shape in shape.members.values():
+ if not member_shape.serialization.get('location'):
+ return True
+ return False
+
def __repr__(self):
return f'{self.__class__.__name__}(name={self.name})'
diff --git a/tests/functional/configured_endpoint_urls/test_configured_endpoint_url.py b/tests/functional/configured_endpoint_urls/test_configured_endpoint_url.py
index 0a067981d0..51649d5bbc 100644
--- a/tests/functional/configured_endpoint_urls/test_configured_endpoint_url.py
+++ b/tests/functional/configured_endpoint_urls/test_configured_endpoint_url.py
@@ -148,7 +148,7 @@ def assert_endpoint_url_used_for_operation(
http_stubber = ClientHTTPStubber(client)
http_stubber.start()
http_stubber.add_response(
- body=(b'' if operation == 'list_buckets' else None)
+ body=(b'' if operation == 'list_buckets' else None)
)
# Call an operation on the client
diff --git a/tests/functional/test_credentials.py b/tests/functional/test_credentials.py
index 1e957f3d6d..a45a73f14e 100644
--- a/tests/functional/test_credentials.py
+++ b/tests/functional/test_credentials.py
@@ -1060,7 +1060,7 @@ def test_token_chosen_from_provider(self):
session = Session(profile='sso-test')
with SessionHTTPStubber(session) as stubber:
self.add_credential_response(stubber)
- stubber.add_response(body=b'')
+ stubber.add_response(body=b'')
with mock.patch.object(
SSOTokenProvider, 'DEFAULT_CACHE_CLS', MockCache
):
@@ -1155,7 +1155,7 @@ def test_credential_context_override(self):
with SessionHTTPStubber(session) as stubber:
s3 = session.create_client('s3')
s3.meta.events.register('before-sign', self._add_fake_creds)
- stubber.add_response(body=b'')
+ stubber.add_response(body=b'')
s3.list_buckets()
request = stubber.requests[0]
assert self.ACCESS_KEY in str(request.headers.get('Authorization'))
diff --git a/tests/functional/test_regions.py b/tests/functional/test_regions.py
index be5f25087d..6e22cc75e5 100644
--- a/tests/functional/test_regions.py
+++ b/tests/functional/test_regions.py
@@ -502,7 +502,7 @@ def create_stubbed_client(self, service_name, region_name, **kwargs):
def test_regionalized_client_endpoint_resolution(self):
client, stubber = self.create_stubbed_client('s3', 'us-east-2')
- stubber.add_response(body=b'')
+ stubber.add_response(body=b'')
client.list_buckets()
self.assertEqual(
stubber.requests[0].url, 'https://s3.us-east-2.amazonaws.com/'
@@ -510,7 +510,7 @@ def test_regionalized_client_endpoint_resolution(self):
def test_regionalized_client_with_unknown_region(self):
client, stubber = self.create_stubbed_client('s3', 'not-real')
- stubber.add_response(body=b'')
+ stubber.add_response(body=b'')
client.list_buckets()
# Validate we don't fall back to partition endpoint for
# regionalized services.
diff --git a/tests/functional/test_s3.py b/tests/functional/test_s3.py
index a4d85db6d4..8a777a6f9a 100644
--- a/tests/functional/test_s3.py
+++ b/tests/functional/test_s3.py
@@ -553,7 +553,7 @@ def test_accesspoint_arn_with_custom_endpoint(self):
self.client, http_stubber = self.create_stubbed_s3_client(
endpoint_url="https://custom.com"
)
- http_stubber.add_response(body=b'')
+ http_stubber.add_response(body=b'')
self.client.list_objects(Bucket=accesspoint_arn)
expected_endpoint = "myendpoint-123456789012.custom.com"
self.assert_endpoint(http_stubber.requests[0], expected_endpoint)
@@ -566,7 +566,7 @@ def test_accesspoint_arn_with_custom_endpoint_and_dualstack(self):
endpoint_url="https://custom.com",
config=Config(s3={"use_dualstack_endpoint": True}),
)
- http_stubber.add_response(body=b'')
+ http_stubber.add_response(body=b'')
self.client.list_objects(Bucket=accesspoint_arn)
expected_endpoint = "myendpoint-123456789012.custom.com"
self.assert_endpoint(http_stubber.requests[0], expected_endpoint)
@@ -615,7 +615,7 @@ def test_signs_with_arn_region(self):
self.client, self.http_stubber = self.create_stubbed_s3_client(
region_name="us-east-1"
)
- self.http_stubber.add_response(body=b'')
+ self.http_stubber.add_response(body=b'')
self.client.list_objects(Bucket=accesspoint_arn)
self.assert_signing_region(self.http_stubber.requests[0], "us-west-2")
@@ -739,7 +739,7 @@ def test_basic_outpost_arn(self):
self.client, self.http_stubber = self.create_stubbed_s3_client(
region_name="us-east-1"
)
- self.http_stubber.add_response(body=b'')
+ self.http_stubber.add_response(body=b'')
self.client.list_objects(Bucket=outpost_arn)
request = self.http_stubber.requests[0]
self.assert_signing_name(request, "s3-outposts")
@@ -761,7 +761,7 @@ def test_basic_outpost_arn_custom_endpoint(self):
self.client, self.http_stubber = self.create_stubbed_s3_client(
endpoint_url="https://custom.com", region_name="us-east-1"
)
- self.http_stubber.add_response(body=b'')
+ self.http_stubber.add_response(body=b'')
self.client.list_objects(Bucket=outpost_arn)
request = self.http_stubber.requests[0]
self.assert_signing_name(request, "s3-outposts")
@@ -965,7 +965,7 @@ def test_s3_object_lambda_arn_with_us_east_1(self):
region_name="us-east-1",
config=Config(s3={"use_arn_region": False}),
)
- self.http_stubber.add_response(body=b'')
+ self.http_stubber.add_response(body=b'')
self.client.list_objects(Bucket=s3_object_lambda_arn)
request = self.http_stubber.requests[0]
self.assert_signing_name(request, "s3-object-lambda")
@@ -983,7 +983,7 @@ def test_basic_s3_object_lambda_arn(self):
self.client, self.http_stubber = self.create_stubbed_s3_client(
region_name="us-east-1"
)
- self.http_stubber.add_response(body=b'')
+ self.http_stubber.add_response(body=b'')
self.client.list_objects(Bucket=s3_object_lambda_arn)
request = self.http_stubber.requests[0]
self.assert_signing_name(request, "s3-object-lambda")
@@ -1051,7 +1051,7 @@ def test_accesspoint_with_global_regions(self):
config=Config(s3={"use_arn_region": True}),
)
- self.http_stubber.add_response(body=b'')
+ self.http_stubber.add_response(body=b'')
self.client.list_objects(Bucket=s3_accesspoint_arn)
request = self.http_stubber.requests[0]
expected_endpoint = (
@@ -1065,7 +1065,7 @@ def test_accesspoint_with_global_regions(self):
region_name="s3-external-1",
)
- self.http_stubber.add_response(body=b'')
+ self.http_stubber.add_response(body=b'')
self.client.list_objects(Bucket=s3_accesspoint_arn)
request = self.http_stubber.requests[0]
expected_endpoint = (
@@ -1154,7 +1154,7 @@ def test_mrap_signing_algorithm_is_sigv4a(self):
self.client, self.http_stubber = self.create_stubbed_s3_client(
region_name="us-west-2"
)
- self.http_stubber.add_response()
+ self.http_stubber.add_response(body=b'')
self.client.list_objects(Bucket=s3_accesspoint_arn)
request = self.http_stubber.requests[0]
self._assert_sigv4a_used(request.headers)
@@ -1241,7 +1241,7 @@ def _assert_mrap_endpoint(
self.client, self.http_stubber = self.create_stubbed_s3_client(
region_name=region, endpoint_url=endpoint_url, config=config
)
- self.http_stubber.add_response()
+ self.http_stubber.add_response(body=b'')
self.client.list_objects(Bucket=arn)
request = self.http_stubber.requests[0]
self.assert_endpoint(request, expected)
@@ -1545,7 +1545,7 @@ def test_content_sha256_set_if_config_value_not_set_list_objects(self):
"s3", self.region, config=config
)
self.http_stubber = ClientHTTPStubber(self.client)
- self.http_stubber.add_response(body=b'')
+ self.http_stubber.add_response(body=b'')
with self.http_stubber:
self.client.list_objects(Bucket="foo")
sent_headers = self.get_sent_headers()
@@ -1563,7 +1563,7 @@ def test_content_sha256_set_s3_on_outpost(self):
"s3", self.region, config=config
)
self.http_stubber = ClientHTTPStubber(self.client)
- self.http_stubber.add_response(body=b'')
+ self.http_stubber.add_response(body=b'')
with self.http_stubber:
self.client.list_objects(Bucket=bucket)
sent_headers = self.get_sent_headers()
@@ -2212,7 +2212,7 @@ def test_checksums_included_in_expected_operations(
"""Validate expected calls include Content-MD5 header"""
client = _create_s3_client()
with ClientHTTPStubber(client) as stub:
- stub.add_response(body=b'')
+ stub.add_response(body=b'')
call = getattr(client, operation)
call(**operation_kwargs)
assert "Content-MD5" in stub.requests[-1].headers
@@ -3658,7 +3658,7 @@ def assert_correct_content_md5(self, request):
self.assertEqual(content_md5, request.headers["Content-MD5"])
def test_escape_keys_in_xml_delete_objects(self):
- self.http_stubber.add_response(body=b'')
+ self.http_stubber.add_response(body=b'')
with self.http_stubber:
self.client.delete_objects(
Bucket="mybucket",
diff --git a/tests/functional/test_s3express.py b/tests/functional/test_s3express.py
index 0cf2307071..a6e04bf661 100644
--- a/tests/functional/test_s3express.py
+++ b/tests/functional/test_s3express.py
@@ -280,7 +280,7 @@ def test_delete_objects_injects_correct_checksum(
with ClientHTTPStubber(default_s3_client) as stubber:
stubber.add_response(body=CREATE_SESSION_RESPONSE)
- stubber.add_response(body=b'')
+ stubber.add_response(body=b'')
default_s3_client.delete_objects(
Bucket=S3EXPRESS_BUCKET,
diff --git a/tests/functional/test_useragent.py b/tests/functional/test_useragent.py
index 660a325318..90386e5157 100644
--- a/tests/functional/test_useragent.py
+++ b/tests/functional/test_useragent.py
@@ -27,7 +27,7 @@ class UACapHTTPStubber(ClientHTTPStubber):
def __init__(self, obj_with_event_emitter):
super().__init__(obj_with_event_emitter, strict=False)
- self.add_response(body=b'') # expect exactly one request
+ self.add_response(body=b'') # expect exactly one request
@property
def captured_ua_string(self):
diff --git a/tests/unit/test_handlers.py b/tests/unit/test_handlers.py
index b62577a406..79fbc497eb 100644
--- a/tests/unit/test_handlers.py
+++ b/tests/unit/test_handlers.py
@@ -1880,9 +1880,7 @@ def test_document_response_params_without_expires(document_expires_mocks):
def operation_model_for_200_error():
operation_model = mock.Mock()
operation_model.has_streaming_output = False
- operation_model.output_shape = mock.Mock()
- operation_model.output_shape.members = {'member': mock.Mock()}
- operation_model.output_shape.members['member'].serialization = {}
+ operation_model.has_modeled_body_output = True
return operation_model
@@ -1944,9 +1942,7 @@ def test_200_response_with_streaming_output_left_untouched(
def test_200_response_with_no_body_left_untouched(
operation_model_for_200_error, response_dict, http_response
):
- operation_model_for_200_error.output_shape.members[
- 'member'
- ].serialization = {'location': 'header'}
+ operation_model_for_200_error.has_modeled_body_output = False
handlers.check_for_200_error(
operation_model_for_200_error, response_dict, http_response
)
diff --git a/tests/unit/test_model.py b/tests/unit/test_model.py
index da95a18bea..f5c75cb0cc 100644
--- a/tests/unit/test_model.py
+++ b/tests/unit/test_model.py
@@ -737,6 +737,91 @@ def test_not_streaming_output_for_operation(self):
self.assertEqual(operation.get_streaming_output(), None)
+class TestOperationModelBody(unittest.TestCase):
+ def setUp(self):
+ super().setUp()
+ self.model = {
+ 'operations': {
+ 'OperationName': {
+ 'name': 'OperationName',
+ 'input': {
+ 'shape': 'OperationRequest',
+ },
+ 'output': {
+ 'shape': 'OperationResponse',
+ },
+ },
+ 'NoBodyOperation': {
+ 'name': 'NoBodyOperation',
+ 'input': {'shape': 'NoBodyOperationRequest'},
+ 'output': {'shape': 'NoBodyOperationResponse'},
+ },
+ },
+ 'shapes': {
+ 'OperationRequest': {
+ 'type': 'structure',
+ 'members': {
+ 'String': {
+ 'shape': 'stringType',
+ },
+ "Body": {
+ 'shape': 'blobType',
+ },
+ },
+ 'payload': 'Body',
+ },
+ 'OperationResponse': {
+ 'type': 'structure',
+ 'members': {
+ 'String': {
+ 'shape': 'stringType',
+ },
+ "Body": {
+ 'shape': 'blobType',
+ },
+ },
+ 'payload': 'Body',
+ },
+ 'NoBodyOperationRequest': {
+ 'type': 'structure',
+ 'members': {
+ 'data': {
+ 'location': 'header',
+ 'locationName': 'x-amz-data',
+ 'shape': 'stringType',
+ }
+ },
+ },
+ 'NoBodyOperationResponse': {
+ 'type': 'structure',
+ 'members': {
+ 'data': {
+ 'location': 'header',
+ 'locationName': 'x-amz-data',
+ 'shape': 'stringType',
+ }
+ },
+ },
+ 'stringType': {
+ 'type': 'string',
+ },
+ 'blobType': {'type': 'blob'},
+ },
+ }
+
+ def test_modeled_body_for_operation_with_body(self):
+ service_model = model.ServiceModel(self.model)
+ operation = service_model.operation_model('OperationName')
+ self.assertTrue(operation.has_modeled_body_input)
+ self.assertTrue(operation.has_modeled_body_output)
+
+ def test_modeled_body_for_operation_with_no_body(self):
+ service_model = model.ServiceModel(self.model)
+ operation = service_model.operation_model('NoBodyOperation')
+ self.assertFalse(operation.has_modeled_body_input)
+ self.assertFalse(operation.has_modeled_body_output)
+
+
class TestDeepMerge(unittest.TestCase):
def setUp(self):
self.shapes = {
diff --git a/tests/unit/test_s3_addressing.py b/tests/unit/test_s3_addressing.py
index d3f6d4f3f8..1629dbf880 100644
--- a/tests/unit/test_s3_addressing.py
+++ b/tests/unit/test_s3_addressing.py
@@ -49,7 +49,7 @@ def enable_hmacv1(self, **kwargs):
def test_list_objects_dns_name(self):
params = {'Bucket': 'safename'}
prepared_request = self.get_prepared_request(
- 'list_objects', params, force_hmacv1=True, body=b''
+ 'list_objects', params, force_hmacv1=True, body=b''
)
self.assertEqual(
prepared_request.url, 'https://safename.s3.amazonaws.com/'
@@ -58,7 +58,7 @@ def test_list_objects_dns_name(self):
def test_list_objects_non_dns_name(self):
params = {'Bucket': 'un_safe_name'}
prepared_request = self.get_prepared_request(
- 'list_objects', params, force_hmacv1=True, body=b''
+ 'list_objects', params, force_hmacv1=True, body=b''
)
self.assertEqual(
prepared_request.url, 'https://s3.amazonaws.com/un_safe_name'
@@ -68,7 +68,7 @@ def test_list_objects_dns_name_non_classic(self):
self.region_name = 'us-west-2'
params = {'Bucket': 'safename'}
prepared_request = self.get_prepared_request(
- 'list_objects', params, force_hmacv1=True, body=b''
+ 'list_objects', params, force_hmacv1=True, body=b''
)
self.assertEqual(
prepared_request.url,
@@ -81,7 +81,7 @@ def test_list_objects_unicode_query_string_eu_central_1(self):
[('Bucket', 'safename'), ('Marker', '\xe4\xf6\xfc-01.txt')]
)
prepared_request = self.get_prepared_request(
- 'list_objects', params, body=b''
+ 'list_objects', params, body=b''
)
self.assertEqual(
prepared_request.url,
@@ -95,7 +95,7 @@ def test_list_objects_in_restricted_regions(self):
self.region_name = 'us-gov-west-1'
params = {'Bucket': 'safename'}
prepared_request = self.get_prepared_request(
- 'list_objects', params, body=b''
+ 'list_objects', params, body=b''
)
# Note how we keep the region specific endpoint here.
self.assertEqual(
@@ -107,7 +107,7 @@ def test_list_objects_in_fips(self):
self.region_name = 'fips-us-gov-west-1'
params = {'Bucket': 'safename'}
prepared_request = self.get_prepared_request(
- 'list_objects', params, body=b''
+ 'list_objects', params, body=b''
)
# Note how we keep the region specific endpoint here.
self.assertEqual(
@@ -119,7 +119,7 @@ def test_list_objects_non_dns_name_non_classic(self):
self.region_name = 'us-west-2'
params = {'Bucket': 'un_safe_name'}
prepared_request = self.get_prepared_request(
- 'list_objects', params, body=b''
+ 'list_objects', params, body=b''
)
self.assertEqual(
prepared_request.url,