Add instance_of validator
This commit is contained in:
parent
69a460d336
commit
692e4613a0
|
@ -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",
|
||||
]
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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_)
|
44
docs/api.rst
44
docs/api.rst
|
@ -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')
|
||||
|
|
|
@ -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!
|
||||
|
|
|
@ -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)
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -1,5 +1,9 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
Tests for `attr._funcs`.
|
||||
"""
|
||||
|
||||
from __future__ import absolute_import, division, print_function
|
||||
|
||||
import pytest
|
||||
|
|
|
@ -46,7 +46,6 @@ class TestGetAttrs(object):
|
|||
"""
|
||||
Returns attributes in correct order.
|
||||
"""
|
||||
@_add_methods
|
||||
class C(object):
|
||||
z = _make_attr()
|
||||
y = _make_attr()
|
||||
|
|
|
@ -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)
|
Loading…
Reference in New Issue