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

387 lines
11 KiB
Python

import re
from contextlib import nullcontext as does_not_raise
from typing import Any, ContextManager, List, Optional
import pytest
from pydantic import BaseConfig, BaseModel, Extra, ValidationError
from pydantic.fields import Field
pytestmark = pytest.mark.xfail(reason='working on V2', strict=False)
def test_alias_generator():
def to_camel(string: str):
return ''.join(x.capitalize() for x in string.split('_'))
class MyModel(BaseModel):
a: List[str] = None
foo_bar: str
class Config:
alias_generator = to_camel
data = {'A': ['foo', 'bar'], 'FooBar': 'foobar'}
v = MyModel(**data)
assert v.a == ['foo', 'bar']
assert v.foo_bar == 'foobar'
assert v.dict(by_alias=True) == data
def test_alias_generator_with_field_schema():
def to_upper_case(string: str):
return string.upper()
class MyModel(BaseModel):
my_shiny_field: Any # Alias from Config.fields will be used
foo_bar: str # Alias from Config.fields will be used
baz_bar: str # Alias will be generated
another_field: str # Alias will be generated
class Config:
alias_generator = to_upper_case
fields = {'my_shiny_field': 'MY_FIELD', 'foo_bar': {'alias': 'FOO'}, 'another_field': {'not_alias': 'a'}}
data = {'MY_FIELD': ['a'], 'FOO': 'bar', 'BAZ_BAR': 'ok', 'ANOTHER_FIELD': '...'}
m = MyModel(**data)
assert m.dict(by_alias=True) == data
def test_alias_generator_wrong_type_error():
def return_bytes(string):
return b'not a string'
with pytest.raises(TypeError) as e:
class MyModel(BaseModel):
bar: Any
class Config:
alias_generator = return_bytes
assert str(e.value) == "Config.alias_generator must return str, not <class 'bytes'>"
def test_infer_alias():
class Model(BaseModel):
a = 'foobar'
class Config:
fields = {'a': '_a'}
assert Model(_a='different').a == 'different'
assert repr(Model.__fields__['a']) == (
"ModelField(name='a', type=str, required=False, default='foobar', alias='_a')"
)
def test_alias_error():
class Model(BaseModel):
a = 123
class Config:
fields = {'a': '_a'}
assert Model(_a='123').a == 123
with pytest.raises(ValidationError) as exc_info:
Model(_a='foo')
assert exc_info.value.errors() == [
{'loc': ('_a',), 'msg': 'value is not a valid integer', 'type': 'type_error.integer'}
]
def test_annotation_config():
class Model(BaseModel):
b: float
a: int = 10
_c: str
class Config:
fields = {'b': 'foobar'}
assert list(Model.__fields__.keys()) == ['b', 'a']
assert [f.alias for f in Model.__fields__.values()] == ['foobar', 'a']
assert Model(foobar='123').b == 123.0
def test_alias_camel_case():
class Model(BaseModel):
one_thing: int
another_thing: int
class Config(BaseConfig):
@classmethod
def get_field_info(cls, name):
field_config = super().get_field_info(name) or {}
if 'alias' not in field_config:
field_config['alias'] = re.sub(r'(?:^|_)([a-z])', lambda m: m.group(1).upper(), name)
return field_config
v = Model(**{'OneThing': 123, 'AnotherThing': '321'})
assert v.one_thing == 123
assert v.another_thing == 321
assert v == {'one_thing': 123, 'another_thing': 321}
def test_get_field_info_inherit():
class ModelOne(BaseModel):
class Config(BaseConfig):
@classmethod
def get_field_info(cls, name):
field_config = super().get_field_info(name) or {}
if 'alias' not in field_config:
field_config['alias'] = re.sub(r'_([a-z])', lambda m: m.group(1).upper(), name)
return field_config
class ModelTwo(ModelOne):
one_thing: int
another_thing: int
third_thing: int
class Config:
fields = {'third_thing': 'Banana'}
v = ModelTwo(**{'oneThing': 123, 'anotherThing': '321', 'Banana': 1})
assert v == {'one_thing': 123, 'another_thing': 321, 'third_thing': 1}
def test_pop_by_field_name():
class Model(BaseModel):
last_updated_by: Optional[str] = None
class Config:
extra = Extra.forbid
allow_population_by_field_name = True
fields = {'last_updated_by': 'lastUpdatedBy'}
assert Model(lastUpdatedBy='foo').dict() == {'last_updated_by': 'foo'}
assert Model(last_updated_by='foo').dict() == {'last_updated_by': 'foo'}
with pytest.raises(ValidationError) as exc_info:
Model(lastUpdatedBy='foo', last_updated_by='bar')
assert exc_info.value.errors() == [
{'loc': ('last_updated_by',), 'msg': 'extra fields not permitted', 'type': 'value_error.extra'}
]
def test_alias_child_precedence():
class Parent(BaseModel):
x: int
class Config:
fields = {'x': 'x1'}
class Child(Parent):
y: int
class Config:
fields = {'y': 'y2', 'x': 'x2'}
assert Child.__fields__['y'].alias == 'y2'
assert Child.__fields__['x'].alias == 'x2'
def test_alias_generator_parent():
class Parent(BaseModel):
x: int
class Config:
allow_population_by_field_name = True
@classmethod
def alias_generator(cls, f_name):
return f_name + '1'
class Child(Parent):
y: int
class Config:
@classmethod
def alias_generator(cls, f_name):
return f_name + '2'
assert Child.__fields__['y'].alias == 'y2'
assert Child.__fields__['x'].alias == 'x2'
def test_alias_generator_on_parent():
class Parent(BaseModel):
x: bool = Field(..., alias='a_b_c')
y: str
class Config:
@staticmethod
def alias_generator(x):
return x.upper()
class Child(Parent):
y: str
z: str
assert Parent.__fields__['x'].alias == 'a_b_c'
assert Parent.__fields__['y'].alias == 'Y'
assert Child.__fields__['x'].alias == 'a_b_c'
assert Child.__fields__['y'].alias == 'Y'
assert Child.__fields__['z'].alias == 'Z'
def test_alias_generator_on_child():
class Parent(BaseModel):
x: bool = Field(..., alias='abc')
y: str
class Child(Parent):
y: str
z: str
class Config:
@staticmethod
def alias_generator(x):
return x.upper()
assert [f.alias for f in Parent.__fields__.values()] == ['abc', 'y']
assert [f.alias for f in Child.__fields__.values()] == ['abc', 'Y', 'Z']
def test_low_priority_alias():
class Parent(BaseModel):
x: bool = Field(..., alias='abc', alias_priority=1)
y: str
class Child(Parent):
y: str
z: str
class Config:
@staticmethod
def alias_generator(x):
return x.upper()
assert [f.alias for f in Parent.__fields__.values()] == ['abc', 'y']
assert [f.alias for f in Child.__fields__.values()] == ['X', 'Y', 'Z']
def test_low_priority_alias_config():
class Parent(BaseModel):
x: bool
y: str
class Config:
fields = {'x': dict(alias='abc', alias_priority=1)}
class Child(Parent):
y: str
z: str
class Config:
@staticmethod
def alias_generator(x):
return x.upper()
assert [f.alias for f in Parent.__fields__.values()] == ['abc', 'y']
assert [f.alias for f in Child.__fields__.values()] == ['X', 'Y', 'Z']
def test_field_vs_config():
class Model(BaseModel):
x: str = Field(..., alias='x_on_field')
y: str
z: str
class Config:
fields = {'x': dict(alias='x_on_config'), 'y': dict(alias='y_on_config')}
assert [f.alias for f in Model.__fields__.values()] == ['x_on_field', 'y_on_config', 'z']
def test_alias_priority():
class Parent(BaseModel):
a: str = Field(..., alias='a_field_parent')
b: str = Field(..., alias='b_field_parent')
c: str = Field(..., alias='c_field_parent')
d: str
e: str
class Config:
fields = {
'a': dict(alias='a_config_parent'),
'c': dict(alias='c_config_parent'),
'd': dict(alias='d_config_parent'),
}
@staticmethod
def alias_generator(x):
return f'{x}_generator_parent'
class Child(Parent):
a: str = Field(..., alias='a_field_child')
class Config:
fields = {'a': dict(alias='a_config_child'), 'b': dict(alias='b_config_child')}
@staticmethod
def alias_generator(x):
return f'{x}_generator_child'
# debug([f.alias for f in Parent.__fields__.values()], [f.alias for f in Child.__fields__.values()])
assert [f.alias for f in Parent.__fields__.values()] == [
'a_field_parent',
'b_field_parent',
'c_field_parent',
'd_config_parent',
'e_generator_parent',
]
assert [f.alias for f in Child.__fields__.values()] == [
'a_field_child',
'b_config_child',
'c_field_parent',
'd_config_parent',
'e_generator_child',
]
def test_empty_string_alias():
class Model(BaseModel):
empty_string_key: int = Field(alias='')
data = {'': 123}
m = Model(**data)
assert m.empty_string_key == 123
assert m.dict(by_alias=True) == data
@pytest.mark.parametrize(
'use_construct, allow_population_by_field_name_config, arg_name, expectation',
[
[False, True, 'bar', does_not_raise()],
[False, True, 'bar_', does_not_raise()],
[False, False, 'bar', does_not_raise()],
[False, False, 'bar_', pytest.raises(ValueError)],
[True, True, 'bar', does_not_raise()],
[True, True, 'bar_', does_not_raise()],
[True, False, 'bar', does_not_raise()],
[True, False, 'bar_', does_not_raise()],
],
)
def test_allow_population_by_field_name_config(
use_construct: bool,
allow_population_by_field_name_config: bool,
arg_name: str,
expectation: ContextManager,
):
expected_value: int = 7
class Foo(BaseModel):
bar_: int = Field(..., alias='bar')
class Config(BaseConfig):
allow_population_by_field_name = allow_population_by_field_name_config
with expectation:
if use_construct:
f = Foo.construct(**{arg_name: expected_value})
else:
f = Foo(**{arg_name: expected_value})
assert f.bar_ == expected_value