diff --git a/CHANGELOG.rst b/CHANGELOG.rst index c8b39249..8110ed39 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -25,8 +25,9 @@ Changes: - ``attr.asdict``\ 's ``dict_factory`` arguments is now propagated on recursion. `#45 `_ -- ``attr.asdict`` and ``attr.has`` are significantly faster. +- ``attr.asdict``, ``attr.has`` and ``attr.fields`` are significantly faster. `#48 `_ + `#51 `_ ---- diff --git a/src/attr/_funcs.py b/src/attr/_funcs.py index a1461a35..ba543fe1 100644 --- a/src/attr/_funcs.py +++ b/src/attr/_funcs.py @@ -3,7 +3,7 @@ from __future__ import absolute_import, division, print_function import copy from ._compat import iteritems -from ._make import Attribute, NOTHING, _fast_attrs_iterate +from ._make import Attribute, NOTHING, fields def asdict(inst, recurse=True, filter=None, dict_factory=dict): @@ -30,7 +30,7 @@ def asdict(inst, recurse=True, filter=None, dict_factory=dict): .. versionadded:: 16.0.0 *dict_factory* """ - attrs = _fast_attrs_iterate(inst) + attrs = fields(inst.__class__) rv = dict_factory() for a in attrs: v = getattr(inst, a.name) diff --git a/src/attr/_make.py b/src/attr/_make.py index 55e7fbc0..793707df 100644 --- a/src/attr/_make.py +++ b/src/attr/_make.py @@ -1,6 +1,5 @@ from __future__ import absolute_import, division, print_function -import copy import hashlib import linecache @@ -417,16 +416,7 @@ def fields(cl): raise ValueError("{cl!r} is not an attrs-decorated class.".format( cl=cl )) - return copy.deepcopy(attrs) - - -def _fast_attrs_iterate(inst): - """ - Fast internal iteration over the attr descriptors. - - Using fields to iterate is slow because it involves deepcopy. - """ - return inst.__class__.__attrs_attrs__ + return attrs def validate(inst): @@ -440,7 +430,7 @@ def validate(inst): if _config._run_validators is False: return - for a in _fast_attrs_iterate(inst): + for a in fields(inst.__class__): if a.validator is not None: a.validator(inst, a, getattr(inst, a.name)) @@ -453,7 +443,7 @@ def _convert(inst): :param inst: Instance of a class with ``attrs`` attributes. """ - for a in _fast_attrs_iterate(inst): + for a in inst.__class__.__attrs_attrs__: if a.convert is not None: setattr(inst, a.name, a.convert(getattr(inst, a.name))) @@ -534,37 +524,38 @@ class Attribute(object): Plus *all* arguments of :func:`attr.ib`. """ - _attributes = [ - "name", "default", "validator", "repr", "cmp", "hash", "init", - "convert" - ] # we can't use ``attrs`` so we have to cheat a little. + __slots__ = ('name', 'default', 'validator', 'repr', 'cmp', 'hash', 'init', + 'convert') _optional = {"convert": None} def __init__(self, **kw): - if len(kw) > len(Attribute._attributes): + if len(kw) > len(Attribute.__slots__): raise TypeError("Too many arguments.") - for a in Attribute._attributes: + for a in Attribute.__slots__: try: - setattr(self, a, kw[a]) + object.__setattr__(self, a, kw[a]) except KeyError: if a in Attribute._optional: - setattr(self, a, self._optional[a]) + object.__setattr__(self, a, self._optional[a]) else: raise TypeError("Missing argument '{arg}'.".format(arg=a)) + def __setattr__(self, name, value): + raise AttributeError("can't set attribute") # To mirror namedtuple. + @classmethod def from_counting_attr(cl, name, ca): return cl(name=name, **dict((k, getattr(ca, k)) for k - in Attribute._attributes + in Attribute.__slots__ if k != "name")) _a = [Attribute(name=name, default=NOTHING, validator=None, repr=True, cmp=True, hash=True, init=True) - for name in Attribute._attributes] + for name in Attribute.__slots__] Attribute = _add_hash( _add_cmp(_add_repr(Attribute, attrs=_a), attrs=_a), attrs=_a ) diff --git a/tests/test_make.py b/tests/test_make.py index 0529daf7..97c5cb99 100644 --- a/tests/test_make.py +++ b/tests/test_make.py @@ -6,9 +6,9 @@ from __future__ import absolute_import, division, print_function import pytest from hypothesis import given -from hypothesis.strategies import booleans +from hypothesis.strategies import booleans, sampled_from -from . import simple_attr +from . import simple_attr, simple_attrs from attr import _config from attr._compat import PY3 from attr._make import ( @@ -23,6 +23,8 @@ from attr._make import ( validate, ) +attrs = simple_attrs.map(lambda c: Attribute.from_counting_attr('name', c)) + class TestCountingAttr(object): """ @@ -184,6 +186,14 @@ class TestAttributes(object): assert "C3()" == repr(C3()) assert C3() == C3() + @given(attr=attrs, attr_name=sampled_from(Attribute.__slots__)) + def test_immutable(self, attr, attr_name): + """ + Attribute instances are immutable. + """ + with pytest.raises(AttributeError): + setattr(attr, attr_name, 1) + @pytest.mark.parametrize("method_name", [ "__repr__", "__eq__", @@ -386,15 +396,6 @@ class TestFields(object): """ assert all(isinstance(a, Attribute) for a in fields(C)) - def test_copies(self, C): - """ - Returns a new list object with new `Attribute` objects. - """ - assert C.__attrs_attrs__ is not fields(C) - assert all(new == original and new is not original - for new, original - in zip(fields(C), C.__attrs_attrs__)) - class TestConvert(object): """