Repository: pydantic/pydantic
Pull request: fix: stop calling parent class `root_validator` if overridden
<!-- 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
We would keep all the root validators even those that were supposed to be overridden.
Now they actually are

<!-- Please give a short summary of the changes. -->

## Related issue number
fix #1895
<!-- Are there any issues opened that will be resolved by merging this change? -->

## Checklist

* [x] Unit tests for the changes exist
* [x] Tests pass on CI and coverage remains at 100%
* [x] Documentation reflects the changes where applicable
* [x] `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:
Root validator called in superclass even if overridden in a subclass without calling super
# Bug

Output of `python -c "import pydantic.utils; print(pydantic.utils.version_info())"`:
```
             pydantic version: 1.6.1
            pydantic compiled: True
                 install path: /Users/jakub/.virtualenvs/server/lib/python3.8/site-packages/pydantic
               python version: 3.8.1 (default, Mar 13 2020, 20:31:03)  [Clang 11.0.0 (clang-1100.0.33.17)]
                     platform: macOS-10.15.5-x86_64-i386-64bit
     optional deps. installed: ['typing-extensions']
```


Methods decorated with `root_validator` that are defined in a subclass do not override the method defined in the superclass. For methods decorated with `validator` they do override the method defined in the superclass properly.

Context:
I want to have a model that declares a bunch of fields and some default validators for these fields, which should be applied in every subclass, unless overridden. That works for fields decorated with `validator`, but for fields decorated with `root_validator` the validator in superclass gets called first.

I couldn't find anything in the docs to disable calling the superclass validator when using the `root_validator` decorator. Also, I looked into the generic models (it seems that it would fit composition better than inheritance) and reusing validators (doesn't seem to be a good candidate to validate field contents because in my case the validation depends on the `exercise_type` and I would still have to define something like `_normalized_choices` in every subclass).

```py
from __future__ import annotations

from pydantic import BaseModel, root_validator, validator
from typing import Union, List, Optional


class ExerciseBase(BaseModel):

    text: Optional[str] = None
    choices: Optional[List[List[str]]] = None

    @root_validator
    def validate_choices(cls, values):
        print("ExerciseBase validate_choices")
        choices = values.get('choices')
        assert choices is None, 'Choices not allowed for this exercise type'
        return values


class FillBlanksExercise(ExerciseBase):
    @root_validator
    def validate_choices(cls, values):
        print("FillBlanksExercise validate_choices")
        choices = values.get('choices')
        if choices is not None:
            text = values.get('text')
            number_of_blanks = text.count('____')
            assert {len(choice) for choice in choices} == {number_of_blanks}, 'Each choice must match number of blanks'
        return values

EXERCISE_TYPES_MAPPING = {
    'FillBlanksExercise': FillBlanksExercise,
    'ChoiceExercise': ChoiceExercise,
}

exercise_data = {
    'text': 'Python is a type of ____?',
    'exercise_type': 'FillBlanksExercise',
    'choices': [['snake'], ['bird'], ['fish'], ['dinosaur']],
}

exercise = FillBlanksExercise(**exercise_data)


# ExerciseBase validate_choices
# FillBlanksExercise validate_choices
# Traceback (most recent call last):
#   File "/Users/jakub/Library/Application Support/JetBrains/PyCharm2020.2/scratches/scratch_2.py", line 85, in <module>
#     exercise = ExerciseBase.build(**exercise_data)
#   File "/Users/jakub/Library/Application Support/JetBrains/PyCharm2020.2/scratches/scratch_2.py", line 22, in build
#     return _class(**kwargs)
#   File "pydantic/main.py", line 346, in pydantic.main.BaseModel.__init__
# pydantic.error_wrappers.ValidationError: 1 validation error for FillBlanksExercise
# __root__
#   Choices not allowed for this exercise type (type=assertion_error)
```

Hints from issue discussion:
I recently ran into this as well, and it certainly was unexpected to me. I ended up writing a small patch to the metaclass which removes overridden validators post-hoc. I post it below in case it is useful to others.

```python
from pydantic import BaseModel
from pydantic.main import ModelMetaclass

def remove_overridden_validators(model: BaseModel) -> BaseModel:
    """
    Currently a Pydantic bug prevents subclasses from overriding root validators.
    (see https://github.com/samuelcolvin/pydantic/issues/1895)
    This function inspects a Pydantic model and removes overriden
    root validators based of their `__name__`.
    Assumes that the latest entries in `__pre_root_validators__` and
    `__post_root_validators__` are earliest in the MRO, which seems to be
    the case.
    """
    model.__pre_root_validators__ = list(
        {validator.__name__: validator
         for validator in model.__pre_root_validators__
         }.values())
    model.__post_root_validators__ = list(
        {validator.__name__: (skip_on_failure, validator)
         for skip_on_failure, validator in model.__post_root_validators__
         }.values())
    return model

class PatchedModelMetaclass(ModelMetaclass):
    def __new__(*args, **kwargs):
        model = ModelMetaclass.__new__(*args, **kwargs)
        return remove_overridden_validators(model)
```
The following bit of code tests that it works as expected:
```python
from pydantic import BaseModel, root_validator

class A(BaseModel):
# class A(BaseModel, metaclass=PatchedModelMetaclass):
    a: int
    @root_validator(pre=True)
    def pre_root(cls, values):
        print("pre rootA")
        return values
    @root_validator(pre=False)
    def post_root(cls, values):
        print("post rootA")
        return values

class B(A):
    @root_validator(pre=True)
    def pre_root(cls, values):
        print("pre rootB")
        return values
    @root_validator(pre=False)
    def post_root(cls, values):
        print("post rootB")
        return values

# This prints only from the validators in B if PatchedModelMetaclass is used
B(a=1)
```

Commit message: fix: stop calling parent class `root_validator` if overridden

Patch to review:
diff --git a/changes/1895-PrettyWood.md b/changes/1895-PrettyWood.md
new file mode 100644
index 00000000000..bd806479486
--- /dev/null
+++ b/changes/1895-PrettyWood.md
@@ -0,0 +1 @@
+stop calling parent class `root_validator` if overridden
diff --git a/pydantic/main.py b/pydantic/main.py
index f6aca41048f..9fe65088f88 100644
--- a/pydantic/main.py
+++ b/pydantic/main.py
@@ -346,8 +346,14 @@ def is_untouched(v: Any) -> bool:
             '__config__': config,
             '__fields__': fields,
             '__validators__': vg.validators,
-            '__pre_root_validators__': unique_list(pre_root_validators + pre_rv_new),
-            '__post_root_validators__': unique_list(post_root_validators + post_rv_new),
+            '__pre_root_validators__': unique_list(
+                pre_root_validators + pre_rv_new,
+                get_name=lambda v: v.__name__,
+            ),
+            '__post_root_validators__': unique_list(
+                post_root_validators + post_rv_new,
+                get_name=lambda skip_on_failure_and_v: skip_on_failure_and_v[1].__name__,
+            ),
             '__schema_cache__': {},
             '__json_encoder__': staticmethod(json_encoder),
             '__custom_root_type__': _custom_root_type,
diff --git a/pydantic/utils.py b/pydantic/utils.py
index 8a8351c6e5f..8acc5156d23 100644
--- a/pydantic/utils.py
+++ b/pydantic/utils.py
@@ -277,16 +277,25 @@ def to_camel(string: str) -> str:
 T = TypeVar('T')
 
 
-def unique_list(input_list: Union[List[T], Tuple[T, ...]]) -> List[T]:
+def unique_list(
+    input_list: Union[List[T], Tuple[T, ...]],
+    *,
+    get_name: Callable[..., str] = lambda x: str(x),
+) -> List[T]:
     """
     Make a list unique while maintaining order.
+    We update the list if another one with the same name is set
+    (e.g. root validator overridden in subclass)
     """
-    result = []
-    unique_set = set()
+    result: List[T] = []
+    result_names: List[str] = []
     for v in input_list:
-        if v not in unique_set:
-            unique_set.add(v)
+        v_name = get_name(v)
+        if v_name not in result_names:
+            result_names.append(v_name)
             result.append(v)
+        else:
+            result[result_names.index(v_name)] = v
 
     return result
 
diff --git a/tests/test_validators.py b/tests/test_validators.py
index c863eda6c4e..6c1c55f0aba 100644
--- a/tests/test_validators.py
+++ b/tests/test_validators.py
@@ -1256,3 +1256,39 @@ def validate_foo(cls, v):
     with pytest.raises(RuntimeError, match='test error'):
         model.foo = 'raise_exception'
     assert model.foo == 'foo'
+
+
+def test_overridden_root_validators(mocker):
+    validate_stub = mocker.stub(name='validate')
+
+    class A(BaseModel):
+        x: str
+
+        @root_validator(pre=True)
+        def pre_root(cls, values):
+            validate_stub('A', 'pre')
+            return values
+
+        @root_validator(pre=False)
+        def post_root(cls, values):
+            validate_stub('A', 'post')
+            return values
+
+    class B(A):
+        @root_validator(pre=True)
+        def pre_root(cls, values):
+            validate_stub('B', 'pre')
+            return values
+
+        @root_validator(pre=False)
+        def post_root(cls, values):
+            validate_stub('B', 'post')
+            return values
+
+    A(x='pika')
+    assert validate_stub.call_args_list == [mocker.call('A', 'pre'), mocker.call('A', 'post')]
+
+    validate_stub.reset_mock()
+
+    B(x='pika')
+    assert validate_stub.call_args_list == [mocker.call('B', 'pre'), mocker.call('B', 'post')]


Code review comments:
diff_hunk: @@ -277,16 +277,25 @@ def to_camel(string: str) -> str:
 T = TypeVar('T')
 
 
-def unique_list(input_list: Union[List[T], Tuple[T, ...]]) -> List[T]:
+def unique_list(
+    input_list: Union[List[T], Tuple[T, ...]],
+    *,
+    get_name: Callable[..., str] = lambda x: str(x),
path: pydantic/utils.py
text: @user1:
```suggestion
    name_factory: Callable[[T], str] = str,
```
?

@author:
No need to be sorry for the change. Way better name, default and typing 😄