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
Original file line number Diff line number Diff line change
@@ -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.
27 changes: 21 additions & 6 deletions pydantic/class_validators.py
Original file line number Diff line number Diff line change
@@ -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__', '<default_module>')
+ '.'
+ getattr(f_cls.__func__, '__qualname__', '<default name>')
samuelcolvin marked this conversation as resolved.
Show resolved Hide resolved
)
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,14 @@ 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__', '<default name>'): v for v in validators}
JensHeinrich marked this conversation as resolved.
Show resolved Hide resolved
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__', '<default name>') 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 +247,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
Original file line number Diff line number Diff line change
@@ -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)