Skip to content

Commit

Permalink
Make and_ validator a first-class API and use it in optional
Browse files Browse the repository at this point in the history
  • Loading branch information
hynek committed May 11, 2017
1 parent 000a56a commit b5cc368
Show file tree
Hide file tree
Showing 4 changed files with 85 additions and 21 deletions.
4 changes: 3 additions & 1 deletion CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +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>`_
- The ``validator`` argument of ``@attr.s`` now can take a ``list`` of validators that all have to pass.
- 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_()``.
`#138 <https://github.com/python-attrs/attrs/issues/138>`_
- Accordingly, ``attr.validators.optional()`` now can take a ``list`` of validators too.
`#161 <https://github.com/python-attrs/attrs/issues/161>`_
Expand Down
8 changes: 8 additions & 0 deletions docs/api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -262,6 +262,14 @@ 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.and_

For convenience, it's also possible to pass a list to :func:`attr.ib`'s validator argument.

Thus the following two statements are equivalent::

x = attr.ib(validator=attr.validators.and_(v1, v2, v3))
x = attr.ib(validator=[v1, v2, v3])

.. autofunction:: attr.validators.provides

Expand Down
30 changes: 21 additions & 9 deletions src/attr/validators.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,19 @@

from __future__ import absolute_import, division, print_function

from ._make import attr, attributes
from ._make import attr, attributes, _AndValidator


def and_(*validators):
"""
A validator that composes multiple validators into one.
When called on a value, it runs all wrapped validators.
:param validators: Arbitrary number of validators.
:type validators: callables
"""
return _AndValidator(validators)


@attributes(repr=False, slots=True)
Expand Down Expand Up @@ -79,7 +91,8 @@ 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
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.
"""
Expand All @@ -88,19 +101,18 @@ def provides(interface):

@attributes(repr=False, slots=True)
class _OptionalValidator(object):
validators = attr()
validator = attr()

def __call__(self, inst, attr, value):
if value is None:
return

for v in self.validators:
v(inst, attr, value)
self.validator(inst, attr, value)

def __repr__(self):
return (
"<optional validator for {type} or None>"
.format(type=repr(self.validators))
"<optional validator for {what} or None>"
.format(what=repr(self.validator))
)


Expand All @@ -117,5 +129,5 @@ def optional(validator):
.. versionchanged:: 17.1.0 *validator* can be a list of validators.
"""
if isinstance(validator, list):
return _OptionalValidator(tuple(validator))
return _OptionalValidator((validator,))
return _OptionalValidator(_AndValidator(validator))
return _OptionalValidator(validator)
64 changes: 53 additions & 11 deletions tests/test_validators.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,9 @@
import pytest
import zope.interface

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

from .utils import simple_attr

Expand Down Expand Up @@ -58,6 +59,53 @@ def test_repr(self):
) == repr(v)


def always_pass(_, __, ___):
"""
Toy validator that always passses.
"""


def always_fail(_, __, ___):
"""
Toy validator that always fails.
"""
0/0


class TestAnd(object):
def test_success(self):
"""
Succeeds if all wrapped validators succeed.
"""
v = and_(instance_of(int), always_pass)

v(None, simple_attr("test"), 42)

def test_fail(self):
"""
Fails if any wrapped validator fails.
"""
v = and_(instance_of(int), always_fail)

with pytest.raises(ZeroDivisionError):
v(None, simple_attr("test"), 42)

def test_sugar(self):
"""
`and_(v1, v2, v3)` and `[v1, v2, v3]` are equivalent.
"""
@attributes
class C(object):
a1 = attr("a1", validator=and_(
instance_of(int),
))
a2 = attr("a2", validator=[
instance_of(int),
])

assert C.__attrs_attrs__[0].validator == C.__attrs_attrs__[1].validator


class IFoo(zope.interface.Interface):
"""
An interface.
Expand Down Expand Up @@ -111,12 +159,6 @@ def test_repr(self):
) == repr(v)


def always_pass(_, __, ___):
"""
Toy validator that always passses.
"""


@pytest.mark.parametrize("validator", [
instance_of(int),
[always_pass, instance_of(int)],
Expand Down Expand Up @@ -162,13 +204,13 @@ def test_repr(self, validator):

if isinstance(validator, list):
assert (
("<optional validator for ({func}, <instance_of validator for "
"type <{type} 'int'>>) or None>")
("<optional validator for _AndValidator(_validators=[{func}, "
"<instance_of validator for type <{type} 'int'>>]) or None>")
.format(func=repr(always_pass), type=TYPE)
) == repr(v)
else:
assert (
("<optional validator for (<instance_of validator for type "
"<{type} 'int'>>,) or None>")
("<optional validator for <instance_of validator for type "
"<{type} 'int'>> or None>")
.format(type=TYPE)
) == repr(v)

0 comments on commit b5cc368

Please sign in to comment.