Skip to content

Commit a817d21

Browse files
authored
Merge pull request #434 from p1c2u/refactor/umnarshaller-format-refactor
Unmarshaller format refactor
2 parents 3ffe2bc + 692a915 commit a817d21

File tree

5 files changed

+91
-68
lines changed

5 files changed

+91
-68
lines changed

docs/customizations.rst

+1-1
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ Here's how you could add support for a ``usdate`` format that handles dates of t
6161
def validate(self, value) -> bool:
6262
return bool(re.match(r"^\d{1,2}/\d{1,2}/\d{4}$", value))
6363
64-
def unmarshal(self, value):
64+
def format(self, value):
6565
return datetime.strptime(value, "%m/%d/%y").date
6666
6767

openapi_core/unmarshalling/schemas/factories.py

-8
Original file line numberDiff line numberDiff line change
@@ -108,14 +108,6 @@ def create(
108108
klass = self.UNMARSHALLERS[schema_type]
109109
return klass(schema, validator, formatter)
110110

111-
def get_formatter(
112-
self, type_format: str, default_formatters: FormattersDict
113-
) -> Optional[Formatter]:
114-
try:
115-
return self.custom_formatters[type_format]
116-
except KeyError:
117-
return default_formatters.get(type_format)
118-
119111
def get_validator(self, schema: Spec) -> Validator:
120112
resolver = schema.accessor.resolver # type: ignore
121113
custom_format_checks = {
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import warnings
12
from typing import Any
23
from typing import Callable
34
from typing import Optional
@@ -8,20 +9,49 @@ class Formatter:
89
def validate(self, value: Any) -> bool:
910
return True
1011

11-
def unmarshal(self, value: Any) -> Any:
12+
def format(self, value: Any) -> Any:
1213
return value
1314

15+
def __getattribute__(self, name: str) -> Any:
16+
if name == "unmarshal":
17+
warnings.warn(
18+
"Unmarshal method is deprecated. " "Use format instead.",
19+
DeprecationWarning,
20+
)
21+
return super().__getattribute__("format")
22+
if name == "format":
23+
try:
24+
attr = super().__getattribute__("unmarshal")
25+
except AttributeError:
26+
return super().__getattribute__("format")
27+
else:
28+
warnings.warn(
29+
"Unmarshal method is deprecated. "
30+
"Rename unmarshal method to format instead.",
31+
DeprecationWarning,
32+
)
33+
return attr
34+
return super().__getattribute__(name)
35+
1436
@classmethod
1537
def from_callables(
1638
cls,
17-
validate: Optional[Callable[[Any], Any]] = None,
39+
validate_callable: Optional[Callable[[Any], Any]] = None,
40+
format_callable: Optional[Callable[[Any], Any]] = None,
1841
unmarshal: Optional[Callable[[Any], Any]] = None,
1942
) -> "Formatter":
2043
attrs = {}
21-
if validate is not None:
22-
attrs["validate"] = staticmethod(validate)
44+
if validate_callable is not None:
45+
attrs["validate"] = staticmethod(validate_callable)
46+
if format_callable is not None:
47+
attrs["format"] = staticmethod(format_callable)
2348
if unmarshal is not None:
24-
attrs["unmarshal"] = staticmethod(unmarshal)
49+
warnings.warn(
50+
"Unmarshal parameter is deprecated. "
51+
"Use format_callable instead.",
52+
DeprecationWarning,
53+
)
54+
attrs["format"] = staticmethod(unmarshal)
2555

2656
klass: Type[Formatter] = type("Formatter", (cls,), attrs)
2757
return klass()

openapi_core/unmarshalling/schemas/unmarshallers.py

+54-53
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
from typing import TYPE_CHECKING
44
from typing import Any
55
from typing import Iterable
6+
from typing import Iterator
67
from typing import List
78
from typing import Optional
89
from typing import cast
@@ -31,6 +32,7 @@
3132
)
3233
from openapi_core.unmarshalling.schemas.exceptions import InvalidSchemaValue
3334
from openapi_core.unmarshalling.schemas.exceptions import UnmarshalError
35+
from openapi_core.unmarshalling.schemas.exceptions import UnmarshallerError
3436
from openapi_core.unmarshalling.schemas.exceptions import ValidateError
3537
from openapi_core.unmarshalling.schemas.formatters import Formatter
3638
from openapi_core.unmarshalling.schemas.util import format_byte
@@ -61,24 +63,25 @@ def __init__(
6163
):
6264
self.schema = schema
6365
self.validator = validator
64-
self.format = schema.getkey("format")
66+
self.schema_format = schema.getkey("format")
6567

6668
if formatter is None:
67-
if self.format not in self.FORMATTERS:
68-
raise FormatterNotFoundError(self.format)
69-
self.formatter = self.FORMATTERS[self.format]
69+
if self.schema_format not in self.FORMATTERS:
70+
raise FormatterNotFoundError(self.schema_format)
71+
self.formatter = self.FORMATTERS[self.schema_format]
7072
else:
7173
self.formatter = formatter
7274

7375
def __call__(self, value: Any) -> Any:
74-
if value is None:
75-
return
76-
7776
self.validate(value)
7877

78+
# skip unmarshalling for nullable in OpenAPI 3.0
79+
if value is None and self.schema.getkey("nullable", False):
80+
return value
81+
7982
return self.unmarshal(value)
8083

81-
def _formatter_validate(self, value: Any) -> None:
84+
def _validate_format(self, value: Any) -> None:
8285
result = self.formatter.validate(value)
8386
if not result:
8487
schema_type = self.schema.getkey("type", "any")
@@ -91,11 +94,14 @@ def validate(self, value: Any) -> None:
9194
schema_type = self.schema.getkey("type", "any")
9295
raise InvalidSchemaValue(value, schema_type, schema_errors=errors)
9396

94-
def unmarshal(self, value: Any) -> Any:
97+
def format(self, value: Any) -> Any:
9598
try:
96-
return self.formatter.unmarshal(value)
97-
except ValueError as exc:
98-
raise InvalidSchemaFormatValue(value, self.format, exc)
99+
return self.formatter.format(value)
100+
except (ValueError, TypeError) as exc:
101+
raise InvalidSchemaFormatValue(value, self.schema_format, exc)
102+
103+
def unmarshal(self, value: Any) -> Any:
104+
return self.format(value)
99105

100106

101107
class StringUnmarshaller(BaseSchemaUnmarshaller):
@@ -192,10 +198,8 @@ def items_unmarshaller(self) -> "BaseSchemaUnmarshaller":
192198
items_schema = self.schema.get("items", Spec.from_dict({}))
193199
return self.unmarshallers_factory.create(items_schema)
194200

195-
def __call__(self, value: Any) -> Optional[List[Any]]:
196-
value = super().__call__(value)
197-
if value is None and self.schema.getkey("nullable", False):
198-
return None
201+
def unmarshal(self, value: Any) -> Optional[List[Any]]:
202+
value = super().unmarshal(value)
199203
return list(map(self.items_unmarshaller, value))
200204

201205

@@ -210,38 +214,31 @@ def object_class_factory(self) -> ModelPathFactory:
210214
return ModelPathFactory()
211215

212216
def unmarshal(self, value: Any) -> Any:
213-
properties = self.unmarshal_raw(value)
217+
properties = self.format(value)
214218

215219
fields: Iterable[str] = properties and properties.keys() or []
216220
object_class = self.object_class_factory.create(self.schema, fields)
217221

218222
return object_class(**properties)
219223

220-
def unmarshal_raw(self, value: Any) -> Any:
221-
try:
222-
value = self.formatter.unmarshal(value)
223-
except ValueError as exc:
224-
schema_format = self.schema.getkey("format")
225-
raise InvalidSchemaFormatValue(value, schema_format, exc)
226-
else:
227-
return self._unmarshal_object(value)
224+
def format(self, value: Any) -> Any:
225+
formatted = super().format(value)
226+
return self._unmarshal_properties(formatted)
228227

229228
def _clone(self, schema: Spec) -> "ObjectUnmarshaller":
230229
return cast(
231230
"ObjectUnmarshaller",
232231
self.unmarshallers_factory.create(schema, "object"),
233232
)
234233

235-
def _unmarshal_object(self, value: Any) -> Any:
234+
def _unmarshal_properties(self, value: Any) -> Any:
236235
properties = {}
237236

238237
if "oneOf" in self.schema:
239238
one_of_properties = None
240239
for one_of_schema in self.schema / "oneOf":
241240
try:
242-
unmarshalled = self._clone(one_of_schema).unmarshal_raw(
243-
value
244-
)
241+
unmarshalled = self._clone(one_of_schema).format(value)
245242
except (UnmarshalError, ValueError):
246243
pass
247244
else:
@@ -259,9 +256,7 @@ def _unmarshal_object(self, value: Any) -> Any:
259256
any_of_properties = None
260257
for any_of_schema in self.schema / "anyOf":
261258
try:
262-
unmarshalled = self._clone(any_of_schema).unmarshal_raw(
263-
value
264-
)
259+
unmarshalled = self._clone(any_of_schema).format(value)
265260
except (UnmarshalError, ValueError):
266261
pass
267262
else:
@@ -319,21 +314,36 @@ def types_unmarshallers(self) -> List["BaseSchemaUnmarshaller"]:
319314
unmarshaller = partial(self.unmarshallers_factory.create, self.schema)
320315
return list(map(unmarshaller, types))
321316

322-
def unmarshal(self, value: Any) -> Any:
323-
for unmarshaller in self.types_unmarshallers:
317+
@property
318+
def type(self) -> List[str]:
319+
types = self.schema.getkey("type", ["any"])
320+
assert isinstance(types, list)
321+
return types
322+
323+
def _get_unmarshallers_iter(self) -> Iterator["BaseSchemaUnmarshaller"]:
324+
for schema_type in self.type:
325+
yield self.unmarshallers_factory.create(
326+
self.schema, type_override=schema_type
327+
)
328+
329+
def _get_best_unmarshaller(self, value: Any) -> "BaseSchemaUnmarshaller":
330+
for unmarshaller in self._get_unmarshallers_iter():
324331
# validate with validator of formatter (usualy type validator)
325332
try:
326-
unmarshaller._formatter_validate(value)
333+
unmarshaller._validate_format(value)
327334
except ValidateError:
328335
continue
329336
else:
330-
return unmarshaller(value)
337+
return unmarshaller
331338

332-
log.warning("failed to unmarshal multi type")
333-
return value
339+
raise UnmarshallerError("Unmarshaller not found for type(s)")
340+
341+
def unmarshal(self, value: Any) -> Any:
342+
unmarshaller = self._get_best_unmarshaller(value)
343+
return unmarshaller(value)
334344

335345

336-
class AnyUnmarshaller(ComplexUnmarshaller):
346+
class AnyUnmarshaller(MultiTypeUnmarshaller):
337347

338348
SCHEMA_TYPES_ORDER = [
339349
"object",
@@ -344,6 +354,10 @@ class AnyUnmarshaller(ComplexUnmarshaller):
344354
"string",
345355
]
346356

357+
@property
358+
def type(self) -> List[str]:
359+
return self.SCHEMA_TYPES_ORDER
360+
347361
def unmarshal(self, value: Any) -> Any:
348362
one_of_schema = self._get_one_of_schema(value)
349363
if one_of_schema:
@@ -357,20 +371,7 @@ def unmarshal(self, value: Any) -> Any:
357371
if all_of_schema:
358372
return self.unmarshallers_factory.create(all_of_schema)(value)
359373

360-
for schema_type in self.SCHEMA_TYPES_ORDER:
361-
unmarshaller = self.unmarshallers_factory.create(
362-
self.schema, type_override=schema_type
363-
)
364-
# validate with validator of formatter (usualy type validator)
365-
try:
366-
unmarshaller._formatter_validate(value)
367-
except ValidateError:
368-
continue
369-
else:
370-
return unmarshaller(value)
371-
372-
log.warning("failed to unmarshal any type")
373-
return value
374+
return super().unmarshal(value)
374375

375376
def _get_one_of_schema(self, value: Any) -> Optional[Spec]:
376377
if "oneOf" not in self.schema:

tests/unit/unmarshalling/test_unmarshal.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -406,7 +406,7 @@ def test_array_null(self, unmarshaller_factory):
406406
spec = Spec.from_dict(schema)
407407
value = None
408408

409-
with pytest.raises(TypeError):
409+
with pytest.raises(InvalidSchemaValue):
410410
unmarshaller_factory(spec)(value)
411411

412412
def test_array_nullable(self, unmarshaller_factory):

0 commit comments

Comments
 (0)