Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use getattr and default for qualname #5126

Merged
merged 11 commits into from Mar 8, 2023
1 change: 1 addition & 0 deletions changes/5126-JensHeinrich.md
@@ -0,0 +1 @@
Implement logic to support creating validators from non standard callables by using defaults to identify them and unwrapping `functools.partial` and `functools.partialmethod` when checking the signature.
31 changes: 25 additions & 6 deletions 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
Expand Down Expand Up @@ -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)
Expand All @@ -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)
)
)
Expand Down Expand Up @@ -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(
Expand Down
32 changes: 31 additions & 1 deletion 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
Expand Down Expand Up @@ -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)