Add instance_of validator

This commit is contained in:
Hynek Schlawack 2015-01-29 12:20:17 +01:00
parent 69a460d336
commit 692e4613a0
12 changed files with 220 additions and 24 deletions

View File

@ -12,6 +12,7 @@ from ._make import (
_add_methods,
_make_attr,
)
from . import validators
__version__ = "15.0.0.dev0"
__author__ = "Hynek Schlawack"
@ -31,4 +32,5 @@ __all__ = [
"ls",
"s",
"to_dict",
"validators",
]

View File

@ -184,8 +184,11 @@ def _attrs_to_script(attrs):
"{name})"
.format(name=a.name))
if a.default_value is not NOTHING:
args.append("{name}={default!r}".format(name=a.name,
default=a.default_value))
args.append(
"{name}=attr_dict['{name}'].default_value".format(
name=a.name,
)
)
lines.append("self.{name} = {name}".format(name=a.name))
elif a.default_factory is not NOTHING:
args.append("{name}=NOTHING".format(name=a.name))

View File

@ -80,9 +80,12 @@ def _make_attr(default_value=NOTHING, default_factory=NOTHING, validator=None):
value is passed while instantiating.
:type default_factory: callable
:param validator: :func:`callable` that is called on the attribute
if an ``attrs``-generated ``__init__`` is used. The return value is
*not* inspected so the validator has to throw an exception itself.
:param validator: :func:`callable` that is called within
``attrs``-generated ``__init__`` methods with the :class:`Attribute` as
the first parameter and the passed value as the second parameter.
The return value is *not* inspected so the validator has to throw an
exception itself.
:type validator: callable
"""
if default_value is not NOTHING and default_factory is not NOTHING:
@ -94,7 +97,7 @@ def _make_attr(default_value=NOTHING, default_factory=NOTHING, validator=None):
return _CountingAttr(
default_value=default_value,
default_factory=default_factory,
validator=None,
validator=validator,
)
@ -143,10 +146,12 @@ def _add_methods(maybe_cl=None, add_repr=True, add_cmp=True, add_hash=True,
# as `@_add_methods()`.
if isinstance(maybe_cl, type):
cl = maybe_cl
cl.__attrs_attrs__ = [
Attribute.from_counting_attr(name=name, ca=ca)
for name, ca in _get_attrs(cl)
]
cl.__attrs_attrs__ = []
# Replace internal `_CountingAttr`s with `Attribute`s.
for name, ca in _get_attrs(cl):
a = Attribute.from_counting_attr(name=name, ca=ca)
cl.__attrs_attrs__.append(a)
setattr(cl, name, a)
return _add_init(_add_hash(_add_cmp(_add_repr(cl))))
else:
def wrap(cl):

41
attr/validators.py Normal file
View File

@ -0,0 +1,41 @@
# -*- coding: utf-8 -*-
from __future__ import absolute_import, division, print_function
class InstanceValidator(object):
# We use a callable class to be able to change the ``__repr__``.
def __init__(self, type_):
self.type_ = type_
def __call__(self, attr, value):
if not isinstance(value, self.type_):
raise TypeError(
"'{name}' must be {type!r} (got {value!r} that is a "
"{actual!r})."
.format(name=attr.name, type=self.type_,
actual=value.__class__, value=value),
attr, self.type_, value,
)
def __repr__(self):
return (
"<instance_of validator for type {type!r}>"
.format(type=self.type_)
)
def instance_of(type_):
"""
A validator that raises a :exc:`TypeError` if the initializer is called
with a wrong type for this particular attribute (checks are perfomed using
:func:`isinstance`).
:param type_: The type to check for.
:type type_: type
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 InstanceValidator(type_)

View File

@ -31,6 +31,24 @@ Core
``attrs`` also comes with a less playful alias ``attr.attr``.
.. autoclass:: attr.Attribute
Instances of this class are frequently used for introspection purposes like:
- Class attributes on ``attrs``-decorated classes.
- :func:`ls` returns a list of them.
- Validators get them passed as the first argument.
.. doctest::
>>> import attr
>>> @attr.s
... class C(object):
... x = attr.ib()
>>> C.x
Attribute(name='x', default_value=NOTHING, default_factory=NOTHING, validator=None)
Helpers
-------
@ -42,7 +60,6 @@ Helpers
.. doctest::
>>> import attr
>>> @attr.s
... class C(object):
... x = attr.ib()
@ -80,6 +97,27 @@ Helpers
{'y': {'y': 3, 'x': 2}, 'x': 1}
.. autoclass:: attr.Attribute
.. _api_validators:
This class is only interesting because it is returned by :func:`ls`.
Validators
----------
``attrs`` comes with some common validators within the ``attrs.validators`` module:
.. autofunction:: attr.validators.instance_of
For example:
.. doctest::
>>> @attr.s
... class C(object):
... x = attr.ib(validator=attr.validators.instance_of(int))
>>> C(42)
C(x=42)
>>> C("42")
Traceback (most recent call last):
...
TypeError: ("'x' must be <type 'int'> (got '42' that is a <type 'str'>).", Attribute(name='x', default_value=NOTHING, default_factory=NOTHING, validator=<instance_of validator for type <type 'int'>>), <type 'int'>, '42')

View File

@ -100,3 +100,22 @@ And sometimes you even want mutable objects as default values (ever used acciden
ConnectionPool(db_string='postgres://localhost', pool=deque([Connection(socket=42)]), debug=False)
More information on why class methods for constructing objects are awesome can be found in this insightful `blog post <http://as.ynchrono.us/2014/12/asynchronous-object-initialization.html>`_.
Although your initializers should be a dumb as possible, it can come handy to do some kind of validation on the arguments.
That's when :func:`attr.ib`\ s ``validator`` argument comes into play.
A validator is simply a callable that takes two arguments: the attribute that it's validating
.. doctest::
>>> @attr.s
... class C(object):
... x = attr.ib(validator=attr.validators.instance_of(int))
>>> C(42)
C(x=42)
>>> C("42")
Traceback (most recent call last):
...
TypeError: ("'x' must be <type 'int'> (got '42' that is a <type 'str'>).", Attribute(name='x', default_value=NOTHING, default_factory=NOTHING, validator=<instance_of validator for type <type 'int'>>), <type 'int'>, '42')
``attrs`` ships with a bunch of validators, make sure to :ref:`check them out <api_validators>` before writing your own!

View File

@ -0,0 +1,21 @@
# -*- coding: utf-8 -*-
from __future__ import absolute_import, division, print_function
import sys
PY3 = sys.version_info[0] == 3
# TYPE is used in exceptions, repr(int) is differnt on Python 2 and 3.
TYPE = "class" if PY3 else "type"
def simple_attr(name):
"""
Return an attribute with a name and no other bells and whistles.
"""
from attr import Attribute
from attr._dunders import NOTHING
return Attribute(name=name, default_value=NOTHING, default_factory=NOTHING,
validator=None)

View File

@ -2,15 +2,18 @@
from __future__ import absolute_import, division, print_function
import pytest
import attr
from attr._make import Attribute, NOTHING
from . import TYPE
@attr.s
class C1(object):
x = attr.ib()
x = attr.ib(validator=attr.validators.instance_of(int))
y = attr.ib()
@ -46,3 +49,15 @@ class TestDarkMagic(object):
"x": 1,
"y": 2,
} == attr.to_dict(C1(x=1, y=2))
def test_validator(self):
"""
`instance_of` raises `TypeError` on type mismatch.
"""
with pytest.raises(TypeError) as e:
C1("1", 2)
assert (
"'x' must be <{type} 'int'> (got '1' that is a <{type} "
"'str'>).".format(type=TYPE),
C1.x, int, "1",
) == e.value.args

View File

@ -4,6 +4,7 @@ from __future__ import absolute_import, division, print_function
import pytest
from . import simple_attr
from attr._make import Attribute
from attr._dunders import (
NOTHING,
@ -14,14 +15,6 @@ from attr._dunders import (
)
def simple_attr(name):
"""
Return an attribute with a name and no other bells and whistles.
"""
return Attribute(name=name, default_value=NOTHING, default_factory=NOTHING,
validator=None)
def make_class():
"""
Return a new simple class.
@ -269,4 +262,4 @@ class TestAddInit(object):
with pytest.raises(VException) as e:
C(42)
assert ((a, 42),) == e.value.args
assert ((a, 42,),) == e.value.args

View File

@ -1,5 +1,9 @@
# -*- coding: utf-8 -*-
"""
Tests for `attr._funcs`.
"""
from __future__ import absolute_import, division, print_function
import pytest

View File

@ -46,7 +46,6 @@ class TestGetAttrs(object):
"""
Returns attributes in correct order.
"""
@_add_methods
class C(object):
z = _make_attr()
y = _make_attr()

56
tests/test_validators.py Normal file
View File

@ -0,0 +1,56 @@
# -*- coding: utf-8 -*-
"""
Tests for `attr.validators`.
"""
from __future__ import absolute_import, division, print_function
import pytest
from attr.validators import instance_of
from . import simple_attr, TYPE
class TestInstanceOf(object):
"""
Tests for `instance_of`.
"""
def test_success(self):
"""
Nothing happens if types match.
"""
v = instance_of(int)
v(simple_attr("test"), 42)
def test_subclass(self):
"""
Subclasses are accepted too.
"""
v = instance_of(int)
v(simple_attr("test"), True) # yep, bools are a subclass of int :(
def test_fail(self):
"""
Raises `TypeError` on wrong types.
"""
v = instance_of(int)
a = simple_attr("test")
with pytest.raises(TypeError) as e:
v(a, "42")
assert (
"'test' must be <{type} 'int'> (got '42' that is a <{type} "
"'str'>).".format(type=TYPE),
a, int, "42",
) == e.value.args
def test_repr(self):
"""
Returned validator has a useful `__repr__`.
"""
v = instance_of(int)
assert (
"<instance_of validator for type <{type} 'int'>>"
.format(type=TYPE)
) == repr(v)