attrs icon indicating copy to clipboard operation
attrs copied to clipboard

Provide a builtin validator that takes a boolean function.

Open tomprince opened this issue 4 years ago • 9 comments

Currently, validators are required to raise an exception, if the value isn't appropriate. If I currently have a function for checking the property, but returns a boolean, instead of raising, to use it as a validator, I have to write a wrapper that converts the return to an exception. It would be nice if attrs provided such a wrapper itself.

tomprince avatar Jul 13 '21 19:07 tomprince

+1, this would be a nice utility!

A basic implementation:

from __future__ import annotations
from typing import TYPE_CHECKING, Callable, TypeVar

import attr


_S = TypeVar("_S")
_T = TypeVar("_T")
if TYPE_CHECKING:
    # TODO: why? attr.Attribute subclasses Generic[_T]...
    #  TypeError: 'type' object is not subscriptable
    PredicateFunc = Callable[[_S, attr.Attribute[_T], _T], bool]
    ValidatorFunc = Callable[[_S, attr.Attribute[_T], _T], None]


def as_validator(predicate: PredicateFunc[_S, _T]) -> ValidatorFunc[_S, _T]:
    """Convert a predicate to an Attrs validator function.

    A "predicate" is a function (or other callable) that takes an
    `attr.Attribute` and the value of that attribute, and returns a boolean.
    """

    def validator(instance: _S, attribute: attr.Attribute[_T], value: _T) -> None:
        if not predicate(instance, attribute, value):
            raise ValueError(f"Invalid value for {instance.__class__.__name__}.{attribute.name}: {value!r}")

    return validator


if __name__ == "__main__":
    # Examples following https://www.attrs.org/en/stable/init.html#validators:

    @attr.s()
    class A(object):
        x: int = attr.ib(validator=as_validator(lambda inst, _, val: val < inst.y))
        y: int = attr.ib()

    A(3, 5)
    try:
        A(5, 3)
    except ValueError as exc:
        print(exc)


    @attr.s()
    class B(object):
        x: int = attr.ib()
        y: int = attr.ib()

        @x.validator
        @as_validator
        def _check_x(self, attribute, value):
            return value < self.y

    B(3, 5)
    try:
        B(5, 3)
    except ValueError as exc:
        print(exc)

gwerbin avatar Jul 14 '21 19:07 gwerbin

If someone would like to cook up a PR, the chances are good for it to be merged!

hynek avatar Nov 04 '21 06:11 hynek

instead of requiring a bool return value, i think it makes more ‘pythonic’ sense if this were to use bool() to see if the result is ‘truthy’?

this would make much more sense for various use cases:

  • None vs actual object, e.g. regular expression match functions return a typing.Match object or None
  • str.split() or other list-producing functions can be used for ‘empty’ vs ‘non-empty’ checks

example usage based on the proof of concept code above:

RE_XYZ = re.compile(...)

@attr.define
class C:
    a: ... = attr.field(validator=as_validator(lambda _, _, val: RE_XYZ.match(value)))

wbolster avatar Nov 22 '21 18:11 wbolster

That's a great idea @wbolster. My example implementation above already uses general "truthiness" with the if not ... check, but the types could be updated to something like this:

class SupportsBool(Protocol):
    def __bool__(self) -> bool: ...

_S = TypeVar("_S")
_T = TypeVar("_T")
if TYPE_CHECKING:
    # TODO: why? attr.Attribute subclasses Generic[_T]...
    #  TypeError: 'type' object is not subscriptable
    PredicateFunc = Callable[[_S, attr.Attribute[_T], _T], SupportsBool]
    ValidatorFunc = Callable[[_S, attr.Attribute[_T], _T], None]

gwerbin avatar Nov 22 '21 19:11 gwerbin

If anyone has suggestions of what the test cases should be, I will try to make the time for a PR.

gwerbin avatar Nov 22 '21 19:11 gwerbin

i think an Any type (usually not the best choice) would do just fine here:

object.__bool__(self) Called to implement truth value testing and the built-in operation bool(); should return False or True. When this method is not defined, __len__() is called, if it is defined, and the object is considered true if its result is nonzero. If a class defines neither __len__() nor __bool__(), all its instances are considered true. – https://docs.python.org/3/reference/datamodel.html#object.bool

in other words: bool(obj) will do the right thing on any object, the right thing being the same thing as what if obj: ... would do

wbolster avatar Nov 22 '21 20:11 wbolster

If someone would like to cook up a PR, the chances are good for it to be merged!

My draft pr is ready. i will raise in with next 4-5 days.Just adding some test cases.

thisisamardeep avatar Nov 27 '21 18:11 thisisamardeep

What's the status of this issue/PR?

Reading through the comments I'd have thought a validators.is_true and validators.is_false would be more consistent with existing validator names?

BEllis avatar Jun 23 '23 10:06 BEllis

I'm afraid the status is that the draft didn't make it to GitHub. 😅

hynek avatar Jul 22 '23 13:07 hynek