Skip to content

Commit

Permalink
Support all Pydantic dump options
Browse files Browse the repository at this point in the history
This adds a `QUART_SCHEMA_PYDANTIC_DUMP_OPTIONS` configuration
variable to pass additional keyword arguments to the model_dump method
including the now removed `by_alias` argument.
  • Loading branch information
pgjones committed May 15, 2024
1 parent cd6c97b commit abf72c1
Show file tree
Hide file tree
Showing 7 changed files with 56 additions and 23 deletions.
17 changes: 10 additions & 7 deletions docs/how_to_guides/configuration.rst
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,21 @@ The following configuration options are used by Quart-Schema. They
should be set as part of the standard `Quart configuration
<https://pgjones.gitlab.io/quart/how_to_guides/configuration.html>`_.

================================== =====
================================== ===================
Configuration key type
---------------------------------- -----
---------------------------------- -------------------
QUART_SCHEMA_CONVERSION_PREFERENCE str
QUART_SCHEMA_SWAGGER_JS_URL str
QUART_SCHEMA_SWAGGER_CSS_URL str
QUART_SCHEMA_REDOC_JS_URL str
QUART_SCHEMA_BY_ALIAS bool
QUART_SCHEMA_PYDANTIC_DUMP_OPTIONS PydanticDumpOptions
QUART_SCHEMA_CONVERT_CASING bool
================================== =====
================================== ===================

which allow the js and css for the documentation UI to be changed and
configured and specifies that responses that are Pydantic models
should be converted to JSON by the field alias names (if
``QUART_SCHEMA_BY_ALIAS`` is ``True``).
configured.

The Pydantic Dump Options should be a dictionary ``dict[str, Any]``
and will be passed to Pydantic's model_dump method as keyword
arguments. The options are typed via
:class:`~quart_schema.PydanticDumpOptions`.
3 changes: 2 additions & 1 deletion src/quart_schema/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
ServerVariable,
Tag,
)
from .typing import ResponseReturnValue
from .typing import PydanticDumpOptions, ResponseReturnValue
from .validation import (
DataSource,
RequestSchemaValidationError,
Expand Down Expand Up @@ -50,6 +50,7 @@
"OAuth2SecurityScheme",
"OpenIdSecurityScheme",
"operation_id",
"PydanticDumpOptions",
"QuartSchema",
"RequestSchemaValidationError",
"ResponseReturnValue",
Expand Down
14 changes: 7 additions & 7 deletions src/quart_schema/conversion.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
from werkzeug.datastructures import Headers
from werkzeug.exceptions import HTTPException

from .typing import Model, ResponseReturnValue, ResponseValue
from .typing import Model, PydanticDumpOptions, ResponseReturnValue, ResponseValue

try:
from pydantic import (
Expand Down Expand Up @@ -100,14 +100,14 @@ def convert_response_return_value(
value = model_dump(
value,
camelize=current_app.config["QUART_SCHEMA_CONVERT_CASING"],
by_alias=current_app.config["QUART_SCHEMA_BY_ALIAS"],
preference=current_app.config["QUART_SCHEMA_CONVERSION_PREFERENCE"],
pydantic_kwargs=current_app.config["QUART_SCHEMA_PYDANTIC_DUMP_OPTIONS"],
)
headers = model_dump(
headers, # type: ignore
kebabize=True,
by_alias=current_app.config["QUART_SCHEMA_BY_ALIAS"],
preference=current_app.config["QUART_SCHEMA_CONVERSION_PREFERENCE"],
pydantic_kwargs=current_app.config["QUART_SCHEMA_PYDANTIC_DUMP_OPTIONS"],
)

new_result: ResponseReturnValue
Expand All @@ -128,23 +128,23 @@ def convert_response_return_value(
def model_dump(
raw: ResponseValue,
*,
by_alias: bool,
camelize: bool = False,
kebabize: bool = False,
preference: Optional[str] = None,
pydantic_kwargs: Optional[PydanticDumpOptions] = None,
) -> dict | list:
if is_pydantic_dataclass(type(raw)):
value = RootModel[type(raw)](raw).model_dump() # type: ignore
value = RootModel[type(raw)](raw).model_dump(**(pydantic_kwargs or {})) # type: ignore
elif isinstance(raw, BaseModel):
value = raw.model_dump(by_alias=by_alias)
value = raw.model_dump(**(pydantic_kwargs or {}))
elif isinstance(raw, Struct) or is_attrs(raw): # type: ignore
value = to_builtins(raw)
elif (
(isinstance(raw, (list, dict)) or is_dataclass(raw))
and PYDANTIC_INSTALLED
and preference != "msgspec"
):
value = TypeAdapter(type(raw)).dump_python(raw)
value = TypeAdapter(type(raw)).dump_python(raw, **(pydantic_kwargs or {}))
elif (
(isinstance(raw, (list, dict)) or is_dataclass(raw))
and MSGSPEC_INSTALLED
Expand Down
2 changes: 1 addition & 1 deletion src/quart_schema/extension.py
Original file line number Diff line number Diff line change
Expand Up @@ -299,7 +299,7 @@ def init_app(self, app: Quart) -> None:
"QUART_SCHEMA_SCALAR_JS_URL",
"https://cdn.jsdelivr.net/npm/@scalar/api-reference",
)
app.config.setdefault("QUART_SCHEMA_BY_ALIAS", False)
app.config.setdefault("QUART_SCHEMA_PYDANTIC_DUMP_OPTIONS", {})
app.config.setdefault("QUART_SCHEMA_CONVERT_CASING", self.convert_casing)
app.config.setdefault("QUART_SCHEMA_CONVERSION_PREFERENCE", self.conversion_preference)
app.json = create_json_provider(app)
Expand Down
8 changes: 4 additions & 4 deletions src/quart_schema/mixins.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,8 @@ async def send_as(self: WebsocketProtocol, value: Any, model_class: Type[Model])
data = model_dump(
value,
camelize=current_app.config["QUART_SCHEMA_CONVERT_CASING"],
by_alias=current_app.config["QUART_SCHEMA_BY_ALIAS"],
preference=current_app.config["QUART_SCHEMA_CONVERSION_PREFERENCE"],
pydantic_kwargs=current_app.config["QUART_SCHEMA_PYDANTIC_DUMP_OPTIONS"],
)
await self.send_json(data) # type: ignore

Expand All @@ -67,23 +67,23 @@ async def _make_request(
json = model_dump(
json,
camelize=self.app.config["QUART_SCHEMA_CONVERT_CASING"],
by_alias=self.app.config["QUART_SCHEMA_BY_ALIAS"],
preference=self.app.config["QUART_SCHEMA_CONVERSION_PREFERENCE"],
pydantic_kwargs=self.app.config["QUART_SCHEMA_PYDANTIC_DUMP_OPTIONS"],
)

if form is not None:
form = model_dump( # type: ignore
form,
camelize=self.app.config["QUART_SCHEMA_CONVERT_CASING"],
by_alias=self.app.config["QUART_SCHEMA_BY_ALIAS"],
preference=self.app.config["QUART_SCHEMA_CONVERSION_PREFERENCE"],
pydantic_kwargs=self.app.config["QUART_SCHEMA_PYDANTIC_DUMP_OPTIONS"],
)
if query_string is not None:
query_string = model_dump( # type: ignore
query_string,
camelize=self.app.config["QUART_SCHEMA_CONVERT_CASING"],
by_alias=self.app.config["QUART_SCHEMA_BY_ALIAS"],
preference=self.app.config["QUART_SCHEMA_CONVERSION_PREFERENCE"],
pydantic_kwargs=self.app.config["QUART_SCHEMA_PYDANTIC_DUMP_OPTIONS"],
)

return await super()._make_request( # type: ignore
Expand Down
31 changes: 30 additions & 1 deletion src/quart_schema/typing.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,19 @@
from __future__ import annotations

from typing import Any, AnyStr, Callable, Dict, List, Optional, Tuple, Type, TYPE_CHECKING, Union
from typing import (
Any,
AnyStr,
Callable,
Dict,
List,
Literal,
Optional,
Tuple,
Type,
TYPE_CHECKING,
TypedDict,
Union,
)

from quart import Quart
from quart.datastructures import FileStorage
Expand All @@ -18,6 +31,12 @@
except ImportError:
from typing_extensions import Protocol # type: ignore

try:
from typing import NotRequired
except ImportError:
from typing_extensions import NotRequired


if TYPE_CHECKING:
from attrs import AttrsInstance
from msgspec import Struct
Expand Down Expand Up @@ -69,3 +88,13 @@ async def _make_request(
http_version: str,
scope_base: Optional[dict],
) -> Response: ...


class PydanticDumpOptions(TypedDict):
by_alias: NotRequired[bool]
exclude_defaults: NotRequired[bool]
exclude_none: NotRequired[bool]
exclude_unset: NotRequired[bool]
round_trip: NotRequired[bool]
serialize_as_any: NotRequired[bool]
warnings: NotRequired[bool | Literal["none", "warn", "error"]]
4 changes: 2 additions & 2 deletions tests/test_conversion.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ class ValidationError(Exception):
def test_model_dump(
type_: Type[Union[ADetails, DCDetails, MDetails, PyDetails, PyDCDetails]]
) -> None:
assert model_dump(type_(name="bob", age=2), by_alias=False) == { # type: ignore
assert model_dump(type_(name="bob", age=2)) == { # type: ignore
"name": "bob",
"age": 2,
}
Expand All @@ -41,7 +41,7 @@ def test_model_dump_list(
preference: str,
) -> None:
assert model_dump(
[type_(name="bob", age=2), type_(name="jim", age=3)], by_alias=False, preference=preference
[type_(name="bob", age=2), type_(name="jim", age=3)], preference=preference
) == [{"name": "bob", "age": 2}, {"name": "jim", "age": 3}]


Expand Down

0 comments on commit abf72c1

Please sign in to comment.