-
-
Notifications
You must be signed in to change notification settings - Fork 1.7k
/
test_aliases.py
249 lines (190 loc) · 7.55 KB
/
test_aliases.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
from contextlib import nullcontext as does_not_raise
from typing import Any, ContextManager, List, Optional
import pytest
from dirty_equals import IsStr
from pydantic import BaseModel, ConfigDict, Extra, ValidationError
from pydantic.fields import Field
def test_alias_generator():
def to_camel(string: str):
return ''.join(x.capitalize() for x in string.split('_'))
class MyModel(BaseModel):
model_config = ConfigDict(alias_generator=to_camel)
a: List[str] = None
foo_bar: str
data = {'A': ['foo', 'bar'], 'FooBar': 'foobar'}
v = MyModel(**data)
assert v.a == ['foo', 'bar']
assert v.foo_bar == 'foobar'
assert v.model_dump(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):
model_config = ConfigDict(alias_generator=return_bytes)
bar: Any
assert str(e.value) == IsStr(regex="alias_generator <function .*> must return str, not <class 'bytes'>")
def test_basic_alias():
class Model(BaseModel):
a: str = Field('foobar', alias='_a')
assert Model().a == 'foobar'
assert Model(_a='different').a == 'different'
assert repr(Model.model_fields['a']) == (
"FieldInfo(annotation=str, required=False, default='foobar', alias='_a', alias_priority=2)"
)
def test_alias_error():
class Model(BaseModel):
a: int = Field(123, alias='_a')
assert Model(_a='123').a == 123
with pytest.raises(ValidationError) as exc_info:
Model(_a='foo')
assert exc_info.value.errors() == [
{
'input': 'foo',
'loc': ('a',),
'msg': 'Input should be a valid integer, unable to parse string as an integer',
'type': 'int_parsing',
}
]
def test_annotation_config():
class Model(BaseModel):
b: float = Field(alias='foobar')
a: int = 10
_c: str
assert list(Model.model_fields.keys()) == ['b', 'a']
assert [f.alias for f in Model.model_fields.values()] == ['foobar', None]
assert Model(foobar='123').b == 123.0
def test_pop_by_field_name():
class Model(BaseModel):
model_config = ConfigDict(extra=Extra.forbid, populate_by_name=True)
last_updated_by: Optional[str] = Field(None, alias='lastUpdatedBy')
assert Model(lastUpdatedBy='foo').model_dump() == {'last_updated_by': 'foo'}
assert Model(last_updated_by='foo').model_dump() == {'last_updated_by': 'foo'}
with pytest.raises(ValidationError) as exc_info:
Model(lastUpdatedBy='foo', last_updated_by='bar')
assert exc_info.value.errors() == [
{
'input': 'bar',
'loc': ('last_updated_by',),
'msg': 'Extra inputs are not permitted',
'type': 'extra_forbidden',
}
]
def test_alias_override_behavior():
class Parent(BaseModel):
# Use `gt` to demonstrate that using `Field` to override an alias does not preserve other attributes
x: int = Field(alias='x1', gt=0)
class Child(Parent):
x: int = Field(..., alias='x2')
y: int = Field(..., alias='y2')
assert Parent.model_fields['x'].alias == 'x1'
assert Child.model_fields['x'].alias == 'x2'
assert Child.model_fields['y'].alias == 'y2'
Parent(x1=1)
with pytest.raises(ValidationError) as exc_info:
Parent(x1=-1)
assert exc_info.value.errors() == [
{'ctx': {'gt': 0}, 'input': -1, 'loc': ('x',), 'msg': 'Input should be greater than 0', 'type': 'greater_than'}
]
Child(x2=1, y2=2)
# Check the gt=0 is not preserved from Parent
Child(x2=-1, y2=2)
# Check the alias from Parent cannot be used
with pytest.raises(ValidationError) as exc_info:
Child(x1=1, y2=2)
assert exc_info.value.errors() == [
{'input': {'x1': 1, 'y2': 2}, 'loc': ('x',), 'msg': 'Field required', 'type': 'missing'}
]
# Check the type hint from Parent _is_ preserved
with pytest.raises(ValidationError) as exc_info:
Child(x2='a', y2=2)
assert exc_info.value.errors() == [
{
'input': 'a',
'loc': ('x',),
'msg': 'Input should be a valid integer, unable to parse string as an ' 'integer',
'type': 'int_parsing',
}
]
def test_alias_generator_parent():
class Parent(BaseModel):
model_config = ConfigDict(populate_by_name=True, alias_generator=lambda f_name: f_name + '1')
x: int
class Child(Parent):
model_config = ConfigDict(alias_generator=lambda f_name: f_name + '2')
y: int
assert Child.model_fields['y'].alias == 'y2'
assert Child.model_fields['x'].alias == 'x2'
def test_alias_generator_on_parent():
class Parent(BaseModel):
model_config = ConfigDict(alias_generator=lambda x: x.upper())
x: bool = Field(..., alias='a_b_c')
y: str
class Child(Parent):
y: str
z: str
assert Parent.model_fields['x'].alias == 'a_b_c'
assert Parent.model_fields['y'].alias == 'Y'
assert Child.model_fields['x'].alias == 'a_b_c'
assert Child.model_fields['y'].alias == 'Y'
assert Child.model_fields['z'].alias == 'Z'
def test_alias_generator_on_child():
class Parent(BaseModel):
x: bool = Field(..., alias='abc')
y: str
class Child(Parent):
model_config = ConfigDict(alias_generator=lambda x: x.upper())
y: str
z: str
assert [f.alias for f in Parent.model_fields.values()] == ['abc', None]
assert [f.alias for f in Child.model_fields.values()] == ['abc', 'Y', 'Z']
def test_low_priority_alias():
# TODO:
# Alternative 1: we could drop alias_priority and tell people to manually override aliases in child classes
# Alternative 2: we could add a new argument `override_with_alias_generator=True` equivalent to `alias_priority=1`
class Parent(BaseModel):
w: bool = Field(..., alias='w_')
x: bool = Field(..., alias='abc', alias_priority=1)
y: str
class Child(Parent):
model_config = ConfigDict(alias_generator=lambda x: x.upper())
y: str
z: str
assert [f.alias for f in Parent.model_fields.values()] == ['w_', 'abc', None]
assert [f.alias for f in Child.model_fields.values()] == ['w_', 'X', 'Y', 'Z']
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.model_dump(by_alias=True) == data
@pytest.mark.parametrize(
'use_construct, populate_by_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_populate_by_name_config(
use_construct: bool,
populate_by_name_config: bool,
arg_name: str,
expectation: ContextManager,
):
expected_value: int = 7
class Foo(BaseModel):
model_config = ConfigDict(populate_by_name=populate_by_name_config)
bar_: int = Field(..., alias='bar')
with expectation:
if use_construct:
f = Foo.model_construct(**{arg_name: expected_value})
else:
f = Foo(**{arg_name: expected_value})
assert f.bar_ == expected_value