Repository: pydantic/pydantic
Pull request: fix: update all modified field values in `root_validator` when `validate_assignment` is on
<!-- Thank you for your contribution! -->
<!-- Unless your change is trivial, please create an issue to discuss the change before creating a PR -->
<!-- See https://pydantic-docs.helpmanual.io/contributing/ for help on Contributing -->

## Change Summary
Following #1971 we now run `root_validator` on assignment when `Config.validate_assignment` is set.
But we still only updated the assigned field and not all concerned fields

ℹ️ Not really a regression but more a fix to #1972 in v1.7.0

## Related issue number
closes #2116
<!-- Are there any issues opened that will be resolved by merging this change? -->

## Checklist

* [ ] Unit tests for the changes exist
* [ ] Tests pass on CI and coverage remains at 100%
* [ ] Documentation reflects the changes where applicable
* [ ] `changes/<pull request or issue id>-<github username>.md` file added describing change
  (see [changes/README.md](https://github.com/samuelcolvin/pydantic/blob/master/changes/README.md) for details)


Problem statement:
validate_assignment with side effect validators 
# Bug

Output of `python -c "import pydantic.utils; print(pydantic.utils.version_info())"`:
```
             pydantic version: 1.7.2
            pydantic compiled: True
                 install path: /home/arthur/Documents/odmantic/.venv/lib/python3.8/site-packages/pydantic
               python version: 3.8.1 (default, Jan  5 2020, 23:12:00)  [GCC 7.3.0]
                     platform: Linux-4.17.14-041714-generic-x86_64-with-glibc2.27
     optional deps. installed: ['typing-extensions']
```

```py
from pydantic import root_validator, BaseModel

class Rectangle(BaseModel):
    width: float
    height: float
    area: float = None

    class Config:
        validate_assignment = True

    @root_validator()
    def set_ts_now(cls, v):
        v["area"] = v["width"] * v["height"]
        return v

r = Rectangle(width=1, height=1)
assert r.area == 1
r.height = 5
assert r.area == 5 # AssertionError, r.area has not been modified
```
I don't know if it should be allowed to use validators this way or if it's a real bug.
I'd be happy to try to implement a fix :smile:

Hints from issue discussion:
Hi @art049 
Yes you're right. Since #1971 has been solved, this is the expected behaviour but it seems we update only the current field and not all. I can make a quick fix if you want should take couple of minutes

Commit message: fix: update all modified field values in `root_validator` when `validate_assignment` is on

closes #2116

Patch to review:
diff --git a/changes/2116-PrettyWood.md b/changes/2116-PrettyWood.md
new file mode 100644
index 00000000000..d2c81cbf7f4
--- /dev/null
+++ b/changes/2116-PrettyWood.md
@@ -0,0 +1 @@
+fix: update all modified field values in `root_validator` when `validate_assignment` is on
\ No newline at end of file
diff --git a/pydantic/main.py b/pydantic/main.py
index 3d57b1bbc09..209dc9a8cca 100644
--- a/pydantic/main.py
+++ b/pydantic/main.py
@@ -406,7 +406,12 @@ def __setattr__(self, name, value):  # noqa: C901 (ignore complexity)
             if errors:
                 raise ValidationError(errors, self.__class__)
 
-        self.__dict__[name] = value
+            # update the whole __dict__ as other values than just `value`
+            # may be changed (e.g. with `root_validator`)
+            object_setattr(self, '__dict__', new_values)
+        else:
+            self.__dict__[name] = value
+
         self.__fields_set__.add(name)
 
     def __getstate__(self) -> 'DictAny':
diff --git a/tests/test_main.py b/tests/test_main.py
index f7ad7ee0590..3472904d0da 100644
--- a/tests/test_main.py
+++ b/tests/test_main.py
@@ -666,6 +666,28 @@ def current_lessequal_500(cls, values):
     ]
 
 
+def test_root_validator_many_values_change():
+    """It should run root_validator on assignment and update ALL concerned fields"""
+
+    class Rectangle(BaseModel):
+        width: float
+        height: float
+        area: float = None
+
+        class Config:
+            validate_assignment = True
+
+        @root_validator
+        def set_ts_now(cls, values):
+            values['area'] = values['width'] * values['height']
+            return values
+
+    r = Rectangle(width=1, height=1)
+    assert r.area == 1
+    r.height = 5
+    assert r.area == 5
+
+
 def test_enum_values():
     FooEnum = Enum('FooEnum', {'foo': 'foo', 'bar': 'bar'})
 


Code review comments:
diff_hunk: @@ -666,6 +666,28 @@ def current_lessequal_500(cls, values):
     ]
 
 
+def test_root_validator_many_values_change():
+    """It should run root_validator on assignment and update ALL concerned fields"""
+
+    class Rectangle(BaseModel):
+        width: float
+        height: float
+        area: float = None
+
+        class Config:
+            validate_assignment = True
+
+        @root_validator
+        def set_ts_now(cls, values):
path: tests/test_main.py
text: @user1:
Wrong validator function name from the issue snippet, my bad
```suggestion
        def set_area(cls, values):
```