Allow single attributes to be excluded

This commit is contained in:
Hynek Schlawack 2015-01-30 17:48:34 +01:00
parent 943d686509
commit e7ebf4bf81
8 changed files with 143 additions and 68 deletions

View File

@ -7,6 +7,7 @@ import sys
PY3 = sys.version_info[0] == 3
PY2 = sys.version_info[0] == 2
PY26 = sys.version_info[0:2] == (2, 6)
if PY2:

View File

@ -36,7 +36,8 @@ Sentinel to indicate the lack of a value when ``None`` is ambiguous.
"""
def attr(default=NOTHING, validator=None):
def attr(default=NOTHING, validator=None, no_repr=False, no_cmp=False,
no_hash=False, no_init=False):
"""
Create a new attribute on a class.
@ -56,10 +57,27 @@ def attr(default=NOTHING, validator=None):
The return value is *not* inspected so the validator has to throw an
exception itself.
:type validator: callable
:param no_repr: Exclude this attribute when generating a ``__repr__``.
:type no_repr: bool
:param no_cmp: Exclude this attribute when generating comparison methods
(``__eq__`` et al).
:type no_cmp: bool
:param no_hash: Exclude this attribute when generating a ``__hash__``.
:type no_hash: bool
:param no_init: Exclude this attribute when generating a ``__init__``.
:type no_init: bool
"""
return _CountingAttr(
default=default,
validator=validator,
no_repr=no_repr,
no_cmp=no_cmp,
no_hash=no_hash,
no_init=no_init,
)
@ -180,7 +198,7 @@ def _attrs_to_tuple(obj, attrs):
def _add_hash(cl, attrs=None):
if attrs is None:
attrs = cl.__attrs_attrs__
attrs = [a for a in cl.__attrs_attrs__ if not a.no_hash]
def hash_(self):
"""
@ -194,7 +212,7 @@ def _add_hash(cl, attrs=None):
def _add_cmp(cl, attrs=None):
if attrs is None:
attrs = cl.__attrs_attrs__
attrs = [a for a in cl.__attrs_attrs__ if not a.no_cmp]
def attrs_to_tuple(obj):
"""
@ -269,7 +287,7 @@ def _add_cmp(cl, attrs=None):
def _add_repr(cl, attrs=None):
if attrs is None:
attrs = cl.__attrs_attrs__
attrs = [a for a in cl.__attrs_attrs__ if not a.no_repr]
def repr_(self):
"""
@ -285,7 +303,7 @@ def _add_repr(cl, attrs=None):
def _add_init(cl):
attrs = cl.__attrs_attrs__
attrs = [a for a in cl.__attrs_attrs__ if not a.no_init]
# We cache the generated init methods for the same kinds of attributes.
sha1 = hashlib.sha1()
@ -371,7 +389,8 @@ class Attribute(object):
:attribute validator: see :func:`attr.ib`
"""
_attributes = [
"name", "default", "validator",
"name", "default", "validator", "no_repr", "no_cmp", "no_hash",
"no_init",
] # we can't use ``attrs`` so we have to cheat a little.
def __init__(self, **kw):
@ -392,7 +411,8 @@ class Attribute(object):
if k != "name"))
_a = [Attribute(name=name, default=NOTHING, validator=None)
_a = [Attribute(name=name, default=NOTHING, validator=None, no_repr=False,
no_cmp=False, no_hash=False, no_init=False)
for name in Attribute._attributes]
Attribute = _add_hash(
_add_cmp(_add_repr(Attribute, attrs=_a), attrs=_a), attrs=_a
@ -401,17 +421,22 @@ Attribute = _add_hash(
class _CountingAttr(object):
__attrs_attrs__ = [
Attribute(name=name, default=NOTHING, validator=None)
Attribute(name=name, default=NOTHING, validator=None, no_repr=False,
no_cmp=False, no_hash=False, no_init=False)
for name
in ("counter", "default",)
in ("counter", "default", "no_repr", "no_cmp", "no_hash", "no_init",)
]
counter = 0
def __init__(self, default, validator):
def __init__(self, default, validator, no_repr, no_cmp, no_hash, no_init):
_CountingAttr.counter += 1
self.counter = _CountingAttr.counter
self.default = default
self.validator = validator
self.no_repr = no_repr
self.no_cmp = no_cmp
self.no_hash = no_hash
self.no_init = no_init
_CountingAttr = _add_cmp(_add_repr(_CountingAttr))

View File

@ -50,7 +50,7 @@ Core
... class C(object):
... x = attr.ib()
>>> C.x
Attribute(name='x', default=NOTHING, validator=None)
Attribute(name='x', default=NOTHING, validator=None, no_repr=False, no_cmp=False, no_hash=False, no_init=False)
.. autofunction:: attr.make_class
@ -98,7 +98,7 @@ Helpers
... x = attr.ib()
... y = attr.ib()
>>> attr.fields(C)
[Attribute(name='x', default=NOTHING, validator=None), Attribute(name='y', default=NOTHING, validator=None)]
[Attribute(name='x', default=NOTHING, validator=None, no_repr=False, no_cmp=False, no_hash=False, no_init=False), Attribute(name='y', default=NOTHING, validator=None, no_repr=False, no_cmp=False, no_hash=False, no_init=False)]
.. autofunction:: attr.has

View File

@ -228,3 +228,14 @@ You can still have power over the attributes if you pass a dictionary of name: `
42
>>> i.y
[]
Finally, you can exclude single attributes from certain methods:
.. doctest::
>>> @attr.s
... class C(object):
... user = attr.ib()
... password = attr.ib(no_repr=True)
>>> C("me", "s3kr3t")
C(user='me')

View File

@ -2,11 +2,44 @@
from __future__ import absolute_import, division, print_function
from attr import Attribute
from attr._make import NOTHING, make_class
def simple_attr(name):
def simple_class(add_cmp=False, add_repr=False, add_hash=False):
"""
Return a new simple class.
"""
return make_class(
"C", ["a", "b"],
add_cmp=add_cmp, add_repr=add_repr, add_hash=add_hash,
add_init=True,
)
def simple_attr(name, default=NOTHING, validator=None, no_repr=False,
no_cmp=False, no_hash=False, no_init=False):
"""
Return an attribute with a name and no other bells and whistles.
"""
from attr import Attribute
from attr._make import NOTHING
return Attribute(name=name, default=NOTHING, validator=None)
return Attribute(
name=name, default=default, validator=validator, no_repr=no_repr,
no_cmp=no_cmp, no_hash=no_hash, no_init=no_init
)
class TestSimpleClass(object):
"""
Tests for the testing helper function `make_class`.
"""
def test_returns_class(self):
"""
Returns a class object.
"""
assert type is simple_class().__class__
def returns_distinct_classes(self):
"""
Each call returns a completely new class.
"""
assert simple_class() is not simple_class()

View File

@ -34,8 +34,11 @@ class TestDarkMagic(object):
`attr.fields` works.
"""
assert [
Attribute(name="x", default=foo, validator=None),
Attribute(name="y", default=attr.Factory(list), validator=None),
Attribute(name="x", default=foo, validator=None, no_repr=False,
no_cmp=False, no_hash=False, no_init=False),
Attribute(name="y", default=attr.Factory(list), validator=None,
no_repr=False, no_cmp=False, no_hash=False,
no_init=False),
] == attr.fields(C2)
def test_asdict(self):
@ -89,6 +92,8 @@ class TestDarkMagic(object):
"""
PC = attr.make_class("PC", ["a", "b"])
assert [
Attribute(name="a", default=NOTHING, validator=None),
Attribute(name="b", default=NOTHING, validator=None),
Attribute(name="a", default=NOTHING, validator=None, no_repr=False,
no_cmp=False, no_hash=False, no_init=False),
Attribute(name="b", default=NOTHING, validator=None, no_repr=False,
no_cmp=False, no_hash=False, no_init=False),
] == attr.fields(PC)

View File

@ -10,30 +10,20 @@ import copy
import pytest
from . import simple_attr
from . import simple_attr, simple_class
from attr._compat import PY26
from attr._make import (
Attribute,
Factory,
NOTHING,
attr,
make_class,
_Nothing,
_add_init,
_add_repr,
attr,
make_class,
)
from attr.validators import instance_of
def simple_class(add_cmp=False, add_repr=False, add_hash=False):
"""
Return a new simple class.
"""
return make_class(
"C", ["a", "b"],
add_cmp=add_cmp, add_repr=add_repr, add_hash=add_hash,
add_init=True,
)
CmpC = simple_class(add_cmp=True)
ReprC = simple_class(add_repr=True)
HashC = simple_class(add_hash=True)
@ -45,27 +35,18 @@ class InitC(object):
InitC = _add_init(InitC)
class TestSimpleClass(object):
"""
Tests for the testing helper function `make_class`.
"""
def test_returns_class(self):
"""
Returns a class object.
"""
assert type is simple_class().__class__
def returns_distinct_classes(self):
"""
Each call returns a completely new class.
"""
assert simple_class() is not simple_class()
class TestAddCmp(object):
"""
Tests for `_add_cmp`.
"""
def test_no_cmp(self):
"""
If `no_cmp` is set, ignore that attribute.
"""
C = make_class("C", {"a": attr(no_cmp=True), "b": attr()})
assert C(1, 2) == C(2, 2)
def test_equal(self):
"""
Equal objects are detected as equal.
@ -168,6 +149,14 @@ class TestAddRepr(object):
"""
Tests for `_add_repr`.
"""
def test_no_repr(self):
"""
If `no_repr` is set, ignore that attribute.
"""
C = make_class("C", {"a": attr(no_repr=True), "b": attr()})
assert "C(b=2)" == repr(C(1, 2))
def test_repr(self):
"""
repr returns a sensible value.
@ -192,6 +181,14 @@ class TestAddHash(object):
"""
Tests for `_add_hash`.
"""
def test_no_hash(self):
"""
If `no_hash` is set, ignore that attribute.
"""
C = make_class("C", {"a": attr(no_hash=True), "b": attr()})
assert hash(C(1, 2)) == hash(C(2, 2))
def test_hash(self):
"""
__hash__ returns different hashes for different values.
@ -203,6 +200,17 @@ class TestAddInit(object):
"""
Tests for `_add_init`.
"""
def test_no_init(self):
"""
If `no_init` is set, ignore that attribute.
"""
C = make_class("C", {"a": attr(no_init=True), "b": attr()})
with pytest.raises(TypeError) as e:
C(a=1, b=2)
msg = e.value if PY26 else e.value.args[0]
assert "__init__() got an unexpected keyword argument 'a'" == msg
def test_sets_attributes(self):
"""
The attributes are initialized using the passed keywords.
@ -217,15 +225,9 @@ class TestAddInit(object):
"""
class C(object):
__attrs_attrs__ = [
Attribute(name="a",
default=2,
validator=None,),
Attribute(name="b",
default="hallo",
validator=None,),
Attribute(name="c",
default=None,
validator=None,),
simple_attr(name="a", default=2),
simple_attr(name="b", default="hallo"),
simple_attr(name="c", default=None),
]
C = _add_init(C)
@ -243,12 +245,8 @@ class TestAddInit(object):
class C(object):
__attrs_attrs__ = [
Attribute(name="a",
default=Factory(list),
validator=None,),
Attribute(name="b",
default=Factory(D),
validator=None,)
simple_attr(name="a", default=Factory(list)),
simple_attr(name="b", default=Factory(D)),
]
C = _add_init(C)
i = C()

View File

@ -92,7 +92,8 @@ class TestTransformAttrs(object):
assert (
"No mandatory attributes allowed after an atribute with a "
"default value or factory. Attribute in question: Attribute"
"(name='y', default=NOTHING, validator=None)",
"(name='y', default=NOTHING, validator=None, no_repr=False, "
"no_cmp=False, no_hash=False, no_init=False)",
) == e.value.args
@ -202,7 +203,8 @@ class TestAttribute(object):
"""
with pytest.raises(TypeError) as e:
Attribute(name="foo", default=NOTHING,
factory=NOTHING, validator=None)
factory=NOTHING, validator=None, no_repr=False,
no_cmp=False, no_hash=False, no_init=False)
assert ("Too many arguments.",) == e.value.args