From 0b9cd4e5370ac03347e70a2ff9b85852b002ddc4 Mon Sep 17 00:00:00 2001 From: Eric Jolibois Date: Wed, 28 Oct 2020 20:10:55 +0100 Subject: [PATCH] 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 --- changes/2042-PrettyWood.md | 2 + .../dataclasses_arbitrary_types_allowed.py | 42 +++++++++++++++++++ .../dataclasses_stdlib_inheritance.py | 27 ++++++++++++ docs/usage/dataclasses.md | 25 +++++++++++ pydantic/dataclasses.py | 10 +++-- pydantic/validators.py | 2 +- tests/test_dataclasses.py | 41 +++++++++++++++++- 7 files changed, 143 insertions(+), 6 deletions(-) create mode 100644 changes/2042-PrettyWood.md create mode 100644 docs/examples/dataclasses_arbitrary_types_allowed.py create mode 100644 docs/examples/dataclasses_stdlib_inheritance.py diff --git a/changes/2042-PrettyWood.md b/changes/2042-PrettyWood.md new file mode 100644 index 0000000..27a874c --- /dev/null +++ b/changes/2042-PrettyWood.md @@ -0,0 +1,2 @@ +fix: _pydantic_ `dataclass` can inherit from stdlib `dataclass` +and `Config.arbitrary_types_allowed` is supported \ No newline at end of file diff --git a/docs/examples/dataclasses_arbitrary_types_allowed.py b/docs/examples/dataclasses_arbitrary_types_allowed.py new file mode 100644 index 0000000..8a4f559 --- /dev/null +++ b/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)) diff --git a/docs/examples/dataclasses_stdlib_inheritance.py b/docs/examples/dataclasses_stdlib_inheritance.py new file mode 100644 index 0000000..0409b46 --- /dev/null +++ b/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) diff --git a/docs/usage/dataclasses.md b/docs/usage/dataclasses.md index 5f56b58..54dbcbb 100644 --- a/docs/usage/dataclasses.md +++ b/docs/usage/dataclasses.md @@ -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 diff --git a/pydantic/dataclasses.py b/pydantic/dataclasses.py index 4fbdd63..28e2f95 100644 --- a/pydantic/dataclasses.py +++ b/pydantic/dataclasses.py @@ -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) diff --git a/pydantic/validators.py b/pydantic/validators.py index 3fc9e43..5beb4ea 100644 --- a/pydantic/validators.py +++ b/pydantic/validators.py @@ -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 diff --git a/tests/test_dataclasses.py b/tests/test_dataclasses.py index 6ad604d..061b73d 100644 --- a/tests/test_dataclasses.py +++ b/tests/test_dataclasses.py @@ -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()]))