attrs
attrs copied to clipboard
Multiple alternatives validator
I'd like to validate that a field can be of several types, one of which is an iterator.
from attrs.validator import instance_of
@define
class C:
x: A | B | tuple[int, ...] = field(validator=instance_of((A, B, tuple)))
I can use instance_of to check the outer type tuple, but I can't use deep_iterable to validate that they are indeed the element type int.
Since we already have validators and_ and not_, it may make sense to also add or_, so that I can represent this with
from attrs.validator import instance_of, deep_iterable, or_
@define
class C:
x: A | B | tuple[int, ...] = field(
validator=or_(
instance_of((A, B)),
deep_iterable(instance_of(int))))
For comparison, this is how I'm writing a custom validator
from attrs.validator import instance_of, deep_iterable
@define
class C:
x: A | B | tuple[int, ...] = field()
@x.validator
def _validate_x(self, attr, value):
v1 = instance_of((A, B))
v2 = deep_iterable(instance_of(int))
try:
v1(self, attr, value)
except TypeError:
v2(self, attr, value)
FTR, it's already possible to do that using only and_ and not_, thanks to De Morgan's laws:
from attrs.validator import instance_of, deep_iterable, and_, not_
@define
class C:
x: A | B | tuple[int, ...] = field(
validator= not_(and_(
not_(instance_of((A, B))),
not_(deep_iterable(instance_of(int))))))
But the error message is not very informative:
>>> @define
... class C:
... x = field(validator=not_( and_( not_( instance_of((A, B)) ), not_( deep_iterable(instance_of(int)) ) ) ))
...
>>> C(A())
C(x=A())
>>> C(B())
C(x=B())
>>> C(())
C(x=())
>>> C((1, 2, 3))
C(x=(1, 2, 3))
>>> C((1, 2, 3, '4'))
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<attrs generated init __main__.C>", line 5, in __init__
File "<REDACTED>/lib/python3.12/site-packages/attr/validators.py", line 670, in __call__
raise ValueError(
ValueError: ("not_ validator child '_AndValidator(_validators=(<not_ validator wrapping <instance_of validator for type (<class '__main__.A'>, <class '__main__.B'>)>, capturing (<class 'ValueError'>, <class 'TypeError'>)>, <not_ validator wrapping <deep_iterable validator for iterables of <instance_of validator for type <class 'int'>>>, capturing (<class 'ValueError'>, <class 'TypeError'>)>))' did not raise a captured error", Attribute(name='x', default=NOTHING, validator=<not_ validator wrapping _AndValidator(_validators=(<not_ validator wrapping <instance_of validator for type (<class '__main__.A'>, <class '__main__.B'>)>, capturing (<class 'ValueError'>, <class 'TypeError'>)>, <not_ validator wrapping <deep_iterable validator for iterables of <instance_of validator for type <class 'int'>>>, capturing (<class 'ValueError'>, <class 'TypeError'>)>)), capturing (<class 'ValueError'>, <class 'TypeError'>)>, repr=True, eq=True, eq_key=None, order=True, order_key=None, hash=None, init=True, metadata=mappingproxy({}), type=None, converter=None, kw_only=False, inherited=False, on_setattr=None, alias='x'), _AndValidator(_validators=(<not_ validator wrapping <instance_of validator for type (<class '__main__.A'>, <class '__main__.B'>)>, capturing (<class 'ValueError'>, <class 'TypeError'>)>, <not_ validator wrapping <deep_iterable validator for iterables of <instance_of validator for type <class 'int'>>>, capturing (<class 'ValueError'>, <class 'TypeError'>)>)), (1, 2, 3, '4'), (<class 'ValueError'>, <class 'TypeError'>))