Repository: pydantic/pydantic

Issue:
validate_assignment does not work with root_validator
Output of `python -c "import pydantic.utils; print(pydantic.utils.version_info())"`:
```
             pydantic version: 1.5.1
            pydantic compiled: True
                 install path: /Users/shane/repos/domain-model-specification/Python/.venv/lib/python3.7/site-packages/pydantic
               python version: 3.7.7 (default, Jul 18 2020, 22:28:30)  [Clang 11.0.3 (clang-1103.0.32.62)]
                     platform: Darwin-19.6.0-x86_64-i386-64bit
     optional deps. installed: ['typing-extensions']
```

# Minimal Example
I'd like to enforce a condition on a whole model during arbitrary changes in its state:
```py
from pydantic import BaseModel, root_validator

class Example(BaseModel):
    current_value: float
    max_value: float

    class Config:
        validate_assignment = True

    @root_validator
    def current_lessequal_max(cls, values):
        current_value = values.get("current_value")
        max_value = values.get("max_value")
        assert abs(current_value) <= max_value, f"current_value cannot be greater than max_value"
        return values
```

Based on my reading of the docs I'd expect this to fail validation if I set `current_value` above `max_value`, or set `max_value` below `current_value`, on an existing instance. Is that not the intended behavior?

Thanks for an excellent library!


Discussion:

Resolving patch:
diff --git a/pydantic/main.py b/pydantic/main.py
--- a/pydantic/main.py
+++ b/pydantic/main.py
@@ -365,11 +365,32 @@ def __setattr__(self, name, value):
         elif not self.__config__.allow_mutation:
             raise TypeError(f'"{self.__class__.__name__}" is immutable and does not support item assignment')
         elif self.__config__.validate_assignment:
+            new_values = {**self.__dict__, name: value}
+
+            for validator in self.__pre_root_validators__:
+                try:
+                    new_values = validator(self.__class__, new_values)
+                except (ValueError, TypeError, AssertionError) as exc:
+                    raise ValidationError([ErrorWrapper(exc, loc=ROOT_KEY)], self.__class__)
+
             known_field = self.__fields__.get(name, None)
             if known_field:
                 value, error_ = known_field.validate(value, self.dict(exclude={name}), loc=name, cls=self.__class__)
                 if error_:
                     raise ValidationError([error_], self.__class__)
+                new_values[name] = value
+
+            errors = []
+            for skip_on_failure, validator in self.__post_root_validators__:
+                if skip_on_failure and errors:
+                    continue
+                try:
+                    new_values = validator(self.__class__, new_values)
+                except (ValueError, TypeError, AssertionError) as exc:
+                    errors.append(ErrorWrapper(exc, loc=ROOT_KEY))
+            if errors:
+                raise ValidationError(errors, self.__class__)
+
         self.__dict__[name] = value
         self.__fields_set__.add(name)
 


Test patch:
diff --git a/tests/test_main.py b/tests/test_main.py
--- a/tests/test_main.py
+++ b/tests/test_main.py
@@ -15,6 +15,7 @@
     Required,
     ValidationError,
     constr,
+    root_validator,
     validator,
 )
 
@@ -594,6 +595,76 @@ def test_validating_assignment_fail():
     ]
 
 
+def test_validating_assignment_pre_root_validator_fail():
+    class Model(BaseModel):
+        current_value: float = Field(..., alias='current')
+        max_value: float
+
+        class Config:
+            validate_assignment = True
+
+        @root_validator(pre=True)
+        def values_are_not_string(cls, values):
+            if any(isinstance(x, str) for x in values.values()):
+                raise ValueError('values cannot be a string')
+            return values
+
+    m = Model(current=100, max_value=200)
+    with pytest.raises(ValidationError) as exc_info:
+        m.current_value = '100'
+    assert exc_info.value.errors() == [
+        {
+            'loc': ('__root__',),
+            'msg': 'values cannot be a string',
+            'type': 'value_error',
+        }
+    ]
+
+
+def test_validating_assignment_post_root_validator_fail():
+    class Model(BaseModel):
+        current_value: float = Field(..., alias='current')
+        max_value: float
+
+        class Config:
+            validate_assignment = True
+
+        @root_validator
+        def current_lessequal_max(cls, values):
+            current_value = values.get('current_value')
+            max_value = values.get('max_value')
+            if current_value > max_value:
+                raise ValueError('current_value cannot be greater than max_value')
+            return values
+
+        @root_validator(skip_on_failure=True)
+        def current_lessequal_300(cls, values):
+            current_value = values.get('current_value')
+            if current_value > 300:
+                raise ValueError('current_value cannot be greater than 300')
+            return values
+
+        @root_validator
+        def current_lessequal_500(cls, values):
+            current_value = values.get('current_value')
+            if current_value > 500:
+                raise ValueError('current_value cannot be greater than 500')
+            return values
+
+    m = Model(current=100, max_value=200)
+    m.current_value = '100'
+    with pytest.raises(ValidationError) as exc_info:
+        m.current_value = 1000
+    assert exc_info.value.errors() == [
+        {'loc': ('__root__',), 'msg': 'current_value cannot be greater than max_value', 'type': 'value_error'},
+        {
+            'loc': ('__root__',),
+            'msg': 'current_value cannot be greater than 500',
+            'type': 'value_error',
+        },
+    ]
+
+
 def test_enum_values():
     FooEnum = Enum('FooEnum', {'foo': 'foo', 'bar': 'bar'})