Skip to content

Commit

Permalink
Add validators
Browse files Browse the repository at this point in the history
  • Loading branch information
hynek committed Jan 28, 2015
1 parent d291e8c commit 372c553
Show file tree
Hide file tree
Showing 6 changed files with 110 additions and 29 deletions.
3 changes: 3 additions & 0 deletions attr/_dunders.py
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,9 @@ def _attrs_to_script(attrs):
lines = []
args = []
for a in attrs:
if a.validator is not None:
lines.append("attr_dict['{name}'].validator({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))
Expand Down
40 changes: 32 additions & 8 deletions attr/_make.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,54 +17,77 @@ class Attribute(object):
*Read-only* representation of an attribute.
:attribute name: The name of the attribute.
:attribute default_value: A value that is used if an ``attrs``-generated
:attribute default_value: Value that is used if an ``attrs``-generated
``__init__`` is used and no value is passed while instantiating.
:attribute default_factory: A :func:`callable` that is called to obtain
:attribute default_factory: :func:`callable` that is called to obtain
a default value if an ``attrs``-generated ``__init__`` is used and no
value is passed while instantiating.
:attribute validator: :func:`callable` that is called on the attribute
if an ``attrs``-generated ``__init__`` is used.
"""
def __init__(self, name, default_value, default_factory):
def __init__(self, name, default_value, default_factory, validator):
self.name = name
self.default_value = default_value
self.default_factory = default_factory
self.validator = validator

@classmethod
def from_counting_attr(cl, name, ca):
return cl(
name=name,
default_value=ca.default_value,
default_factory=ca.default_factory,
validator=ca.validator
)


_a = [Attribute(name=name, default_value=NOTHING, default_factory=NOTHING)
_a = [Attribute(name=name, default_value=NOTHING, default_factory=NOTHING,
validator=None)
for name in ("name", "default_value", "default_factory",)]
Attribute = _add_cmp(_add_repr(Attribute, attrs=_a), attrs=_a)


class _CountingAttr(object):
__attrs_attrs__ = [
Attribute(name=name, default_value=NOTHING, default_factory=NOTHING)
Attribute(name=name, default_value=NOTHING, default_factory=NOTHING,
validator=None)
for name
in ("counter", "default_value", "default_factory",)
]
counter = 0

def __init__(self, default_value=NOTHING, default_factory=NOTHING):
def __init__(self, default_value, default_factory, validator):
_CountingAttr.counter += 1
self.counter = _CountingAttr.counter
self.default_value = default_value
self.default_factory = default_factory
self.validator = validator


_CountingAttr = _add_cmp(_add_repr(_CountingAttr))


def _make_attr(default_value=NOTHING, default_factory=NOTHING):
def _make_attr(default_value=NOTHING, default_factory=NOTHING, validator=None):
"""
Create a new attribute on a class.
Does nothing unless the class is also decorated with :func:`attr.s`!
.. warning::
Does nothing unless the class is also decorated with :func:`attr.s`!
:param default_value: Value that is used if an ``attrs``-generated
``__init__`` is used and no value is passed while instantiating.
:type default_value: Any value.
:param default_factory: :func:`callable` that is called to obtain
a default value if an ``attrs``-generated ``__init__`` is used and no
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.
:type validator: callable
"""
if default_value is not NOTHING and default_factory is not NOTHING:
raise ValueError(
Expand All @@ -75,6 +98,7 @@ def _make_attr(default_value=NOTHING, default_factory=NOTHING):
return _CountingAttr(
default_value=default_value,
default_factory=default_factory,
validator=None,
)


Expand Down
2 changes: 1 addition & 1 deletion docs/api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ Helpers
... x = attr.ib()
... y = attr.ib()
>>> attr.ls(C)
[Attribute(name='x', default_value=NOTHING, default_factory=NOTHING), Attribute(name='y', default_value=NOTHING, default_factory=NOTHING)]
[Attribute(name='x', default_value=NOTHING, default_factory=NOTHING), Attribute(name='y', default_value=NOTHING, default_factory=NOTHING, validator=None)]


.. autofunction:: attr.has
Expand Down
2 changes: 1 addition & 1 deletion docs/why.rst
Original file line number Diff line number Diff line change
Expand Up @@ -186,7 +186,7 @@ So why the self-fork?
Basically after nearly a year of usage I ran into annoyances and regretted certain decision I made early-on to make too many people happy.
In the end, *I* wasn't happy using it anymore.

So I learned my lesson and ``attrib`` is the result of that.
So I learned my lesson and ``attrs`` is the result of that.

.. note::
Nevertheless, ``characteristic`` is **not** dead.
Expand Down
6 changes: 4 additions & 2 deletions tests/test_dark_magic.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,10 @@ def test_ls(self):
`attr.ls` works.
"""
assert [
Attribute(name="x", default_value=None, default_factory=NOTHING),
Attribute(name="y", default_value=NOTHING, default_factory=list),
Attribute(name="x", default_value=None, default_factory=NOTHING,
validator=None),
Attribute(name="y", default_value=NOTHING, default_factory=list,
validator=None),
] == attr.ls(C2)

def test_to_dict(self):
Expand Down
86 changes: 69 additions & 17 deletions tests/test_dunders.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

from __future__ import absolute_import, division, print_function

import pytest

from attr._make import Attribute
from attr._dunders import (
NOTHING,
Expand All @@ -13,10 +15,17 @@


def simple_attr(name):
return Attribute(name=name, default_value=NOTHING, default_factory=NOTHING)
"""
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.
"""
class C(object):
__attrs_attrs__ = [simple_attr("a"), simple_attr("b")]

Expand Down Expand Up @@ -54,6 +63,9 @@ def returns_distinct_classes(self):


class TestAddCmp(object):
"""
Tests for `_add_cmp`.
"""
def test_equal(self):
"""
Equal objects are detected as equal.
Expand Down Expand Up @@ -153,6 +165,9 @@ def test_ge_unordable(self):


class TestAddRepr(object):
"""
Tests for `_add_repr`.
"""
def test_repr(self):
"""
Test repr returns a sensible value.
Expand All @@ -161,6 +176,9 @@ def test_repr(self):


class TestAddHash(object):
"""
Tests for `_add_hash`.
"""
def test_hash(self):
"""
__hash__ returns different hashes for different values.
Expand All @@ -169,6 +187,9 @@ def test_hash(self):


class TestAddInit(object):
"""
Tests for `_add_init`.
"""
def test_sets_attributes(self):
"""
The attributes are initialized using the passed keywords.
Expand All @@ -182,16 +203,20 @@ def test_default_value(self):
If a default value is present, it's used as fallback.
"""
class C(object):
__attrs_attrs__ = [Attribute("a",
default_value=2,
default_factory=NOTHING),
Attribute("b",
default_value="hallo",
default_factory=NOTHING),
Attribute("c",
default_value=None,
default_factory=NOTHING),
]
__attrs_attrs__ = [
Attribute("a",
default_value=2,
default_factory=NOTHING,
validator=None,),
Attribute("b",
default_value="hallo",
default_factory=NOTHING,
validator=None,),
Attribute("c",
default_value=None,
default_factory=NOTHING,
validator=None,),
]

C = _add_init(C)
i = C()
Expand All @@ -207,13 +232,40 @@ class D(object):
pass

class C(object):
__attrs_attrs__ = [Attribute("a",
default_value=NOTHING,
default_factory=list),
Attribute("b",
default_value=NOTHING,
default_factory=D)]
__attrs_attrs__ = [
Attribute("a",
default_value=NOTHING,
default_factory=list,
validator=None,),
Attribute("b",
default_value=NOTHING,
default_factory=D,
validator=None,)
]
C = _add_init(C)
i = C()
assert [] == i.a
assert isinstance(i.b, D)

def test_validator(self):
"""
If a validator is passed, call it on the argument.
"""
class VException(Exception):
pass

def raiser(arg):
raise VException(arg)

class C(object):
__attrs_attrs__ = [
Attribute("a",
default_value=NOTHING,
default_factory=NOTHING,
validator=raiser),
]
C = _add_init(C)

with pytest.raises(VException) as e:
C(42)
assert (42,) == e.value.args

0 comments on commit 372c553

Please sign in to comment.