fix: keep the order of the fields when validate_assignment is set (#2075)

Following #2000 and #2046 we can't `pop` the current value when assigning
a new one (which was probably the most efficient) as we want to keep the
order in the `__dict__`.
So let's do a shallow copy of the `__dict__` without the current value

fix #2073
This commit is contained in:
Eric Jolibois
2020-10-31 19:47:22 +01:00
committed by GitHub
parent b3f7b28f13
commit 4680940146
3 changed files with 29 additions and 12 deletions
+1
View File
@@ -0,0 +1 @@
fix: keep the order of the fields when `validate_assignment` is set
+8 -8
View File
@@ -384,14 +384,14 @@ class BaseModel(Representation, metaclass=ModelMetaclass):
known_field = self.__fields__.get(name, None)
if known_field:
original_value = self.__dict__.pop(name)
try:
value, error_ = known_field.validate(value, self.__dict__, loc=name, cls=self.__class__)
if error_:
raise ValidationError([error_], self.__class__)
except Exception:
self.__dict__[name] = original_value
raise
# We want to
# - make sure validators are called without the current value for this field inside `values`
# - keep other values (e.g. submodels) untouched (using `BaseModel.dict()` will change them into dicts)
# - keep the order of the fields
dict_without_original_value = {k: v for k, v in self.__dict__.items() if k != name}
value, error_ = known_field.validate(value, dict_without_original_value, loc=name, cls=self.__class__)
if error_:
raise ValidationError([error_], self.__class__)
else:
new_values[name] = value
+20 -4
View File
@@ -5,7 +5,7 @@ from typing import Dict, List, Optional, Tuple
import pytest
from pydantic import BaseModel, ConfigError, Extra, ValidationError, errors, validator
from pydantic import BaseModel, ConfigError, Extra, Field, ValidationError, errors, validator
from pydantic.class_validators import make_generic_validator, root_validator
from pydantic.typing import Literal
@@ -1162,7 +1162,8 @@ def test_field_that_is_being_validated_is_excluded_from_validator_values(mocker)
class Model(BaseModel):
foo: str
bar: str
bar: str = Field(alias='pika')
baz: str
class Config:
validate_assignment = True
@@ -1170,12 +1171,27 @@ def test_field_that_is_being_validated_is_excluded_from_validator_values(mocker)
@validator('foo')
def validate_foo(cls, v, values):
check_values({**values})
return v
model = Model(foo='foo_value', bar='bar_value')
@validator('bar')
def validate_bar(cls, v, values):
check_values({**values})
return v
model = Model(foo='foo_value', pika='bar_value', baz='baz_value')
check_values.reset_mock()
assert list(dict(model).items()) == [('foo', 'foo_value'), ('bar', 'bar_value'), ('baz', 'baz_value')]
model.foo = 'new_foo_value'
check_values.assert_called_once_with({'bar': 'bar_value'})
check_values.assert_called_once_with({'bar': 'bar_value', 'baz': 'baz_value'})
check_values.reset_mock()
model.bar = 'new_bar_value'
check_values.assert_called_once_with({'foo': 'new_foo_value', 'baz': 'baz_value'})
# ensure field order is the same
assert list(dict(model).items()) == [('foo', 'new_foo_value'), ('bar', 'new_bar_value'), ('baz', 'baz_value')]
def test_exceptions_in_field_validators_restore_original_field_value():