Skip to content

Commit c35483f

Browse files
committed
Format validators and Format unmarshallers; eprecate custim formatters
1 parent d8db7ce commit c35483f

File tree

14 files changed

+407
-91
lines changed

14 files changed

+407
-91
lines changed

Diff for: docs/customizations.rst

+33-21
Original file line numberDiff line numberDiff line change
@@ -42,42 +42,54 @@ Pass custom defined media type deserializers dictionary with supported mimetypes
4242
media_type_deserializers_factory=media_type_deserializers_factory,
4343
)
4444
45-
Formats
46-
-------
45+
Format validators
46+
-----------------
4747

4848
OpenAPI defines a ``format`` keyword that hints at how a value should be interpreted, e.g. a ``string`` with the type ``date`` should conform to the RFC 3339 date format.
4949

50-
Openapi-core comes with a set of built-in formatters, but it's also possible to add custom formatters in `SchemaUnmarshallersFactory` and pass it to `RequestValidator` or `ResponseValidator`.
50+
OpenAPI comes with a set of built-in format validators, but it's also possible to add custom ones.
5151

5252
Here's how you could add support for a ``usdate`` format that handles dates of the form MM/DD/YYYY:
5353

5454
.. code-block:: python
5555
56-
from openapi_core.unmarshalling.schemas.factories import SchemaUnmarshallersFactory
57-
from openapi_schema_validator import OAS30Validator
58-
from datetime import datetime
5956
import re
6057
61-
class USDateFormatter:
62-
def validate(self, value) -> bool:
63-
return bool(re.match(r"^\d{1,2}/\d{1,2}/\d{4}$", value))
64-
65-
def format(self, value):
66-
return datetime.strptime(value, "%m/%d/%y").date
67-
58+
def validate_usdate(value):
59+
return bool(re.match(r"^\d{1,2}/\d{1,2}/\d{4}$", value))
6860
69-
custom_formatters = {
70-
'usdate': USDateFormatter(),
61+
extra_format_validators = {
62+
'usdate': validate_usdate,
7163
}
72-
schema_unmarshallers_factory = SchemaUnmarshallersFactory(
73-
OAS30Validator,
74-
custom_formatters=custom_formatters,
75-
)
7664
7765
result = validate_response(
7866
request, response,
7967
spec=spec,
80-
cls=ResponseValidator,
81-
schema_unmarshallers_factory=schema_unmarshallers_factory,
68+
extra_format_validators=extra_format_validators,
8269
)
8370
71+
Format unmarshallers
72+
--------------------
73+
74+
Based on ``format`` keyword, openapi-core can also unmarshal values to specific formats.
75+
76+
Openapi-core comes with a set of built-in format unmarshallers, but it's also possible to add custom ones.
77+
78+
Here's an example with the ``usdate`` format that converts a value to date object:
79+
80+
.. code-block:: python
81+
82+
from datetime import datetime
83+
84+
def unmarshal_usdate(value):
85+
return datetime.strptime(value, "%m/%d/%y").date
86+
87+
extra_format_unmarshallers = {
88+
'usdate': unmarshal_usdate,
89+
}
90+
91+
result = unmarshal_response(
92+
request, response,
93+
spec=spec,
94+
extra_format_unmarshallers=extra_format_unmarshallers,
95+
)

Diff for: openapi_core/unmarshalling/request/unmarshallers.py

+14
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,9 @@
3232
from openapi_core.unmarshalling.schemas import (
3333
oas31_schema_unmarshallers_factory,
3434
)
35+
from openapi_core.unmarshalling.schemas.datatypes import (
36+
FormatUnmarshallersDict,
37+
)
3538
from openapi_core.unmarshalling.schemas.factories import (
3639
SchemaUnmarshallersFactory,
3740
)
@@ -72,6 +75,7 @@
7275
V31WebhookRequestValidator,
7376
)
7477
from openapi_core.validation.request.validators import WebhookRequestValidator
78+
from openapi_core.validation.schemas.datatypes import FormatValidatorsDict
7579
from openapi_core.validation.schemas.factories import SchemaValidatorsFactory
7680

7781

@@ -84,10 +88,14 @@ def __init__(
8488
parameter_deserializers_factory: ParameterDeserializersFactory = parameter_deserializers_factory,
8589
media_type_deserializers_factory: MediaTypeDeserializersFactory = media_type_deserializers_factory,
8690
schema_validators_factory: Optional[SchemaValidatorsFactory] = None,
91+
format_validators: Optional[FormatValidatorsDict] = None,
92+
extra_format_validators: Optional[FormatValidatorsDict] = None,
8793
security_provider_factory: SecurityProviderFactory = security_provider_factory,
8894
schema_unmarshallers_factory: Optional[
8995
SchemaUnmarshallersFactory
9096
] = None,
97+
format_unmarshallers: Optional[FormatUnmarshallersDict] = None,
98+
extra_format_unmarshallers: Optional[FormatUnmarshallersDict] = None,
9199
):
92100
BaseUnmarshaller.__init__(
93101
self,
@@ -97,7 +105,11 @@ def __init__(
97105
parameter_deserializers_factory=parameter_deserializers_factory,
98106
media_type_deserializers_factory=media_type_deserializers_factory,
99107
schema_validators_factory=schema_validators_factory,
108+
format_validators=format_validators,
109+
extra_format_validators=extra_format_validators,
100110
schema_unmarshallers_factory=schema_unmarshallers_factory,
111+
format_unmarshallers=format_unmarshallers,
112+
extra_format_unmarshallers=extra_format_unmarshallers,
101113
)
102114
BaseRequestValidator.__init__(
103115
self,
@@ -107,6 +119,8 @@ def __init__(
107119
parameter_deserializers_factory=parameter_deserializers_factory,
108120
media_type_deserializers_factory=media_type_deserializers_factory,
109121
schema_validators_factory=schema_validators_factory,
122+
format_validators=format_validators,
123+
extra_format_validators=extra_format_validators,
110124
security_provider_factory=security_provider_factory,
111125
)
112126

Diff for: openapi_core/unmarshalling/schemas/datatypes.py

+1-7
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,6 @@
11
from typing import Any
22
from typing import Callable
33
from typing import Dict
4-
from typing import Optional
5-
6-
from openapi_core.unmarshalling.schemas.formatters import Formatter
74

85
FormatUnmarshaller = Callable[[Any], Any]
9-
10-
CustomFormattersDict = Dict[str, Formatter]
11-
FormattersDict = Dict[Optional[str], Formatter]
12-
UnmarshallersDict = Dict[str, Callable[[Any], Any]]
6+
FormatUnmarshallersDict = Dict[str, FormatUnmarshaller]

Diff for: openapi_core/unmarshalling/schemas/factories.py

+44-28
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,10 @@
22
import warnings
33
from typing import Optional
44

5-
if sys.version_info >= (3, 8):
6-
from functools import cached_property
7-
else:
8-
from backports.cached_property import cached_property
9-
105
from openapi_core.spec import Spec
11-
from openapi_core.unmarshalling.schemas.datatypes import CustomFormattersDict
12-
from openapi_core.unmarshalling.schemas.datatypes import FormatUnmarshaller
13-
from openapi_core.unmarshalling.schemas.datatypes import UnmarshallersDict
6+
from openapi_core.unmarshalling.schemas.datatypes import (
7+
FormatUnmarshallersDict,
8+
)
149
from openapi_core.unmarshalling.schemas.exceptions import (
1510
FormatterNotFoundError,
1611
)
@@ -19,6 +14,8 @@
1914
)
2015
from openapi_core.unmarshalling.schemas.unmarshallers import SchemaUnmarshaller
2116
from openapi_core.unmarshalling.schemas.unmarshallers import TypesUnmarshaller
17+
from openapi_core.validation.schemas.datatypes import CustomFormattersDict
18+
from openapi_core.validation.schemas.datatypes import FormatValidatorsDict
2219
from openapi_core.validation.schemas.factories import SchemaValidatorsFactory
2320

2421

@@ -27,55 +24,74 @@ def __init__(
2724
self,
2825
schema_validators_factory: SchemaValidatorsFactory,
2926
types_unmarshaller: TypesUnmarshaller,
30-
format_unmarshallers: Optional[UnmarshallersDict] = None,
27+
format_unmarshallers: Optional[FormatUnmarshallersDict] = None,
3128
custom_formatters: Optional[CustomFormattersDict] = None,
3229
):
3330
self.schema_validators_factory = schema_validators_factory
3431
self.types_unmarshaller = types_unmarshaller
35-
if custom_formatters is None:
36-
custom_formatters = {}
3732
if format_unmarshallers is None:
3833
format_unmarshallers = {}
3934
self.format_unmarshallers = format_unmarshallers
35+
if custom_formatters is None:
36+
custom_formatters = {}
37+
else:
38+
warnings.warn(
39+
"custom_formatters is deprecated. "
40+
"Use extra_format_validators to validate custom formats "
41+
"and use extra_format_unmarshallers to unmarshal custom formats.",
42+
DeprecationWarning,
43+
)
4044
self.custom_formatters = custom_formatters
4145

42-
@cached_property
43-
def formats_unmarshaller(self) -> FormatsUnmarshaller:
44-
return FormatsUnmarshaller(
45-
self.format_unmarshallers,
46-
self.custom_formatters,
47-
)
48-
49-
def create(self, schema: Spec) -> SchemaUnmarshaller:
46+
def create(
47+
self,
48+
schema: Spec,
49+
format_validators: Optional[FormatValidatorsDict] = None,
50+
format_unmarshallers: Optional[FormatUnmarshallersDict] = None,
51+
extra_format_validators: Optional[FormatValidatorsDict] = None,
52+
extra_format_unmarshallers: Optional[FormatUnmarshallersDict] = None,
53+
) -> SchemaUnmarshaller:
5054
"""Create unmarshaller from the schema."""
5155
if schema is None:
5256
raise TypeError("Invalid schema")
5357

5458
if schema.getkey("deprecated", False):
5559
warnings.warn("The schema is deprecated", DeprecationWarning)
5660

57-
formatters_checks = {
58-
name: formatter.validate
59-
for name, formatter in self.custom_formatters.items()
60-
}
61+
if extra_format_validators is None:
62+
extra_format_validators = {}
63+
extra_format_validators.update(
64+
{
65+
name: formatter.validate
66+
for name, formatter in self.custom_formatters.items()
67+
}
68+
)
6169
schema_validator = self.schema_validators_factory.create(
62-
schema, **formatters_checks
70+
schema,
71+
format_validators=format_validators,
72+
extra_format_validators=extra_format_validators,
6373
)
6474

6575
schema_format = schema.getkey("format")
6676

77+
formats_unmarshaller = FormatsUnmarshaller(
78+
format_unmarshallers or self.format_unmarshallers,
79+
extra_format_unmarshallers,
80+
self.custom_formatters,
81+
)
82+
6783
# FIXME: don;t raise exception on unknown format
84+
# See https://github.com/p1c2u/openapi-core/issues/515
6885
if (
6986
schema_format
70-
and schema_format
71-
not in self.schema_validators_factory.format_checker.checkers
72-
and schema_format not in self.custom_formatters
87+
and schema_format not in schema_validator
88+
and schema_format not in formats_unmarshaller
7389
):
7490
raise FormatterNotFoundError(schema_format)
7591

7692
return SchemaUnmarshaller(
7793
schema,
7894
schema_validator,
7995
self.types_unmarshaller,
80-
self.formats_unmarshaller,
96+
formats_unmarshaller,
8197
)

Diff for: openapi_core/unmarshalling/schemas/unmarshallers.py

+21-3
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,13 @@
1212
from openapi_core.extensions.models.factories import ModelPathFactory
1313
from openapi_core.schema.schemas import get_properties
1414
from openapi_core.spec import Spec
15-
from openapi_core.unmarshalling.schemas.datatypes import CustomFormattersDict
1615
from openapi_core.unmarshalling.schemas.datatypes import FormatUnmarshaller
17-
from openapi_core.unmarshalling.schemas.datatypes import UnmarshallersDict
16+
from openapi_core.unmarshalling.schemas.datatypes import (
17+
FormatUnmarshallersDict,
18+
)
1819
from openapi_core.unmarshalling.schemas.exceptions import FormatUnmarshalError
1920
from openapi_core.unmarshalling.schemas.exceptions import UnmarshallerError
21+
from openapi_core.validation.schemas.datatypes import CustomFormattersDict
2022
from openapi_core.validation.schemas.validators import SchemaValidator
2123

2224
log = logging.getLogger(__name__)
@@ -212,12 +214,16 @@ def get_unmarshaller(
212214
class FormatsUnmarshaller:
213215
def __init__(
214216
self,
215-
format_unmarshallers: Optional[UnmarshallersDict] = None,
217+
format_unmarshallers: Optional[FormatUnmarshallersDict] = None,
218+
extra_format_unmarshallers: Optional[FormatUnmarshallersDict] = None,
216219
custom_formatters: Optional[CustomFormattersDict] = None,
217220
):
218221
if format_unmarshallers is None:
219222
format_unmarshallers = {}
220223
self.format_unmarshallers = format_unmarshallers
224+
if extra_format_unmarshallers is None:
225+
extra_format_unmarshallers = {}
226+
self.extra_format_unmarshallers = extra_format_unmarshallers
221227
if custom_formatters is None:
222228
custom_formatters = {}
223229
self.custom_formatters = custom_formatters
@@ -237,11 +243,23 @@ def get_unmarshaller(
237243
if schema_format in self.custom_formatters:
238244
formatter = self.custom_formatters[schema_format]
239245
return formatter.format
246+
if schema_format in self.extra_format_unmarshallers:
247+
return self.extra_format_unmarshallers[schema_format]
240248
if schema_format in self.format_unmarshallers:
241249
return self.format_unmarshallers[schema_format]
242250

243251
return None
244252

253+
def __contains__(self, schema_format: str) -> bool:
254+
for content in [
255+
self.custom_formatters,
256+
self.extra_format_unmarshallers,
257+
self.format_unmarshallers,
258+
]:
259+
if schema_format in content:
260+
return True
261+
return False
262+
245263

246264
class SchemaUnmarshaller:
247265
def __init__(

Diff for: openapi_core/unmarshalling/unmarshallers.py

+19-1
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,13 @@
1818
ParameterDeserializersFactory,
1919
)
2020
from openapi_core.spec import Spec
21+
from openapi_core.unmarshalling.schemas.datatypes import (
22+
FormatUnmarshallersDict,
23+
)
2124
from openapi_core.unmarshalling.schemas.factories import (
2225
SchemaUnmarshallersFactory,
2326
)
27+
from openapi_core.validation.schemas.datatypes import FormatValidatorsDict
2428
from openapi_core.validation.schemas.factories import SchemaValidatorsFactory
2529
from openapi_core.validation.validators import BaseValidator
2630

@@ -36,9 +40,13 @@ def __init__(
3640
parameter_deserializers_factory: ParameterDeserializersFactory = parameter_deserializers_factory,
3741
media_type_deserializers_factory: MediaTypeDeserializersFactory = media_type_deserializers_factory,
3842
schema_validators_factory: Optional[SchemaValidatorsFactory] = None,
43+
format_validators: Optional[FormatValidatorsDict] = None,
44+
extra_format_validators: Optional[FormatValidatorsDict] = None,
3945
schema_unmarshallers_factory: Optional[
4046
SchemaUnmarshallersFactory
4147
] = None,
48+
format_unmarshallers: Optional[FormatUnmarshallersDict] = None,
49+
extra_format_unmarshallers: Optional[FormatUnmarshallersDict] = None,
4250
):
4351
if schema_validators_factory is None and schema_unmarshallers_factory:
4452
schema_validators_factory = (
@@ -51,6 +59,8 @@ def __init__(
5159
parameter_deserializers_factory=parameter_deserializers_factory,
5260
media_type_deserializers_factory=media_type_deserializers_factory,
5361
schema_validators_factory=schema_validators_factory,
62+
format_validators=format_validators,
63+
extra_format_validators=extra_format_validators,
5464
)
5565
self.schema_unmarshallers_factory = (
5666
schema_unmarshallers_factory or self.schema_unmarshallers_factory
@@ -59,9 +69,17 @@ def __init__(
5969
raise NotImplementedError(
6070
"schema_unmarshallers_factory is not assigned"
6171
)
72+
self.format_unmarshallers = format_unmarshallers
73+
self.extra_format_unmarshallers = extra_format_unmarshallers
6274

6375
def _unmarshal_schema(self, schema: Spec, value: Any) -> Any:
64-
unmarshaller = self.schema_unmarshallers_factory.create(schema)
76+
unmarshaller = self.schema_unmarshallers_factory.create(
77+
schema,
78+
format_validators=self.format_validators,
79+
extra_format_validators=self.extra_format_validators,
80+
format_unmarshallers=self.format_unmarshallers,
81+
extra_format_unmarshallers=self.extra_format_unmarshallers,
82+
)
6583
return unmarshaller.unmarshal(value)
6684

6785
def _get_param_or_header_value(

0 commit comments

Comments
 (0)