Repository: pydantic/pydantic

Issue:
Re-use validators?
Is it possible to create a validator and import it everywhere else it's used?

I have validators for certain fields that are present in multiple classes that I'd like to call simply without defining them.


Discussion:
I'm on my mobile, but should be as simple as doing something like:

```py
class MyModel(BaseModel):
    check_foo = validator('foo')(reused_validator)
```

Have a try and let me know if that works or doesn't work or I have the syntax slightly wrong.
@samuelcolvin I don't think that works due to this check:

```python
        if not in_ipython():  # pragma: no branch
            ref = f.__module__ + '.' + f.__qualname__
            if ref in _FUNCS:
                raise ConfigError(f'duplicate validator function "{ref}"')
            _FUNCS.add(ref)
```

I think we can probably fix this though.
Here's a self contained demonstration that it doesn't currently work.
```python
from pydantic import BaseModel, validator


def double_validator(cls, value):
    return value * 2


class A(BaseModel):
    x: int

    double = validator('x')(double_validator)


class B(BaseModel):
    x: int

    double = validator('x')(double_validator)

# pydantic.errors.ConfigError: duplicate validator function "__main__.double_validator"
```
Humm, possibly. We could either drop that check or make it optional.

Or you could setup a validator which just calls the reused validator.

Maybe `allow_name_reuse` as another argument on a validator?

Resolving patch:
diff --git a/pydantic/class_validators.py b/pydantic/class_validators.py
--- a/pydantic/class_validators.py
+++ b/pydantic/class_validators.py
@@ -51,6 +51,7 @@ def validator(
     always: bool = False,
     check_fields: bool = True,
     whole: bool = None,
+    allow_reuse: bool = False,
 ) -> Callable[[AnyCallable], classmethod]:
     """
     Decorate methods on the class indicating that they should be used to validate fields
@@ -60,6 +61,7 @@ def validator(
       whole object
     :param always: whether this method and other validators should be called even if the value is missing
     :param check_fields: whether to check that the fields actually exist on the model
+    :param allow_reuse: whether to track and raise an error if another validator refers to the decorated function
     """
     if not fields:
         raise ConfigError('validator with no fields specified')
@@ -78,12 +80,14 @@ def validator(
         each_item = not whole
 
     def dec(f: AnyCallable) -> classmethod:
-        _check_validator_name(f)
-        f_cls = classmethod(f)
+        f_cls = _prepare_validator(f, allow_reuse)
         setattr(
             f_cls,
             VALIDATOR_CONFIG_KEY,
-            (fields, Validator(func=f, pre=pre, each_item=each_item, always=always, check_fields=check_fields)),
+            (
+                fields,
+                Validator(func=f_cls.__func__, pre=pre, each_item=each_item, always=always, check_fields=check_fields),
+            ),
         )
         return f_cls
 
@@ -91,33 +95,33 @@ def dec(f: AnyCallable) -> classmethod:
 
 
 def root_validator(
-    _func: Optional[AnyCallable] = None, *, pre: bool = False
+    _func: Optional[AnyCallable] = None, *, pre: bool = False, allow_reuse: bool = False
 ) -> Union[classmethod, Callable[[AnyCallable], classmethod]]:
     if _func:
-        _check_validator_name(_func)
-        f_cls = classmethod(_func)
-        setattr(f_cls, ROOT_VALIDATOR_CONFIG_KEY, Validator(func=_func, pre=pre))
+        f_cls = _prepare_validator(_func, allow_reuse)
+        setattr(f_cls, ROOT_VALIDATOR_CONFIG_KEY, Validator(func=f_cls.__func__, pre=pre))
         return f_cls
 
     def dec(f: AnyCallable) -> classmethod:
-        _check_validator_name(f)
-        f_cls = classmethod(f)
-        setattr(f_cls, ROOT_VALIDATOR_CONFIG_KEY, Validator(func=f, pre=pre))
+        f_cls = _prepare_validator(f, allow_reuse)
+        setattr(f_cls, ROOT_VALIDATOR_CONFIG_KEY, Validator(func=f_cls.__func__, pre=pre))
         return f_cls
 
     return dec
 
 
-def _check_validator_name(f: AnyCallable) -> None:
+def _prepare_validator(function: AnyCallable, allow_reuse: bool) -> classmethod:
     """
-    avoid validators with duplicated names since without this, validators can be overwritten silently
-    which generally isn't the intended behaviour, don't run in ipython - see #312
+    Avoid validators with duplicated names since without this, validators can be overwritten silently
+    which generally isn't the intended behaviour, don't run in ipython (see #312) or if allow_reuse is False.
     """
-    if not in_ipython():  # pragma: no branch
-        ref = f.__module__ + '.' + f.__qualname__
+    f_cls = function if isinstance(function, classmethod) else classmethod(function)
+    if not in_ipython() and not allow_reuse:
+        ref = f_cls.__func__.__module__ + '.' + f_cls.__func__.__qualname__
         if ref in _FUNCS:
-            raise ConfigError(f'duplicate validator function "{ref}"')
+            raise ConfigError(f'duplicate validator function "{ref}"; if this is intended, set `allow_reuse=True`')
         _FUNCS.add(ref)
+    return f_cls
 
 
 class ValidatorGroup:


Test patch:
diff --git a/tests/test_validators.py b/tests/test_validators.py
--- a/tests/test_validators.py
+++ b/tests/test_validators.py
@@ -1,4 +1,5 @@
 from datetime import datetime
+from itertools import product
 from typing import Dict, List, Optional, Tuple
 
 import pytest
@@ -235,7 +236,9 @@ def duplicate_name(cls, v):  # noqa
                 return v
 
     assert str(exc_info.value) == (
-        'duplicate validator function ' '"tests.test_validators.test_duplicates.<locals>.Model.duplicate_name"'
+        'duplicate validator function '
+        '"tests.test_validators.test_duplicates.<locals>.Model.duplicate_name"; '
+        'if this is intended, set `allow_reuse=True`'
     )
 
 
@@ -756,7 +759,7 @@ def repeat_b(cls, v):
             return v * 2
 
         @root_validator
-        def root_validator(cls, values):
+        def example_root_validator(cls, values):
             root_val_values.append(values)
             if 'snap' in values.get('b', ''):
                 raise ValueError('foobar')
@@ -906,3 +909,99 @@ def root_validator_child(cls, values):
     assert len(Child.__pre_root_validators__) == 0
     assert Child(a=123).dict() == {'extra2': 2, 'extra1': 1, 'a': 123}
     assert calls == ["parent validator: {'a': 123}", "child validator: {'extra1': 1, 'a': 123}"]
+
+
+def reusable_validator(num):
+    return num * 2
+
+
+def test_reuse_global_validators():
+    class Model(BaseModel):
+        x: int
+        y: int
+
+        double_x = validator('x', allow_reuse=True)(reusable_validator)
+        double_y = validator('y', allow_reuse=True)(reusable_validator)
+
+    assert dict(Model(x=1, y=1)) == {'x': 2, 'y': 2}
+
+
+def declare_with_reused_validators(include_root, allow_1, allow_2, allow_3):
+    class Model(BaseModel):
+        a: str
+        b: str
+
+        @validator('a', allow_reuse=allow_1)
+        def duplicate_name(cls, v):
+            return v
+
+        @validator('b', allow_reuse=allow_2)  # noqa F811
+        def duplicate_name(cls, v):  # noqa F811
+            return v
+
+        if include_root:
+
+            @root_validator(allow_reuse=allow_3)  # noqa F811
+            def duplicate_name(cls, values):  # noqa F811
+                return values
+
+
+@pytest.fixture
+def reset_tracked_validators():
+    from pydantic.class_validators import _FUNCS
+
+    original_tracked_validators = set(_FUNCS)
+    yield
+    _FUNCS.clear()
+    _FUNCS.update(original_tracked_validators)
+
+
+@pytest.mark.parametrize('include_root,allow_1,allow_2,allow_3', product(*[[True, False]] * 4))
+def test_allow_reuse(include_root, allow_1, allow_2, allow_3, reset_tracked_validators):
+    duplication_count = int(not allow_1) + int(not allow_2) + int(include_root and not allow_3)
+    if duplication_count > 1:
+        with pytest.raises(ConfigError) as exc_info:
+            declare_with_reused_validators(include_root, allow_1, allow_2, allow_3)
+        assert str(exc_info.value).startswith('duplicate validator function')
+    else:
+        declare_with_reused_validators(include_root, allow_1, allow_2, allow_3)
+
+
+@pytest.mark.parametrize('validator_classmethod,root_validator_classmethod', product(*[[True, False]] * 2))
+def test_root_validator_classmethod(validator_classmethod, root_validator_classmethod, reset_tracked_validators):
+    root_val_values = []
+
+    class Model(BaseModel):
+        a: int = 1
+        b: str
+
+        def repeat_b(cls, v):
+            return v * 2
+
+        if validator_classmethod:
+            repeat_b = classmethod(repeat_b)
+        repeat_b = validator('b')(repeat_b)
+
+        def example_root_validator(cls, values):
+            root_val_values.append(values)
+            if 'snap' in values.get('b', ''):
+                raise ValueError('foobar')
+            return dict(values, b='changed')
+
+        if root_validator_classmethod:
+            example_root_validator = classmethod(example_root_validator)
+        example_root_validator = root_validator(example_root_validator)
+
+    assert Model(a='123', b='bar').dict() == {'a': 123, 'b': 'changed'}
+
+    with pytest.raises(ValidationError) as exc_info:
+        Model(b='snap dragon')
+    assert exc_info.value.errors() == [{'loc': ('__root__',), 'msg': 'foobar', 'type': 'value_error'}]
+
+    with pytest.raises(ValidationError) as exc_info:
+        Model(a='broken', b='bar')
+    assert exc_info.value.errors() == [
+        {'loc': ('a',), 'msg': 'value is not a valid integer', 'type': 'type_error.integer'}
+    ]
+
+    assert root_val_values == [{'a': 123, 'b': 'barbar'}, {'a': 1, 'b': 'snap dragonsnap dragon'}, {'b': 'barbar'}]