Skip to content

Commit

Permalink
[RFC] Enhancement/one of validator (#181)
Browse files Browse the repository at this point in the history
  • Loading branch information
dieb authored and hynek committed May 16, 2017
1 parent a328e67 commit 461e1b2
Show file tree
Hide file tree
Showing 4 changed files with 107 additions and 9 deletions.
3 changes: 3 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,9 @@ Changes:
`#128 <https://github.com/python-attrs/attrs/pull/128>`_
- ``__attrs_post_init__()`` is now run if validation is disabled.
`#130 <https://github.com/python-attrs/attrs/pull/130>`_
- Added `attr.validators.in_(options)`` that, given the allowed `options`, checks whether the attribute value is in it.
This can be used to check constants, enums, mappings, etc.
`#181 <https://github.com/python-attrs/attrs/pull/181>`_
- Added ``attr.validators.and_()`` that composes multiple validators into one.
`#161 <https://github.com/python-attrs/attrs/issues/161>`_
- For convenience, the ``validator`` argument of ``@attr.s`` now can take a ``list`` of validators that are wrapped using ``and_()``.
Expand Down
29 changes: 27 additions & 2 deletions docs/api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -270,6 +270,33 @@ Validators
...
TypeError: ("'x' must be <type 'int'> (got None that is a <type 'NoneType'>).", Attribute(name='x', default=NOTHING, validator=<instance_of validator for type <type 'int'>>, repr=True, cmp=True, hash=None, init=True), <type 'int'>, None)

.. autofunction:: attr.validators.in_

For example:

.. doctest::

>>> import enum
>>> class State(enum.Enum):
... ON = "on"
... OFF = "off"
>>> @attr.s
... class C(object):
... state = attr.ib(validator=attr.validators.in_(State))
... val = attr.ib(validator=attr.validators.in_([1, 2, 3]))
>>> C(State.ON, 1)
C(state=<State.ON: 'on'>, val=1)
>>> C("on", 1)
Traceback (most recent call last):
...
ValueError: 'state' must be in <enum 'State'> (got 'on')
>>> C(State.ON, 4)
Traceback (most recent call last):
...
ValueError: 'val' must be in [1, 2, 3] (got 4)

.. autofunction:: attr.validators.provides

.. autofunction:: attr.validators.and_

For convenience, it's also possible to pass a list to :func:`attr.ib`'s validator argument.
Expand All @@ -279,8 +306,6 @@ Validators
x = attr.ib(validator=attr.validators.and_(v1, v2, v3))
x = attr.ib(validator=[v1, v2, v3])

.. autofunction:: attr.validators.provides

.. autofunction:: attr.validators.optional

For example:
Expand Down
48 changes: 42 additions & 6 deletions src/attr/validators.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,9 +48,9 @@ def instance_of(type):
:param type: The type to check for.
:type type: type or tuple of types
The :exc:`TypeError` is raised with a human readable error message, the
attribute (of type :class:`attr.Attribute`), the expected type, and the
value it got.
:raises TypeError: With a human readable error message, the attribute
(of type :class:`attr.Attribute`), the expected type, and the value it
got.
"""
return _InstanceOfValidator(type)

Expand Down Expand Up @@ -87,9 +87,9 @@ def provides(interface):
:param zope.interface.Interface interface: The interface to check for.
The :exc:`TypeError` is raised with a human readable error message, the
attribute (of type :class:`attr.Attribute`), the expected interface, and
the value it got.
:raises TypeError: With a human readable error message, the attribute
(of type :class:`attr.Attribute`), the expected interface, and the
value it got.
"""
return _ProvidesValidator(interface)

Expand Down Expand Up @@ -127,3 +127,39 @@ def optional(validator):
if isinstance(validator, list):
return _OptionalValidator(_AndValidator(validator))
return _OptionalValidator(validator)


@attributes(repr=False, slots=True)
class _InValidator(object):
options = attr()

def __call__(self, inst, attr, value):
if value not in self.options:
raise ValueError(
"'{name}' must be in {options!r} (got {value!r})"
.format(name=attr.name, options=self.options, value=value)
)

def __repr__(self):
return (
"<in_ validator with options {options!r}>"
.format(options=self.options)
)


def in_(options):
"""
A validator that raises a :exc:`ValueError` if the initializer is called
with a value that does not belong in the options provided. The check is
performed using ``value in options``.
:param options: Allowed options.
:type options: list, tuple, :class:`enum.Enum`, ...
:raises ValueError: With a human readable error message, the attribute (of
type :class:`attr.Attribute`), the expected options, and the value it
got.
.. versionadded:: 17.1.0
"""
return _InValidator(options)
36 changes: 35 additions & 1 deletion tests/test_validators.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
import pytest
import zope.interface

from attr.validators import and_, instance_of, provides, optional
from attr.validators import and_, instance_of, provides, optional, in_
from attr._compat import TYPE
from attr._make import attributes, attr

Expand Down Expand Up @@ -214,3 +214,37 @@ def test_repr(self, validator):
"<{type} 'int'>> or None>")
.format(type=TYPE)
) == repr(v)


class TestIn_(object):
"""
Tests for `in_`.
"""
def test_success_with_value(self):
"""
If the value is in our options, nothing happens.
"""
v = in_([1, 2, 3])
a = simple_attr("test")
v(1, a, 3)

def test_fail(self):
"""
Raise ValueError if the value is outside our options.
"""
v = in_([1, 2, 3])
a = simple_attr("test")
with pytest.raises(ValueError) as e:
v(None, a, None)
assert (
"'test' must be in [1, 2, 3] (got None)",
) == e.value.args

def test_repr(self):
"""
Returned validator has a useful `__repr__`.
"""
v = in_([3, 4, 5])
assert(
("<in_ validator with options [3, 4, 5]>")
) == repr(v)

0 comments on commit 461e1b2

Please sign in to comment.