Skip to content

Commit

Permalink
fix: keep the order of the fields when validate_assignment is set (#…
Browse files Browse the repository at this point in the history
…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
  • Loading branch information
PrettyWood committed Oct 31, 2020
1 parent b3f7b28 commit 4680940
Show file tree
Hide file tree
Showing 3 changed files with 29 additions and 12 deletions.
1 change: 1 addition & 0 deletions changes/2073-PrettyWood.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
fix: keep the order of the fields when `validate_assignment` is set
16 changes: 8 additions & 8 deletions pydantic/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -384,14 +384,14 @@ def __setattr__(self, name, value): # noqa: C901 (ignore complexity)

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

Expand Down
24 changes: 20 additions & 4 deletions tests/test_validators.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

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

Expand Down Expand Up @@ -1162,20 +1162,36 @@ 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

@validator('foo')
def validate_foo(cls, v, values):
check_values({**values})
return v

@validator('bar')
def validate_bar(cls, v, values):
check_values({**values})
return v

model = Model(foo='foo_value', bar='bar_value')
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():
Expand Down

0 comments on commit 4680940

Please sign in to comment.