Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Multiple alternatives validator #1214

Open
brunokim opened this issue Dec 28, 2023 · 1 comment
Open

Multiple alternatives validator #1214

brunokim opened this issue Dec 28, 2023 · 1 comment
Labels

Comments

@brunokim
Copy link

brunokim commented Dec 28, 2023

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
Copy link
Author

brunokim commented Dec 28, 2023

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

@hynek hynek added the Feature label Dec 29, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

2 participants