Skip to content

Commit 5d0a7d0

Browse files
authored
Merge pull request #1075 from python-openapi/feature/typed-style-deserializers
Typed style deserializers
2 parents 33e4503 + 0c301b7 commit 5d0a7d0

File tree

21 files changed

+450
-148
lines changed

21 files changed

+450
-148
lines changed

openapi_core/casting/schemas/casters.py

Lines changed: 25 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
from typing import Any
22
from typing import Generic
33
from typing import Iterable
4-
from typing import List
54
from typing import Mapping
65
from typing import Optional
76
from typing import Type
@@ -28,6 +27,14 @@ def __init__(
2827
self.schema_caster = schema_caster
2928

3029
def __call__(self, value: Any) -> Any:
30+
self.validate(value)
31+
32+
return self.cast(value)
33+
34+
def validate(self, value: Any) -> None:
35+
pass
36+
37+
def cast(self, value: Any) -> Any:
3138
return value
3239

3340

@@ -37,18 +44,9 @@ def __call__(self, value: Any) -> Any:
3744
class PrimitiveTypeCaster(Generic[PrimitiveType], PrimitiveCaster):
3845
primitive_type: Type[PrimitiveType] = NotImplemented
3946

40-
def __call__(self, value: Union[str, bytes]) -> Any:
41-
self.validate(value)
42-
47+
def cast(self, value: Union[str, bytes]) -> PrimitiveType:
4348
return self.primitive_type(value) # type: ignore [call-arg]
4449

45-
def validate(self, value: Any) -> None:
46-
# FIXME: don't cast data from media type deserializer
47-
# See https://github.com/python-openapi/openapi-core/issues/706
48-
# if not isinstance(value, (str, bytes)):
49-
# raise ValueError("should cast only from string or bytes")
50-
pass
51-
5250

5351
class IntegerCaster(PrimitiveTypeCaster[int]):
5452
primitive_type = int
@@ -61,22 +59,18 @@ class NumberCaster(PrimitiveTypeCaster[float]):
6159
class BooleanCaster(PrimitiveTypeCaster[bool]):
6260
primitive_type = bool
6361

64-
def __call__(self, value: Union[str, bytes]) -> Any:
65-
self.validate(value)
66-
67-
return self.primitive_type(forcebool(value))
68-
6962
def validate(self, value: Any) -> None:
7063
super().validate(value)
7164

72-
# FIXME: don't cast data from media type deserializer
73-
# See https://github.com/python-openapi/openapi-core/issues/706
7465
if isinstance(value, bool):
7566
return
7667

7768
if value.lower() not in ["false", "true"]:
7869
raise ValueError("not a boolean format")
7970

71+
def cast(self, value: Union[str, bytes]) -> bool:
72+
return self.primitive_type(forcebool(value))
73+
8074

8175
class ArrayCaster(PrimitiveCaster):
8276
@property
@@ -85,19 +79,21 @@ def items_caster(self) -> "SchemaCaster":
8579
items_schema = self.schema.get("items", SchemaPath.from_dict({}))
8680
return self.schema_caster.evolve(items_schema)
8781

88-
def __call__(self, value: Any) -> List[Any]:
82+
def validate(self, value: Any) -> None:
8983
# str and bytes are not arrays according to the OpenAPI spec
9084
if isinstance(value, (str, bytes)) or not isinstance(value, Iterable):
91-
raise CastError(value, self.schema["type"])
85+
raise ValueError("not an array format")
9286

93-
try:
94-
return list(map(self.items_caster.cast, value))
95-
except (ValueError, TypeError):
96-
raise CastError(value, self.schema["type"])
87+
def cast(self, value: list[Any]) -> list[Any]:
88+
return list(map(self.items_caster.cast, value))
9789

9890

9991
class ObjectCaster(PrimitiveCaster):
100-
def __call__(self, value: Any) -> Any:
92+
def validate(self, value: Any) -> None:
93+
if not isinstance(value, dict):
94+
raise ValueError("not an object format")
95+
96+
def cast(self, value: dict[str, Any]) -> dict[str, Any]:
10197
return self._cast_proparties(value)
10298

10399
def evolve(self, schema: SchemaPath) -> "ObjectCaster":
@@ -109,9 +105,11 @@ def evolve(self, schema: SchemaPath) -> "ObjectCaster":
109105
self.schema_caster.evolve(schema),
110106
)
111107

112-
def _cast_proparties(self, value: Any, schema_only: bool = False) -> Any:
108+
def _cast_proparties(
109+
self, value: dict[str, Any], schema_only: bool = False
110+
) -> dict[str, Any]:
113111
if not isinstance(value, dict):
114-
raise CastError(value, self.schema["type"])
112+
raise ValueError("not an object format")
115113

116114
all_of_schemas = self.schema_validator.iter_all_of_schemas(value)
117115
for all_of_schema in all_of_schemas:

openapi_core/casting/schemas/datatypes.py

Lines changed: 0 additions & 4 deletions
This file was deleted.

openapi_core/casting/schemas/exceptions.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
from dataclasses import dataclass
22
from typing import Any
33

4-
from openapi_core.exceptions import OpenAPIError
4+
from openapi_core.deserializing.exceptions import DeserializeError
55

66

77
@dataclass
8-
class CastError(OpenAPIError):
8+
class CastError(DeserializeError):
99
"""Schema cast operation error"""
1010

1111
value: Any

openapi_core/deserializing/media_types/__init__.py

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,8 @@
1212
from openapi_core.deserializing.media_types.util import plain_loads
1313
from openapi_core.deserializing.media_types.util import urlencoded_form_loads
1414
from openapi_core.deserializing.media_types.util import xml_loads
15-
from openapi_core.deserializing.styles import style_deserializers_factory
1615

17-
__all__ = ["media_type_deserializers_factory"]
16+
__all__ = ["media_type_deserializers", "MediaTypeDeserializersFactory"]
1817

1918
media_type_deserializers: MediaTypeDeserializersDict = defaultdict(
2019
lambda: binary_loads,
@@ -30,8 +29,3 @@
3029
"multipart/form-data": data_form_loads,
3130
}
3231
)
33-
34-
media_type_deserializers_factory = MediaTypeDeserializersFactory(
35-
style_deserializers_factory,
36-
media_type_deserializers=media_type_deserializers,
37-
)

openapi_core/deserializing/media_types/factories.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
from jsonschema_path import SchemaPath
55

6+
from openapi_core.casting.schemas.factories import SchemaCastersFactory
67
from openapi_core.deserializing.media_types.datatypes import (
78
MediaTypeDeserializersDict,
89
)
@@ -12,6 +13,7 @@
1213
from openapi_core.deserializing.media_types.deserializers import (
1314
MediaTypesDeserializer,
1415
)
16+
from openapi_core.deserializing.styles.datatypes import StyleDeserializersDict
1517
from openapi_core.deserializing.styles.factories import (
1618
StyleDeserializersFactory,
1719
)
@@ -28,6 +30,31 @@ def __init__(
2830
media_type_deserializers = {}
2931
self.media_type_deserializers = media_type_deserializers
3032

33+
@classmethod
34+
def from_schema_casters_factory(
35+
cls,
36+
schema_casters_factory: SchemaCastersFactory,
37+
style_deserializers: Optional[StyleDeserializersDict] = None,
38+
media_type_deserializers: Optional[MediaTypeDeserializersDict] = None,
39+
) -> "MediaTypeDeserializersFactory":
40+
from openapi_core.deserializing.media_types import (
41+
media_type_deserializers as default_media_type_deserializers,
42+
)
43+
from openapi_core.deserializing.styles import (
44+
style_deserializers as default_style_deserializers,
45+
)
46+
47+
style_deserializers_factory = StyleDeserializersFactory(
48+
schema_casters_factory,
49+
style_deserializers=style_deserializers
50+
or default_style_deserializers,
51+
)
52+
return cls(
53+
style_deserializers_factory,
54+
media_type_deserializers=media_type_deserializers
55+
or default_media_type_deserializers,
56+
)
57+
3158
def create(
3259
self,
3360
mimetype: str,

openapi_core/deserializing/styles/__init__.py

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
from openapi_core.deserializing.styles.util import simple_loads
1111
from openapi_core.deserializing.styles.util import space_delimited_loads
1212

13-
__all__ = ["style_deserializers_factory"]
13+
__all__ = ["style_deserializers", "StyleDeserializersFactory"]
1414

1515
style_deserializers: StyleDeserializersDict = {
1616
"matrix": matrix_loads,
@@ -21,7 +21,3 @@
2121
"pipeDelimited": pipe_delimited_loads,
2222
"deepObject": deep_object_loads,
2323
}
24-
25-
style_deserializers_factory = StyleDeserializersFactory(
26-
style_deserializers=style_deserializers,
27-
)

openapi_core/deserializing/styles/deserializers.py

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,10 @@
33
from typing import Mapping
44
from typing import Optional
55

6+
from jsonschema_path import SchemaPath
7+
8+
from openapi_core.casting.schemas.casters import SchemaCaster
9+
from openapi_core.casting.schemas.exceptions import CastError
610
from openapi_core.deserializing.exceptions import DeserializeError
711
from openapi_core.deserializing.styles.datatypes import DeserializerCallable
812

@@ -13,13 +17,16 @@ def __init__(
1317
style: str,
1418
explode: bool,
1519
name: str,
16-
schema_type: str,
20+
schema: SchemaPath,
21+
caster: SchemaCaster,
1722
deserializer_callable: Optional[DeserializerCallable] = None,
1823
):
1924
self.style = style
2025
self.explode = explode
2126
self.name = name
22-
self.schema_type = schema_type
27+
self.schema = schema
28+
self.schema_type = schema.getkey("type", "")
29+
self.caster = caster
2330
self.deserializer_callable = deserializer_callable
2431

2532
def deserialize(self, location: Mapping[str, Any]) -> Any:
@@ -28,8 +35,13 @@ def deserialize(self, location: Mapping[str, Any]) -> Any:
2835
return location[self.name]
2936

3037
try:
31-
return self.deserializer_callable(
38+
value = self.deserializer_callable(
3239
self.explode, self.name, self.schema_type, location
3340
)
34-
except (ValueError, TypeError, AttributeError):
35-
raise DeserializeError(self.style, self.name)
41+
except (ValueError, TypeError, AttributeError) as exc:
42+
raise DeserializeError(self.style, self.name) from exc
43+
44+
try:
45+
return self.caster.cast(value)
46+
except (ValueError, TypeError, AttributeError) as exc:
47+
raise CastError(value, self.schema_type) from exc

openapi_core/deserializing/styles/factories.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,18 @@
22

33
from jsonschema_path import SchemaPath
44

5+
from openapi_core.casting.schemas.factories import SchemaCastersFactory
56
from openapi_core.deserializing.styles.datatypes import StyleDeserializersDict
67
from openapi_core.deserializing.styles.deserializers import StyleDeserializer
78

89

910
class StyleDeserializersFactory:
1011
def __init__(
1112
self,
13+
schema_casters_factory: SchemaCastersFactory,
1214
style_deserializers: Optional[StyleDeserializersDict] = None,
1315
):
16+
self.schema_casters_factory = schema_casters_factory
1417
if style_deserializers is None:
1518
style_deserializers = {}
1619
self.style_deserializers = style_deserializers
@@ -22,9 +25,8 @@ def create(
2225
schema: SchemaPath,
2326
name: str,
2427
) -> StyleDeserializer:
25-
schema_type = schema.getkey("type", "")
26-
2728
deserialize_callable = self.style_deserializers.get(style)
29+
caster = self.schema_casters_factory.create(schema)
2830
return StyleDeserializer(
29-
style, explode, name, schema_type, deserialize_callable
31+
style, explode, name, schema, caster, deserialize_callable
3032
)

openapi_core/unmarshalling/request/protocols.py

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -8,16 +8,12 @@
88
from openapi_spec_validator.validation.types import SpecValidatorType
99

1010
from openapi_core.casting.schemas.factories import SchemaCastersFactory
11-
from openapi_core.deserializing.media_types import (
12-
media_type_deserializers_factory,
13-
)
1411
from openapi_core.deserializing.media_types.datatypes import (
1512
MediaTypeDeserializersDict,
1613
)
1714
from openapi_core.deserializing.media_types.factories import (
1815
MediaTypeDeserializersFactory,
1916
)
20-
from openapi_core.deserializing.styles import style_deserializers_factory
2117
from openapi_core.deserializing.styles.factories import (
2218
StyleDeserializersFactory,
2319
)
@@ -43,8 +39,12 @@ def __init__(
4339
self,
4440
spec: SchemaPath,
4541
base_url: Optional[str] = None,
46-
style_deserializers_factory: StyleDeserializersFactory = style_deserializers_factory,
47-
media_type_deserializers_factory: MediaTypeDeserializersFactory = media_type_deserializers_factory,
42+
style_deserializers_factory: Optional[
43+
StyleDeserializersFactory
44+
] = None,
45+
media_type_deserializers_factory: Optional[
46+
MediaTypeDeserializersFactory
47+
] = None,
4848
schema_casters_factory: Optional[SchemaCastersFactory] = None,
4949
schema_validators_factory: Optional[SchemaValidatorsFactory] = None,
5050
path_finder_cls: Optional[PathFinderType] = None,
@@ -74,8 +74,12 @@ def __init__(
7474
self,
7575
spec: SchemaPath,
7676
base_url: Optional[str] = None,
77-
style_deserializers_factory: StyleDeserializersFactory = style_deserializers_factory,
78-
media_type_deserializers_factory: MediaTypeDeserializersFactory = media_type_deserializers_factory,
77+
style_deserializers_factory: Optional[
78+
StyleDeserializersFactory
79+
] = None,
80+
media_type_deserializers_factory: Optional[
81+
MediaTypeDeserializersFactory
82+
] = None,
7983
schema_casters_factory: Optional[SchemaCastersFactory] = None,
8084
schema_validators_factory: Optional[SchemaValidatorsFactory] = None,
8185
path_finder_cls: Optional[PathFinderType] = None,

openapi_core/unmarshalling/request/unmarshallers.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,16 +4,12 @@
44
from openapi_spec_validator.validation.types import SpecValidatorType
55

66
from openapi_core.casting.schemas.factories import SchemaCastersFactory
7-
from openapi_core.deserializing.media_types import (
8-
media_type_deserializers_factory,
9-
)
107
from openapi_core.deserializing.media_types.datatypes import (
118
MediaTypeDeserializersDict,
129
)
1310
from openapi_core.deserializing.media_types.factories import (
1411
MediaTypeDeserializersFactory,
1512
)
16-
from openapi_core.deserializing.styles import style_deserializers_factory
1713
from openapi_core.deserializing.styles.factories import (
1814
StyleDeserializersFactory,
1915
)
@@ -85,8 +81,12 @@ def __init__(
8581
self,
8682
spec: SchemaPath,
8783
base_url: Optional[str] = None,
88-
style_deserializers_factory: StyleDeserializersFactory = style_deserializers_factory,
89-
media_type_deserializers_factory: MediaTypeDeserializersFactory = media_type_deserializers_factory,
84+
style_deserializers_factory: Optional[
85+
StyleDeserializersFactory
86+
] = None,
87+
media_type_deserializers_factory: Optional[
88+
MediaTypeDeserializersFactory
89+
] = None,
9090
schema_casters_factory: Optional[SchemaCastersFactory] = None,
9191
schema_validators_factory: Optional[SchemaValidatorsFactory] = None,
9292
path_finder_cls: Optional[PathFinderType] = None,

0 commit comments

Comments
 (0)