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

dataclass validation #334

Merged
merged 4 commits into from
Dec 27, 2018
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions HISTORY.rst
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ v0.17.0 (unreleased)
* fix schema generation for fields annotated as ``: dict``, #330 by @nkonin
* support for passing Config class in dataclasses decorator, #276 by @jarekkar
(**breaking change**: this supersedes the ``validate_assignment`` argument with ``config``)
* support for nested dataclasses, #334 by @samuelcolvin

v0.16.1 (2018-12-10)
....................
Expand Down
14 changes: 14 additions & 0 deletions docs/examples/ex_nested_dataclasses.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
from pydantic import UrlStr
from pydantic.dataclasses import dataclass

@dataclass
class NavbarButton:
href: UrlStr

@dataclass
class Navbar:
button: NavbarButton

navbar = Navbar(button=('https://example.com',))
print(navbar)
# > Navbar(button=NavbarButton(href='https://example.com'))
13 changes: 11 additions & 2 deletions docs/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ dataclasses

.. note::

New in version ``v0.14.0``.
New in version ``v0.14``.

If you don't want to use pydantic's ``BaseModel`` you can instead get the same data validation on standard
`dataclasses <https://docs.python.org/3/library/dataclasses.html>`_ (introduced in python 3.7).
Expand All @@ -126,7 +126,16 @@ created by the standard library ``dataclass`` decorator.
``pydantic.dataclasses.dataclass``'s arguments are the same as the standard decorator, except one extra
key word argument ``config`` which has the same meaning as :ref:`Config <config>`.

Currently validators don't work on validators, if it's something you want please create an issue on github.

Since version ``v0.17`` nested dataclasses are support both in dataclasses and normal models.

.. literalinclude:: examples/ex_nested_dataclasses.py

(This script is complete, it should run "as is")

Dataclasses attributes can be populated by tuples, dictionaries or instances of that dataclass.

Currently validators don't work with dataclasses, if it's something you want please create an issue on github.

Choices
.......
Expand Down
20 changes: 18 additions & 2 deletions pydantic/dataclasses.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import dataclasses

from pydantic import ValidationError

from . import ValidationError, errors
from .main import create_model, validate_model


Expand All @@ -13,6 +12,21 @@ def _pydantic_post_init(self):
self.__post_init_original__()


def _validate_dataclass(cls, v):
if isinstance(v, cls):
return v
elif isinstance(v, (cls, list, tuple)):
return cls(*v)
elif isinstance(v, dict):
return cls(**v)
else:
raise errors.DataclassTypeError(class_name=cls.__name__)


def _get_validators(cls):
yield cls.__validate__


def setattr_validate_assignment(self, name, value):
if self.__initialised__:
d = dict(self.__dict__)
Expand All @@ -37,6 +51,8 @@ def _process_class(_cls, init, repr, eq, order, unsafe_hash, frozen, config):
cls.__pydantic_model__ = create_model(cls.__name__, __config__=config, **fields)

cls.__initialised__ = False
cls.__validate__ = classmethod(_validate_dataclass)
cls.__get_validators__ = classmethod(_get_validators)

if cls.__pydantic_model__.__config__.validate_assignment and not frozen:
cls.__setattr__ = setattr_validate_assignment
Expand Down
5 changes: 5 additions & 0 deletions pydantic/errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -266,3 +266,8 @@ class JsonTypeError(PydanticTypeError):
class PatternError(PydanticValueError):
code = 'regex_pattern'
msg_template = 'Invalid regular expression'


class DataclassTypeError(PydanticTypeError):
code = 'dataclass'
msg_template = 'instance of {class_name}, tuple or dict expected'
1 change: 1 addition & 0 deletions pydantic/fields.py
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,7 @@ def _populate_validators(self):
class_validators_ = self.class_validators.values()
if not self.sub_fields:
get_validators = getattr(self.type_, 'get_validators', None)
get_validators = get_validators or getattr(self.type_, '__get_validators__', None)
v_funcs = (
*tuple(v.func for v in class_validators_ if not v.whole and v.pre),
*(
Expand Down
85 changes: 83 additions & 2 deletions tests/test_dataclasses.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import dataclasses

import pytest

import pydantic
from pydantic import BaseConfig, ValidationError
from pydantic import BaseModel, ValidationError


def test_simple():
Expand Down Expand Up @@ -155,7 +157,7 @@ class MyDataclass:
with pytest.raises(ValidationError) as exc_info:
d.a = 'xxxx'

assert issubclass(MyDataclass.__pydantic_model__.__config__, BaseConfig)
assert issubclass(MyDataclass.__pydantic_model__.__config__, BaseModel.Config)
assert exc_info.value.errors() == [
{
'loc': ('a',),
Expand All @@ -179,3 +181,82 @@ class MyDataclass:
d.a = 'xxxx'

assert d.a == 'xxxx'


def test_nested_dataclass():
@pydantic.dataclasses.dataclass
class Nested:
number: int

@pydantic.dataclasses.dataclass
class Outer:
n: Nested

navbar = Outer(n=Nested(number='1'))
assert isinstance(navbar.n, Nested)
assert navbar.n.number == 1

navbar = Outer(n=('2',))
assert isinstance(navbar.n, Nested)
assert navbar.n.number == 2

navbar = Outer(n={'number': '3'})
assert isinstance(navbar.n, Nested)
assert navbar.n.number == 3

with pytest.raises(ValidationError) as exc_info:
Outer(n='not nested')
assert exc_info.value.errors() == [
{
'loc': ('n',),
'msg': 'instance of Nested, tuple or dict expected',
'type': 'type_error.dataclass',
'ctx': {'class_name': 'Nested'},
}
]

with pytest.raises(ValidationError) as exc_info:
Outer(n=('x',))
assert exc_info.value.errors() == [
{'loc': ('n', 'number'), 'msg': 'value is not a valid integer', 'type': 'type_error.integer'}
]


def test_arbitrary_types_allowed():
@dataclasses.dataclass
class Button:
href: str

class Config:
arbitrary_types_allowed = True

@pydantic.dataclasses.dataclass(config=Config)
class Navbar:
button: Button

btn = Button(href='a')
navbar = Navbar(button=btn)
assert navbar.button.href == 'a'

with pytest.raises(ValidationError) as exc_info:
Navbar(button=('b',))
assert exc_info.value.errors() == [
{
'loc': ('button',),
'msg': 'instance of Button expected',
'type': 'type_error.arbitrary_type',
'ctx': {'expected_arbitrary_type': 'Button'},
}
]


def test_nested_dataclass_model():
@pydantic.dataclasses.dataclass
class Nested:
number: int

class Outer(BaseModel):
n: Nested

navbar = Outer(n=Nested(number='1'))
assert navbar.n.number == 1