Skip to content

Commit ba68efd

Browse files
authored
Merge pull request #678 from python-openapi/fix/mimetype-parameters-handling
Mimetype parameters handling
2 parents 7a17349 + 5610b66 commit ba68efd

File tree

9 files changed

+101
-28
lines changed

9 files changed

+101
-28
lines changed

Diff for: openapi_core/deserializing/media_types/deserializers.py

+3-1
Original file line numberDiff line numberDiff line change
@@ -16,16 +16,18 @@ def __init__(
1616
self,
1717
mimetype: str,
1818
deserializer_callable: Optional[DeserializerCallable] = None,
19+
**parameters: str,
1920
):
2021
self.mimetype = mimetype
2122
self.deserializer_callable = deserializer_callable
23+
self.parameters = parameters
2224

2325
def deserialize(self, value: Any) -> Any:
2426
if self.deserializer_callable is None:
2527
warnings.warn(f"Unsupported {self.mimetype} mimetype")
2628
return value
2729

2830
try:
29-
return self.deserializer_callable(value)
31+
return self.deserializer_callable(value, **self.parameters)
3032
except (ParseError, ValueError, TypeError, AttributeError):
3133
raise MediaTypeDeserializeError(self.mimetype, value)

Diff for: openapi_core/deserializing/media_types/factories.py

+7-1
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
from typing import Mapping
12
from typing import Optional
23

34
from openapi_core.deserializing.media_types.datatypes import (
@@ -23,18 +24,23 @@ def __init__(
2324
def create(
2425
self,
2526
mimetype: str,
27+
parameters: Optional[Mapping[str, str]] = None,
2628
extra_media_type_deserializers: Optional[
2729
MediaTypeDeserializersDict
2830
] = None,
2931
) -> CallableMediaTypeDeserializer:
32+
if parameters is None:
33+
parameters = {}
3034
if extra_media_type_deserializers is None:
3135
extra_media_type_deserializers = {}
3236
deserialize_callable = self.get_deserializer_callable(
3337
mimetype,
3438
extra_media_type_deserializers=extra_media_type_deserializers,
3539
)
3640

37-
return CallableMediaTypeDeserializer(mimetype, deserialize_callable)
41+
return CallableMediaTypeDeserializer(
42+
mimetype, deserialize_callable, **parameters
43+
)
3844

3945
def get_deserializer_callable(
4046
self,

Diff for: openapi_core/deserializing/media_types/util.py

+13-4
Original file line numberDiff line numberDiff line change
@@ -5,17 +5,26 @@
55
from urllib.parse import parse_qsl
66

77

8-
def plain_loads(value: Union[str, bytes]) -> str:
8+
def plain_loads(value: Union[str, bytes], **parameters: str) -> str:
9+
charset = "utf-8"
10+
if "charset" in parameters:
11+
charset = parameters["charset"]
912
if isinstance(value, bytes):
10-
value = value.decode("ASCII", errors="surrogateescape")
13+
try:
14+
return value.decode(charset)
15+
# fallback safe decode
16+
except UnicodeDecodeError:
17+
return value.decode("ASCII", errors="surrogateescape")
1118
return value
1219

1320

14-
def urlencoded_form_loads(value: Any) -> Dict[str, Any]:
21+
def urlencoded_form_loads(value: Any, **parameters: str) -> Dict[str, Any]:
1522
return dict(parse_qsl(value))
1623

1724

18-
def data_form_loads(value: Union[str, bytes]) -> Dict[str, Any]:
25+
def data_form_loads(
26+
value: Union[str, bytes], **parameters: str
27+
) -> Dict[str, Any]:
1928
if isinstance(value, bytes):
2029
value = value.decode("ASCII", errors="surrogateescape")
2130
parser = Parser()

Diff for: openapi_core/templating/media_types/datatypes.py

+4-1
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
11
from collections import namedtuple
2+
from dataclasses import dataclass
3+
from typing import Mapping
4+
from typing import Optional
25

3-
MediaType = namedtuple("MediaType", ["value", "key"])
6+
MediaType = namedtuple("MediaType", ["mime_type", "parameters", "media_type"])

Diff for: openapi_core/templating/media_types/finders.py

+27-6
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
"""OpenAPI core templating media types finders module"""
22
import fnmatch
3+
from typing import Mapping
4+
from typing import Tuple
35

46
from openapi_core.spec import Spec
57
from openapi_core.templating.media_types.datatypes import MediaType
@@ -12,15 +14,34 @@ def __init__(self, content: Spec):
1214

1315
def get_first(self) -> MediaType:
1416
mimetype, media_type = next(self.content.items())
15-
return MediaType(media_type, mimetype)
17+
return MediaType(mimetype, {}, media_type)
1618

1719
def find(self, mimetype: str) -> MediaType:
18-
if mimetype in self.content:
19-
return MediaType(self.content / mimetype, mimetype)
20+
if mimetype is None:
21+
raise MediaTypeNotFound(mimetype, list(self.content.keys()))
2022

21-
if mimetype:
23+
mime_type, parameters = self._parse_mimetype(mimetype)
24+
25+
# simple mime type
26+
for m in [mimetype, mime_type]:
27+
if m in self.content:
28+
return MediaType(mime_type, parameters, self.content / m)
29+
30+
# range mime type
31+
if mime_type:
2232
for key, value in self.content.items():
23-
if fnmatch.fnmatch(mimetype, key):
24-
return MediaType(value, key)
33+
if fnmatch.fnmatch(mime_type, key):
34+
return MediaType(key, parameters, value)
2535

2636
raise MediaTypeNotFound(mimetype, list(self.content.keys()))
37+
38+
def _parse_mimetype(self, mimetype: str) -> Tuple[str, Mapping[str, str]]:
39+
mimetype_parts = mimetype.split("; ")
40+
mime_type = mimetype_parts[0]
41+
parameters = {}
42+
if len(mimetype_parts) > 1:
43+
parameters_list = (
44+
param_str.split("=") for param_str in mimetype_parts[1:]
45+
)
46+
parameters = dict(parameters_list)
47+
return mime_type, parameters

Diff for: openapi_core/validation/validators.py

+8-3
Original file line numberDiff line numberDiff line change
@@ -86,10 +86,13 @@ def _find_media_type(
8686
return finder.get_first()
8787
return finder.find(mimetype)
8888

89-
def _deserialise_media_type(self, mimetype: str, value: Any) -> Any:
89+
def _deserialise_media_type(
90+
self, mimetype: str, parameters: Mapping[str, str], value: Any
91+
) -> Any:
9092
deserializer = self.media_type_deserializers_factory.create(
9193
mimetype,
9294
extra_media_type_deserializers=self.extra_media_type_deserializers,
95+
parameters=parameters,
9396
)
9497
return deserializer.deserialize(value)
9598

@@ -194,8 +197,10 @@ def _convert_content_schema_value_and_schema(
194197
content: Spec,
195198
mimetype: Optional[str] = None,
196199
) -> Tuple[Any, Optional[Spec]]:
197-
media_type, mime_type = self._find_media_type(content, mimetype)
198-
deserialised = self._deserialise_media_type(mime_type, raw)
200+
mime_type, parameters, media_type = self._find_media_type(
201+
content, mimetype
202+
)
203+
deserialised = self._deserialise_media_type(mime_type, parameters, raw)
199204
casted = self._cast(media_type, deserialised)
200205

201206
if "schema" not in media_type:

Diff for: tests/integration/test_petstore.py

+5-3
Original file line numberDiff line numberDiff line change
@@ -230,13 +230,15 @@ def test_get_pets_response_no_schema(self, spec):
230230

231231
assert result.body is None
232232

233-
data = "<html></html>"
234-
response = MockResponse(data, status_code=404, mimetype="text/html")
233+
data = b"<html></html>"
234+
response = MockResponse(
235+
data, status_code=404, mimetype="text/html; charset=utf-8"
236+
)
235237

236238
response_result = unmarshal_response(request, response, spec=spec)
237239

238240
assert response_result.errors == []
239-
assert response_result.data == data
241+
assert response_result.data == data.decode("utf-8")
240242

241243
def test_get_pets_invalid_response(self, spec, response_unmarshaller):
242244
host_url = "http://petstore.swagger.io/v1"

Diff for: tests/unit/deserializing/test_media_types_deserializers.py

+23-7
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,15 @@ class TestMediaTypeDeserializer:
1414
def deserializer_factory(self):
1515
def create_deserializer(
1616
media_type,
17+
parameters=None,
1718
media_type_deserializers=media_type_deserializers,
1819
extra_media_type_deserializers=None,
1920
):
2021
return MediaTypeDeserializersFactory(
2122
media_type_deserializers,
2223
).create(
2324
media_type,
25+
parameters=parameters,
2426
extra_media_type_deserializers=extra_media_type_deserializers,
2527
)
2628

@@ -49,19 +51,33 @@ def test_no_deserializer(self, deserializer_factory):
4951
assert result == value
5052

5153
@pytest.mark.parametrize(
52-
"mimetype",
54+
"mimetype,parameters,value,expected",
5355
[
54-
"text/plain",
55-
"text/html",
56+
(
57+
"text/plain",
58+
{"charset": "iso-8859-2"},
59+
b"\xb1\xb6\xbc\xe6",
60+
"ąśźć",
61+
),
62+
(
63+
"text/plain",
64+
{"charset": "utf-8"},
65+
b"\xc4\x85\xc5\x9b\xc5\xba\xc4\x87",
66+
"ąśźć",
67+
),
68+
("text/plain", {}, b"\xc4\x85\xc5\x9b\xc5\xba\xc4\x87", "ąśźć"),
69+
("text/plain", {}, "somestr", "somestr"),
70+
("text/html", {}, "somestr", "somestr"),
5671
],
5772
)
58-
def test_plain_valid(self, deserializer_factory, mimetype):
59-
deserializer = deserializer_factory(mimetype)
60-
value = "somestr"
73+
def test_plain_valid(
74+
self, deserializer_factory, mimetype, parameters, value, expected
75+
):
76+
deserializer = deserializer_factory(mimetype, parameters=parameters)
6177

6278
result = deserializer.deserialize(value)
6379

64-
assert result == value
80+
assert result == expected
6581

6682
@pytest.mark.parametrize(
6783
"mimetype",

Diff for: tests/unit/templating/test_media_types_finders.py

+11-2
Original file line numberDiff line numberDiff line change
@@ -22,17 +22,26 @@ def content(self, spec):
2222
def finder(self, content):
2323
return MediaTypeFinder(content)
2424

25+
def test_charset(self, finder, content):
26+
mimetype = "text/html; charset=utf-8"
27+
28+
mimetype, parameters, _ = finder.find(mimetype)
29+
assert mimetype == "text/*"
30+
assert parameters == {"charset": "utf-8"}
31+
2532
def test_exact(self, finder, content):
2633
mimetype = "application/json"
2734

28-
_, mimetype = finder.find(mimetype)
35+
mimetype, parameters, _ = finder.find(mimetype)
2936
assert mimetype == "application/json"
37+
assert parameters == {}
3038

3139
def test_match(self, finder, content):
3240
mimetype = "text/html"
3341

34-
_, mimetype = finder.find(mimetype)
42+
mimetype, parameters, _ = finder.find(mimetype)
3543
assert mimetype == "text/*"
44+
assert parameters == {}
3645

3746
def test_not_found(self, finder, content):
3847
mimetype = "unknown"

0 commit comments

Comments
 (0)