fix: pydantic dataclass can inherit from stdlib dataclass and arbitrary_types_allowed is supported (#2051)

* fix: pydantic dataclasses can inherit from stdlib dataclasses

closes #2042

* docs: add some documentation

* fix: support arbitrary_types_allowed with stdlib dataclass

closes #2054

* docs: add documentation for custom types
This commit is contained in:
Eric Jolibois
2020-10-28 20:10:55 +01:00
committed by GitHub
parent e899692654
commit 0b9cd4e537
7 changed files with 143 additions and 6 deletions
+2
View File
@@ -0,0 +1,2 @@
fix: _pydantic_ `dataclass` can inherit from stdlib `dataclass`
and `Config.arbitrary_types_allowed` is supported
@@ -0,0 +1,42 @@
import dataclasses
import pydantic
class ArbitraryType:
def __init__(self, value):
self.value = value
def __repr__(self):
return f'ArbitraryType(value={self.value!r})'
@dataclasses.dataclass
class DC:
a: ArbitraryType
b: str
# valid as it is a builtin dataclass without validation
my_dc = DC(a=ArbitraryType(value=3), b='qwe')
try:
class Model(pydantic.BaseModel):
dc: DC
other: str
Model(dc=my_dc, other='other')
except RuntimeError as e: # invalid as it is now a pydantic dataclass
print(e)
class Model(pydantic.BaseModel):
dc: DC
other: str
class Config:
arbitrary_types_allowed = True
m = Model(dc=my_dc, other='other')
print(repr(m))
@@ -0,0 +1,27 @@
import dataclasses
import pydantic
@dataclasses.dataclass
class Z:
z: int
@dataclasses.dataclass
class Y(Z):
y: int = 0
@pydantic.dataclasses.dataclass
class X(Y):
x: int = 0
foo = X(x=b'1', y='2', z='3')
print(foo)
try:
X(z='pika')
except pydantic.ValidationError as e:
print(e)
+25
View File
@@ -49,6 +49,8 @@ Dataclasses attributes can be populated by tuples, dictionaries or instances of
## Stdlib dataclasses and _pydantic_ dataclasses
### Convert stdlib dataclasses into _pydantic_ dataclasses
Stdlib dataclasses (nested or not) can be easily converted into _pydantic_ dataclasses by just decorating
them with `pydantic.dataclasses.dataclass`.
@@ -57,6 +59,18 @@ them with `pydantic.dataclasses.dataclass`.
```
_(This script is complete, it should run "as is")_
### Inherit from stdlib dataclasses
Stdlib dataclasses (nested or not) can also be inherited and _pydantic_ will automatically validate
all the inherited fields.
```py
{!.tmp_examples/dataclasses_stdlib_inheritance.py!}
```
_(This script is complete, it should run "as is")_
### Use of stdlib dataclasses with `BaseModel`
Bear in mind that stdlib dataclasses (nested or not) are **automatically converted** into _pydantic_ dataclasses
when mixed with `BaseModel`!
@@ -65,6 +79,17 @@ when mixed with `BaseModel`!
```
_(This script is complete, it should run "as is")_
### Use custom types
Since stdlib dataclasses are automatically converted to add validation using
custom types may cause some unexpected behaviour.
In this case you can simply add `arbitrary_types_allowed` in the config!
```py
{!.tmp_examples/dataclasses_arbitrary_types_allowed.py!}
```
_(This script is complete, it should run "as is")_
## Initialize hooks
When you initialize a dataclass, it is possible to execute code *after* validation
+6 -4
View File
@@ -8,7 +8,7 @@ from .main import create_model, validate_model
from .utils import ClassAttribute
if TYPE_CHECKING:
from .main import BaseModel # noqa: F401
from .main import BaseConfig, BaseModel # noqa: F401
from .typing import CallableGenerator
DataclassT = TypeVar('DataclassT', bound='Dataclass')
@@ -120,7 +120,9 @@ def _process_class(
# ```
# with the exact same fields as the base dataclass
if is_builtin_dataclass(_cls):
_cls = type(_cls.__name__, (_cls,), {'__post_init__': _pydantic_post_init})
_cls = type(
_cls.__name__, (_cls,), {'__annotations__': _cls.__annotations__, '__post_init__': _pydantic_post_init}
)
else:
_cls.__post_init__ = _pydantic_post_init
cls: Type['Dataclass'] = dataclasses.dataclass( # type: ignore
@@ -214,10 +216,10 @@ def dataclass(
return wrap(_cls)
def make_dataclass_validator(_cls: Type[Any], **kwargs: Any) -> 'CallableGenerator':
def make_dataclass_validator(_cls: Type[Any], config: Type['BaseConfig']) -> 'CallableGenerator':
"""
Create a pydantic.dataclass from a builtin dataclass to add type validation
and yield the validators
"""
cls = dataclass(_cls, **kwargs)
cls = dataclass(_cls, config=config)
yield from _get_validators(cls)
+1 -1
View File
@@ -593,7 +593,7 @@ def find_validators( # noqa: C901 (ignore complexity)
yield make_literal_validator(type_)
return
if is_builtin_dataclass(type_):
yield from make_dataclass_validator(type_)
yield from make_dataclass_validator(type_, config)
return
if type_ is Enum:
yield enum_validator
+40 -1
View File
@@ -2,7 +2,7 @@ import dataclasses
from collections.abc import Hashable
from datetime import datetime
from pathlib import Path
from typing import ClassVar, Dict, FrozenSet, Optional
from typing import ClassVar, Dict, FrozenSet, List, Optional
import pytest
@@ -738,3 +738,42 @@ def test_override_builtin_dataclass_nested_schema():
'title': 'File',
'type': 'object',
}
def test_inherit_builtin_dataclass():
@dataclasses.dataclass
class Z:
z: int
@dataclasses.dataclass
class Y(Z):
y: int
@pydantic.dataclasses.dataclass
class X(Y):
x: int
pika = X(x='2', y='4', z='3')
assert pika.x == 2
assert pika.y == 4
assert pika.z == 3
def test_dataclass_arbitrary():
class ArbitraryType:
def __init__(self):
...
@dataclasses.dataclass
class Test:
foo: ArbitraryType
bar: List[ArbitraryType]
class TestModel(BaseModel):
a: ArbitraryType
b: Test
class Config:
arbitrary_types_allowed = True
TestModel(a=ArbitraryType(), b=(ArbitraryType(), [ArbitraryType()]))