mirror of
https://github.com/kennethreitz/pydantic.git
synced 2026-06-05 23:00:18 +00:00
594effa279
* 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>
327 lines
7.6 KiB
Python
327 lines
7.6 KiB
Python
from types import SimpleNamespace
|
|
from typing import Dict, List
|
|
|
|
import pytest
|
|
|
|
from pydantic import BaseModel, PydanticUserError, ValidationError, root_validator
|
|
|
|
pytestmark = pytest.mark.xfail(reason='working on V2', strict=False)
|
|
|
|
|
|
def test_getdict():
|
|
class TestCls:
|
|
a = 1
|
|
b: int
|
|
|
|
def __init__(self):
|
|
self.c = 3
|
|
|
|
@property
|
|
def d(self):
|
|
return 4
|
|
|
|
def __getattr__(self, key):
|
|
if key == 'e':
|
|
return 5
|
|
else:
|
|
raise AttributeError()
|
|
|
|
t = TestCls()
|
|
# gd = GetterDict(t)
|
|
gd = object(t)
|
|
assert gd.keys() == ['a', 'c', 'd']
|
|
assert gd.get('a') == 1
|
|
assert gd['a'] == 1
|
|
with pytest.raises(KeyError):
|
|
assert gd['foobar']
|
|
assert gd.get('b', None) is None
|
|
assert gd.get('b', 1234) == 1234
|
|
assert gd.get('c', None) == 3
|
|
assert gd.get('d', None) == 4
|
|
assert gd.get('e', None) == 5
|
|
assert gd.get('f', 'missing') == 'missing'
|
|
assert list(gd.values()) == [1, 3, 4]
|
|
assert list(gd.items()) == [('a', 1), ('c', 3), ('d', 4)]
|
|
assert list(gd) == ['a', 'c', 'd']
|
|
assert gd == {'a': 1, 'c': 3, 'd': 4}
|
|
assert 'a' in gd
|
|
assert len(gd) == 3
|
|
assert str(gd) == "{'a': 1, 'c': 3, 'd': 4}"
|
|
assert repr(gd) == "GetterDict[TestCls]({'a': 1, 'c': 3, 'd': 4})"
|
|
|
|
|
|
def test_orm_mode_root():
|
|
class PokemonCls:
|
|
def __init__(self, *, en_name: str, jp_name: str):
|
|
self.en_name = en_name
|
|
self.jp_name = jp_name
|
|
|
|
class Pokemon(BaseModel):
|
|
en_name: str
|
|
jp_name: str
|
|
|
|
class Config:
|
|
orm_mode = True
|
|
|
|
class PokemonList(BaseModel):
|
|
__root__: List[Pokemon]
|
|
|
|
class Config:
|
|
orm_mode = True
|
|
|
|
pika = PokemonCls(en_name='Pikachu', jp_name='ピカチュウ')
|
|
bulbi = PokemonCls(en_name='Bulbasaur', jp_name='フシギダネ')
|
|
|
|
pokemons = PokemonList.from_orm([pika, bulbi])
|
|
assert pokemons.__root__ == [
|
|
Pokemon(en_name='Pikachu', jp_name='ピカチュウ'),
|
|
Pokemon(en_name='Bulbasaur', jp_name='フシギダネ'),
|
|
]
|
|
|
|
class PokemonDict(BaseModel):
|
|
__root__: Dict[str, Pokemon]
|
|
|
|
class Config:
|
|
orm_mode = True
|
|
|
|
pokemons = PokemonDict.from_orm({'pika': pika, 'bulbi': bulbi})
|
|
assert pokemons.__root__ == {
|
|
'pika': Pokemon(en_name='Pikachu', jp_name='ピカチュウ'),
|
|
'bulbi': Pokemon(en_name='Bulbasaur', jp_name='フシギダネ'),
|
|
}
|
|
|
|
|
|
def test_orm_mode():
|
|
class PetCls:
|
|
def __init__(self, *, name: str, species: str):
|
|
self.name = name
|
|
self.species = species
|
|
|
|
class PersonCls:
|
|
def __init__(self, *, name: str, age: float = None, pets: List[PetCls]):
|
|
self.name = name
|
|
self.age = age
|
|
self.pets = pets
|
|
|
|
class Pet(BaseModel):
|
|
name: str
|
|
species: str
|
|
|
|
class Config:
|
|
orm_mode = True
|
|
|
|
class Person(BaseModel):
|
|
name: str
|
|
age: float = None
|
|
pets: List[Pet]
|
|
|
|
class Config:
|
|
orm_mode = True
|
|
|
|
bones = PetCls(name='Bones', species='dog')
|
|
orion = PetCls(name='Orion', species='cat')
|
|
anna = PersonCls(name='Anna', age=20, pets=[bones, orion])
|
|
|
|
anna_model = Person.from_orm(anna)
|
|
|
|
assert anna_model.dict() == {
|
|
'name': 'Anna',
|
|
'pets': [{'name': 'Bones', 'species': 'dog'}, {'name': 'Orion', 'species': 'cat'}],
|
|
'age': 20.0,
|
|
}
|
|
|
|
|
|
def test_not_orm_mode():
|
|
class Pet(BaseModel):
|
|
name: str
|
|
species: str
|
|
|
|
with pytest.raises(PydanticUserError):
|
|
Pet.from_orm(None)
|
|
|
|
|
|
def test_object_with_getattr():
|
|
class FooGetAttr:
|
|
def __getattr__(self, key: str):
|
|
if key == 'foo':
|
|
return 'Foo'
|
|
else:
|
|
raise AttributeError
|
|
|
|
class Model(BaseModel):
|
|
foo: str
|
|
bar: int = 1
|
|
|
|
class Config:
|
|
orm_mode = True
|
|
|
|
class ModelInvalid(BaseModel):
|
|
foo: str
|
|
bar: int
|
|
|
|
class Config:
|
|
orm_mode = True
|
|
|
|
foo = FooGetAttr()
|
|
model = Model.from_orm(foo)
|
|
assert model.foo == 'Foo'
|
|
assert model.bar == 1
|
|
assert model.dict(exclude_unset=True) == {'foo': 'Foo'}
|
|
with pytest.raises(ValidationError):
|
|
ModelInvalid.from_orm(foo)
|
|
|
|
|
|
def test_properties():
|
|
class XyProperty:
|
|
x = 4
|
|
|
|
@property
|
|
def y(self):
|
|
return '5'
|
|
|
|
class Model(BaseModel):
|
|
x: int
|
|
y: int
|
|
|
|
class Config:
|
|
orm_mode = True
|
|
|
|
model = Model.from_orm(XyProperty())
|
|
assert model.x == 4
|
|
assert model.y == 5
|
|
|
|
|
|
def test_extra_allow():
|
|
class TestCls:
|
|
x = 1
|
|
y = 2
|
|
|
|
class Model(BaseModel):
|
|
x: int
|
|
|
|
class Config:
|
|
orm_mode = True
|
|
extra = 'allow'
|
|
|
|
model = Model.from_orm(TestCls())
|
|
assert model.dict() == {'x': 1}
|
|
|
|
|
|
def test_extra_forbid():
|
|
class TestCls:
|
|
x = 1
|
|
y = 2
|
|
|
|
class Model(BaseModel):
|
|
x: int
|
|
|
|
class Config:
|
|
orm_mode = True
|
|
extra = 'forbid'
|
|
|
|
model = Model.from_orm(TestCls())
|
|
assert model.dict() == {'x': 1}
|
|
|
|
|
|
def test_root_validator():
|
|
validator_value = None
|
|
|
|
class TestCls:
|
|
x = 1
|
|
y = 2
|
|
|
|
class Model(BaseModel):
|
|
x: int
|
|
y: int
|
|
z: int
|
|
|
|
@root_validator(pre=True)
|
|
def change_input_data(cls, value):
|
|
nonlocal validator_value
|
|
validator_value = value
|
|
return {**value, 'z': value['x'] + value['y']}
|
|
|
|
class Config:
|
|
orm_mode = True
|
|
|
|
model = Model.from_orm(TestCls())
|
|
assert model.dict() == {'x': 1, 'y': 2, 'z': 3}
|
|
# assert isinstance(validator_value, GetterDict)
|
|
assert validator_value == {'x': 1, 'y': 2}
|
|
|
|
|
|
def test_custom_getter_dict():
|
|
class TestCls:
|
|
x = 1
|
|
y = 2
|
|
|
|
def custom_getter_dict(obj):
|
|
assert isinstance(obj, TestCls)
|
|
return {'x': 42, 'y': 24}
|
|
|
|
class Model(BaseModel):
|
|
x: int
|
|
y: int
|
|
|
|
class Config:
|
|
orm_mode = True
|
|
getter_dict = custom_getter_dict
|
|
|
|
model = Model.from_orm(TestCls())
|
|
assert model.dict() == {'x': 42, 'y': 24}
|
|
|
|
|
|
def test_recursive_parsing():
|
|
class Getter: # GetterDict
|
|
# try to read the modified property name
|
|
# either as an attribute or as a key
|
|
def get(self, key, default):
|
|
key = key + key
|
|
try:
|
|
v = self._obj[key]
|
|
return Getter(v) if isinstance(v, dict) else v
|
|
except TypeError:
|
|
return getattr(self._obj, key, default)
|
|
except KeyError:
|
|
return default
|
|
|
|
class Model(BaseModel):
|
|
class Config:
|
|
orm_mode = True
|
|
getter_dict = Getter
|
|
|
|
class ModelA(Model):
|
|
a: int
|
|
|
|
class ModelB(Model):
|
|
b: ModelA
|
|
|
|
# test recursive parsing with object attributes
|
|
dct = dict(bb=SimpleNamespace(aa=1))
|
|
assert ModelB.from_orm(dct) == ModelB(b=ModelA(a=1))
|
|
|
|
# test recursive parsing with dict keys
|
|
obj = dict(bb=dict(aa=1))
|
|
assert ModelB.from_orm(obj) == ModelB(b=ModelA(a=1))
|
|
|
|
|
|
def test_nested_orm():
|
|
class User(BaseModel):
|
|
first_name: str
|
|
last_name: str
|
|
|
|
class Config:
|
|
orm_mode = True
|
|
|
|
class State(BaseModel):
|
|
user: User
|
|
|
|
class Config:
|
|
orm_mode = True
|
|
|
|
# Pass an "orm instance"
|
|
State.from_orm(SimpleNamespace(user=SimpleNamespace(first_name='John', last_name='Appleseed')))
|
|
|
|
# Pass dictionary data directly
|
|
State(**{'user': {'first_name': 'John', 'last_name': 'Appleseed'}})
|