Skip to content

Commit

Permalink
Native support for TypedDict. (#604)
Browse files Browse the repository at this point in the history
Allow using instances of `TypedDict` as a type for `data` kwarg.

Support openapi docs, `DTO` and `Partial`.
  • Loading branch information
peterschutt authored Oct 18, 2022
1 parent 3556784 commit d0441a9
Show file tree
Hide file tree
Showing 19 changed files with 386 additions and 89 deletions.
2 changes: 2 additions & 0 deletions .flake8
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,5 @@ classmethod-decorators =
classmethod
validator
root_validator
per-file-ignores =
starlite/types/builtin_types.py:E800,F401
2 changes: 1 addition & 1 deletion docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ class User(BaseModel):
id: UUID4
```

Alternatively, you can **use a dataclass** – either from dataclasses or from pydantic:
Alternatively, you can **use a dataclass** – either from dataclasses or from pydantic, or a [`TypedDict`][typing.TypedDict]:

```python title="my_app/models/user.py"
from uuid import UUID
Expand Down
6 changes: 6 additions & 0 deletions docs/reference/utils/0-predicate-utils.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,10 @@

::: starlite.utils.predicates.is_class_and_subclass

::: starlite.utils.predicates.is_dataclass_class_or_instance_typeguard

::: starlite.utils.predicates.is_dataclass_class_typeguard

::: starlite.utils.predicates.is_optional_union

::: starlite.utils.predicates.is_typeddict_typeguard
2 changes: 1 addition & 1 deletion docs/usage/11-data-transfer-objects.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
<!-- markdownlint-disable MD052 -->

Starlite includes a [`DTOFactory`][starlite.dto.DTOFactory] class that allows you to create DTOs from pydantic models,
dataclasses and any other class supported via plugins.
dataclasses, [`TypedDict`][typing.TypedDict], and any other class supported via plugins.

An instance of the factory must first be created, optionally passing plugins to it as a kwarg. It can then be used to
create a [`DTO`][starlite.dto.DTO] by calling the instance like a function. Additionally, it can exclude (drop)
Expand Down
3 changes: 2 additions & 1 deletion docs/usage/4-request-data/0-request-data.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@ async def create_user(data: User) -> User:
...
```

The type of `data` does not need to be a pydantic model - it can be any supported type, e.g. a dataclass:
The type of `data` does not need to be a pydantic model - it can be any supported type, e.g. a dataclass, or a
[`TypedDict`][typing.TypedDict]:

```python
from starlite import post
Expand Down
38 changes: 26 additions & 12 deletions starlite/dto.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from dataclasses import asdict, is_dataclass
from dataclasses import asdict
from inspect import isawaitable
from typing import (
TYPE_CHECKING,
Expand All @@ -22,7 +22,13 @@

from starlite.exceptions import ImproperlyConfiguredException
from starlite.plugins import PluginProtocol, get_plugin_for_value
from starlite.utils import convert_dataclass_to_model, is_async_callable
from starlite.utils import (
convert_dataclass_to_model,
convert_typeddict_to_model,
is_async_callable,
is_dataclass_class_or_instance_typeguard,
is_typeddict_typeguard,
)

if TYPE_CHECKING:
from typing import Awaitable
Expand Down Expand Up @@ -86,6 +92,8 @@ def from_model_instance(cls, model_instance: T) -> "DTO[T]":
values = cast("Dict[str, Any]", result)
elif isinstance(model_instance, BaseModel):
values = model_instance.dict()
elif isinstance(model_instance, dict):
values = dict(model_instance) # copy required as `_from_value_mapping()` mutates `values`.
else:
values = asdict(model_instance)
return cls._from_value_mapping(mapping=values)
Expand Down Expand Up @@ -133,8 +141,10 @@ def to_model_instance(self) -> T:

class DTOFactory:
def __init__(self, plugins: Optional[List[PluginProtocol]] = None) -> None:
"""Create [`DTO`][starlite.dto.DTO] types from pydantic models,
dataclasses and other types supported via plugins.
"""Create [`DTO`][starlite.dto.DTO] types.
Pydantic models, [`TypedDict`][typing.TypedDict] and dataclasses are natively supported. Other types supported
via plugins.
Args:
plugins (list[PluginProtocol] | None): Plugins used to support `DTO` construction from arbitrary types.
Expand All @@ -150,8 +160,8 @@ def __call__(
field_definitions: Optional[Dict[str, Tuple[Any, Any]]] = None,
) -> Type[DTO[T]]:
"""
Given a supported model class - either pydantic, dataclass or a class supported via plugins,
create a DTO pydantic model class.
Given a supported model class - either pydantic, [`TypedDict`][typing.TypedDict], dataclass or a class supported
via plugins, create a DTO pydantic model class.
An instance of the factory must first be created, passing any plugins to it.
It can then be used to create a DTO by calling the instance like a function. Additionally, it can exclude (drop)
Expand Down Expand Up @@ -193,8 +203,8 @@ def create_obj(data: MyClassDTO) -> MyClass:
Args:
name (str): This becomes the name of the generated pydantic model.
source (type[T]): A type that is either a subclass of `BaseModel`, a `dataclass` or any other type with a
plugin registered.
source (type[T]): A type that is either a subclass of `BaseModel`, [`TypedDict`][typing.TypedDict], a
`dataclass` or any other type with a plugin registered.
exclude (list[str] | None): Names of attributes on `source`. Named Attributes will not have a field
generated on the resultant pydantic model.
field_mapping (dict[str, str | tuple[str, Any]] | None): Keys are names of attributes on `source`. Values
Expand All @@ -208,7 +218,8 @@ def create_obj(data: MyClassDTO) -> MyClass:
Raises:
[ImproperlyConfiguredException][starlite.exceptions.ImproperlyConfiguredException]: If `source` is not a
pydantic model or dataclass, and there is no plugin registered for its type.
pydantic model, [`TypedDict`][typing.TypedDict] or dataclass, and there is no plugin registered for its
type.
"""
field_definitions = field_definitions or {}
exclude = exclude or []
Expand All @@ -228,14 +239,17 @@ def create_obj(data: MyClassDTO) -> MyClass:
def _get_fields_from_source(
self, source: Type[T] # pyright: ignore
) -> Tuple[Dict[str, ModelField], Optional[PluginProtocol]]:
"""Converts a `BaseModel` subclass, `dataclass` or any other type that
has a plugin registered into a mapping of `str` to `ModelField`."""
"""Converts a `BaseModel` subclass, [`TypedDict`][typing.TypedDict],
`dataclass` or any other type that has a plugin registered into a
mapping of `str` to `ModelField`."""
plugin: Optional[PluginProtocol] = None
if issubclass(source, BaseModel):
source.update_forward_refs()
fields = source.__fields__
elif is_dataclass(source):
elif is_dataclass_class_or_instance_typeguard(source):
fields = convert_dataclass_to_model(source).__fields__
elif is_typeddict_typeguard(source):
fields = convert_typeddict_to_model(source).__fields__
else:
plugin = get_plugin_for_value(value=source, plugins=self.plugins)
if not plugin:
Expand Down
17 changes: 13 additions & 4 deletions starlite/openapi/schema.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
from dataclasses import is_dataclass
from datetime import datetime
from decimal import Decimal
from enum import Enum, EnumMeta
Expand Down Expand Up @@ -32,7 +31,15 @@
)
from starlite.openapi.enums import OpenAPIFormat, OpenAPIType
from starlite.openapi.utils import get_openapi_type_for_complex_type
from starlite.utils.model import convert_dataclass_to_model, create_parsed_model_field
from starlite.utils import (
is_dataclass_class_or_instance_typeguard,
is_typeddict_typeguard,
)
from starlite.utils.model import (
convert_dataclass_to_model,
convert_typeddict_to_model,
create_parsed_model_field,
)

if TYPE_CHECKING:
from starlite.plugins.base import PluginProtocol
Expand All @@ -44,7 +51,7 @@ def normalize_example_value(value: Any) -> Any:
value = round(float(value), 2)
if isinstance(value, Enum):
value = value.value
if is_dataclass(value):
if is_dataclass_class_or_instance_typeguard(value):
value = convert_dataclass_to_model(value)
if isinstance(value, BaseModel):
value = value.dict()
Expand Down Expand Up @@ -184,8 +191,10 @@ def get_schema_for_field_type(field: ModelField, plugins: List["PluginProtocol"]
return TYPE_MAP[field_type].copy()
if is_pydantic_model(field_type):
return OpenAPI310PydanticSchema(schema_class=field_type)
if is_dataclass(field_type):
if is_dataclass_class_or_instance_typeguard(field_type):
return OpenAPI310PydanticSchema(schema_class=convert_dataclass_to_model(field_type))
if is_typeddict_typeguard(field_type):
return OpenAPI310PydanticSchema(schema_class=convert_typeddict_to_model(field_type))
if isinstance(field_type, EnumMeta):
enum_values: List[Union[str, int]] = [v.value for v in field_type] # type: ignore
openapi_type = OpenAPIType.STRING if isinstance(enum_values[0], str) else OpenAPIType.INTEGER
Expand Down
27 changes: 27 additions & 0 deletions starlite/types/builtin_types.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# nopycln: file

from typing import TYPE_CHECKING

if TYPE_CHECKING:

from typing import Type, Union

from pydantic_factories.protocols import DataclassProtocol
from typing_extensions import TypeAlias, TypedDict


__all__ = [
"DataclassClass",
"DataclassClassOrInstance",
"NoneType",
"TypedDictClass",
]

DataclassClass: "TypeAlias" = "Type[DataclassProtocol]"

DataclassClassOrInstance: "TypeAlias" = "Union[DataclassClass, DataclassProtocol]"

NoneType = type(None)

# mypy issue: https://github.com/python/mypy/issues/11030
TypedDictClass: "TypeAlias" = "Type[TypedDict]" # type:ignore[valid-type]
Loading

0 comments on commit d0441a9

Please sign in to comment.