Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,5 @@ docs/_build/
htmlcov
dist
.cache
.hypothesis
.hypothesis
.pytest_cache
3 changes: 3 additions & 0 deletions changelog.d/300.change.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
The order of attributes that are passed into ``attr.make_class()`` or the ``these`` argument of ``@attr.s()`` is now retained if the dictionary is ordered (i.e. ``dict`` on Python 3.6 and later, ``collections.OrderedDict`` otherwise).

Before, the order was always determined by the order in which the attributes have been defined which may not be desirable when creating classes programatically.
3 changes: 3 additions & 0 deletions changelog.d/339.change.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
The order of attributes that are passed into ``attr.make_class()`` or the ``these`` argument of ``@attr.s()`` is now retained if the dictionary is ordered (i.e. ``dict`` on Python 3.6 and later, ``collections.OrderedDict`` otherwise).

Before, the order was always determined by the order in which the attributes have been defined which may not be desirable when creating classes programatically.
3 changes: 3 additions & 0 deletions changelog.d/343.change.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
The order of attributes that are passed into ``attr.make_class()`` or the ``these`` argument of ``@attr.s()`` is now retained if the dictionary is ordered (i.e. ``dict`` on Python 3.6 and later, ``collections.OrderedDict`` otherwise).

Before, the order was always determined by the order in which the attributes have been defined which may not be desirable when creating classes programatically.
7 changes: 7 additions & 0 deletions src/attr/_compat.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,13 @@
PYPY = platform.python_implementation() == "PyPy"


if PYPY or sys.version_info[:2] >= (3, 6):

This comment was marked as spam.

This comment was marked as spam.

This comment was marked as spam.

This comment was marked as spam.

This comment was marked as spam.

This comment was marked as spam.

This comment was marked as spam.

This comment was marked as spam.

This comment was marked as spam.

This comment was marked as spam.

ordered_dict = dict
else:
from collections import OrderedDict
ordered_dict = OrderedDict


if PY2:
from UserDict import IterableUserDict

Expand Down
32 changes: 28 additions & 4 deletions src/attr/_make.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@
from operator import itemgetter

from . import _config
from ._compat import PY2, isclass, iteritems, metadata_proxy, set_closure_cell
from ._compat import (
PY2, isclass, iteritems, metadata_proxy, ordered_dict, set_closure_cell
)
from .exceptions import (
DefaultAlreadySetError, FrozenInstanceError, NotAnAttrsClassError,
UnannotatedAttributeError
Expand Down Expand Up @@ -233,6 +235,13 @@ def _get_annotations(cls):
return anns


def _counter_getter(e):
"""
Key function for sorting to avoid re-creating a lambda for every class.
"""
return e[1].counter


def _transform_attrs(cls, these, auto_attribs):
"""
Transform all `_CountingAttr`s on a class into `Attribute`s.
Expand All @@ -245,11 +254,14 @@ def _transform_attrs(cls, these, auto_attribs):
anns = _get_annotations(cls)

if these is not None:
ca_list = sorted((
ca_list = [
(name, ca)
for name, ca
in iteritems(these)
), key=lambda e: e[1].counter)
]

if not isinstance(these, ordered_dict):
ca_list.sort(key=_counter_getter)
elif auto_attribs is True:
ca_names = {
name
Expand Down Expand Up @@ -593,6 +605,11 @@ def attrs(maybe_cls=None, these=None, repr_ns=None,
If *these* is not ``None``, ``attrs`` will *not* search the class body
for attributes and will *not* remove any attributes from it.

If *these* is an ordered dict (:class:`dict` on Python 3.6+,
:class:`collections.OrderedDict` otherwise), the order is deduced from
the order of the attributes inside *these*. Otherwise the order
of the definition of the attributes is used.

:type these: :class:`dict` of :class:`str` to :func:`attr.ib`

:param str repr_ns: When using nested classes, there's no way in Python 2
Expand Down Expand Up @@ -681,6 +698,7 @@ def attrs(maybe_cls=None, these=None, repr_ns=None,
.. versionadded:: 17.3.0 *auto_attribs*
.. versionchanged:: 18.1.0
If *these* is passed, no attributes are deleted from the class body.
.. versionchanged:: 18.1.0 If *these* is ordered, the order is retained.
"""
def wrap(cls):
if getattr(cls, "__class__", None) is None:
Expand Down Expand Up @@ -1513,6 +1531,11 @@ def make_class(name, attrs, bases=(object,), **attributes_arguments):

:param attrs: A list of names or a dictionary of mappings of names to
attributes.

If *attrs* is a list or an ordered dict (:class:`dict` on Python 3.6+,
:class:`collections.OrderedDict` otherwise), the order is deduced from
the order of the names or attributes inside *attrs*. Otherwise the
order of the definition of the attributes is used.
:type attrs: :class:`list` or :class:`dict`

:param tuple bases: Classes that the new class will subclass.
Expand All @@ -1522,7 +1545,8 @@ def make_class(name, attrs, bases=(object,), **attributes_arguments):
:return: A new class with *attrs*.
:rtype: type

.. versionadded:: 17.1.0 *bases*
.. versionadded:: 17.1.0 *bases*
.. versionchanged:: 18.1.0 If *attrs* is ordered, the order is retained.
"""
if isinstance(attrs, dict):
cls_dict = attrs
Expand Down
39 changes: 33 additions & 6 deletions tests/test_make.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
import attr

from attr import _config
from attr._compat import PY2
from attr._compat import PY2, ordered_dict
from attr._make import (
Attribute, Factory, _AndValidator, _Attributes, _ClassBuilder,
_CountingAttr, _transform_attrs, and_, fields, make_class, validate
Expand Down Expand Up @@ -281,6 +281,20 @@ class C(object):
assert 5 == C().x
assert "C(x=5)" == repr(C())

def test_these_ordered(self):
"""
If these is passed ordered attrs, their order respect instead of the
counter.
"""
b = attr.ib(default=2)
a = attr.ib(default=1)

@attr.s(these=ordered_dict([("a", a), ("b", b)]))
class C(object):
pass

assert "C(a=1, b=2)" == repr(C())

def test_multiple_inheritance(self):
"""
Order of attributes doesn't get mixed up by multiple inheritance.
Expand Down Expand Up @@ -610,6 +624,18 @@ def test_missing_sys_getframe(self, monkeypatch):

assert 1 == len(C.__attrs_attrs__)

def test_make_class_ordered(self):
"""
If `make_class()` is passed ordered attrs, their order is respected
instead of the counter.
"""
b = attr.ib(default=2)
a = attr.ib(default=1)

C = attr.make_class("C", ordered_dict([("a", a), ("b", b)]))

assert "C(a=1, b=2)" == repr(C())


class TestFields(object):
"""
Expand Down Expand Up @@ -686,13 +712,14 @@ def test_convert_factory_property(self, val, init):
"""
Property tests for attributes with convert, and a factory default.
"""
C = make_class("C", {
"y": attr.ib(),
"x": attr.ib(
C = make_class("C", ordered_dict([
("y", attr.ib()),
("x", attr.ib(
init=init,
default=Factory(lambda: val),
converter=lambda v: v + 1),
})
converter=lambda v: v + 1
)),
]))
c = C(2)

assert c.x == val + 1
Expand Down