diff --git a/src/validators/utils.py b/src/validators/utils.py index 28d3c85..5ecb640 100644 --- a/src/validators/utils.py +++ b/src/validators/utils.py @@ -20,8 +20,9 @@ def __init__(self, function: Callable[..., Any], arg_dict: Dict[str, Any], messa def __repr__(self): """Repr Validation Failure.""" + func_name = getattr(self.func, "__name__", type(self.func).__name__) return ( - f"ValidationError(func={self.func.__name__}, " + f"ValidationError(func={func_name}, " + f"args={ ({k: v for (k, v) in self.__dict__.items() if k != 'func'}) })" ) @@ -36,9 +37,14 @@ def __bool__(self): def _func_args_as_dict(func: Callable[..., Any], *args: Any, **kwargs: Any): """Return function's positional and key value arguments as an ordered dictionary.""" + try: + arg_names = getfullargspec(func)[0] + except (TypeError, ValueError): + # Some builtins have no inspectable signature. Retain the raw call + # instead of letting error reporting raise another exception. + return {"args": args, "kwargs": kwargs} return dict( - list(zip(dict.fromkeys(chain(getfullargspec(func)[0], kwargs.keys())), args)) - + list(kwargs.items()) + list(zip(dict.fromkeys(chain(arg_names, kwargs.keys())), args)) + list(kwargs.items()) ) diff --git a/tests/test_validation_failure.py b/tests/test_validation_failure.py index 6750200..3faf568 100644 --- a/tests/test_validation_failure.py +++ b/tests/test_validation_failure.py @@ -1,7 +1,13 @@ """Test validation Failure.""" +# standard +from functools import partial + +# external +import pytest + # local -from validators import between +from validators import ValidationError, between, validator failed_obj_repr = "ValidationError(func=between" @@ -31,3 +37,44 @@ def test_arguments_as_properties(self): assert self.is_in_between.__dict__["value"] == 3 assert self.is_in_between.__dict__["min_val"] == 4 assert self.is_in_between.__dict__["max_val"] == 5 + + +def test_partial_validation_error_representation(): + """Validation errors support partial callables without a name attribute.""" + + def above(value: int, minimum: int) -> bool: + return value > minimum + + check = validator(partial(above, minimum=3)) + error = check(1) + assert isinstance(error, ValidationError) + assert repr(error) == "ValidationError(func=partial, args={'value': 1})" + assert str(error) == repr(error) + assert check(4) is True + + +def test_callable_instance_validation_error_representation(): + """Callable instance errors use their type name.""" + + class Positive: + def __call__(self, *, value: int) -> bool: + return value > 0 + + error = validator(Positive())(value=-1) + assert isinstance(error, ValidationError) + assert repr(error) == "ValidationError(func=Positive, args={'value': -1})" + + +def test_builtin_validation_error_keeps_raw_arguments(): + """An unavailable builtin signature must not hide its validation error.""" + check = validator(int) + error = check("invalid") + assert isinstance(error, ValidationError) + assert "func=int" in repr(error) + assert error.__dict__["args"] == ("invalid",) + assert error.__dict__["kwargs"] == {} + assert "invalid literal" in error.reason + assert check("42") is True + + with pytest.raises(ValidationError, match="func=int"): + check("invalid", r_ve=True)