Files
pydantic/tests/test_orm_mode.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

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'}})