mirror of
https://github.com/kennethreitz/pydantic.git
synced 2026-06-05 23:00:18 +00:00
42acd8f8d2
* Fix issue with self-referencing dataclass * Fix mypy issue
1000 lines
25 KiB
Python
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
|