Repository: pydantic/pydantic

Issue:
Attribute error for `functools.partial`
### Initial Checks

- [X] I have searched GitHub for a duplicate issue and I'm sure this is something new
- [X] I have searched Google & StackOverflow for a solution and couldn't find anything
- [X] I have read and followed [the docs](https://pydantic-docs.helpmanual.io) and still think this is a bug
- [X] I am confident that the issue is with pydantic (not my code, or another library in the ecosystem like [FastAPI](https://fastapi.tiangolo.com) or [mypy](https://mypy.readthedocs.io/en/stable))


### Description

Using a `partialmethod`or `partial` as a `validator` or `root_validator` creates an unhelpful `AttributeError`:
```
tests/conftest.py:14: in <module>
    class TestClass(BaseModel):
tests/conftest.py:17: in TestClass
    _custom_validate = validator("name")(validate)
pydantic/class_validators.py:93: in pydantic.class_validators.validator.dec
    ???
pydantic/class_validators.py:150: in pydantic.class_validators._prepare_validator
    ???
E   AttributeError: 'functools.partial' object has no attribute '__qualname__'
```

The problem is rooted at [this code](https://github.com/pydantic/pydantic/blob/314abcabbde0936ec2e8e5ff6532d64a221210c5/pydantic/class_validators.py#L150).

IMO an try except block with a test for partial methods would be useful here, telling the User to supply the "allow_reuse" parameter for partials.

### Example Code

```Python
from functools import partial

from pydantic import validator, BaseModel
from pydantic.main import ModelMetaclass
from pydantic.utils import GetterDict

def custom_validator(additional_stuff: str, cls: ModelMetaclass, values: GetterDict):
    print(additional_stuff)
    return values

validate = partial(custom_validator, "TEXT")

class TestClass(BaseModel):
    name: str

    _custom_validate = validator("name")(validate)
```


### Python, Pydantic & OS Version

```Text
pydantic version: 1.10.5
            pydantic compiled: True
                 install path: XXX/.venv/lib/python3.10/site-packages/pydantic
               python version: 3.10.7 (main, Nov 24 2022, 19:45:47) [GCC 12.2.0]
                     platform: Linux-5.19.0-31-generic-x86_64-with-glibc2.36
     optional deps. installed: ['typing-extensions']
```


### Affected Components

- [ ] [Compatibility between releases](https://pydantic-docs.helpmanual.io/changelog/)
- [X] [Data validation/parsing](https://pydantic-docs.helpmanual.io/usage/models/#basic-model-usage)
- [ ] [Data serialization](https://pydantic-docs.helpmanual.io/usage/exporting_models/) - `.model_dump()` and `.model_dump_json()`
- [ ] [JSON Schema](https://pydantic-docs.helpmanual.io/usage/schema/)
- [ ] [Dataclasses](https://pydantic-docs.helpmanual.io/usage/dataclasses/)
- [ ] [Model Config](https://pydantic-docs.helpmanual.io/usage/model_config/)
- [ ] [Field Types](https://pydantic-docs.helpmanual.io/usage/types/) - adding or changing a particular data type
- [ ] [Function validation decorator](https://pydantic-docs.helpmanual.io/usage/validation_decorator/)
- [ ] [Generic Models](https://pydantic-docs.helpmanual.io/usage/models/#generic-models)
- [ ] [Other Model behaviour](https://pydantic-docs.helpmanual.io/usage/models/) - `model_construct()`, pickling, private attributes, ORM mode
- [ ] [Plugins](https://pydantic-docs.helpmanual.io/) and integration with other tools - mypy, FastAPI, python-devtools, Hypothesis, VS Code, PyCharm, etc.


Comments:
Thanks @JensHeinrich for reporting this issue.

I can confirm the issue. `partial()` returns an instance, not a class or function.

it may fix by accessing the function in the following line. I mean replacing `f_cls.__func__.__qualname__` with `f_cls.__func__.func.__qualname__` in case of `partial`

https://github.com/pydantic/pydantic/blob/73373c3e08fe5fe23e4b05f549ea34e0da6a16b7/pydantic/_internal/_decorators.py#L204

Would you like to open a PR?
I am already working on a PR, but at the moment I just add a better error
Even if the `ref` is created in a better way (or the check just skipped by using `allow_reuse=True`) another `AttributeError` is raised [here](https://github.com/pydantic/pydantic/blob/10b8ec7dde62f5e4948a09b9114c5ac64d786ded/pydantic/class_validators.py#L170)
Also signature creation doesn't work on it
> Even if the `ref` is created in a better way (or the check just skipped by using `allow_reuse=True`) another `AttributeError` is raised [here](https://github.com/pydantic/pydantic/blob/10b8ec7dde62f5e4948a09b9114c5ac64d786ded/pydantic/class_validators.py#L170)

This one also can be fixed by the same change that I mentioned before.

> I am already working on a PR, but at the moment I just add a better error

I think we can make it work at least on `1.10.x` but I am not sure about `V2`.

@samuelcolvin Do you think we should prevent using `functools.partial` as a validator?

TBH I feel `functools` is broken here and we are just adding a workaround.
Even a `lambda` gets an `__qualname__`
> TBH I feel `functools` is broken here and we are just adding a workaround. Even a `lambda` gets an `__qualname__`

agreed. Feel free to create an issue on cpython and copy me into it, I'd be interest to see what they say.

Also, can you create a PR against main to see if this is working there?
This one can be related https://bugs.python.org/issue34475
I created an issue  python/cpython#102323
I personally find the responses in the links from the last two comments reasonable; it seems we should just not assume that `__qualname__` definitely exists.
I'm good with using a default value instead, too.
I am working on a PR for v1.10 already
I would only allow those with `reuse=True`. Would you support that @samuelcolvin @dmontagu ?

Patch:
diff --git a/pydantic/class_validators.py b/pydantic/class_validators.py
--- a/pydantic/class_validators.py
+++ b/pydantic/class_validators.py
@@ -1,6 +1,6 @@
 import warnings
 from collections import ChainMap
-from functools import wraps
+from functools import partial, partialmethod, wraps
 from itertools import chain
 from types import FunctionType
 from typing import TYPE_CHECKING, Any, Callable, Dict, Iterable, List, Optional, Set, Tuple, Type, Union, overload
@@ -147,7 +147,11 @@ def _prepare_validator(function: AnyCallable, allow_reuse: bool) -> 'AnyClassMet
     """
     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__
+        ref = (
+            getattr(f_cls.__func__, '__module__', '<No __module__>')
+            + '.'
+            + getattr(f_cls.__func__, '__qualname__', f'<No __qualname__: id:{id(f_cls.__func__)}>')
+        )
         if ref in _FUNCS:
             raise ConfigError(f'duplicate validator function "{ref}"; if this is intended, set `allow_reuse=True`')
         _FUNCS.add(ref)
@@ -165,14 +169,18 @@ def get_validators(self, name: str) -> Optional[Dict[str, Validator]]:
         if name != ROOT_KEY:
             validators += self.validators.get('*', [])
         if validators:
-            return {v.func.__name__: v for v in validators}
+            return {getattr(v.func, '__name__', f'<No __name__: id:{id(v.func)}>'): v for v in validators}
         else:
             return None
 
     def check_for_unused(self) -> None:
         unused_validators = set(
             chain.from_iterable(
-                (v.func.__name__ for v in self.validators[f] if v.check_fields)
+                (
+                    getattr(v.func, '__name__', f'<No __name__: id:{id(v.func)}>')
+                    for v in self.validators[f]
+                    if v.check_fields
+                )
                 for f in (self.validators.keys() - self.used_validators)
             )
         )
@@ -243,8 +251,19 @@ def make_generic_validator(validator: AnyCallable) -> 'ValidatorCallable':
     """
     from inspect import signature
 
-    sig = signature(validator)
-    args = list(sig.parameters.keys())
+    if not isinstance(validator, (partial, partialmethod)):
+        # This should be the default case, so overhead is reduced
+        sig = signature(validator)
+        args = list(sig.parameters.keys())
+    else:
+        # Fix the generated argument lists of partial methods
+        sig = signature(validator.func)
+        args = [
+            k
+            for k in signature(validator.func).parameters.keys()
+            if k not in validator.args | validator.keywords.keys()
+        ]
+
     first_arg = args.pop(0)
     if first_arg == 'self':
         raise ConfigError(


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,8 +1,9 @@
 from collections import deque
 from datetime import datetime
 from enum import Enum
+from functools import partial, partialmethod
 from itertools import product
-from typing import Dict, List, Optional, Tuple, Union
+from typing import Any, Callable, Dict, List, Optional, Tuple, Union
 
 import pytest
 from typing_extensions import Literal
@@ -1345,3 +1346,32 @@ class Model(BaseModel):
         {'loc': ('foo',), 'msg': 'the list has duplicated items', 'type': 'value_error.list.unique_items'},
         {'loc': ('bar',), 'msg': 'the list has duplicated items', 'type': 'value_error.list.unique_items'},
     ]
+
+
+@pytest.mark.parametrize(
+    'func,allow_reuse',
+    [
+        pytest.param(partial, False, id='`partial` and check for reuse'),
+        pytest.param(partial, True, id='`partial` and ignore reuse'),
+        pytest.param(partialmethod, False, id='`partialmethod` and check for reuse'),
+        pytest.param(partialmethod, True, id='`partialmethod` and ignore reuse'),
+    ],
+)
+def test_functool_as_validator(
+    reset_tracked_validators,
+    func: Callable,
+    allow_reuse: bool,
+):
+    def custom_validator(
+        cls,
+        v: Any,
+        allowed: str,
+    ) -> Any:
+        assert v == allowed, f'Only {allowed} allowed as value; given: {v}'
+        return v
+
+    validate = func(custom_validator, allowed='TEXT')
+
+    class TestClass(BaseModel):
+        name: str
+        _custom_validate = validator('name', allow_reuse=allow_reuse)(validate)