Files
pydantic/tests/test_dataclasses.py
T
Alexander Hirner c188b3fba8 Feature/dataclass schema defaults (#980)
* default dataclass field from default_factory

* silence mypy regarding issue 708

* test dataclass factory function default

* test default factory ending up in schema

* add changes md

* add docs for dataclass default factory and basemodel access

* fully print default factory schema
2019-11-09 12:29:15 +00:00

581 lines
14 KiB
Python

import dataclasses
from datetime import datetime
from pathlib import Path
from typing import ClassVar, Dict, FrozenSet, Optional
import pytest
import pydantic
from pydantic import BaseModel, ValidationError, validator
def test_simple():
@pydantic.dataclasses.dataclass
class MyDataclass:
a: int
b: float
d = MyDataclass('1', '2.5')
assert d.a == 1
assert d.b == 2.5
d = MyDataclass(b=10, a=20)
assert d.a == 20
assert d.b == 10
def test_value_error():
@pydantic.dataclasses.dataclass
class MyDataclass:
a: int
b: int
with pytest.raises(ValidationError) as exc_info:
MyDataclass(1, 'wrong')
assert exc_info.value.errors() == [
{'loc': ('b',), 'msg': 'value is not a valid integer', 'type': 'type_error.integer'}
]
def test_frozen():
@pydantic.dataclasses.dataclass(frozen=True)
class MyDataclass:
a: int
d = MyDataclass(1)
assert d.a == 1
with pytest.raises(AttributeError):
d.a = 7
def test_validate_assignment():
class Config:
validate_assignment = True
@pydantic.dataclasses.dataclass(config=Config)
class MyDataclass:
a: int
d = MyDataclass(1)
assert d.a == 1
d.a = '7'
assert d.a == 7
def test_validate_assignment_error():
class Config:
validate_assignment = True
@pydantic.dataclasses.dataclass(config=Config)
class MyDataclass:
a: int
d = MyDataclass(1)
with pytest.raises(ValidationError) as exc_info:
d.a = 'xxx'
assert exc_info.value.errors() == [
{'loc': ('a',), 'msg': 'value is not a valid integer', 'type': 'type_error.integer'}
]
def test_not_validate_assignment():
@pydantic.dataclasses.dataclass
class MyDataclass:
a: int
d = MyDataclass(1)
assert d.a == 1
d.a = '7'
assert d.a == '7'
def test_validate_assignment_value_change():
class Config:
validate_assignment = True
@pydantic.dataclasses.dataclass(config=Config, frozen=False)
class MyDataclass:
a: int
@validator('a')
def double_a(cls, v):
return v * 2
d = MyDataclass(2)
assert d.a == 4
d.a = 3
assert d.a == 6
def test_validate_assignment_extra():
class Config:
validate_assignment = True
@pydantic.dataclasses.dataclass(config=Config, frozen=False)
class MyDataclass:
a: int
d = MyDataclass(1)
assert d.a == 1
d.extra_field = 1.23
assert d.extra_field == 1.23
d.extra_field = 'bye'
assert d.extra_field == 'bye'
def test_post_init():
post_init_called = False
@pydantic.dataclasses.dataclass
class MyDataclass:
a: int
def __post_init__(self):
nonlocal post_init_called
post_init_called = True
d = MyDataclass('1')
assert d.a == 1
assert post_init_called
def test_post_init_inheritance_chain():
parent_post_init_called = False
post_init_called = False
@pydantic.dataclasses.dataclass
class ParentDataclass:
a: int
def __post_init__(self):
nonlocal parent_post_init_called
parent_post_init_called = True
@pydantic.dataclasses.dataclass
class MyDataclass(ParentDataclass):
b: int
def __post_init__(self):
super().__post_init__()
nonlocal post_init_called
post_init_called = True
d = MyDataclass(a=1, b=2)
assert d.a == 1
assert d.b == 2
assert parent_post_init_called
assert post_init_called
def test_post_init_post_parse():
post_init_post_parse_called = False
@pydantic.dataclasses.dataclass
class MyDataclass:
a: int
def __post_init_post_parse__(self):
nonlocal post_init_post_parse_called
post_init_post_parse_called = True
d = MyDataclass('1')
assert d.a == 1
assert post_init_post_parse_called
def test_post_init_post_parse_types():
@pydantic.dataclasses.dataclass
class CustomType(object):
b: int
@pydantic.dataclasses.dataclass
class MyDataclass:
a: CustomType
def __post_init__(self):
assert type(self.a) == dict
def __post_init_post_parse__(self):
assert type(self.a) == CustomType
d = MyDataclass(**{'a': {'b': 1}})
assert d.a.b == 1
def test_post_init_assignment():
from dataclasses import field
# Based on: https://docs.python.org/3/library/dataclasses.html#post-init-processing
@pydantic.dataclasses.dataclass
class C:
a: float
b: float
c: float = field(init=False)
def __post_init__(self):
self.c = self.a + self.b
c = C(0.1, 0.2)
assert c.a == 0.1
assert c.b == 0.2
assert c.c == 0.30000000000000004
def test_inheritance():
@pydantic.dataclasses.dataclass
class A:
a: str = None
@pydantic.dataclasses.dataclass
class B(A):
b: int = None
b = B(a='a', b=12)
assert b.a == 'a'
assert b.b == 12
with pytest.raises(ValidationError):
B(a='a', b='b')
def test_validate_long_string_error():
class Config:
max_anystr_length = 3
@pydantic.dataclasses.dataclass(config=Config)
class MyDataclass:
a: str
with pytest.raises(ValidationError) as exc_info:
MyDataclass('xxxx')
assert exc_info.value.errors() == [
{
'loc': ('a',),
'msg': 'ensure this value has at most 3 characters',
'type': 'value_error.any_str.max_length',
'ctx': {'limit_value': 3},
}
]
def test_validate_assigment_long_string_error():
class Config:
max_anystr_length = 3
validate_assignment = True
@pydantic.dataclasses.dataclass(config=Config)
class MyDataclass:
a: str
d = MyDataclass('xxx')
with pytest.raises(ValidationError) as exc_info:
d.a = 'xxxx'
assert issubclass(MyDataclass.__pydantic_model__.__config__, BaseModel.Config)
assert exc_info.value.errors() == [
{
'loc': ('a',),
'msg': 'ensure this value has at most 3 characters',
'type': 'value_error.any_str.max_length',
'ctx': {'limit_value': 3},
}
]
def test_no_validate_assigment_long_string_error():
class Config:
max_anystr_length = 3
validate_assignment = False
@pydantic.dataclasses.dataclass(config=Config)
class MyDataclass:
a: str
d = MyDataclass('xxx')
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
def test_fields():
@pydantic.dataclasses.dataclass
class User:
id: int
name: str = 'John Doe'
signup_ts: datetime = None
user = User(id=123)
fields = user.__pydantic_model__.__fields__
assert fields['id'].required is True
assert fields['id'].default is None
assert fields['name'].required is False
assert fields['name'].default == 'John Doe'
assert fields['signup_ts'].required is False
assert fields['signup_ts'].default is None
def test_default_factory_field():
@pydantic.dataclasses.dataclass
class User:
id: int
aliases: Dict[str, str] = dataclasses.field(default_factory=lambda: {'John': 'Joey'})
user = User(id=123)
fields = user.__pydantic_model__.__fields__
assert fields['id'].required is True
assert fields['id'].default is None
assert fields['aliases'].required is False
assert fields['aliases'].default == {'John': 'Joey'}
def test_schema():
@pydantic.dataclasses.dataclass
class User:
id: int
name: str = 'John Doe'
aliases: Dict[str, str] = dataclasses.field(default_factory=lambda: {'John': 'Joey'})
signup_ts: datetime = None
user = User(id=123)
assert user.__pydantic_model__.schema() == {
'title': 'User',
'type': 'object',
'properties': {
'id': {'title': 'Id', 'type': 'integer'},
'name': {'title': 'Name', 'default': 'John Doe', 'type': 'string'},
'aliases': {
'title': 'Aliases',
'default': {'John': 'Joey'},
'type': 'object',
'additionalProperties': {'type': 'string'},
},
'signup_ts': {'title': 'Signup Ts', 'type': 'string', 'format': 'date-time'},
},
'required': ['id'],
}
def test_nested_schema():
@pydantic.dataclasses.dataclass
class Nested:
number: int
@pydantic.dataclasses.dataclass
class Outer:
n: Nested
assert Outer.__pydantic_model__.schema() == {
'title': 'Outer',
'type': 'object',
'properties': {'n': {'$ref': '#/definitions/Nested'}},
'required': ['n'],
'definitions': {
'Nested': {
'title': 'Nested',
'type': 'object',
'properties': {'number': {'title': 'Number', 'type': 'integer'}},
'required': ['number'],
}
},
}
def test_initvar():
InitVar = dataclasses.InitVar
@pydantic.dataclasses.dataclass
class TestInitVar:
x: int
y: InitVar
tiv = TestInitVar(1, 2)
assert tiv.x == 1
with pytest.raises(AttributeError):
tiv.y
def test_derived_field_from_initvar():
InitVar = dataclasses.InitVar
@pydantic.dataclasses.dataclass
class DerivedWithInitVar:
plusone: int = dataclasses.field(init=False)
number: InitVar[int]
def __post_init__(self, number):
self.plusone = number + 1
derived = DerivedWithInitVar(1)
assert derived.plusone == 2
with pytest.raises(TypeError):
DerivedWithInitVar('Not A Number')
def test_initvars_post_init():
@pydantic.dataclasses.dataclass
class PathDataPostInit:
path: Path
base_path: dataclasses.InitVar[Optional[Path]] = None
def __post_init__(self, base_path):
if base_path is not None:
self.path = base_path / self.path
path_data = PathDataPostInit('world')
assert 'path' in path_data.__dict__
assert 'base_path' not in path_data.__dict__
assert path_data.path == Path('world')
with pytest.raises(TypeError) as exc_info:
PathDataPostInit('world', base_path='/hello')
assert str(exc_info.value) == "unsupported operand type(s) for /: 'str' and 'str'"
def test_initvars_post_init_post_parse():
@pydantic.dataclasses.dataclass
class PathDataPostInitPostParse:
path: Path
base_path: dataclasses.InitVar[Optional[Path]] = None
def __post_init_post_parse__(self, base_path):
if base_path is not None:
self.path = base_path / self.path
path_data = PathDataPostInitPostParse('world')
assert 'path' in path_data.__dict__
assert 'base_path' not in path_data.__dict__
assert path_data.path == Path('world')
assert PathDataPostInitPostParse('world', base_path='/hello').path == Path('/hello/world')
def test_classvar():
@pydantic.dataclasses.dataclass
class TestClassVar:
klassvar: ClassVar = "I'm a Class variable"
x: int
tcv = TestClassVar(2)
assert tcv.klassvar == "I'm a Class variable"
def test_frozenset_field():
@pydantic.dataclasses.dataclass
class TestFrozenSet:
set: FrozenSet[int]
test_set = frozenset({1, 2, 3})
object_under_test = TestFrozenSet(set=test_set)
assert object_under_test.set == test_set
def test_inheritance_post_init():
post_init_called = False
@pydantic.dataclasses.dataclass
class Base:
a: int
def __post_init__(self):
nonlocal post_init_called
post_init_called = True
@pydantic.dataclasses.dataclass
class Child(Base):
b: int
Child(a=1, b=2)
assert post_init_called