Files
pydantic/tests/test_dataclasses.py
T
Yurii Karabas 42acd8f8d2 Fix issue with self-referencing dataclass (#3713)
* Fix issue with self-referencing dataclass

* Fix mypy issue
2022-05-11 19:09:13 +01:00

1000 lines
25 KiB
Python

import dataclasses
import pickle
from collections.abc import Hashable
from datetime import datetime
from pathlib import Path
from typing import Callable, ClassVar, Dict, FrozenSet, List, Optional, Union
import pytest
from typing_extensions import Literal
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_model_name():
@pydantic.dataclasses.dataclass
class MyDataClass:
model_name: str
d = MyDataClass('foo')
assert d.model_name == 'foo'
d = MyDataClass(model_name='foo')
assert d.model_name == 'foo'
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():
class Button:
def __init__(self, href: str):
self.href = href
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_factory() == {'John': 'Joey'}
def test_default_factory_singleton_field():
class MySingleton:
pass
class MyConfig:
arbitrary_types_allowed = True
MY_SINGLETON = MySingleton()
@pydantic.dataclasses.dataclass(config=MyConfig)
class Foo:
singleton: MySingleton = dataclasses.field(default_factory=lambda: MY_SINGLETON)
# Returning a singleton from a default_factory is supported
assert Foo().singleton is Foo().singleton
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
age: Optional[int] = dataclasses.field(
default=None, metadata=dict(title='The age of the user', description='do not lie!')
)
height: Optional[int] = pydantic.Field(None, title='The height in cm', ge=50, le=300)
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',
'type': 'object',
'additionalProperties': {'type': 'string'},
},
'signup_ts': {'title': 'Signup Ts', 'type': 'string', 'format': 'date-time'},
'age': {
'title': 'The age of the user',
'description': 'do not lie!',
'type': 'integer',
},
'height': {
'title': 'The height in cm',
'minimum': 50,
'maximum': 300,
'type': 'integer',
},
},
'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
def test_hashable_required():
@pydantic.dataclasses.dataclass
class MyDataclass:
v: Hashable
MyDataclass(v=None)
with pytest.raises(ValidationError) as exc_info:
MyDataclass(v=[])
assert exc_info.value.errors() == [
{'loc': ('v',), 'msg': 'value is not a valid hashable', 'type': 'type_error.hashable'}
]
with pytest.raises(TypeError) as exc_info:
MyDataclass()
assert "__init__() missing 1 required positional argument: 'v'" in str(exc_info.value)
@pytest.mark.parametrize('default', [1, None, ...])
def test_hashable_optional(default):
@pydantic.dataclasses.dataclass
class MyDataclass:
v: Hashable = default
MyDataclass()
MyDataclass(v=None)
def test_override_builtin_dataclass():
@dataclasses.dataclass
class File:
hash: str
name: Optional[str]
size: int
content: Optional[bytes] = None
FileChecked = pydantic.dataclasses.dataclass(File)
f = FileChecked(hash='xxx', name=b'whatever.txt', size='456')
assert f.name == 'whatever.txt'
assert f.size == 456
with pytest.raises(ValidationError) as e:
FileChecked(hash=[1], name='name', size=3)
assert e.value.errors() == [{'loc': ('hash',), 'msg': 'str type expected', 'type': 'type_error.str'}]
def test_override_builtin_dataclass_2():
@dataclasses.dataclass
class Meta:
modified_date: Optional[datetime]
seen_count: int
@pydantic.dataclasses.dataclass
@dataclasses.dataclass
class File(Meta):
filename: str
f = File(filename=b'thefilename', modified_date='2020-01-01T00:00', seen_count='7')
assert f.filename == 'thefilename'
assert f.modified_date == datetime(2020, 1, 1, 0, 0)
assert f.seen_count == 7
def test_override_builtin_dataclass_nested():
@dataclasses.dataclass
class Meta:
modified_date: Optional[datetime]
seen_count: int
@dataclasses.dataclass
class File:
filename: str
meta: Meta
class Foo(BaseModel):
file: File
FileChecked = pydantic.dataclasses.dataclass(File)
f = FileChecked(filename=b'thefilename', meta=Meta(modified_date='2020-01-01T00:00', seen_count='7'))
assert f.filename == 'thefilename'
assert f.meta.modified_date == datetime(2020, 1, 1, 0, 0)
assert f.meta.seen_count == 7
with pytest.raises(ValidationError) as e:
FileChecked(filename=b'thefilename', meta=Meta(modified_date='2020-01-01T00:00', seen_count=['7']))
assert e.value.errors() == [
{'loc': ('meta', 'seen_count'), 'msg': 'value is not a valid integer', 'type': 'type_error.integer'}
]
foo = Foo.parse_obj(
{
'file': {
'filename': b'thefilename',
'meta': {'modified_date': '2020-01-01T00:00', 'seen_count': '7'},
},
}
)
assert foo.file.filename == 'thefilename'
assert foo.file.meta.modified_date == datetime(2020, 1, 1, 0, 0)
assert foo.file.meta.seen_count == 7
def test_override_builtin_dataclass_nested_schema():
@dataclasses.dataclass
class Meta:
modified_date: Optional[datetime]
seen_count: int
@dataclasses.dataclass
class File:
filename: str
meta: Meta
FileChecked = pydantic.dataclasses.dataclass(File)
assert FileChecked.__pydantic_model__.schema() == {
'definitions': {
'Meta': {
'properties': {
'modified_date': {'format': 'date-time', 'title': 'Modified ' 'Date', 'type': 'string'},
'seen_count': {'title': 'Seen Count', 'type': 'integer'},
},
'required': ['modified_date', 'seen_count'],
'title': 'Meta',
'type': 'object',
}
},
'properties': {
'filename': {'title': 'Filename', 'type': 'string'},
'meta': {'$ref': '#/definitions/Meta'},
},
'required': ['filename', 'meta'],
'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()]))
def test_forward_stdlib_dataclass_params():
@dataclasses.dataclass(frozen=True)
class Item:
name: str
class Example(BaseModel):
item: Item
other: str
class Config:
arbitrary_types_allowed = True
e = Example(item=Item(name='pika'), other='bulbi')
e.other = 'bulbi2'
with pytest.raises(dataclasses.FrozenInstanceError):
e.item.name = 'pika2'
def test_pydantic_callable_field():
"""pydantic callable fields behaviour should be the same as stdlib dataclass"""
def foo(arg1, arg2):
return arg1, arg2
def bar(x: int, y: float, z: str) -> bool:
return str(x + y) == z
class PydanticModel(BaseModel):
required_callable: Callable
required_callable_2: Callable[[int, float, str], bool]
default_callable: Callable = foo
default_callable_2: Callable[[int, float, str], bool] = bar
@pydantic.dataclasses.dataclass
class PydanticDataclass:
required_callable: Callable
required_callable_2: Callable[[int, float, str], bool]
default_callable: Callable = foo
default_callable_2: Callable[[int, float, str], bool] = bar
@dataclasses.dataclass
class StdlibDataclass:
required_callable: Callable
required_callable_2: Callable[[int, float, str], bool]
default_callable: Callable = foo
default_callable_2: Callable[[int, float, str], bool] = bar
pyd_m = PydanticModel(required_callable=foo, required_callable_2=bar)
pyd_dc = PydanticDataclass(required_callable=foo, required_callable_2=bar)
std_dc = StdlibDataclass(required_callable=foo, required_callable_2=bar)
assert (
pyd_m.required_callable
is pyd_m.default_callable
is pyd_dc.required_callable
is pyd_dc.default_callable
is std_dc.required_callable
is std_dc.default_callable
)
assert (
pyd_m.required_callable_2
is pyd_m.default_callable_2
is pyd_dc.required_callable_2
is pyd_dc.default_callable_2
is std_dc.required_callable_2
is std_dc.default_callable_2
)
def test_pickle_overriden_builtin_dataclass(create_module):
module = create_module(
# language=Python
"""\
import dataclasses
import pydantic
@dataclasses.dataclass
class BuiltInDataclassForPickle:
value: int
class ModelForPickle(pydantic.BaseModel):
# pickle can only work with top level classes as it imports them
dataclass: BuiltInDataclassForPickle
class Config:
validate_assignment = True
"""
)
obj = module.ModelForPickle(dataclass=module.BuiltInDataclassForPickle(value=5))
pickled_obj = pickle.dumps(obj)
restored_obj = pickle.loads(pickled_obj)
assert restored_obj.dataclass.value == 5
assert restored_obj == obj
# ensure the restored dataclass is still a pydantic dataclass
with pytest.raises(ValidationError, match='value\n +value is not a valid integer'):
restored_obj.dataclass.value = 'value of a wrong type'
def test_config_field_info_create_model():
# works
class A1(BaseModel):
a: str
class Config:
fields = {'a': {'description': 'descr'}}
assert A1.schema()['properties'] == {'a': {'title': 'A', 'description': 'descr', 'type': 'string'}}
@pydantic.dataclasses.dataclass(config=A1.Config)
class A2:
a: str
assert A2.__pydantic_model__.schema()['properties'] == {
'a': {'title': 'A', 'description': 'descr', 'type': 'string'}
}
def test_discrimated_union_basemodel_instance_value():
@pydantic.dataclasses.dataclass
class A:
l: Literal['a']
@pydantic.dataclasses.dataclass
class B:
l: Literal['b']
@pydantic.dataclasses.dataclass
class Top:
sub: Union[A, B] = dataclasses.field(metadata=dict(discriminator='l'))
t = Top(sub=A(l='a'))
assert isinstance(t, Top)
assert Top.__pydantic_model__.schema() == {
'title': 'Top',
'type': 'object',
'properties': {
'sub': {
'title': 'Sub',
'discriminator': {'propertyName': 'l', 'mapping': {'a': '#/definitions/A', 'b': '#/definitions/B'}},
'anyOf': [{'$ref': '#/definitions/A'}, {'$ref': '#/definitions/B'}],
}
},
'required': ['sub'],
'definitions': {
'A': {
'title': 'A',
'type': 'object',
'properties': {'l': {'title': 'L', 'enum': ['a'], 'type': 'string'}},
'required': ['l'],
},
'B': {
'title': 'B',
'type': 'object',
'properties': {'l': {'title': 'L', 'enum': ['b'], 'type': 'string'}},
'required': ['l'],
},
},
}
def test_keeps_custom_properties():
class StandardClass:
"""Class which modifies instance creation."""
a: str
def __new__(cls, *args, **kwargs):
instance = super().__new__(cls)
instance._special_property = 1
return instance
StandardLibDataclass = dataclasses.dataclass(StandardClass)
PydanticDataclass = pydantic.dataclasses.dataclass(StandardClass)
clases_to_test = [StandardLibDataclass, PydanticDataclass]
test_string = 'string'
for cls in clases_to_test:
instance = cls(a=test_string)
assert instance._special_property == 1
assert instance.a == test_string
def test_self_reference_dataclass():
@pydantic.dataclasses.dataclass
class MyDataclass:
self_reference: 'MyDataclass'
assert MyDataclass.__pydantic_model__.__fields__['self_reference'].type_ is MyDataclass