From e7ebf4bf8185681fb8f178597ffd73d183dfdc1a Mon Sep 17 00:00:00 2001 From: Hynek Schlawack Date: Fri, 30 Jan 2015 17:48:34 +0100 Subject: [PATCH] Allow single attributes to be excluded --- attr/_compat.py | 1 + attr/_make.py | 45 +++++++++++++++----- docs/api.rst | 4 +- docs/examples.rst | 11 +++++ tests/__init__.py | 41 ++++++++++++++++-- tests/test_dark_magic.py | 13 ++++-- tests/test_dunders.py | 90 ++++++++++++++++++++-------------------- tests/test_make.py | 6 ++- 8 files changed, 143 insertions(+), 68 deletions(-) diff --git a/attr/_compat.py b/attr/_compat.py index 3648b108..acc927de 100644 --- a/attr/_compat.py +++ b/attr/_compat.py @@ -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: diff --git a/attr/_make.py b/attr/_make.py index 156706ed..7b888cb8 100644 --- a/attr/_make.py +++ b/attr/_make.py @@ -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)) diff --git a/docs/api.rst b/docs/api.rst index 2f8b2b3c..2e4603ef 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -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 diff --git a/docs/examples.rst b/docs/examples.rst index b8b8e697..d8bc378e 100644 --- a/docs/examples.rst +++ b/docs/examples.rst @@ -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') diff --git a/tests/__init__.py b/tests/__init__.py index b12b91a3..b816c771 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -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() diff --git a/tests/test_dark_magic.py b/tests/test_dark_magic.py index f0353e1d..f766356c 100644 --- a/tests/test_dark_magic.py +++ b/tests/test_dark_magic.py @@ -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) diff --git a/tests/test_dunders.py b/tests/test_dunders.py index 446b94fe..aea11398 100644 --- a/tests/test_dunders.py +++ b/tests/test_dunders.py @@ -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() diff --git a/tests/test_make.py b/tests/test_make.py index 973610d4..90934f08 100644 --- a/tests/test_make.py +++ b/tests/test_make.py @@ -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