import sys from collections import defaultdict from copy import deepcopy from enum import Enum from typing import ( Any, Callable, ClassVar, Counter, DefaultDict, Dict, List, Mapping, Optional, Set, Type, TypeVar, get_type_hints, ) from uuid import UUID, uuid4 import pytest from typing_extensions import Final, Literal from pydantic import BaseConfig, BaseModel, Extra, Field, PrivateAttr, SecretStr, ValidationError, constr def test_success(): # same as below but defined here so class definition occurs inside the test class Model(BaseModel): a: float b: int = 10 m = Model(a=10.2) assert m.a == 10.2 assert m.b == 10 class UltraSimpleModel(BaseModel): a: float b: int = 10 def test_ultra_simple_missing(): with pytest.raises(ValidationError) as exc_info: UltraSimpleModel() assert exc_info.value.errors() == [{'loc': ('a',), 'msg': 'Field required', 'type': 'missing', 'input': {}}] assert str(exc_info.value) == ( '1 validation error for UltraSimpleModel\n' 'a\n' ' Field required [type=missing, input_value={}, input_type=dict]' ) def test_ultra_simple_failed(): with pytest.raises(ValidationError) as exc_info: UltraSimpleModel(a='x', b='x') assert exc_info.value.errors() == [ { 'type': 'float_parsing', 'loc': ('a',), 'msg': 'Input should be a valid number, unable to parse string as an number', 'input': 'x', }, { 'type': 'int_parsing', 'loc': ('b',), 'msg': 'Input should be a valid integer, unable to parse string as an integer', 'input': 'x', }, ] def test_ultra_simple_repr(): m = UltraSimpleModel(a=10.2) assert str(m) == 'a=10.2 b=10' assert repr(m) == 'UltraSimpleModel(a=10.2, b=10)' assert repr(m.__fields__['a']) == 'FieldInfo(annotation=float, required=True)' assert repr(m.__fields__['b']) == 'FieldInfo(annotation=int, required=False, default=10)' assert dict(m) == {'a': 10.2, 'b': 10} assert m.dict() == {'a': 10.2, 'b': 10} assert m.json() == '{"a": 10.2, "b": 10}' assert str(m) == 'a=10.2 b=10' def test_default_factory_field(): def myfunc(): return 1 class Model(BaseModel): a: int = Field(default_factory=myfunc) m = Model() assert str(m) == 'a=1' assert repr(m.__fields__['a']) == 'FieldInfo(annotation=int, required=False, default_factory=myfunc)' assert dict(m) == {'a': 1} assert m.json() == '{"a": 1}' def test_comparing(): m = UltraSimpleModel(a=10.2, b='100') assert m == {'a': 10.2, 'b': 100} assert m == UltraSimpleModel(a=10.2, b=100) @pytest.fixture(scope='session', name='NoneCheckModel') def none_check_model_fix(): class NoneCheckModel(BaseModel): existing_str_value: str = 'foo' required_str_value: str = ... required_str_none_value: Optional[str] = ... existing_bytes_value: bytes = b'foo' required_bytes_value: bytes = ... required_bytes_none_value: Optional[bytes] = ... return NoneCheckModel def test_nullable_strings_success(NoneCheckModel): m = NoneCheckModel( required_str_value='v1', required_str_none_value=None, required_bytes_value='v2', required_bytes_none_value=None ) assert m.required_str_value == 'v1' assert m.required_str_none_value is None assert m.required_bytes_value == b'v2' assert m.required_bytes_none_value is None def test_nullable_strings_fails(NoneCheckModel): with pytest.raises(ValidationError) as exc_info: NoneCheckModel( required_str_value=None, required_str_none_value=None, required_bytes_value=None, required_bytes_none_value=None, ) assert exc_info.value.errors() == [ { 'type': 'string_type', 'loc': ('required_str_value',), 'msg': 'Input should be a valid string', 'input': None, }, { 'type': 'bytes_type', 'loc': ('required_bytes_value',), 'msg': 'Input should be a valid bytes', 'input': None, }, ] @pytest.fixture(name='ParentModel', scope='session') def parent_sub_model_fixture(): class ParentModel(BaseModel): grape: bool banana: UltraSimpleModel return ParentModel def test_parent_sub_model(ParentModel): m = ParentModel(grape=1, banana={'a': 1}) assert m.grape is True assert m.banana.a == 1.0 assert m.banana.b == 10 assert repr(m) == 'ParentModel(grape=True, banana=UltraSimpleModel(a=1.0, b=10))' def test_parent_sub_model_fails(ParentModel): with pytest.raises(ValidationError): ParentModel(grape=1, banana=123) def test_not_required(): class Model(BaseModel): a: float = None assert Model(a=12.2).a == 12.2 assert Model().a is None with pytest.raises(ValidationError) as exc_info: Model(a=None) assert exc_info.value.errors() == [ { 'type': 'float_type', 'loc': ('a',), 'msg': 'Input should be a valid number', 'input': None, }, ] def test_allow_extra(): class Model(BaseModel): a: float = ... class Config: extra = Extra.allow assert Model(a='10.2', b=12).dict() == {'a': 10.2, 'b': 12} def test_allow_extra_repr(): class Model(BaseModel): a: float = ... class Config: extra = Extra.allow assert str(Model(a='10.2', b=12)) == 'a=10.2 b=12' def test_forbidden_extra_success(): class ForbiddenExtra(BaseModel): foo: str = 'whatever' class Config: extra = Extra.forbid m = ForbiddenExtra() assert m.foo == 'whatever' def test_forbidden_extra_fails(): class ForbiddenExtra(BaseModel): foo: str = 'whatever' class Config: extra = Extra.forbid with pytest.raises(ValidationError) as exc_info: ForbiddenExtra(foo='ok', bar='wrong', spam='xx') assert exc_info.value.errors() == [ { 'type': 'extra_forbidden', 'loc': ('bar',), 'msg': 'Extra inputs are not permitted', 'input': 'wrong', }, { 'type': 'extra_forbidden', 'loc': ('spam',), 'msg': 'Extra inputs are not permitted', 'input': 'xx', }, ] def test_assign_extra_no_validate(): class Model(BaseModel): a: float class Config: validate_assignment = True model = Model(a=0.2) with pytest.raises(ValidationError, match='Extra inputs are not permitted'): model.b = 2 def test_assign_extra_validate(): class Model(BaseModel): a: float class Config: validate_assignment = True model = Model(a=0.2) with pytest.raises(ValidationError, match='Extra inputs are not permitted'): model.b = 2 def test_extra_allowed(): class Model(BaseModel): a: float class Config: extra = Extra.allow model = Model(a=0.2, b=0.1) assert model.b == 0.1 assert not hasattr(model, 'c') model.c = 1 assert hasattr(model, 'c') assert model.c == 1 def test_extra_ignored(): class Model(BaseModel): a: float class Config: extra = Extra.ignore model = Model(a=0.2, b=0.1) assert not hasattr(model, 'b') with pytest.raises(ValueError, match='"Model" object has no field "c"'): model.c = 1 def test_set_attr(): m = UltraSimpleModel(a=10.2) assert m.dict() == {'a': 10.2, 'b': 10} m.b = 20 assert m.dict() == {'a': 10.2, 'b': 20} def test_set_attr_invalid(): class UltraSimpleModel(BaseModel): a: float = ... b: int = 10 m = UltraSimpleModel(a=10.2) assert m.dict() == {'a': 10.2, 'b': 10} with pytest.raises(ValueError) as exc_info: m.c = 20 assert '"UltraSimpleModel" object has no field "c"' in exc_info.value.args[0] def test_any(): class AnyModel(BaseModel): a: Any = 10 b: object = 20 m = AnyModel() assert m.a == 10 assert m.b == 20 m = AnyModel(a='foobar', b='barfoo') assert m.a == 'foobar' assert m.b == 'barfoo' def test_population_by_field_name(): class Model(BaseModel): a: str = Field(alias='_a') class Config: allow_population_by_field_name = True assert Model(a='different').a == 'different' assert Model(a='different').dict() == {'a': 'different'} assert Model(a='different').dict(by_alias=True) == {'_a': 'different'} def test_field_order(): class Model(BaseModel): c: float b: int = 10 a: str d: dict = {} assert list(Model.__fields__.keys()) == ['c', 'b', 'a', 'd'] def test_required(): # same as below but defined here so class definition occurs inside the test class Model(BaseModel): a: float b: int = 10 m = Model(a=10.2) assert m.dict() == dict(a=10.2, b=10) with pytest.raises(ValidationError) as exc_info: Model() assert exc_info.value.errors() == [{'type': 'missing', 'loc': ('a',), 'msg': 'Field required', 'input': {}}] def test_mutability(): class TestModel(BaseModel): a: int = 10 class Config: allow_mutation = True extra = Extra.forbid frozen = False m = TestModel() assert m.a == 10 m.a = 11 assert m.a == 11 def test_frozen_model(): class FrozenModel(BaseModel): a: int = 10 class Config: extra = Extra.forbid frozen = True m = FrozenModel() assert m.a == 10 with pytest.raises(TypeError) as exc_info: m.a = 11 assert '"FrozenModel" is frozen and does not support item assignment' in exc_info.value.args[0] def test_not_frozen_are_not_hashable(): class TestModel(BaseModel): a: int = 10 m = TestModel() with pytest.raises(TypeError) as exc_info: hash(m) assert "unhashable type: 'TestModel'" in exc_info.value.args[0] def test_with_declared_hash(): class Foo(BaseModel): x: int def __hash__(self): return self.x**2 class Bar(Foo): y: int def __hash__(self): return self.y**3 class Buz(Bar): z: int assert hash(Foo(x=2)) == 4 assert hash(Bar(x=2, y=3)) == 27 assert hash(Buz(x=2, y=3, z=4)) == 27 def test_frozen_with_hashable_fields_are_hashable(): class TestModel(BaseModel): a: int = 10 class Config: frozen = True m = TestModel() assert m.__hash__ is not None assert isinstance(hash(m), int) def test_frozen_with_unhashable_fields_are_not_hashable(): class TestModel(BaseModel): a: int = 10 y: List[int] = [1, 2, 3] class Config: frozen = True m = TestModel() with pytest.raises(TypeError) as exc_info: hash(m) assert "unhashable type: 'list'" in exc_info.value.args[0] def test_hash_function_give_different_result_for_different_object(): class TestModel(BaseModel): a: int = 10 class Config: frozen = True m = TestModel() m2 = TestModel() m3 = TestModel(a=11) assert hash(m) == hash(m2) assert hash(m) != hash(m3) # Redefined `TestModel` class TestModel(BaseModel): a: int = 10 class Config: frozen = True m4 = TestModel() assert hash(m) != hash(m4) @pytest.fixture(name='ValidateAssignmentModel', scope='session') def validate_assignment_fixture(): class ValidateAssignmentModel(BaseModel): a: int = 2 b: constr(min_length=1) class Config: validate_assignment = True return ValidateAssignmentModel def test_validating_assignment_pass(ValidateAssignmentModel): p = ValidateAssignmentModel(a=5, b='hello') p.a = 2 assert p.a == 2 assert p.dict() == {'a': 2, 'b': 'hello'} p.b = 'hi' assert p.b == 'hi' assert p.dict() == {'a': 2, 'b': 'hi'} def test_validating_assignment_fail(ValidateAssignmentModel): p = ValidateAssignmentModel(a=5, b='hello') with pytest.raises(ValidationError) as exc_info: p.a = 'b' assert exc_info.value.errors() == [ { 'type': 'int_parsing', 'loc': ('a',), 'msg': 'Input should be a valid integer, unable to parse string as an integer', 'input': 'b', }, ] with pytest.raises(ValidationError) as exc_info: p.b = '' assert exc_info.value.errors() == [ { 'type': 'string_too_short', 'loc': ('b',), 'msg': 'String should have at least 1 characters', 'input': '', 'ctx': {'min_length': 1}, } ] def test_enum_values(): FooEnum = Enum('FooEnum', {'foo': 'foo', 'bar': 'bar'}) class Model(BaseModel): foo: FooEnum class Config: use_enum_values = True m = Model(foo='foo') # this is the actual value, so has not "values" field assert m.foo == FooEnum.foo assert isinstance(m.foo, FooEnum) def test_literal_enum_values(): FooEnum = Enum('FooEnum', {'foo': 'foo_value', 'bar': 'bar_value'}) class Model(BaseModel): baz: Literal[FooEnum.foo] boo: str = 'hoo' class Config: use_enum_values = True m = Model(baz=FooEnum.foo) assert m.dict() == {'baz': 'foo_value', 'boo': 'hoo'} assert m.baz.value == 'foo_value' with pytest.raises(ValidationError) as exc_info: Model(baz=FooEnum.bar) # insert_assert(exc_info.value.errors()) assert exc_info.value.errors() == [ { 'type': 'literal_error', 'loc': ('baz',), 'msg': "Input should be ", 'input': FooEnum.bar, 'ctx': {'expected': ""}, } ] def test_enum_raw(): FooEnum = Enum('FooEnum', {'foo': 'foo', 'bar': 'bar'}) class Model(BaseModel): foo: FooEnum = None m = Model(foo='foo') assert isinstance(m.foo, FooEnum) assert m.foo != 'foo' assert m.foo.value == 'foo' def test_set_tuple_values(): class Model(BaseModel): foo: set bar: tuple m = Model(foo=['a', 'b'], bar=['c', 'd']) assert m.foo == {'a', 'b'} assert m.bar == ('c', 'd') assert m.dict() == {'foo': {'a', 'b'}, 'bar': ('c', 'd')} def test_default_copy(): class User(BaseModel): friends: List[int] = Field(default_factory=lambda: []) u1 = User() u2 = User() assert u1.friends is not u2.friends class ArbitraryType: pass def test_arbitrary_type_allowed_validation_success(): class ArbitraryTypeAllowedModel(BaseModel): t: ArbitraryType class Config: arbitrary_types_allowed = True arbitrary_type_instance = ArbitraryType() m = ArbitraryTypeAllowedModel(t=arbitrary_type_instance) assert m.t == arbitrary_type_instance class OtherClass: pass def test_arbitrary_type_allowed_validation_fails(): class ArbitraryTypeAllowedModel(BaseModel): t: ArbitraryType class Config: arbitrary_types_allowed = True input_value = OtherClass() with pytest.raises(ValidationError) as exc_info: ArbitraryTypeAllowedModel(t=input_value) # insert_assert(exc_info.value.errors()) assert exc_info.value.errors() == [ { 'type': 'is_instance_of', 'loc': ('t',), 'msg': 'Input should be an instance of ArbitraryType', 'input': input_value, 'ctx': {'class': 'ArbitraryType'}, } ] def test_arbitrary_types_not_allowed(): with pytest.raises(TypeError, match='Unable to generate pydantic-core schema for str: return cls.__name__ assert Model.class_name == 'Model' assert Model().class_name == 'Model' def test_model_iteration(): class Foo(BaseModel): a: int = 1 b: int = 2 class Bar(BaseModel): c: int d: Foo m = Bar(c=3, d={}) assert m.dict() == {'c': 3, 'd': {'a': 1, 'b': 2}} assert list(m) == [('c', 3), ('d', Foo())] assert dict(m) == {'c': 3, 'd': Foo()} @pytest.mark.parametrize( 'exclude,expected,raises_match', [ pytest.param( {'foos': {0: {'a'}, 1: {'a'}}}, {'c': 3, 'foos': [{'b': 2}, {'b': 4}]}, None, id='excluding fields of indexed list items', ), pytest.param( {'foos': {'a'}}, TypeError, 'expected integer keys', id='should fail trying to exclude string keys on list field (1).', ), pytest.param( {'foos': {0: ..., 'a': ...}}, TypeError, 'expected integer keys', id='should fail trying to exclude string keys on list field (2).', ), pytest.param( {'foos': {0: 1}}, TypeError, 'Unexpected type', id='should fail using integer key to specify list item field name (1)', ), pytest.param( {'foos': {'__all__': 1}}, TypeError, 'Unexpected type', id='should fail using integer key to specify list item field name (2)', ), pytest.param( {'foos': {'__all__': {'a'}}}, {'c': 3, 'foos': [{'b': 2}, {'b': 4}]}, None, id='using "__all__" to exclude specific nested field', ), pytest.param( {'foos': {0: {'b'}, '__all__': {'a'}}}, {'c': 3, 'foos': [{}, {'b': 4}]}, None, id='using "__all__" to exclude specific nested field in combination with more specific exclude', ), pytest.param( {'foos': {'__all__'}}, {'c': 3, 'foos': []}, None, id='using "__all__" to exclude all list items', ), pytest.param( {'foos': {1, '__all__'}}, {'c': 3, 'foos': []}, None, id='using "__all__" and other items should get merged together, still excluding all list items', ), pytest.param( {'foos': {1: {'a'}, -1: {'b'}}}, {'c': 3, 'foos': [{'a': 1, 'b': 2}, {}]}, None, id='using negative and positive indexes, referencing the same items should merge excludes', ), ], ) def test_model_export_nested_list(exclude, expected, raises_match): class Foo(BaseModel): a: int = 1 b: int = 2 class Bar(BaseModel): c: int foos: List[Foo] m = Bar(c=3, foos=[Foo(a=1, b=2), Foo(a=3, b=4)]) if isinstance(expected, type) and issubclass(expected, Exception): with pytest.raises(expected, match=raises_match): m.dict(exclude=exclude) else: original_exclude = deepcopy(exclude) assert m.dict(exclude=exclude) == expected assert exclude == original_exclude @pytest.mark.parametrize( 'excludes,expected', [ pytest.param( {'bars': {0}}, {'a': 1, 'bars': [{'y': 2}, {'w': -1, 'z': 3}]}, id='excluding first item from list field using index', ), pytest.param({'bars': {'__all__'}}, {'a': 1, 'bars': []}, id='using "__all__" to exclude all list items'), pytest.param( {'bars': {'__all__': {'w'}}}, {'a': 1, 'bars': [{'x': 1}, {'y': 2}, {'z': 3}]}, id='exclude single dict key from all list items', ), ], ) def test_model_export_dict_exclusion(excludes, expected): class Foo(BaseModel): a: int = 1 bars: List[Dict[str, int]] m = Foo(a=1, bars=[{'w': 0, 'x': 1}, {'y': 2}, {'w': -1, 'z': 3}]) original_excludes = deepcopy(excludes) assert m.dict(exclude=excludes) == expected assert excludes == original_excludes @pytest.mark.skip(reason='not implemented') def test_model_exclude_config_field_merging(): """Test merging field exclude values from config.""" class Model(BaseModel): b: int = Field(2, exclude=...) class Config: fields = { 'b': {'exclude': ...}, } assert Model.__fields__['b'].field_info.exclude is ... class Model(BaseModel): b: int = Field(2, exclude={'a': {'test'}}) class Config: fields = { 'b': {'exclude': ...}, } assert Model.__fields__['b'].field_info.exclude == {'a': {'test'}} class Model(BaseModel): b: int = Field(2, exclude={'foo'}) class Config: fields = { 'b': {'exclude': {'bar'}}, } assert Model.__fields__['b'].field_info.exclude == {'foo': ..., 'bar': ...} @pytest.mark.skip(reason='not implemented') def test_model_exclude_copy_on_model_validation(): """When `Config.copy_on_model_validation` is set, it should keep private attributes and excluded fields""" class User(BaseModel): _priv: int = PrivateAttr() id: int username: str password: SecretStr = Field(exclude=True) hobbies: List[str] my_user = User(id=42, username='JohnDoe', password='hashedpassword', hobbies=['scuba diving']) my_user._priv = 13 assert my_user.id == 42 assert my_user.password.get_secret_value() == 'hashedpassword' assert my_user.dict() == {'id': 42, 'username': 'JohnDoe', 'hobbies': ['scuba diving']} class Transaction(BaseModel): id: str user: User = Field(..., exclude={'username'}) value: int class Config: fields = {'value': {'exclude': True}} t = Transaction( id='1234567890', user=my_user, value=9876543210, ) assert t.user is not my_user assert t.user.hobbies == ['scuba diving'] assert t.user.hobbies is my_user.hobbies # `Config.copy_on_model_validation` does a shallow copy assert t.user._priv == 13 assert t.user.password.get_secret_value() == 'hashedpassword' assert t.dict() == {'id': '1234567890', 'user': {'id': 42, 'hobbies': ['scuba diving']}} @pytest.mark.skip(reason='not implemented') def test_model_exclude_copy_on_model_validation_shallow(): """When `Config.copy_on_model_validation` is set and `Config.copy_on_model_validation_shallow` is set, do the same as the previous test but perform a shallow copy""" class User(BaseModel): class Config: copy_on_model_validation = 'shallow' hobbies: List[str] my_user = User(hobbies=['scuba diving']) class Transaction(BaseModel): user: User = Field(...) t = Transaction(user=my_user) assert t.user is not my_user assert t.user.hobbies is my_user.hobbies # unlike above, this should be a shallow copy @pytest.mark.skip(reason='not implemented') @pytest.mark.parametrize('comv_value', [True, False]) def test_copy_on_model_validation_warning(comv_value): class User(BaseModel): class Config: # True interpreted as 'shallow', False interpreted as 'none' copy_on_model_validation = comv_value hobbies: List[str] my_user = User(hobbies=['scuba diving']) class Transaction(BaseModel): user: User with pytest.warns(DeprecationWarning, match="`copy_on_model_validation` should be a string: 'deep', 'shallow' or"): t = Transaction(user=my_user) if comv_value: assert t.user is not my_user else: assert t.user is my_user assert t.user.hobbies is my_user.hobbies @pytest.mark.skip(reason='not implemented') def test_validation_deep_copy(): """By default, Config.copy_on_model_validation should do a deep copy""" class A(BaseModel): name: str class Config: copy_on_model_validation = 'deep' class B(BaseModel): list_a: List[A] a = A(name='a') b = B(list_a=[a]) assert b.list_a == [A(name='a')] a.name = 'b' assert b.list_a == [A(name='a')] @pytest.mark.skip(reason='not implemented') @pytest.mark.parametrize( 'kinds', [ {'sub_fields', 'model_fields', 'model_config', 'sub_config', 'combined_config'}, {'sub_fields', 'model_fields', 'combined_config'}, {'sub_fields', 'model_fields'}, {'combined_config'}, {'model_config', 'sub_config'}, {'model_config', 'sub_fields'}, {'model_fields', 'sub_config'}, ], ) @pytest.mark.parametrize( 'exclude,expected', [ (None, {'a': 0, 'c': {'a': [3, 5], 'c': 'foobar'}, 'd': {'c': 'foobar'}}), ({'c', 'd'}, {'a': 0}), ({'a': ..., 'c': ..., 'd': {'a': ..., 'c': ...}}, {'d': {}}), ], ) def test_model_export_exclusion_with_fields_and_config(kinds, exclude, expected): """Test that exporting models with fields using the export parameter works.""" class ChildConfig: pass if 'sub_config' in kinds: ChildConfig.fields = {'b': {'exclude': ...}, 'a': {'exclude': {1}}} class ParentConfig: pass if 'combined_config' in kinds: ParentConfig.fields = { 'b': {'exclude': ...}, 'c': {'exclude': {'b': ..., 'a': {1}}}, 'd': {'exclude': {'a': ..., 'b': ...}}, } elif 'model_config' in kinds: ParentConfig.fields = {'b': {'exclude': ...}, 'd': {'exclude': {'a'}}} class Sub(BaseModel): a: List[int] = Field([3, 4, 5], exclude={1} if 'sub_fields' in kinds else None) b: int = Field(4, exclude=... if 'sub_fields' in kinds else None) c: str = 'foobar' Config = ChildConfig class Model(BaseModel): a: int = 0 b: int = Field(2, exclude=... if 'model_fields' in kinds else None) c: Sub = Sub() d: Sub = Field(Sub(), exclude={'a'} if 'model_fields' in kinds else None) Config = ParentConfig m = Model() assert m.dict(exclude=exclude) == expected, 'Unexpected model export result' @pytest.mark.skip(reason='not implemented') def test_model_export_exclusion_inheritance(): class Sub(BaseModel): s1: str = 'v1' s2: str = 'v2' s3: str = 'v3' s4: str = Field('v4', exclude=...) class Parent(BaseModel): a: int b: int = Field(..., exclude=...) c: int d: int s: Sub = Sub() class Config: fields = {'a': {'exclude': ...}, 's': {'exclude': {'s1'}}} class Child(Parent): class Config: fields = {'c': {'exclude': ...}, 's': {'exclude': {'s2'}}} actual = Child(a=0, b=1, c=2, d=3).dict() expected = {'d': 3, 's': {'s3': 'v3'}} assert actual == expected, 'Unexpected model export result' @pytest.mark.skip(reason='not implemented') def test_model_export_with_true_instead_of_ellipsis(): class Sub(BaseModel): s1: int = 1 class Model(BaseModel): a: int = 2 b: int = Field(3, exclude=True) c: int = Field(4) s: Sub = Sub() class Config: fields = {'c': {'exclude': True}} m = Model() assert m.dict(exclude={'s': True}) == {'a': 2} @pytest.mark.skip(reason='not implemented') def test_model_export_inclusion(): class Sub(BaseModel): s1: str = 'v1' s2: str = 'v2' s3: str = 'v3' s4: str = 'v4' class Model(BaseModel): a: Sub = Sub() b: Sub = Field(Sub(), include={'s1'}) c: Sub = Field(Sub(), include={'s1', 's2'}) class Config: fields = {'a': {'include': {'s2', 's1', 's3'}}, 'b': {'include': {'s1', 's2', 's3', 's4'}}} Model.__fields__['a'].field_info.include == {'s1': ..., 's2': ..., 's3': ...} Model.__fields__['b'].field_info.include == {'s1': ...} Model.__fields__['c'].field_info.include == {'s1': ..., 's2': ...} actual = Model().dict(include={'a': {'s3', 's4'}, 'b': ..., 'c': ...}) # s1 included via field, s2 via config and s3 via .dict call: expected = {'a': {'s3': 'v3'}, 'b': {'s1': 'v1'}, 'c': {'s1': 'v1', 's2': 'v2'}} assert actual == expected, 'Unexpected model export result' @pytest.mark.skip(reason='not implemented') def test_model_export_inclusion_inheritance(): class Sub(BaseModel): s1: str = Field('v1', include=...) s2: str = Field('v2', include=...) s3: str = Field('v3', include=...) s4: str = 'v4' class Parent(BaseModel): a: int b: int c: int s: Sub = Field(Sub(), include={'s1', 's2'}) # overrides includes set in Sub model class Config: # b will be included since fields are set idependently fields = {'b': {'include': ...}} class Child(Parent): class Config: # b is still included even if it doesn't occur here since fields # are still considered separately. # s however, is merged, resulting in only s1 being included. fields = {'a': {'include': ...}, 's': {'include': {'s1'}}} actual = Child(a=0, b=1, c=2).dict() expected = {'a': 0, 'b': 1, 's': {'s1': 'v1'}} assert actual == expected, 'Unexpected model export result' def test_custom_init_subclass_params(): class DerivedModel(BaseModel): def __init_subclass__(cls, something): cls.something = something # if this raises a TypeError, then there is a regression of issue 867: # pydantic.main.MetaModel.__new__ should include **kwargs at the end of the # method definition and pass them on to the super call at the end in order # to allow the special method __init_subclass__ to be defined with custom # parameters on extended BaseModel classes. class NewModel(DerivedModel, something=2): something = 1 assert NewModel.something == 2 def test_recursive_model(): class MyModel(BaseModel): field: Optional['MyModel'] # noqa: F821 m = MyModel(field={'field': {'field': None}}) assert m.dict() == {'field': {'field': {'field': None}}} def test_two_defaults(): with pytest.raises(ValueError, match='^cannot specify both default and default_factory$'): class Model(BaseModel): a: int = Field(default=3, default_factory=lambda: 3) def test_default_factory(): class ValueModel(BaseModel): uid: UUID = uuid4() m1 = ValueModel() m2 = ValueModel() assert m1.uid == m2.uid class DynamicValueModel(BaseModel): uid: UUID = Field(default_factory=uuid4) m1 = DynamicValueModel() m2 = DynamicValueModel() assert isinstance(m1.uid, UUID) assert m1.uid != m2.uid # With a callable: we still should be able to set callables as defaults class FunctionModel(BaseModel): a: int = 1 uid: Callable[[], UUID] = Field(uuid4) m = FunctionModel() assert m.uid is uuid4 # Returning a singleton from a default_factory is supported class MySingleton: pass MY_SINGLETON = MySingleton() class SingletonFieldModel(BaseModel): singleton: MySingleton = Field(default_factory=lambda: MY_SINGLETON) class Config: arbitrary_types_allowed = True assert SingletonFieldModel().singleton is SingletonFieldModel().singleton def test_default_factory_called_once(): """It should call only once the given factory by default""" class Seq: def __init__(self): self.v = 0 def __call__(self): self.v += 1 return self.v class MyModel(BaseModel): id: int = Field(default_factory=Seq()) m1 = MyModel() assert m1.id == 1 m2 = MyModel() assert m2.id == 2 assert m1.id == 1 def test_default_factory_called_once_2(): """It should call only once the given factory by default""" v = 0 def factory(): nonlocal v v += 1 return v class MyModel(BaseModel): id: int = Field(default_factory=factory) m1 = MyModel() assert m1.id == 1 m2 = MyModel() assert m2.id == 2 def test_default_factory_validate_children(): class Child(BaseModel): x: int class Parent(BaseModel): children: List[Child] = Field(default_factory=list) Parent(children=[{'x': 1}, {'x': 2}]) with pytest.raises(ValidationError) as exc_info: Parent(children=[{'x': 1}, {'y': 2}]) # insert_assert(exc_info.value.errors()) assert exc_info.value.errors() == [ {'type': 'missing', 'loc': ('children', 1, 'x'), 'msg': 'Field required', 'input': {'y': 2}} ] def test_default_factory_parse(): class Inner(BaseModel): val: int = Field(0) class Outer(BaseModel): inner_1: Inner = Field(default_factory=Inner) inner_2: Inner = Field(Inner()) default = Outer().dict() parsed = Outer.parse_obj(default) assert parsed.dict() == {'inner_1': {'val': 0}, 'inner_2': {'val': 0}} assert repr(parsed) == 'Outer(inner_1=Inner(val=0), inner_2=Inner(val=0))' def test_reuse_same_field(): required_field = Field(...) class Model1(BaseModel): required: str = required_field class Model2(BaseModel): required: str = required_field with pytest.raises(ValidationError): Model1.parse_obj({}) with pytest.raises(ValidationError): Model2.parse_obj({}) def test_base_config_type_hinting(): class M(BaseModel): a: int get_type_hints(M.__config__) @pytest.mark.xfail(reason='https://github.com/pydantic/pydantic-core/pull/237') def test_allow_mutation_field(): """assigning a allow_mutation=False field should raise a TypeError""" class Entry(BaseModel): id: float = Field(allow_mutation=False) val: float class Config: validate_assignment = True r = Entry(id=1, val=100) assert r.val == 100 r.val = 101 assert r.val == 101 assert r.id == 1 with pytest.raises(TypeError, match='"id" has allow_mutation set to False and cannot be assigned'): r.id = 2 def test_repr_field(): class Model(BaseModel): a: int = Field() b: float = Field(repr=True) c: bool = Field(repr=False) m = Model(a=1, b=2.5, c=True) assert repr(m) == 'Model(a=1, b=2.5)' assert repr(m.__fields__['a']) == 'FieldInfo(annotation=int, required=True)' assert repr(m.__fields__['b']) == 'FieldInfo(annotation=float, required=True)' assert repr(m.__fields__['c']) == 'FieldInfo(annotation=bool, required=True, repr=False)' def test_inherited_model_field_copy(): """It should copy models used as fields by default""" class Image(BaseModel): path: str def __hash__(self): return id(self) class Item(BaseModel): images: Set[Image] image_1 = Image(path='my_image1.png') image_2 = Image(path='my_image2.png') item = Item(images={image_1, image_2}) assert image_1 in item.images assert id(image_1) in {id(image) for image in item.images} assert id(image_2) in {id(image) for image in item.images} def test_inherited_model_field_untouched(): """It should not copy models used as fields if explicitly asked""" class Image(BaseModel): path: str def __hash__(self): return id(self) class Config: copy_on_model_validation = 'none' class Item(BaseModel): images: List[Image] image_1 = Image(path='my_image1.png') image_2 = Image(path='my_image2.png') item = Item(images=(image_1, image_2)) assert image_1 in item.images assert id(image_1) == id(item.images[0]) assert id(image_2) == id(item.images[1]) def test_mapping_retains_type_subclass(): class CustomMap(dict): pass class Model(BaseModel): x: Mapping[str, Mapping[str, int]] m = Model(x=CustomMap(outer=CustomMap(inner=42))) assert isinstance(m.x, CustomMap) assert isinstance(m.x['outer'], CustomMap) assert m.x['outer']['inner'] == 42 def test_mapping_retains_type_defaultdict(): class Model(BaseModel): x: Mapping[str, int] d = defaultdict(int) d['foo'] = '2' d['bar'] m = Model(x=d) assert isinstance(m.x, defaultdict) assert m.x['foo'] == 2 assert m.x['bar'] == 0 def test_mapping_retains_type_fallback_error(): class CustomMap(dict): def __init__(self, *args, **kwargs): if args or kwargs: raise TypeError('test') super().__init__(*args, **kwargs) class Model(BaseModel): x: Mapping[str, int] d = CustomMap() d['one'] = 1 d['two'] = 2 with pytest.raises(TypeError, match='test'): Model(x=d) def test_typing_coercion_dict(): class Model(BaseModel): x: Dict[str, int] m = Model(x={'one': 1, 'two': 2}) assert repr(m) == "Model(x={'one': 1, 'two': 2})" KT = TypeVar('KT') VT = TypeVar('VT') class MyDict(Dict[KT, VT]): def __repr__(self): return f'MyDict({super().__repr__()})' def test_dict_subclasses_bare(): class Model(BaseModel): a: MyDict assert repr(Model(a=MyDict({'a': 1})).a) == "MyDict({'a': 1})" assert repr(Model(a=MyDict({b'x': (1, 2)})).a) == "MyDict({b'x': (1, 2)})" def test_dict_subclasses_typed(): class Model(BaseModel): a: MyDict[str, int] assert repr(Model(a=MyDict({'a': 1})).a) == "MyDict({'a': 1})" def test_typing_coercion_defaultdict(): class Model(BaseModel): x: DefaultDict[int, str] d = defaultdict(str) d['1'] m = Model(x=d) assert isinstance(m.x, defaultdict) assert repr(m.x) == "defaultdict(, {1: ''})" def test_typing_coercion_counter(): class Model(BaseModel): x: Counter[str] m = Model(x={'a': 10}) assert isinstance(m.x, Counter) assert repr(m.x) == "Counter({'a': 10})" def test_typing_counter_value_validation(): class Model(BaseModel): x: Counter[str] with pytest.raises(ValidationError) as exc_info: Model(x={'a': 'a'}) # insert_assert(exc_info.value.errors()) assert exc_info.value.errors() == [ { 'type': 'int_parsing', 'loc': ('x', 'a'), 'msg': 'Input should be a valid integer, unable to parse string as an integer', 'input': 'a', } ] def test_class_kwargs_config(): class Base(BaseModel, extra='forbid', alias_generator=str.upper): a: int assert Base.__config__.extra is Extra.forbid assert Base.__config__.alias_generator is str.upper # assert Base.__fields__['a'].alias == 'A' class Model(Base, extra='allow'): b: int assert Model.__config__.extra is Extra.allow # overwritten as intended assert Model.__config__.alias_generator is str.upper # inherited as intended # assert Model.__fields__['b'].alias == 'B' # alias_generator still works def test_class_kwargs_config_json_encoders(): class Model(BaseModel, json_encoders={int: str}): pass assert Model.__config__.json_encoders == {int: str} def test_class_kwargs_config_and_attr_conflict(): class Model(BaseModel, extra='allow', alias_generator=str.upper): b: int class Config: extra = 'forbid' title = 'Foobar' assert Model.__config__.extra is Extra.allow assert Model.__config__.alias_generator is str.upper assert Model.__config__.title == 'Foobar' def test_class_kwargs_custom_config(): class Base(BaseModel): class Config(BaseConfig): some_config = 'value' with pytest.raises(TypeError, match=r'__init_subclass__\(\) takes no keyword arguments'): class Model(Base, some_config='new_value'): a: int @pytest.mark.skipif(sys.version_info < (3, 10), reason='need 3.10 version') def test_new_union_origin(): """On 3.10+, origin of `int | str` is `types.UnionType`, not `typing.Union`""" class Model(BaseModel): x: int | str assert Model(x=3).x == 3 assert Model(x='3').x == '3' assert Model(x='pika').x == 'pika' # assert Model.schema() == { # 'title': 'Model', # 'type': 'object', # 'properties': {'x': {'title': 'X', 'anyOf': [{'type': 'integer'}, {'type': 'string'}]}}, # 'required': ['x'], # } @pytest.mark.xfail(reason='implement final') @pytest.mark.parametrize( 'ann', [Final, Final[int]], ids=['no-arg', 'with-arg'], ) @pytest.mark.parametrize( 'value', [None, Field(...)], ids=['none', 'field'], ) def test_final_field_decl_without_default_val(ann, value): class Model(BaseModel): a: ann if value is not None: a = value Model.model_rebuild(ann=ann) assert 'a' not in Model.__class_vars__ assert 'a' in Model.__fields__ assert Model.__fields__['a'].final @pytest.mark.xfail(reason='waiting for https://github.com/pydantic/pydantic-core/pull/237') @pytest.mark.parametrize( 'ann', [Final, Final[int]], ids=['no-arg', 'with-arg'], ) def test_final_field_decl_with_default_val(ann): class Model(BaseModel): a: ann = 10 Model.model_rebuild(ann=ann) assert 'a' in Model.__class_vars__ assert 'a' not in Model.__fields__ @pytest.mark.xfail(reason='waiting for https://github.com/pydantic/pydantic-core/pull/237') def test_final_field_reassignment(): class Model(BaseModel): a: Final[int] obj = Model(a=10) with pytest.raises( TypeError, match=r'^"Model" object "a" field is final and does not support reassignment$', ): obj.a = 20 @pytest.mark.xfail(reason='waiting for https://github.com/pydantic/pydantic-core/pull/237') def test_field_by_default_is_not_final(): class Model(BaseModel): a: int assert not Model.__fields__['a'].final def test_post_init(): calls = [] class SubModel(BaseModel): a: int b: int def model_post_init(self, **kwargs) -> None: assert self.dict() == {'a': 3, 'b': 4} calls.append('submodel_post_init') class Model(BaseModel): c: int d: int sub: SubModel def model_post_init(self, **kwargs) -> None: assert self.dict() == {'c': 1, 'd': 2, 'sub': {'a': 3, 'b': 4}} calls.append('model_post_init') m = Model(c=1, d='2', sub={'a': 3, 'b': '4'}) assert m.dict() == {'c': 1, 'd': 2, 'sub': {'a': 3, 'b': 4}} assert calls == ['submodel_post_init', 'model_post_init'] def test_extra_args_to_field_type_error(): with pytest.raises(TypeError, match='unexpected keyword argument'): class Model(BaseModel): a: int = Field(thing=1)