Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Merged
merged 4 commits into from Oct 28, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 2 additions & 0 deletions changes/2042-PrettyWood.md
@@ -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
@@ -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
@@ -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
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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we should also add a comment about the arbitrary_types_allowed logic change since that now behaves different from nested pydantic dataclasses or next pydantic models.

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
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
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
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()]))