Skip to content

Commit 621f6fd

Browse files
committed
Keep validation errors usable for builtin and partial callables
1 parent 6ffd862 commit 621f6fd

2 files changed

Lines changed: 57 additions & 4 deletions

File tree

‎src/validators/utils.py‎

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,9 @@ def __init__(self, function: Callable[..., Any], arg_dict: Dict[str, Any], messa
2020

2121
def __repr__(self):
2222
"""Repr Validation Failure."""
23+
func_name = getattr(self.func, "__name__", type(self.func).__name__)
2324
return (
24-
f"ValidationError(func={self.func.__name__}, "
25+
f"ValidationError(func={func_name}, "
2526
+ f"args={ ({k: v for (k, v) in self.__dict__.items() if k != 'func'}) })"
2627
)
2728

@@ -36,9 +37,14 @@ def __bool__(self):
3637

3738
def _func_args_as_dict(func: Callable[..., Any], *args: Any, **kwargs: Any):
3839
"""Return function's positional and key value arguments as an ordered dictionary."""
40+
try:
41+
arg_names = getfullargspec(func)[0]
42+
except (TypeError, ValueError):
43+
# Some builtins have no inspectable signature. Retain the raw call
44+
# instead of letting error reporting raise another exception.
45+
return {"args": args, "kwargs": kwargs}
3946
return dict(
40-
list(zip(dict.fromkeys(chain(getfullargspec(func)[0], kwargs.keys())), args))
41-
+ list(kwargs.items())
47+
list(zip(dict.fromkeys(chain(arg_names, kwargs.keys())), args)) + list(kwargs.items())
4248
)
4349

4450

‎tests/test_validation_failure.py‎

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,13 @@
11
"""Test validation Failure."""
22

3+
# standard
4+
from functools import partial
5+
6+
# external
7+
import pytest
8+
39
# local
4-
from validators import between
10+
from validators import ValidationError, between, validator
511

612
failed_obj_repr = "ValidationError(func=between"
713

@@ -31,3 +37,44 @@ def test_arguments_as_properties(self):
3137
assert self.is_in_between.__dict__["value"] == 3
3238
assert self.is_in_between.__dict__["min_val"] == 4
3339
assert self.is_in_between.__dict__["max_val"] == 5
40+
41+
42+
def test_partial_validation_error_representation():
43+
"""Validation errors support partial callables without a name attribute."""
44+
45+
def above(value: int, minimum: int) -> bool:
46+
return value > minimum
47+
48+
check = validator(partial(above, minimum=3))
49+
error = check(1)
50+
assert isinstance(error, ValidationError)
51+
assert repr(error) == "ValidationError(func=partial, args={'value': 1})"
52+
assert str(error) == repr(error)
53+
assert check(4) is True
54+
55+
56+
def test_callable_instance_validation_error_representation():
57+
"""Callable instance errors use their type name."""
58+
59+
class Positive:
60+
def __call__(self, *, value: int) -> bool:
61+
return value > 0
62+
63+
error = validator(Positive())(value=-1)
64+
assert isinstance(error, ValidationError)
65+
assert repr(error) == "ValidationError(func=Positive, args={'value': -1})"
66+
67+
68+
def test_builtin_validation_error_keeps_raw_arguments():
69+
"""An unavailable builtin signature must not hide its validation error."""
70+
check = validator(int)
71+
error = check("invalid")
72+
assert isinstance(error, ValidationError)
73+
assert "func=int" in repr(error)
74+
assert error.__dict__["args"] == ("invalid",)
75+
assert error.__dict__["kwargs"] == {}
76+
assert "invalid literal" in error.reason
77+
assert check("42") is True
78+
79+
with pytest.raises(ValidationError, match="func=int"):
80+
check("invalid", r_ve=True)

0 commit comments

Comments
 (0)