diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 4bf43254..277ba606 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -52,6 +52,9 @@ Changes: `#128 `_ - ``__attrs_post_init__()`` is now run if validation is disabled. `#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 `_ - Added ``attr.validators.and_()`` that composes multiple validators into one. `#161 `_ - For convenience, the ``validator`` argument of ``@attr.s`` now can take a ``list`` of validators that are wrapped using ``and_()``. diff --git a/docs/api.rst b/docs/api.rst index 53d01cc1..43e57f1e 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -270,6 +270,33 @@ Validators ... TypeError: ("'x' must be (got None that is a ).", Attribute(name='x', default=NOTHING, validator=>, repr=True, cmp=True, hash=None, init=True), , 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=, val=1) + >>> C("on", 1) + Traceback (most recent call last): + ... + ValueError: 'state' must be in (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. @@ -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: diff --git a/src/attr/validators.py b/src/attr/validators.py index 8224ca4f..4474cb2b 100644 --- a/src/attr/validators.py +++ b/src/attr/validators.py @@ -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) @@ -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) @@ -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 ( + "" + .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) diff --git a/tests/test_validators.py b/tests/test_validators.py index 22e2130f..2b8f4ed6 100644 --- a/tests/test_validators.py +++ b/tests/test_validators.py @@ -7,7 +7,7 @@ from __future__ import absolute_import, division, print_function 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 @@ -214,3 +214,37 @@ class TestOptional(object): "<{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( + ("") + ) == repr(v)