Skip to content

Commit

Permalink
fix: pydantic dataclass can inherit from stdlib dataclass and `arbitr…
Browse files Browse the repository at this point in the history
…ary_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
  • Loading branch information
PrettyWood committed Oct 28, 2020
1 parent e899692 commit 0b9cd4e
Show file tree
Hide file tree
Showing 7 changed files with 143 additions and 6 deletions.
2 changes: 2 additions & 0 deletions changes/2042-PrettyWood.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
fix: _pydantic_ `dataclass` can inherit from stdlib `dataclass`
and `Config.arbitrary_types_allowed` is supported
42 changes: 42 additions & 0 deletions docs/examples/dataclasses_arbitrary_types_allowed.py
Original file line number Diff line number Diff line change
@@ -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))
27 changes: 27 additions & 0 deletions docs/examples/dataclasses_stdlib_inheritance.py
Original file line number Diff line number Diff line change
@@ -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 changes: 25 additions & 0 deletions docs/usage/dataclasses.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`.

Expand All @@ -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`!

Expand All @@ -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
Expand Down
10 changes: 6 additions & 4 deletions pydantic/dataclasses.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
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')
Expand Down Expand Up @@ -120,7 +120,9 @@ def _pydantic_post_init(self: 'Dataclass', *initvars: Any) -> None:
# ```
# 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
Expand Down Expand Up @@ -214,10 +216,10 @@ def wrap(cls: Type[Any]) -> Type['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)
2 changes: 1 addition & 1 deletion pydantic/validators.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
41 changes: 40 additions & 1 deletion tests/test_dataclasses.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
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

Expand Down Expand Up @@ -738,3 +738,42 @@ class File:
'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()]))

0 comments on commit 0b9cd4e

Please sign in to comment.