attrs icon indicating copy to clipboard operation
attrs copied to clipboard

Multiple alternatives validator

Open brunokim opened this issue 1 year ago • 1 comments

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)

brunokim avatar Dec 28 '23 14:12 brunokim

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'>))

brunokim avatar Dec 28 '23 16:12 brunokim