Skip to content

list of validator #158

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

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,10 @@ Changes:
- Validators can now be defined conveniently inline by using the attribute as a decorator.
Check out the `examples <https://attrs.readthedocs.io/en/stable/examples.html#validators>`_ to see it in action!
`#143 <https://github.com/python-attrs/attrs/issues/143>`_

- New ``list_of`` validator ensures that an attribute is a homogeneous list with elements of a specified class or subclass.
See `examples <https://attrs.readthedocs.io/en/stable/examples.html#validators>`_.
`#157 <https://github.com/python-attrs/attrs/issues/157>`_


----

Expand Down
30 changes: 30 additions & 0 deletions docs/api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -257,6 +257,36 @@ 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.list_of


For example:

.. doctest::

>>> @attr.s
... class C(object):
... x = attr.ib(validator=attr.validators.list_of(int))

>>> C([42])
C(x=[42])

>>> C(42)
Traceback (most recent call last):
...
TypeError: ("'x' must be of type <class 'list'> (got a <class 'int'>).", Attribute(name='x', default=NOTHING, validator=<list_of validator for type <class 'int'>>, repr=True, cmp=True, hash=None, init=True, convert=None, metadata=mappingproxy({})), <class 'int'>, 42)

>>> C(["42"])
Traceback (most recent call last):
...
TypeError: ("'x' must be a list of <class 'int'> elements.", Attribute(name='x', default=NOTHING, validator=<list_of validator for type <class 'int'>>, repr=True, cmp=True, hash=None, init=True, convert=None, metadata=mappingproxy({})), <class 'int'>, ['42'])

>>> C([None])
Traceback (most recent call last):
...
TypeError: ("'x' must be a list of <class 'int'> elements.", Attribute(name='x', default=NOTHING, validator=<list_of validator for type <class 'int'>>, repr=True, cmp=True, hash=None, init=True, convert=None, metadata=mappingproxy({})), <class 'int'>, [None])


.. autofunction:: attr.validators.provides

.. autofunction:: attr.validators.optional
Expand Down
47 changes: 47 additions & 0 deletions src/attr/validators.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,53 @@ def instance_of(type):
return _InstanceOfValidator(type)


@attributes(repr=False, slots=True)
class _ListOfValidator(object):
type = attr()

def __call__(self, inst, attr, list_of_values):
"""
We use a callable class to be able to change the ``__repr__``.
"""
if not isinstance(list_of_values, list):
raise TypeError(
"'{name}' must be of type {type!r} (got a {actual!r})."
.format(name=attr.name, type=list,
actual=list_of_values.__class__),
attr, self.type, list_of_values,
)
if not all(isinstance(value, self.type) for value in list_of_values):
raise TypeError(
"'{name}' must be a list of {type!r} elements."
.format(name=attr.name, type=self.type),
attr, self.type, list_of_values,
)

def __repr__(self):
return (
"<list_of validator for type {type!r}>"
.format(type=self.type)
)


def list_of(type):
"""A validator that raises a :exc:`TypeError` if the initializer is
called with a non-list type or with a list that contains one or
more elements of a wrong type. None values are not permitted. As
with instance_of, checks are perfomed using :func:`isinstance`
therefore it's also valid to pass a tuple of types.

: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.

"""
return _ListOfValidator(type)


@attributes(repr=False, slots=True)
class _ProvidesValidator(object):
interface = attr()
Expand Down
63 changes: 62 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 instance_of, provides, optional
from attr.validators import instance_of, list_of, provides, optional
from attr._compat import TYPE

from .utils import simple_attr
Expand Down Expand Up @@ -58,6 +58,67 @@ def test_repr(self):
) == repr(v)


class TestListOf(object):
"""
Tests for `list_of`.
"""
def test_success(self):
"""
Nothing happens if types match.
"""
v = list_of(int)
v(None, simple_attr("test"), [42, 55])

def test_subclass(self):
"""
Subclasses are accepted too.
"""
class MyInt(int):
pass
v = list_of(int)
v(None, simple_attr("test"), [MyInt(42)])

def test_fail_not_a_list(self):
"""
Raises `TypeError` on wrong types.
"""
v = list_of(int)
a = simple_attr("test")
with pytest.raises(TypeError) as e:
v(None, a, 42)
assert (
"'test' must be of type <{type} 'list'>"
" (got a <{type} 'int'>).".format(type=TYPE),
a, int, 42,

) == e.value.args

def test_fail_list_of_wrong_type(self):
"""
Raises `TypeError` on wrong types.
"""
v = list_of(int)
a = simple_attr("test")
with pytest.raises(TypeError) as e:
v(None, a, ["42"])
assert (
"'test' must be a list of <{type}"
" 'int'> elements.".format(type=TYPE),
a, int, ["42"],

) == e.value.args

def test_repr(self):
"""
Returned validator has a useful `__repr__`.
"""
v = list_of(int)
assert (
"<list_of validator for type <{type} 'int'>>"
.format(type=TYPE)
) == repr(v)


class IFoo(zope.interface.Interface):
"""
An interface.
Expand Down