Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* feat: add support for `NamedTuple` and `TypedDict` types * support `total=False` * tests: fix ci with python < 3.8 without typing-extensions * chore: improve mypy * chore: @samuelcolvin remarks * refactor: move tests in dedicated file * docs: add annotated types section with examples * feat: support properly required and optional fields * chore(deps-dev): bump typing_extensions * docs: add a note for `typing_extensions` * chore: update message to be more accurate * feat: pass down config to created models * feat: add util methods to create model from TypedDict or NamedTuple * refactor: rename into typeddict and namedtuple * test: add utils tests * chore: fix lint * chore: improve test * refactor: rename utils to match the rest * chore: update change * docs: add section for create_model_from_{namedtuple,typeddict} * refactor: rename typed_dict/named_tuple * feat: support schema with TypedDict * feat: support schema for NamedTuple * feat: add json support for NamedTuple * chore: rewording * refactor: use parse_obj * fix: add check for max items in tuple * docs: separate typing.NamedTuple and collections.namedtuple
- Loading branch information
1 parent
5025707
commit c314f5a
Showing
16 changed files
with
629 additions
and
4 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
Add support for `NamedTuple` and `TypedDict` types. | ||
Those two types are now handled and validated when used inside `BaseModel` or _pydantic_ `dataclass`. | ||
Two utils are also added `create_model_from_namedtuple` and `create_model_from_typeddict`. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
from typing import NamedTuple | ||
|
||
from pydantic import BaseModel, ValidationError | ||
|
||
|
||
class Point(NamedTuple): | ||
x: int | ||
y: int | ||
|
||
|
||
class Model(BaseModel): | ||
p: Point | ||
|
||
|
||
print(Model(p=('1', '2'))) | ||
|
||
try: | ||
Model(p=('1.3', '2')) | ||
except ValidationError as e: | ||
print(e) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,45 @@ | ||
from typing import TypedDict | ||
|
||
from pydantic import BaseModel, Extra, ValidationError | ||
|
||
|
||
# `total=False` means keys are non-required | ||
class UserIdentity(TypedDict, total=False): | ||
name: str | ||
surname: str | ||
|
||
|
||
class User(TypedDict): | ||
identity: UserIdentity | ||
age: int | ||
|
||
|
||
class Model(BaseModel): | ||
u: User | ||
|
||
class Config: | ||
extra = Extra.forbid | ||
|
||
|
||
print(Model(u={'identity': {'name': 'Smith', 'surname': 'John'}, 'age': '37'})) | ||
|
||
print(Model(u={'identity': {'name': None, 'surname': 'John'}, 'age': '37'})) | ||
|
||
print(Model(u={'identity': {}, 'age': '37'})) | ||
|
||
|
||
try: | ||
Model(u={'identity': {'name': ['Smith'], 'surname': 'John'}, 'age': '24'}) | ||
except ValidationError as e: | ||
print(e) | ||
|
||
try: | ||
Model( | ||
u={ | ||
'identity': {'name': 'Smith', 'surname': 'John'}, | ||
'age': '37', | ||
'email': 'john.smith@me.com', | ||
} | ||
) | ||
except ValidationError as e: | ||
print(e) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
from typing import TypedDict | ||
|
||
from pydantic import ValidationError, create_model_from_typeddict | ||
|
||
|
||
class User(TypedDict): | ||
name: str | ||
id: int | ||
|
||
|
||
class Config: | ||
extra = 'forbid' | ||
|
||
|
||
UserM = create_model_from_typeddict(User, __config__=Config) | ||
print(repr(UserM(name=123, id='3'))) | ||
|
||
try: | ||
UserM(name=123, id='3', other='no') | ||
except ValidationError as e: | ||
print(e) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,59 @@ | ||
from typing import TYPE_CHECKING, Any, Dict, FrozenSet, NamedTuple, Type | ||
|
||
from .fields import Required | ||
from .main import BaseModel, create_model | ||
|
||
if TYPE_CHECKING: | ||
|
||
class TypedDict(Dict[str, Any]): | ||
__annotations__: Dict[str, Type[Any]] | ||
__total__: bool | ||
__required_keys__: FrozenSet[str] | ||
__optional_keys__: FrozenSet[str] | ||
|
||
|
||
def create_model_from_typeddict(typeddict_cls: Type['TypedDict'], **kwargs: Any) -> Type['BaseModel']: | ||
""" | ||
Create a `BaseModel` based on the fields of a `TypedDict`. | ||
Since `typing.TypedDict` in Python 3.8 does not store runtime information about optional keys, | ||
we warn the user if that's the case (see https://bugs.python.org/issue38834). | ||
""" | ||
field_definitions: Dict[str, Any] | ||
|
||
# Best case scenario: with python 3.9+ or when `TypedDict` is imported from `typing_extensions` | ||
if hasattr(typeddict_cls, '__required_keys__'): | ||
field_definitions = { | ||
field_name: (field_type, Required if field_name in typeddict_cls.__required_keys__ else None) | ||
for field_name, field_type in typeddict_cls.__annotations__.items() | ||
} | ||
else: | ||
import warnings | ||
|
||
warnings.warn( | ||
'You should use `typing_extensions.TypedDict` instead of `typing.TypedDict` for better support! ' | ||
'Without it, there is no way to differentiate required and optional fields when subclassed. ' | ||
'Fields will therefore be considered all required or all optional depending on class totality.', | ||
UserWarning, | ||
) | ||
default_value = Required if typeddict_cls.__total__ else None | ||
field_definitions = { | ||
field_name: (field_type, default_value) for field_name, field_type in typeddict_cls.__annotations__.items() | ||
} | ||
|
||
return create_model(typeddict_cls.__name__, **kwargs, **field_definitions) | ||
|
||
|
||
def create_model_from_namedtuple(namedtuple_cls: Type['NamedTuple'], **kwargs: Any) -> Type['BaseModel']: | ||
""" | ||
Create a `BaseModel` based on the fields of a named tuple. | ||
A named tuple can be created with `typing.NamedTuple` and declared annotations | ||
but also with `collections.namedtuple`, in this case we consider all fields | ||
to have type `Any`. | ||
""" | ||
namedtuple_annotations: Dict[str, Type[Any]] = getattr( | ||
namedtuple_cls, '__annotations__', {k: Any for k in namedtuple_cls._fields} | ||
) | ||
field_definitions: Dict[str, Any] = { | ||
field_name: (field_type, Required) for field_name, field_type in namedtuple_annotations.items() | ||
} | ||
return create_model(namedtuple_cls.__name__, **kwargs, **field_definitions) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.