Skip to content

Commit

Permalink
spike
Browse files Browse the repository at this point in the history
  • Loading branch information
hynek committed Feb 8, 2015
1 parent 6a1a740 commit a3af3a9
Show file tree
Hide file tree
Showing 4 changed files with 85 additions and 30 deletions.
52 changes: 36 additions & 16 deletions attr/_make.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
import hashlib
import linecache

from ._compat import exec_
from ._compat import exec_, iteritems


class _Nothing(object):
Expand Down Expand Up @@ -84,20 +84,33 @@ def attr(default=NOTHING, validator=None, no_repr=False, no_cmp=False,
)


def _transform_attrs(cl):
def _transform_attrs(cl, these):
"""
Transforms all `_CountingAttr`s on a class into `Attribute`s and saves the
list in `__attrs_attrs__`.
If *these* is passed, use that and don't look for them on the class.
"""
cl.__attrs_attrs__ = []
if these is None:
ca_list = [(name, attr)
for name, attr
in cl.__dict__.items()
if isinstance(attr, _CountingAttr)]
else:
ca_list = [(name, ca)
for name, ca
in iteritems(these)]

cl.__attrs_attrs__ = [
Attribute.from_counting_attr(name=attr_name, ca=ca)
for attr_name, ca
in sorted(ca_list, key=lambda e: e[1].counter)
]

had_default = False
for attr_name, ca in sorted(
((name, attr) for name, attr
in cl.__dict__.items()
if isinstance(attr, _CountingAttr)),
key=lambda e: e[1].counter
):
a = Attribute.from_counting_attr(name=attr_name, ca=ca)
for a in cl.__attrs_attrs__:
if these is None:
setattr(cl, a.name, a)
if had_default is True and a.default is NOTHING:
raise ValueError(
"No mandatory attributes allowed after an atribute with a "
Expand All @@ -106,16 +119,23 @@ def _transform_attrs(cl):
)
elif had_default is False and a.default is not NOTHING:
had_default = True
cl.__attrs_attrs__.append(a)
setattr(cl, attr_name, a)


def attributes(maybe_cl=None, no_repr=False, no_cmp=False, no_hash=False,
no_init=False):
def attributes(maybe_cl=None, these=None,
no_repr=False, no_cmp=False, no_hash=False, no_init=False):
"""
A class decorator that adds `dunder
<https://wiki.python.org/moin/DunderAlias>`_\ -methods according to the
specified attributes using :func:`attr.ib`.
specified attributes using :func:`attr.ib` or the *these* argument.
:param these: A dictionary of name to :func:`attr.ib` mappings. This is
useful if you don't want to define your attributes within the class
body because you can't (e.g. if you want to add ``__repr__`` methods to
Django models) or don't want to (e.g. if you want to use
:class:`properties <property>`).
If *these* is not `None`, the class body is *ignored*.
:type these: class:`dict` of :class:`str` to :func:`attr.ib`
:param no_repr: Don't create a ``__repr__`` method with a human readable
represantation of ``attrs`` attributes..
Expand Down Expand Up @@ -146,7 +166,7 @@ def attributes(maybe_cl=None, no_repr=False, no_cmp=False, no_hash=False,
:type no_init: bool
"""
def wrap(cl):
_transform_attrs(cl)
_transform_attrs(cl, these)
if not no_repr:
cl = _add_repr(cl)
if not no_cmp:
Expand Down
40 changes: 30 additions & 10 deletions docs/examples.rst
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,36 @@ If playful naming turns you off, ``attrs`` comes with no-nonsense aliases:
>>> attr.fields(Coordinates) == attr.fields(SeriousCoordinates)
True

For private attributes, ``attrs`` will strip the leading underscores for keyword arguments:

.. doctest::

>>> @attr.s
... class C(object):
... _x = attr.ib()
>>> C(x=1)
C(_x=1)

An additional way (not unlike ``characteristic``) of defining attributes is supported too.
This is useful in times when you can't or won't define attributes in your class's body:

.. doctest::

>>> @attr.s(these={"_x": attr.ib()})
... class ReadOnlyXSquared(object):
... @property
... def x(self):
... return self._x ** 2
>>> rox = ReadOnlyXSquared(x=5)
>>> rox
ReadOnlyXSquared(_x=5)
>>> rox.x
25
>>> rox.x = 6
Traceback (most recent call last):
...
AttributeError: can't set attribute


Converting to Dictionaries
--------------------------
Expand Down Expand Up @@ -211,16 +241,6 @@ If you like `zope.interface <http://docs.zope.org/zope.interface/api.html#zope-i
Other Goodies
-------------

For private attributes, ``attrs`` will strip the leading underscores for keyword arguments:

.. doctest::

>>> @attr.s
... class C(object):
... _x = attr.ib()
>>> C(x=1)
C(_x=1)

Do you like Rich Hickey?
I'm glad to report that Clojure's core feature is part of ``attrs``: `assoc <https://clojuredocs.org/clojure.core/assoc>`_!
I guess that means Clojure can be shut down now, sorry Rich!
Expand Down
1 change: 1 addition & 0 deletions docs/why.rst
Original file line number Diff line number Diff line change
Expand Up @@ -213,6 +213,7 @@ Main Differences
- The attributes are defined *within* the class definition such that code analyzers know about their existence.
This is useful in IDEs like PyCharm or linters like PyLint.
``attrs``'s classes look much more idiomatic than ``characteristic``'s.
Since it's useful to use ``attrs`` with classes you don't control (e.g. Django models), a similar way to ``characteristic``'s is still supported.
- The names are held shorter and easy to both type and read.
- It is generally more opinionated towards typical uses.
This ensures I'll not wake up in a year hating to use it.
Expand Down
22 changes: 18 additions & 4 deletions tests/test_make.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

import pytest

from . import simple_attr
from attr._make import (
Attribute,
NOTHING,
Expand Down Expand Up @@ -49,7 +50,7 @@ def test_normal(self):
Transforms every `_CountingAttr` and leaves others (a) be.
"""
C = make_tc()
_transform_attrs(C)
_transform_attrs(C, None)
assert ["z", "y", "x"] == [a.name for a in C.__attrs_attrs__]

def test_empty(self):
Expand All @@ -60,7 +61,7 @@ def test_empty(self):
class C(object):
pass

_transform_attrs(C)
_transform_attrs(C, None)

assert [] == C.__attrs_attrs__

Expand All @@ -74,7 +75,7 @@ def test_transforms_to_attribute(self, attribute):
All `_CountingAttr`s are transformed into `Attribute`s.
"""
C = make_tc()
_transform_attrs(C)
_transform_attrs(C, None)

assert isinstance(getattr(C, attribute), Attribute)

Expand All @@ -88,14 +89,27 @@ class C(object):
y = attr()

with pytest.raises(ValueError) as e:
_transform_attrs(C)
_transform_attrs(C, None)
assert (
"No mandatory attributes allowed after an atribute with a "
"default value or factory. Attribute in question: Attribute"
"(name='y', default=NOTHING, validator=None, no_repr=False, "
"no_cmp=False, no_hash=False, no_init=False)",
) == e.value.args

def test_these(self):
"""
If these is passed, use it and ignore body.
"""
class C(object):
y = attr()

_transform_attrs(C, {"x": attr()})
assert [
simple_attr("x"),
] == C.__attrs_attrs__
assert isinstance(C.y, _CountingAttr)


class TestAttributes(object):
"""
Expand Down

0 comments on commit a3af3a9

Please sign in to comment.