Make Attribute instances immutable and slotted. (#51)

This commit is contained in:
Tin Tvrtković 2016-08-13 16:16:37 +02:00 committed by Hynek Schlawack
parent 74b31faca8
commit 46ac6b6046
4 changed files with 30 additions and 37 deletions

View File

@ -25,8 +25,9 @@ Changes:
- ``attr.asdict``\ 's ``dict_factory`` arguments is now propagated on recursion.
`#45 <https://github.com/hynek/attrs/issues/45>`_
- ``attr.asdict`` and ``attr.has`` are significantly faster.
- ``attr.asdict``, ``attr.has`` and ``attr.fields`` are significantly faster.
`#48 <https://github.com/hynek/attrs/issues/48>`_
`#51 <https://github.com/hynek/attrs/issues/51>`_
----

View File

@ -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)

View File

@ -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
)

View File

@ -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):
"""