From a7125ca4ba641053aeb4838955ba0f80bbee8028 Mon Sep 17 00:00:00 2001 From: p1c2u Date: Fri, 4 Jun 2021 11:29:44 +0100 Subject: [PATCH] Deserializing refactor --- openapi_core/deserializing/exceptions.py | 9 ------ .../media_types/deserializers.py | 26 +++++++++++++-- .../deserializing/media_types/exceptions.py | 15 +++++++++ .../deserializing/media_types/factories.py | 10 ++++-- .../deserializing/parameters/deserializers.py | 32 ++++++++++++++----- .../deserializing/parameters/exceptions.py | 28 ++++++++++++++-- .../deserializing/parameters/factories.py | 23 ++++++++----- openapi_core/deserializing/parameters/util.py | 2 ++ tests/integration/validation/test_petstore.py | 4 +-- .../integration/validation/test_validators.py | 6 ++-- .../test_media_types_deserializers.py | 9 ++++++ .../test_parameters_deserializers.py | 18 +++++++++-- 12 files changed, 143 insertions(+), 39 deletions(-) create mode 100644 openapi_core/deserializing/media_types/exceptions.py create mode 100644 openapi_core/deserializing/parameters/util.py diff --git a/openapi_core/deserializing/exceptions.py b/openapi_core/deserializing/exceptions.py index b8ae2ed3..f2a0d834 100644 --- a/openapi_core/deserializing/exceptions.py +++ b/openapi_core/deserializing/exceptions.py @@ -1,14 +1,5 @@ -from dataclasses import dataclass - from openapi_core.exceptions import OpenAPIError -@dataclass class DeserializeError(OpenAPIError): """Deserialize operation error""" - value: str - style: str - - def __str__(self): - return "Failed to deserialize value {value} with style {style}".format( - value=self.value, style=self.style) diff --git a/openapi_core/deserializing/media_types/deserializers.py b/openapi_core/deserializing/media_types/deserializers.py index b47b8848..a7d65f28 100644 --- a/openapi_core/deserializing/media_types/deserializers.py +++ b/openapi_core/deserializing/media_types/deserializers.py @@ -1,7 +1,27 @@ -from openapi_core.deserializing.exceptions import DeserializeError +import warnings +from openapi_core.deserializing.media_types.exceptions import ( + MediaTypeDeserializeError, +) -class PrimitiveDeserializer: + +class BaseMediaTypeDeserializer: + + def __init__(self, mimetype): + self.mimetype = mimetype + + def __call__(self, value): + raise NotImplementedError + + +class UnsupportedMimetypeDeserializer(BaseMediaTypeDeserializer): + + def __call__(self, value): + warnings.warn(f"Unsupported {self.mimetype} mimetype") + return value + + +class CallableMediaTypeDeserializer(BaseMediaTypeDeserializer): def __init__(self, mimetype, deserializer_callable): self.mimetype = mimetype @@ -11,4 +31,4 @@ def __call__(self, value): try: return self.deserializer_callable(value) except (ValueError, TypeError, AttributeError): - raise DeserializeError(value, self.mimetype) + raise MediaTypeDeserializeError(self.mimetype, value) diff --git a/openapi_core/deserializing/media_types/exceptions.py b/openapi_core/deserializing/media_types/exceptions.py new file mode 100644 index 00000000..45a16c7f --- /dev/null +++ b/openapi_core/deserializing/media_types/exceptions.py @@ -0,0 +1,15 @@ +from dataclasses import dataclass + +from openapi_core.deserializing.exceptions import DeserializeError + + +@dataclass +class MediaTypeDeserializeError(DeserializeError): + """Media type deserialize operation error""" + mimetype: str + value: str + + def __str__(self): + return ( + "Failed to deserialize value with {mimetype} mimetype: {value}" + ).format(value=self.value, mimetype=self.mimetype) diff --git a/openapi_core/deserializing/media_types/factories.py b/openapi_core/deserializing/media_types/factories.py index c0cd409d..8316c373 100644 --- a/openapi_core/deserializing/media_types/factories.py +++ b/openapi_core/deserializing/media_types/factories.py @@ -5,7 +5,7 @@ ) from openapi_core.deserializing.media_types.deserializers import ( - PrimitiveDeserializer, + CallableMediaTypeDeserializer, UnsupportedMimetypeDeserializer, ) @@ -25,10 +25,14 @@ def __init__(self, custom_deserializers=None): def create(self, mimetype): deserialize_callable = self.get_deserializer_callable( mimetype) - return PrimitiveDeserializer( + + if deserialize_callable is None: + return UnsupportedMimetypeDeserializer(mimetype) + + return CallableMediaTypeDeserializer( mimetype, deserialize_callable) def get_deserializer_callable(self, mimetype): if mimetype in self.custom_deserializers: return self.custom_deserializers[mimetype] - return self.MEDIA_TYPE_DESERIALIZERS.get(mimetype, lambda x: x) + return self.MEDIA_TYPE_DESERIALIZERS.get(mimetype) diff --git a/openapi_core/deserializing/parameters/deserializers.py b/openapi_core/deserializing/parameters/deserializers.py index e2691fc2..e9c544ac 100644 --- a/openapi_core/deserializing/parameters/deserializers.py +++ b/openapi_core/deserializing/parameters/deserializers.py @@ -2,20 +2,36 @@ from openapi_core.deserializing.exceptions import DeserializeError from openapi_core.deserializing.parameters.exceptions import ( - EmptyParameterValue, + EmptyQueryParameterValue, ) -from openapi_core.schema.parameters import get_aslist, get_explode, get_style +from openapi_core.schema.parameters import get_aslist, get_explode -class PrimitiveDeserializer: +class BaseParameterDeserializer: - def __init__(self, param_or_header, deserializer_callable): + def __init__(self, param_or_header, style): self.param_or_header = param_or_header + self.style = style + + def __call__(self, value): + raise NotImplementedError + + +class UnsupportedStyleDeserializer(BaseParameterDeserializer): + + def __call__(self, value): + warnings.warn(f"Unsupported {self.style} style") + return value + + +class CallableParameterDeserializer(BaseParameterDeserializer): + + def __init__(self, param_or_header, style, deserializer_callable): + super().__init__(param_or_header, style) self.deserializer_callable = deserializer_callable self.aslist = get_aslist(self.param_or_header) self.explode = get_explode(self.param_or_header) - self.style = get_style(self.param_or_header) def __call__(self, value): # if "in" not defined then it's a Header @@ -29,12 +45,12 @@ def __call__(self, value): location_name = self.param_or_header.getkey('in', 'header') if (location_name == 'query' and value == "" and not allow_empty_values): - name = self.param_or_header.getkey('name', 'header') - raise EmptyParameterValue(value, self.style, name) + name = self.param_or_header['name'] + raise EmptyQueryParameterValue(name) if not self.aslist or self.explode: return value try: return self.deserializer_callable(value) except (ValueError, TypeError, AttributeError): - raise DeserializeError(value, self.style) + raise DeserializeError(location_name, self.style, value) diff --git a/openapi_core/deserializing/parameters/exceptions.py b/openapi_core/deserializing/parameters/exceptions.py index 0966d93e..f3c04f8c 100644 --- a/openapi_core/deserializing/parameters/exceptions.py +++ b/openapi_core/deserializing/parameters/exceptions.py @@ -4,8 +4,32 @@ @dataclass -class EmptyParameterValue(DeserializeError): +class BaseParameterDeserializeError(DeserializeError): + """Base parameter deserialize operation error""" + location: str + + +@dataclass +class ParameterDeserializeError(BaseParameterDeserializeError): + """Parameter deserialize operation error""" + style: str + value: str + + def __str__(self): + return ( + "Failed to deserialize value " + "of {location} parameter with style {style}: {value}" + ).format(location=self.location, style=self.style, value=self.value) + + +@dataclass(init=False) +class EmptyQueryParameterValue(BaseParameterDeserializeError): name: str + def __init__(self, name): + super().__init__(location='query') + self.name = name + def __str__(self): - return "Value of parameter cannot be empty: {0}".format(self.name) + return "Value of {name} {location} parameter cannot be empty".format( + name=self.name, location=self.location) diff --git a/openapi_core/deserializing/parameters/factories.py b/openapi_core/deserializing/parameters/factories.py index 64cb8c3c..b69c7985 100644 --- a/openapi_core/deserializing/parameters/factories.py +++ b/openapi_core/deserializing/parameters/factories.py @@ -1,20 +1,27 @@ +from functools import partial + from openapi_core.deserializing.parameters.deserializers import ( - PrimitiveDeserializer, + CallableParameterDeserializer, UnsupportedStyleDeserializer, ) +from openapi_core.deserializing.parameters.util import split from openapi_core.schema.parameters import get_style class ParameterDeserializersFactory: PARAMETER_STYLE_DESERIALIZERS = { - 'form': lambda x: x.split(','), - 'simple': lambda x: x.split(','), - 'spaceDelimited': lambda x: x.split(' '), - 'pipeDelimited': lambda x: x.split('|'), + 'form': partial(split, separator=','), + 'simple': partial(split, separator=','), + 'spaceDelimited': partial(split, separator=' '), + 'pipeDelimited': partial(split, separator='|'), } - def create(self, param): - style = get_style(param) + def create(self, param_or_header): + style = get_style(param_or_header) + + if style not in self.PARAMETER_STYLE_DESERIALIZERS: + return UnsupportedStyleDeserializer(param_or_header, style) deserialize_callable = self.PARAMETER_STYLE_DESERIALIZERS[style] - return PrimitiveDeserializer(param, deserialize_callable) + return CallableParameterDeserializer( + param_or_header, style, deserialize_callable) diff --git a/openapi_core/deserializing/parameters/util.py b/openapi_core/deserializing/parameters/util.py new file mode 100644 index 00000000..c0d7e8a1 --- /dev/null +++ b/openapi_core/deserializing/parameters/util.py @@ -0,0 +1,2 @@ +def split(value, separator=','): + return value.split(separator) diff --git a/tests/integration/validation/test_petstore.py b/tests/integration/validation/test_petstore.py index bed9afb8..6f338aa7 100644 --- a/tests/integration/validation/test_petstore.py +++ b/tests/integration/validation/test_petstore.py @@ -8,7 +8,7 @@ from openapi_core.casting.schemas.exceptions import CastError from openapi_core.deserializing.exceptions import DeserializeError from openapi_core.deserializing.parameters.exceptions import ( - EmptyParameterValue, + EmptyQueryParameterValue, ) from openapi_core.extensions.models.models import BaseModel from openapi_core.exceptions import ( @@ -375,7 +375,7 @@ def test_get_pets_empty_value(self, spec): path_pattern=path_pattern, args=query_params, ) - with pytest.raises(EmptyParameterValue): + with pytest.raises(EmptyQueryParameterValue): spec_validate_parameters(spec, request) body = spec_validate_body(spec, request) diff --git a/tests/integration/validation/test_validators.py b/tests/integration/validation/test_validators.py index b6e8c35b..0cd7150c 100644 --- a/tests/integration/validation/test_validators.py +++ b/tests/integration/validation/test_validators.py @@ -3,7 +3,9 @@ import pytest from openapi_core.casting.schemas.exceptions import CastError -from openapi_core.deserializing.exceptions import DeserializeError +from openapi_core.deserializing.media_types.exceptions import ( + MediaTypeDeserializeError, +) from openapi_core.extensions.models.models import BaseModel from openapi_core.exceptions import ( MissingRequiredParameter, MissingRequiredRequestBody, @@ -572,7 +574,7 @@ def test_invalid_media_type(self, validator): result = validator.validate(request, response) assert len(result.errors) == 1 - assert type(result.errors[0]) == DeserializeError + assert type(result.errors[0]) == MediaTypeDeserializeError assert result.data is None assert result.headers == {} diff --git a/tests/unit/deserializing/test_media_types_deserializers.py b/tests/unit/deserializing/test_media_types_deserializers.py index 1a962a57..52f7e5a4 100644 --- a/tests/unit/deserializing/test_media_types_deserializers.py +++ b/tests/unit/deserializing/test_media_types_deserializers.py @@ -15,6 +15,15 @@ def create_deserializer(media_type, custom_deserializers=None): custom_deserializers=custom_deserializers).create(media_type) return create_deserializer + def test_unsupported(self, deserializer_factory): + mimetype = 'application/unsupported' + value = '' + + with pytest.warns(UserWarning): + result = deserializer_factory(mimetype)(value) + + assert result == value + def test_json_empty(self, deserializer_factory): mimetype = 'application/json' value = '' diff --git a/tests/unit/deserializing/test_parameters_deserializers.py b/tests/unit/deserializing/test_parameters_deserializers.py index a86a09ff..9bb80f90 100644 --- a/tests/unit/deserializing/test_parameters_deserializers.py +++ b/tests/unit/deserializing/test_parameters_deserializers.py @@ -4,7 +4,7 @@ ParameterDeserializersFactory, ) from openapi_core.deserializing.parameters.exceptions import ( - EmptyParameterValue, + EmptyQueryParameterValue, ) from openapi_core.spec.paths import SpecPath @@ -17,6 +17,20 @@ def create_deserializer(param): return ParameterDeserializersFactory().create(param) return create_deserializer + def test_unsupported(self, deserializer_factory): + spec = { + 'name': 'param', + 'in': 'header', + 'style': 'unsupported' + } + param = SpecPath.from_spec(spec) + value = '' + + with pytest.warns(UserWarning): + result = deserializer_factory(param)(value) + + assert result == value + def test_query_empty(self, deserializer_factory): spec = { 'name': 'param', @@ -25,7 +39,7 @@ def test_query_empty(self, deserializer_factory): param = SpecPath.from_spec(spec) value = '' - with pytest.raises(EmptyParameterValue): + with pytest.raises(EmptyQueryParameterValue): deserializer_factory(param)(value) def test_query_valid(self, deserializer_factory):