Skip to content

Commit 9f4455a

Browse files
committed
Bugfix list model class handling and type identification
issubclass errors if called with an argument that is not a class, hence the need to check. In addition isinstance(type_, list) is not applicable as the `type_` is not an instance, hence the switch to check the origin.
1 parent 21ce379 commit 9f4455a

File tree

2 files changed

+65
-38
lines changed

2 files changed

+65
-38
lines changed

src/quart_schema/conversion.py

+41-37
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
from __future__ import annotations
22

33
from dataclasses import fields, is_dataclass
4-
from typing import Any, Optional, Type, TypeVar, Union
4+
from inspect import isclass
5+
from typing import Any, Dict, List, Optional, Type, TypeVar, Union
56

67
import humps
78
from quart import current_app
@@ -160,7 +161,7 @@ def model_dump(
160161

161162

162163
def model_load(
163-
data: dict,
164+
data: Union[dict, list],
164165
model_class: Type[T],
165166
exception_class: Type[Exception],
166167
*,
@@ -171,26 +172,10 @@ def model_load(
171172
data = humps.decamelize(data)
172173

173174
try:
174-
if (
175-
is_pydantic_dataclass(model_class)
176-
or issubclass(model_class, BaseModel)
177-
or (
178-
(isinstance(model_class, (list, dict)) or is_dataclass(model_class))
179-
and PYDANTIC_INSTALLED
180-
and preference != "msgspec"
181-
)
182-
):
183-
return TypeAdapter(model_class).validate_python(data) # type: ignore
184-
elif (
185-
issubclass(model_class, Struct)
186-
or is_attrs(model_class)
187-
or (
188-
(isinstance(model_class, (list, dict)) or is_dataclass(model_class))
189-
and MSGSPEC_INSTALLED
190-
and preference != "pydantic"
191-
)
192-
):
193-
return convert(data, model_class, strict=False) # type: ignore
175+
if _use_pydantic(model_class, preference):
176+
return TypeAdapter(model_class).validate_python(data)
177+
elif _use_msgspec(model_class, preference):
178+
return convert(data, model_class, strict=False)
194179
elif not PYDANTIC_INSTALLED and not MSGSPEC_INSTALLED:
195180
raise TypeError(f"Cannot load {model_class} - try installing msgspec or pydantic")
196181
else:
@@ -200,19 +185,9 @@ def model_load(
200185

201186

202187
def model_schema(model_class: Type[Model], *, preference: Optional[str] = None) -> dict:
203-
if (
204-
is_pydantic_dataclass(model_class)
205-
or issubclass(model_class, BaseModel)
206-
or (isinstance(model_class, (list, dict)) and preference != "msgspec")
207-
or (is_dataclass(model_class) and preference != "msgspec")
208-
):
188+
if _use_pydantic(model_class, preference):
209189
return TypeAdapter(model_class).json_schema(ref_template=PYDANTIC_REF_TEMPLATE)
210-
elif (
211-
issubclass(model_class, Struct)
212-
or is_attrs(model_class)
213-
or (isinstance(model_class, (list, dict)) and preference != "pydantic")
214-
or (is_dataclass(model_class) and preference != "pydantic")
215-
):
190+
elif _use_msgspec(model_class, preference):
216191
_, schema = schema_components([model_class], ref_template=MSGSPEC_REF_TEMPLATE)
217192
return list(schema.values())[0]
218193
elif not PYDANTIC_INSTALLED and not MSGSPEC_INSTALLED:
@@ -230,12 +205,12 @@ def convert_headers(
230205
fields_ = set(model_class.__pydantic_fields__.keys())
231206
elif is_dataclass(model_class):
232207
fields_ = {field.name for field in fields(model_class)}
233-
elif issubclass(model_class, BaseModel):
208+
elif isclass(model_class) and issubclass(model_class, BaseModel):
234209
fields_ = set(model_class.model_fields.keys())
210+
elif isclass(model_class) and issubclass(model_class, Struct):
211+
fields_ = set(model_class.__struct_fields__)
235212
elif is_attrs(model_class):
236213
fields_ = {field.name for field in attrs_fields(model_class)}
237-
elif issubclass(model_class, Struct):
238-
fields_ = set(model_class.__struct_fields__)
239214
else:
240215
raise TypeError(f"Cannot convert to {model_class}")
241216

@@ -252,3 +227,32 @@ def convert_headers(
252227
return model_class(**result)
253228
except (TypeError, MsgSpecValidationError, ValueError) as error:
254229
raise exception_class(error)
230+
231+
232+
def _is_list_or_dict(type_: Type) -> bool:
233+
origin = getattr(type_, "__origin__", None)
234+
return origin in (dict, Dict, list, List)
235+
236+
237+
def _use_pydantic(model_class: Type, preference: Optional[str]) -> bool:
238+
return (
239+
is_pydantic_dataclass(model_class)
240+
or (isclass(model_class) and issubclass(model_class, BaseModel))
241+
or (
242+
(_is_list_or_dict(model_class) or is_dataclass(model_class))
243+
and PYDANTIC_INSTALLED
244+
and preference != "msgspec"
245+
)
246+
)
247+
248+
249+
def _use_msgspec(model_class: Type, preference: Optional[str]) -> bool:
250+
return (
251+
(isclass(model_class) and issubclass(model_class, Struct))
252+
or is_attrs(model_class)
253+
or (
254+
(_is_list_or_dict(model_class) or is_dataclass(model_class))
255+
and MSGSPEC_INSTALLED
256+
and preference != "pydantic"
257+
)
258+
)

tests/test_conversion.py

+24-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
from dataclasses import dataclass
2-
from typing import Type, Union
2+
from typing import List, Type, Union
33

44
import pytest
55
from attrs import define
@@ -54,6 +54,29 @@ def test_model_load(
5454
)
5555

5656

57+
@pytest.mark.parametrize(
58+
"type_, preference",
59+
[
60+
(ADetails, "msgspec"),
61+
(DCDetails, "msgspec"),
62+
(DCDetails, "pydantic"),
63+
(MDetails, "msgspec"),
64+
(PyDetails, "pydantic"),
65+
(PyDCDetails, "pydantic"),
66+
],
67+
)
68+
def test_model_load_list(
69+
type_: Type[Union[ADetails, DCDetails, MDetails, PyDetails, PyDCDetails]],
70+
preference: str,
71+
) -> None:
72+
assert model_load(
73+
[{"name": "bob", "age": 2}],
74+
List[type_], # type: ignore
75+
exception_class=ValidationError,
76+
preference=preference,
77+
) == [type_(name="bob", age=2)]
78+
79+
5780
@pytest.mark.parametrize("type_", [ADetails, DCDetails, MDetails, PyDetails, PyDCDetails])
5881
def test_model_load_error(
5982
type_: Type[Union[ADetails, DCDetails, MDetails, PyDetails, PyDCDetails]]

0 commit comments

Comments
 (0)