diff --git a/openapi_core/unmarshalling/schemas/__init__.py b/openapi_core/unmarshalling/schemas/__init__.py index 47b40055..00b17d0c 100644 --- a/openapi_core/unmarshalling/schemas/__init__.py +++ b/openapi_core/unmarshalling/schemas/__init__.py @@ -1,12 +1,25 @@ -from openapi_schema_validator import OAS30Validator +from functools import partial + +from isodate.isodatetime import parse_datetime +from openapi_schema_validator import OAS30ReadValidator +from openapi_schema_validator import OAS30WriteValidator from openapi_schema_validator import OAS31Validator +from openapi_schema_validator._format import oas30_format_checker +from openapi_schema_validator._format import oas31_format_checker from openapi_core.unmarshalling.schemas.enums import ValidationContext from openapi_core.unmarshalling.schemas.factories import ( SchemaUnmarshallersFactory, ) +from openapi_core.unmarshalling.schemas.formatters import Formatter +from openapi_core.unmarshalling.schemas.util import format_byte +from openapi_core.unmarshalling.schemas.util import format_date +from openapi_core.unmarshalling.schemas.util import format_number +from openapi_core.unmarshalling.schemas.util import format_uuid __all__ = [ + "oas30_format_unmarshallers", + "oas31_format_unmarshallers", "oas30_request_schema_unmarshallers_factory", "oas30_response_schema_unmarshallers_factory", "oas31_request_schema_unmarshallers_factory", @@ -14,18 +27,34 @@ "oas31_schema_unmarshallers_factory", ] +oas30_format_unmarshallers = { + # string compatible + "date": format_date, + "date-time": parse_datetime, + "binary": bytes, + "uuid": format_uuid, + "byte": format_byte, +} +oas31_format_unmarshallers = oas30_format_unmarshallers + oas30_request_schema_unmarshallers_factory = SchemaUnmarshallersFactory( - OAS30Validator, + OAS30WriteValidator, + base_format_checker=oas30_format_checker, + format_unmarshallers=oas30_format_unmarshallers, context=ValidationContext.REQUEST, ) oas30_response_schema_unmarshallers_factory = SchemaUnmarshallersFactory( - OAS30Validator, + OAS30ReadValidator, + base_format_checker=oas30_format_checker, + format_unmarshallers=oas30_format_unmarshallers, context=ValidationContext.RESPONSE, ) oas31_schema_unmarshallers_factory = SchemaUnmarshallersFactory( OAS31Validator, + base_format_checker=oas31_format_checker, + format_unmarshallers=oas31_format_unmarshallers, ) # alias to v31 version (request/response are the same bcs no context needed) diff --git a/openapi_core/unmarshalling/schemas/datatypes.py b/openapi_core/unmarshalling/schemas/datatypes.py index 96008373..77365527 100644 --- a/openapi_core/unmarshalling/schemas/datatypes.py +++ b/openapi_core/unmarshalling/schemas/datatypes.py @@ -1,3 +1,5 @@ +from typing import Any +from typing import Callable from typing import Dict from typing import Optional @@ -5,3 +7,4 @@ CustomFormattersDict = Dict[str, Formatter] FormattersDict = Dict[Optional[str], Formatter] +UnmarshallersDict = Dict[str, Callable[[Any], Any]] diff --git a/openapi_core/unmarshalling/schemas/exceptions.py b/openapi_core/unmarshalling/schemas/exceptions.py index 2d6fafad..73230201 100644 --- a/openapi_core/unmarshalling/schemas/exceptions.py +++ b/openapi_core/unmarshalling/schemas/exceptions.py @@ -19,6 +19,8 @@ class UnmarshallerError(UnmarshalError): @dataclass class InvalidSchemaValue(ValidateError): + """Value not valid for schema""" + value: str type: str schema_errors: Iterable[Exception] = field(default_factory=list) @@ -30,28 +32,31 @@ def __str__(self) -> str: @dataclass -class InvalidSchemaFormatValue(UnmarshallerError): - """Value failed to format with formatter""" +class InvalidFormatValue(UnmarshallerError): + """Value not valid for format""" value: str type: str - original_exception: Exception def __str__(self) -> str: - return ( - "Failed to format value {value} to format {type}: {exception}" - ).format( + return ("value {value} not valid for format {type}").format( value=self.value, type=self.type, - exception=self.original_exception, ) -@dataclass -class FormatterNotFoundError(UnmarshallerError): - """Formatter not found to unmarshal""" +class FormatUnmarshalError(UnmarshallerError): + """Unable to unmarshal value for format""" - type_format: str + value: str + type: str + original_exception: Exception def __str__(self) -> str: - return f"Formatter not found for {self.type_format} format" + return ( + "Unable to unmarshal value {value} for format {type}: {exception}" + ).format( + value=self.value, + type=self.type, + exception=self.original_exception, + ) diff --git a/openapi_core/unmarshalling/schemas/factories.py b/openapi_core/unmarshalling/schemas/factories.py index 5bec2d37..ce03c08e 100644 --- a/openapi_core/unmarshalling/schemas/factories.py +++ b/openapi_core/unmarshalling/schemas/factories.py @@ -1,6 +1,8 @@ import sys import warnings +from functools import partial from typing import Any +from typing import Callable from typing import Dict from typing import Iterable from typing import Optional @@ -11,16 +13,15 @@ from functools import cached_property else: from backports.cached_property import cached_property +from jsonschema._format import FormatChecker from jsonschema.protocols import Validator from openapi_schema_validator import OAS30Validator from openapi_core.spec import Spec from openapi_core.unmarshalling.schemas.datatypes import CustomFormattersDict from openapi_core.unmarshalling.schemas.datatypes import FormattersDict +from openapi_core.unmarshalling.schemas.datatypes import UnmarshallersDict from openapi_core.unmarshalling.schemas.enums import ValidationContext -from openapi_core.unmarshalling.schemas.exceptions import ( - FormatterNotFoundError, -) from openapi_core.unmarshalling.schemas.formatters import Formatter from openapi_core.unmarshalling.schemas.unmarshallers import AnyUnmarshaller from openapi_core.unmarshalling.schemas.unmarshallers import ArrayUnmarshaller @@ -47,39 +48,97 @@ class SchemaValidatorsFactory: - - CONTEXTS = { - ValidationContext.REQUEST: "write", - ValidationContext.RESPONSE: "read", - } - def __init__( self, schema_validator_class: Type[Validator], + base_format_checker: Optional[FormatChecker] = None, + formatters: Optional[CustomFormattersDict] = None, + format_unmarshallers: Optional[UnmarshallersDict] = None, custom_formatters: Optional[CustomFormattersDict] = None, - context: Optional[ValidationContext] = None, ): self.schema_validator_class = schema_validator_class + if base_format_checker is None: + base_format_checker = self.schema_validator_class.FORMAT_CHECKER + self.base_format_checker = base_format_checker + if formatters is None: + formatters = {} + self.formatters = formatters + if format_unmarshallers is None: + format_unmarshallers = {} + self.format_unmarshallers = format_unmarshallers if custom_formatters is None: custom_formatters = {} self.custom_formatters = custom_formatters - self.context = context - def create(self, schema: Spec) -> Validator: - resolver = schema.accessor.resolver # type: ignore - custom_format_checks = { + @cached_property + def format_checker(self) -> FormatChecker: + format_checks: Dict[str, Callable[[Any], bool]] = { name: formatter.validate - for name, formatter in self.custom_formatters.items() + for formatters_list in [self.formatters, self.custom_formatters] + for name, formatter in formatters_list.items() } - format_checker = build_format_checker(**custom_format_checks) - kwargs = { - "resolver": resolver, - "format_checker": format_checker, - } - if self.context is not None: - kwargs[self.CONTEXTS[self.context]] = True + format_checks.update( + { + name: self._create_checker(name) + for name, _ in self.format_unmarshallers.items() + } + ) + return build_format_checker(self.base_format_checker, **format_checks) + + def _create_checker(self, name: str) -> Callable[[Any], bool]: + if name in self.base_format_checker.checkers: + return partial(self.base_format_checker.check, format=name) + + return lambda x: True + + def get_checker(self, name: str) -> Callable[[Any], bool]: + if name in self.format_checker.checkers: + return partial(self.format_checker.check, format=name) + + return lambda x: True + + def create(self, schema: Spec) -> Validator: + resolver = schema.accessor.resolver # type: ignore with schema.open() as schema_dict: - return self.schema_validator_class(schema_dict, **kwargs) + return self.schema_validator_class( + schema_dict, + resolver=resolver, + format_checker=self.format_checker, + ) + + +class SchemaFormattersFactory: + def __init__( + self, + validators_factory: SchemaValidatorsFactory, + formatters: Optional[CustomFormattersDict] = None, + format_unmarshallers: Optional[UnmarshallersDict] = None, + custom_formatters: Optional[CustomFormattersDict] = None, + ): + self.validators_factory = validators_factory + if formatters is None: + formatters = {} + self.formatters = formatters + if format_unmarshallers is None: + format_unmarshallers = {} + self.format_unmarshallers = format_unmarshallers + if custom_formatters is None: + custom_formatters = {} + self.custom_formatters = custom_formatters + + def create(self, schema_format: str) -> Optional[Formatter]: + if schema_format in self.custom_formatters: + return self.custom_formatters[schema_format] + if schema_format in self.formatters: + return self.formatters[schema_format] + if schema_format in self.format_unmarshallers: + validate_callable = self.validators_factory.get_checker( + schema_format + ) + format_callable = self.format_unmarshallers[schema_format] + return Formatter.from_callables(validate_callable, format_callable) + + return None class SchemaUnmarshallersFactory: @@ -104,12 +163,20 @@ class SchemaUnmarshallersFactory: def __init__( self, schema_validator_class: Type[Validator], + base_format_checker: Optional[FormatChecker] = None, + formatters: Optional[CustomFormattersDict] = None, + format_unmarshallers: Optional[UnmarshallersDict] = None, custom_formatters: Optional[CustomFormattersDict] = None, context: Optional[ValidationContext] = None, ): self.schema_validator_class = schema_validator_class + self.base_format_checker = base_format_checker if custom_formatters is None: custom_formatters = {} + self.formatters = formatters + if format_unmarshallers is None: + format_unmarshallers = {} + self.format_unmarshallers = format_unmarshallers self.custom_formatters = custom_formatters self.context = context @@ -117,8 +184,19 @@ def __init__( def validators_factory(self) -> SchemaValidatorsFactory: return SchemaValidatorsFactory( self.schema_validator_class, + self.base_format_checker, + self.formatters, + self.format_unmarshallers, + self.custom_formatters, + ) + + @cached_property + def formatters_factory(self) -> SchemaFormattersFactory: + return SchemaFormattersFactory( + self.validators_factory, + self.formatters, + self.format_unmarshallers, self.custom_formatters, - self.context, ) def create( @@ -134,7 +212,7 @@ def create( validator = self.validators_factory.create(schema) schema_format = schema.getkey("format") - formatter = self.custom_formatters.get(schema_format) + formatter = self.formatters_factory.create(schema_format) schema_type = type_override or schema.getkey("type", "any") if isinstance(schema_type, Iterable) and not isinstance( diff --git a/openapi_core/unmarshalling/schemas/unmarshallers.py b/openapi_core/unmarshalling/schemas/unmarshallers.py index d064a1ff..eeedc364 100644 --- a/openapi_core/unmarshalling/schemas/unmarshallers.py +++ b/openapi_core/unmarshalling/schemas/unmarshallers.py @@ -25,12 +25,8 @@ from openapi_core.spec import Spec from openapi_core.unmarshalling.schemas.datatypes import FormattersDict from openapi_core.unmarshalling.schemas.enums import ValidationContext -from openapi_core.unmarshalling.schemas.exceptions import ( - FormatterNotFoundError, -) -from openapi_core.unmarshalling.schemas.exceptions import ( - InvalidSchemaFormatValue, -) +from openapi_core.unmarshalling.schemas.exceptions import FormatUnmarshalError +from openapi_core.unmarshalling.schemas.exceptions import InvalidFormatValue from openapi_core.unmarshalling.schemas.exceptions import InvalidSchemaValue from openapi_core.unmarshalling.schemas.exceptions import UnmarshalError from openapi_core.unmarshalling.schemas.exceptions import UnmarshallerError @@ -55,9 +51,7 @@ class BaseSchemaUnmarshaller: - FORMATTERS: FormattersDict = { - None: Formatter(), - } + formatter: Formatter = Formatter() def __init__( self, @@ -71,11 +65,7 @@ def __init__( self.validator = validator self.schema_format = schema.getkey("format") - if formatter is None: - if self.schema_format not in self.FORMATTERS: - raise FormatterNotFoundError(self.schema_format) - self.formatter = self.FORMATTERS[self.schema_format] - else: + if formatter is not None: self.formatter = formatter self.validators_factory = validators_factory @@ -94,7 +84,13 @@ def _validate_format(self, value: Any) -> None: result = self.formatter.validate(value) if not result: schema_type = self.schema.getkey("type", "any") - raise InvalidSchemaValue(value, schema_type) + raise InvalidFormatValue(value, schema_type) + + def _unmarshal_format(self, value: Any) -> Any: + try: + return self.formatter.format(value) + except (ValueError, TypeError) as exc: + raise FormatUnmarshalError(value, self.schema_format, exc) def validate(self, value: Any) -> None: errors_iter = self.validator.iter_errors(value) @@ -103,12 +99,6 @@ def validate(self, value: Any) -> None: schema_type = self.schema.getkey("type", "any") raise InvalidSchemaValue(value, schema_type, schema_errors=errors) - def format(self, value: Any) -> Any: - try: - return self.formatter.format(value) - except (ValueError, TypeError) as exc: - raise InvalidSchemaFormatValue(value, self.schema_format, exc) - def _get_best_unmarshaller(self, value: Any) -> "BaseSchemaUnmarshaller": if "format" not in self.schema: one_of_schema = self._get_one_of_schema(value) @@ -138,7 +128,7 @@ def _get_best_unmarshaller(self, value: Any) -> "BaseSchemaUnmarshaller": def unmarshal(self, value: Any) -> Any: unmarshaller = self._get_best_unmarshaller(value) - return unmarshaller.format(value) + return unmarshaller._unmarshal_format(value) def _get_one_of_schema( self, @@ -199,70 +189,29 @@ def _iter_all_of_schemas( class StringUnmarshaller(BaseSchemaUnmarshaller): - FORMATTERS: FormattersDict = { - None: Formatter.from_callables(partial(is_string, None), str), - "password": Formatter.from_callables( - partial(oas30_format_checker.check, format="password"), str - ), - "date": Formatter.from_callables( - partial(oas30_format_checker.check, format="date"), format_date - ), - "date-time": Formatter.from_callables( - partial(oas30_format_checker.check, format="date-time"), - parse_datetime, - ), - "binary": Formatter.from_callables( - partial(oas30_format_checker.check, format="binary"), bytes - ), - "uuid": Formatter.from_callables( - partial(oas30_format_checker.check, format="uuid"), format_uuid - ), - "byte": Formatter.from_callables( - partial(oas30_format_checker.check, format="byte"), format_byte - ), - } + formatter = Formatter.from_callables(partial(is_string, None), str) class IntegerUnmarshaller(BaseSchemaUnmarshaller): - FORMATTERS: FormattersDict = { - None: Formatter.from_callables(partial(is_integer, None), int), - "int32": Formatter.from_callables( - partial(oas30_format_checker.check, format="int32"), int - ), - "int64": Formatter.from_callables( - partial(oas30_format_checker.check, format="int64"), int - ), - } + formatter = Formatter.from_callables(partial(is_integer, None), int) class NumberUnmarshaller(BaseSchemaUnmarshaller): - FORMATTERS: FormattersDict = { - None: Formatter.from_callables( - partial(is_number, None), format_number - ), - "float": Formatter.from_callables( - partial(oas30_format_checker.check, format="float"), float - ), - "double": Formatter.from_callables( - partial(oas30_format_checker.check, format="double"), float - ), - } + formatter = Formatter.from_callables( + partial(is_number, None), format_number + ) class BooleanUnmarshaller(BaseSchemaUnmarshaller): - FORMATTERS: FormattersDict = { - None: Formatter.from_callables(partial(is_bool, None), forcebool), - } + formatter = Formatter.from_callables(partial(is_bool, None), forcebool) class NullUnmarshaller(BaseSchemaUnmarshaller): - FORMATTERS: FormattersDict = { - None: Formatter.from_callables(partial(is_null, None), None), - } + formatter = Formatter.from_callables(partial(is_null, None), None) class ComplexUnmarshaller(BaseSchemaUnmarshaller): @@ -287,9 +236,7 @@ def __init__( class ArrayUnmarshaller(ComplexUnmarshaller): - FORMATTERS: FormattersDict = { - None: Formatter.from_callables(partial(is_array, None), list), - } + formatter = Formatter.from_callables(partial(is_array, None), list) @property def items_unmarshaller(self) -> "BaseSchemaUnmarshaller": @@ -306,24 +253,22 @@ def unmarshal(self, value: Any) -> Optional[List[Any]]: class ObjectUnmarshaller(ComplexUnmarshaller): - FORMATTERS: FormattersDict = { - None: Formatter.from_callables(partial(is_object, None), dict), - } + formatter = Formatter.from_callables(partial(is_object, None), dict) @property def object_class_factory(self) -> ModelPathFactory: return ModelPathFactory() def unmarshal(self, value: Any) -> Any: - properties = self.format(value) + properties = self._unmarshal_format(value) fields: Iterable[str] = properties and properties.keys() or [] object_class = self.object_class_factory.create(self.schema, fields) return object_class(**properties) - def format(self, value: Any, schema_only: bool = False) -> Any: - formatted = super().format(value) + def _unmarshal_format(self, value: Any, schema_only: bool = False) -> Any: + formatted = super()._unmarshal_format(value) return self._unmarshal_properties(formatted, schema_only=schema_only) def _clone(self, schema: Spec) -> "ObjectUnmarshaller": @@ -339,21 +284,21 @@ def _unmarshal_properties( one_of_schema = self._get_one_of_schema(value) if one_of_schema is not None: - one_of_properties = self._clone(one_of_schema).format( + one_of_properties = self._clone(one_of_schema)._unmarshal_format( value, schema_only=True ) properties.update(one_of_properties) any_of_schemas = self._iter_any_of_schemas(value) for any_of_schema in any_of_schemas: - any_of_properties = self._clone(any_of_schema).format( + any_of_properties = self._clone(any_of_schema)._unmarshal_format( value, schema_only=True ) properties.update(any_of_properties) all_of_schemas = self._iter_all_of_schemas(value) for all_of_schema in all_of_schemas: - all_of_properties = self._clone(all_of_schema).format( + all_of_properties = self._clone(all_of_schema)._unmarshal_format( value, schema_only=True ) properties.update(all_of_properties) @@ -426,7 +371,7 @@ def _get_best_unmarshaller(self, value: Any) -> "BaseSchemaUnmarshaller": # validate with validator of formatter (usualy type validator) try: unmarshaller._validate_format(value) - except ValidateError: + except InvalidFormatValue: continue else: return unmarshaller diff --git a/openapi_core/unmarshalling/schemas/util.py b/openapi_core/unmarshalling/schemas/util.py index ca240f48..59eec629 100644 --- a/openapi_core/unmarshalling/schemas/util.py +++ b/openapi_core/unmarshalling/schemas/util.py @@ -1,6 +1,6 @@ """OpenAPI core schemas util module""" from base64 import b64decode -from copy import copy +from copy import deepcopy from datetime import date from datetime import datetime from functools import lru_cache @@ -10,6 +10,7 @@ from typing import Union from uuid import UUID +from jsonschema._format import FormatChecker from openapi_schema_validator import oas30_format_checker @@ -35,11 +36,19 @@ def format_number(value: str) -> Union[int, float]: @lru_cache() -def build_format_checker(**custom_format_checks: Callable[[Any], Any]) -> Any: - if not custom_format_checks: - return oas30_format_checker - - fc = copy(oas30_format_checker) - for name, check in custom_format_checks.items(): +def build_format_checker( + format_checker: Optional[FormatChecker] = None, + **format_checks: Callable[[Any], bool] +) -> Any: + if format_checker is None: + fc = FormatChecker() + else: + if not format_checks: + return format_checker + fc = deepcopy(format_checker) + + for name, check in format_checks.items(): + if name in fc.checkers: + continue fc.checks(name)(check) return fc diff --git a/openapi_core/validation/request/exceptions.py b/openapi_core/validation/request/exceptions.py index f141c351..22b2e08e 100644 --- a/openapi_core/validation/request/exceptions.py +++ b/openapi_core/validation/request/exceptions.py @@ -34,7 +34,8 @@ def __str__(self) -> str: class InvalidRequestBody(RequestBodyError, ValidateError): - """Invalid request body""" + def __str__(self) -> str: + return f"Invalid request body" class MissingRequestBodyError(RequestBodyError): diff --git a/poetry.lock b/poetry.lock index a9725c27..1af87e55 100644 --- a/poetry.lock +++ b/poetry.lock @@ -883,14 +883,14 @@ setuptools = "*" [[package]] name = "openapi-schema-validator" -version = "0.4.0" +version = "0.4.1" description = "OpenAPI schema validation for Python" category = "main" optional = false python-versions = ">=3.7.0,<4.0.0" files = [ - {file = "openapi_schema_validator-0.4.0-py3-none-any.whl", hash = "sha256:f1faaae0b1076d6f6bf6ad5d8bb53f49d9cc49621f5e224e2bc121ef76016c04"}, - {file = "openapi_schema_validator-0.4.0.tar.gz", hash = "sha256:fb591258bbe1e24f381d83cff2e9a1a6fc547936adb46143fdd089f6ea411cc8"}, + {file = "openapi_schema_validator-0.4.1-py3-none-any.whl", hash = "sha256:eb3d6da7a974098aed646e5ea8dd9c8860d8cec2eb087a9c5ab559226cc709ba"}, + {file = "openapi_schema_validator-0.4.1.tar.gz", hash = "sha256:582d960f633549b6b981e51cc78e05e9fa9ae2b5ff1239a061ec6f53d39eff90"}, ] [package.dependencies] @@ -1736,4 +1736,4 @@ starlette = [] [metadata] lock-version = "2.0" python-versions = "^3.7.0" -content-hash = "2298aeb7e7f20f9f479ddc603380eb9868c2914a969de8ca6b097eb3cee16a3e" +content-hash = "2ea378dcd253b0e619db0ff0adbee91e93326d5f9fa02c27179918e375186373" diff --git a/pyproject.toml b/pyproject.toml index 545b9a77..0717d1f9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -59,7 +59,7 @@ flask = {version = "*", optional = true} isodate = "*" more-itertools = "*" parse = "*" -openapi-schema-validator = ">=0.3.0,<0.5" +openapi-schema-validator = "^0.4.1" openapi-spec-validator = "^0.5.0" requests = {version = "*", optional = true} werkzeug = "*" diff --git a/tests/unit/unmarshalling/test_unmarshal.py b/tests/unit/unmarshalling/test_unmarshal.py index 1151d167..1ee1aab9 100644 --- a/tests/unit/unmarshalling/test_unmarshal.py +++ b/tests/unit/unmarshalling/test_unmarshal.py @@ -5,17 +5,18 @@ import pytest from isodate.tzinfo import UTC from isodate.tzinfo import FixedOffset +from openapi_schema_validator import OAS30ReadValidator from openapi_schema_validator import OAS30Validator +from openapi_schema_validator import OAS30WriteValidator from openapi_schema_validator import OAS31Validator from openapi_core.spec.paths import Spec +from openapi_core.unmarshalling.schemas import oas30_format_checker +from openapi_core.unmarshalling.schemas import oas30_format_unmarshallers +from openapi_core.unmarshalling.schemas import oas31_format_checker from openapi_core.unmarshalling.schemas.enums import ValidationContext -from openapi_core.unmarshalling.schemas.exceptions import ( - FormatterNotFoundError, -) -from openapi_core.unmarshalling.schemas.exceptions import ( - InvalidSchemaFormatValue, -) +from openapi_core.unmarshalling.schemas.exceptions import FormatUnmarshalError +from openapi_core.unmarshalling.schemas.exceptions import InvalidFormatValue from openapi_core.unmarshalling.schemas.exceptions import InvalidSchemaValue from openapi_core.unmarshalling.schemas.exceptions import UnmarshalError from openapi_core.unmarshalling.schemas.factories import ( @@ -27,11 +28,17 @@ @pytest.fixture def schema_unmarshaller_factory(): def create_unmarshaller( - validator, schema, custom_formatters=None, context=None + validator, + schema, + base_format_checker=None, + custom_formatters=None, + context=None, ): custom_formatters = custom_formatters or {} return SchemaUnmarshallersFactory( validator, + format_unmarshallers=oas30_format_unmarshallers, + base_format_checker=base_format_checker, custom_formatters=custom_formatters, context=context, ).create(schema) @@ -42,7 +49,11 @@ def create_unmarshaller( class TestOAS30SchemaUnmarshallerUnmarshal: @pytest.fixture def unmarshaller_factory(self, schema_unmarshaller_factory): - return partial(schema_unmarshaller_factory, OAS30Validator) + return partial( + schema_unmarshaller_factory, + OAS30Validator, + base_format_checker=oas30_format_checker, + ) def test_no_schema(self, unmarshaller_factory): spec = None @@ -58,7 +69,7 @@ def test_schema_type_invalid(self, unmarshaller_factory): spec = Spec.from_dict(schema, validator=None) value = "test" - with pytest.raises(InvalidSchemaFormatValue): + with pytest.raises(FormatUnmarshalError): unmarshaller_factory(spec).unmarshal(value) def test_schema_custom_format_invalid(self, unmarshaller_factory): @@ -78,7 +89,7 @@ def format(self, value): spec = Spec.from_dict(schema, validator=None) value = "test" - with pytest.raises(InvalidSchemaFormatValue): + with pytest.raises(FormatUnmarshalError): unmarshaller_factory( spec, custom_formatters=custom_formatters, @@ -88,7 +99,11 @@ def format(self, value): class TestOAS30SchemaUnmarshallerCall: @pytest.fixture def unmarshaller_factory(self, schema_unmarshaller_factory): - return partial(schema_unmarshaller_factory, OAS30Validator) + return partial( + schema_unmarshaller_factory, + OAS30Validator, + base_format_checker=oas30_format_checker, + ) def test_deprecated(self, unmarshaller_factory): schema = { @@ -297,12 +312,12 @@ def format(self, value): custom_format: formatter, } - with pytest.raises(InvalidSchemaFormatValue): + with pytest.raises(FormatUnmarshalError): unmarshaller_factory(spec, custom_formatters=custom_formatters)( value ) - def test_string_format_unknown(self, unmarshaller_factory): + def test_string_format_unknown_and_invalid(self, unmarshaller_factory): unknown_format = "unknown" schema = { "type": "string", @@ -311,7 +326,7 @@ def test_string_format_unknown(self, unmarshaller_factory): spec = Spec.from_dict(schema, validator=None) value = "x" - with pytest.raises(FormatterNotFoundError): + with pytest.raises(InvalidSchemaValue): unmarshaller_factory(spec)(value) def test_string_format_invalid_value(self, unmarshaller_factory): @@ -323,12 +338,16 @@ def test_string_format_invalid_value(self, unmarshaller_factory): spec = Spec.from_dict(schema, validator=None) value = "x" - with pytest.raises( - FormatterNotFoundError, - match="Formatter not found for custom format", - ): + with pytest.raises(InvalidSchemaValue) as exc_info: unmarshaller_factory(spec)(value) + schema_errors = exc_info.value.schema_errors + assert exc_info.value == InvalidSchemaValue( + type="string", + value=value, + schema_errors=schema_errors, + ) + def test_integer_valid(self, unmarshaller_factory): schema = { "type": "integer", @@ -917,13 +936,13 @@ def test_nultiple_types_not_supported( unmarshaller_factory(spec)(value) -class TestOAS30ReadSchemaUnmarshallerCall: +class TestOAS30ResponseSchemaUnmarshallerCall: @pytest.fixture def unmarshaller_factory(self, schema_unmarshaller_factory): return partial( schema_unmarshaller_factory, - OAS30Validator, - context=ValidationContext.RESPONSE, + OAS30ReadValidator, + base_format_checker=oas30_format_checker, ) def test_read_only_properties(self, unmarshaller_factory): @@ -964,13 +983,13 @@ def test_write_only_properties_invalid(self, unmarshaller_factory): unmarshaller_factory(spec)({"id": 10}) -class TestOAS30WriteSchemaUnmarshallerCall: +class TestOAS30RequestSchemaUnmarshallerCall: @pytest.fixture def unmarshaller_factory(self, schema_unmarshaller_factory): return partial( schema_unmarshaller_factory, - OAS30Validator, - context=ValidationContext.REQUEST, + OAS30WriteValidator, + base_format_checker=oas30_format_checker, ) def test_write_only_properties(self, unmarshaller_factory): @@ -1014,7 +1033,11 @@ def test_read_only_properties_invalid(self, unmarshaller_factory): class TestOAS31SchemaUnmarshallerCall: @pytest.fixture def unmarshaller_factory(self, schema_unmarshaller_factory): - return partial(schema_unmarshaller_factory, OAS31Validator) + return partial( + schema_unmarshaller_factory, + OAS31Validator, + base_format_checker=oas31_format_checker, + ) def test_null(self, unmarshaller_factory): schema = {"type": "null"} diff --git a/tests/unit/unmarshalling/test_validate.py b/tests/unit/unmarshalling/test_validate.py index 22b49dc8..ace62bad 100644 --- a/tests/unit/unmarshalling/test_validate.py +++ b/tests/unit/unmarshalling/test_validate.py @@ -7,9 +7,6 @@ from openapi_core.unmarshalling.schemas import ( oas30_request_schema_unmarshallers_factory, ) -from openapi_core.unmarshalling.schemas.exceptions import ( - FormatterNotFoundError, -) from openapi_core.unmarshalling.schemas.exceptions import InvalidSchemaValue @@ -72,7 +69,7 @@ def test_string_format_custom_missing(self, validator_factory): spec = Spec.from_dict(schema, validator=None) value = "x" - with pytest.raises(FormatterNotFoundError): + with pytest.raises(InvalidSchemaValue): validator_factory(spec).validate(value) @pytest.mark.parametrize("value", [False, True]) @@ -615,11 +612,27 @@ def test_string_format_byte_invalid(self, value, validator_factory): [ "test", b"stream", + ], + ) + def test_string_format_unknown(self, value, validator_factory): + unknown_format = "unknown" + schema = { + "type": "string", + "format": unknown_format, + } + spec = Spec.from_dict(schema, validator=None) + + with pytest.raises(InvalidSchemaValue): + validator_factory(spec).validate(value) + + @pytest.mark.parametrize( + "value", + [ datetime.date(1989, 1, 2), datetime.datetime(1989, 1, 2, 0, 0, 0), ], ) - def test_string_format_unknown(self, value, validator_factory): + def test_string_format_unknown_and_invalid(self, value, validator_factory): unknown_format = "unknown" schema = { "type": "string", @@ -627,7 +640,7 @@ def test_string_format_unknown(self, value, validator_factory): } spec = Spec.from_dict(schema, validator=None) - with pytest.raises(FormatterNotFoundError): + with pytest.raises(InvalidSchemaValue): validator_factory(spec).validate(value) @pytest.mark.parametrize("value", ["", "a", "ab"])