Files
pydantic/tests/test_create_model.py
T
Samuel Colvin 594effa279 Switching to pydantic_core (#4516)
* working on core schema generation

* adapting main.py

* getting tests to run

* fix tests

* disable pyright, fix mypy

* moving to class-based model generation

* working on validators

* change how models are created

* start fixing test_main.py

* fixing mypy

* SelfType

* recursive models working, more tests fixed

* fix tests on <3.10

* get docs build to pass

* starting to cleanup types.py

* starting works on custom types

* working on using annotated-types

* using annoated types for constraints

* lots of cleanup, fixing network tests

* network tests passing 🎉

* working on types

* working on types and cleanup

* fixing UUID type, restructing again

* more types and newer pydantic-core

* working on Iterable

* more test_types tests

* support newer pydantic-core, fixing more test_types.py

* working through more test_types.py

* test_types.py at last passing locally 🎉

* fixing more tests in test_types.py

* fix datetime_parse tests and linting

* get tests running again, rename to test_datetime.py

* renaming internal modules

* working through mypy errors

* fixing mypy

* refactoring _generate_schema.py

* test_main.py passing

* uprev deps

* fix conftest and linting?

* importing Annotated

* ltining

* import Annotated from typing_extensions

* fixing 3.7 compatibility

* fixing tests on 3.9

* fix linting

* fixing SecretField and 3.9 tests

* customising get_type_hints

* ignore warnings on 3.11

* spliting repr out of utils

* removing unused bits of _repr, fix tests for 3.7

* more cleanup, removing many type aliases

* clean up repr

* support namedtuples and typeddicts

* test is_union

* removing errors, uprev pydantic-core

* fix tests on 3.8

* fixing private attributes and model_post_init

* renaming and cleanup

* remove unnecessary PydanticMetadata inheritance

* fixing forward refs and mypy tests

* fix signatures, change how xfail works

* revert mypy tests to 3.7 syntax

* correct model title

* try to fix tests

* fixing ClassVar forward refs

* uprev pydantic-core, new error format

* add "force" argument to model_rebuild

* Apply suggestions from code review

Suggestions from @tiangolo and @hramezani 🙏

Co-authored-by: Hasan Ramezani <hasan.r67@gmail.com>
Co-authored-by: Sebastián Ramírez <tiangolo@gmail.com>

* more suggestions from @tiangolo

* extra -> json_schema_extra on Field

Co-authored-by: Hasan Ramezani <hasan.r67@gmail.com>
Co-authored-by: Sebastián Ramírez <tiangolo@gmail.com>
2022-11-02 12:01:17 +00:00

269 lines
8.1 KiB
Python

from typing import Generic, Optional, Tuple, TypeVar
import pytest
from pydantic import BaseModel, Extra, Field, ValidationError, create_model, errors, validator
from pydantic.fields import ModelPrivateAttr
from pydantic.generics import GenericModel
pytestmark = pytest.mark.xfail(reason='working on V2', strict=False)
def test_create_model():
model = create_model('FooModel', foo=(str, ...), bar=123)
assert issubclass(model, BaseModel)
assert issubclass(model.__config__, BaseModel.Config)
assert model.__name__ == 'FooModel'
assert model.__fields__.keys() == {'foo', 'bar'}
assert model.__validators__ == {}
assert model.__config__.__name__ == 'Config'
assert model.__module__ == 'pydantic.main'
def test_create_model_usage():
model = create_model('FooModel', foo=(str, ...), bar=123)
m = model(foo='hello')
assert m.foo == 'hello'
assert m.bar == 123
with pytest.raises(ValidationError):
model()
with pytest.raises(ValidationError):
model(foo='hello', bar='xxx')
def test_create_model_pickle(create_module):
"""
Pickle will work for dynamically created model only if it was defined globally with its class name
and module where it's defined was specified
"""
@create_module
def module():
import pickle
from pydantic import create_model
FooModel = create_model('FooModel', foo=(str, ...), bar=123, __module__=__name__)
m = FooModel(foo='hello')
d = pickle.dumps(m)
m2 = pickle.loads(d)
assert m2.foo == m.foo == 'hello'
assert m2.bar == m.bar == 123
assert m2 == m
assert m2 is not m
def test_invalid_name():
with pytest.warns(RuntimeWarning):
model = create_model('FooModel', _foo=(str, ...))
assert len(model.__fields__) == 0
def test_field_wrong_tuple():
with pytest.raises(errors.PydanticUserError):
create_model('FooModel', foo=(1, 2, 3))
def test_config_and_base():
with pytest.raises(errors.PydanticUserError):
create_model('FooModel', __config__=BaseModel.Config, __base__=BaseModel)
def test_inheritance():
class BarModel(BaseModel):
x = 1
y = 2
model = create_model('FooModel', foo=(str, ...), bar=(int, 123), __base__=BarModel)
assert model.__fields__.keys() == {'foo', 'bar', 'x', 'y'}
m = model(foo='a', x=4)
assert m.dict() == {'bar': 123, 'foo': 'a', 'x': 4, 'y': 2}
def test_custom_config():
class Config:
fields = {'foo': 'api-foo-field'}
model = create_model('FooModel', foo=(int, ...), __config__=Config)
assert model(**{'api-foo-field': '987'}).foo == 987
assert issubclass(model.__config__, BaseModel.Config)
with pytest.raises(ValidationError):
model(foo=654)
def test_custom_config_inherits():
class Config(BaseModel.Config):
fields = {'foo': 'api-foo-field'}
model = create_model('FooModel', foo=(int, ...), __config__=Config)
assert model(**{'api-foo-field': '987'}).foo == 987
assert issubclass(model.__config__, BaseModel.Config)
with pytest.raises(ValidationError):
model(foo=654)
def test_custom_config_extras():
class Config(BaseModel.Config):
extra = Extra.forbid
model = create_model('FooModel', foo=(int, ...), __config__=Config)
assert model(foo=654)
with pytest.raises(ValidationError):
model(bar=654)
def test_inheritance_validators():
class BarModel(BaseModel):
@validator('a', check_fields=False)
def check_a(cls, v):
if 'foobar' not in v:
raise ValueError('"foobar" not found in a')
return v
model = create_model('FooModel', a='cake', __base__=BarModel)
assert model().a == 'cake'
assert model(a='this is foobar good').a == 'this is foobar good'
with pytest.raises(ValidationError):
model(a='something else')
def test_inheritance_validators_always():
class BarModel(BaseModel):
@validator('a', check_fields=False, always=True)
def check_a(cls, v):
if 'foobar' not in v:
raise ValueError('"foobar" not found in a')
return v
model = create_model('FooModel', a='cake', __base__=BarModel)
with pytest.raises(ValidationError):
model()
assert model(a='this is foobar good').a == 'this is foobar good'
with pytest.raises(ValidationError):
model(a='something else')
def test_inheritance_validators_all():
class BarModel(BaseModel):
@validator('*')
def check_all(cls, v):
return v * 2
model = create_model('FooModel', a=(int, ...), b=(int, ...), __base__=BarModel)
assert model(a=2, b=6).dict() == {'a': 4, 'b': 12}
def test_funky_name():
model = create_model('FooModel', **{'this-is-funky': (int, ...)})
m = model(**{'this-is-funky': '123'})
assert m.dict() == {'this-is-funky': 123}
with pytest.raises(ValidationError) as exc_info:
model()
assert exc_info.value.errors() == [
{'loc': ('this-is-funky',), 'msg': 'field required', 'type': 'value_error.missing'}
]
def test_repeat_base_usage():
class Model(BaseModel):
a: str
assert Model.__fields__.keys() == {'a'}
model = create_model('FooModel', b=1, __base__=Model)
assert Model.__fields__.keys() == {'a'}
assert model.__fields__.keys() == {'a', 'b'}
model2 = create_model('Foo2Model', c=1, __base__=Model)
assert Model.__fields__.keys() == {'a'}
assert model.__fields__.keys() == {'a', 'b'}
assert model2.__fields__.keys() == {'a', 'c'}
model3 = create_model('Foo2Model', d=1, __base__=model)
assert Model.__fields__.keys() == {'a'}
assert model.__fields__.keys() == {'a', 'b'}
assert model2.__fields__.keys() == {'a', 'c'}
assert model3.__fields__.keys() == {'a', 'b', 'd'}
def test_dynamic_and_static():
class A(BaseModel):
x: int
y: float
z: str
DynamicA = create_model('A', x=(int, ...), y=(float, ...), z=(str, ...))
for field_name in ('x', 'y', 'z'):
assert A.__fields__[field_name].default == DynamicA.__fields__[field_name].default
def test_config_field_info_create_model():
class Config:
fields = {'a': {'description': 'descr'}}
m1 = create_model('M1', __config__=Config, a=(str, ...))
assert m1.schema()['properties'] == {'a': {'title': 'A', 'description': 'descr', 'type': 'string'}}
m2 = create_model('M2', __config__=Config, a=(str, Field(...)))
assert m2.schema()['properties'] == {'a': {'title': 'A', 'description': 'descr', 'type': 'string'}}
def test_generics_model():
T = TypeVar('T')
class TestGenericModel(GenericModel):
pass
AAModel = create_model(
'AAModel', __base__=(TestGenericModel, Generic[T]), __cls_kwargs__={'orm_mode': True}, aa=(int, Field(0))
)
result = AAModel[int](aa=1)
assert result.aa == 1
assert result.__config__.orm_mode is True
@pytest.mark.parametrize('base', [ModelPrivateAttr, object])
def test_set_name(base):
calls = []
class class_deco(base):
def __init__(self, fn):
super().__init__()
self.fn = fn
def __set_name__(self, owner, name):
calls.append((owner, name))
def __get__(self, obj, type=None):
return self.fn(obj) if obj else self
class A(BaseModel):
x: int
@class_deco
def _some_func(self):
return self.x
assert calls == [(A, '_some_func')]
a = A(x=2)
# we don't test whether calling the method on a PrivateAttr works:
# attribute access on privateAttributes is more complicated, it doesn't
# get added to the class namespace (and will also get set on the instance
# with _init_private_attributes), so the descriptor protocol won't work.
if base is object:
assert a._some_func == 2
def test_create_model_with_slots():
field_definitions = {'__slots__': (Optional[Tuple[str, ...]], None), 'foobar': (Optional[int], None)}
with pytest.warns(RuntimeWarning, match='__slots__ should not be passed to create_model'):
model = create_model('PartialPet', **field_definitions)
assert model.__fields__.keys() == {'foobar'}