Skip to content

Commit 1b419e3

Browse files
Insoleethynek
authored andcommitted
Add attr.astuple
Fixes #77 and fixes #78
1 parent ca2a1e1 commit 1b419e3

File tree

9 files changed

+291
-16
lines changed

9 files changed

+291
-16
lines changed

.gitignore

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,4 +6,4 @@ docs/_build/
66
htmlcov
77
dist
88
.cache
9-
.hypothesis
9+
.hypothesis

CHANGELOG.rst

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -11,15 +11,17 @@ The third digit is only for regressions.
1111
Changes:
1212
^^^^^^^^
1313

14+
- Add ``attr.astuple()`` that -- similarly to ``attr.asdict()`` -- returns the instance as a tuple.
15+
`#77 <https://github.com/hynek/attrs/issues/77>`_
1416
- Converts now work with frozen classes.
1517
`#76 <https://github.com/hynek/attrs/issues/76>`_
1618
- Instantiation of ``attrs`` classes with converters is now significantly faster.
1719
`#80 <https://github.com/hynek/attrs/pull/80>`_
1820
- Pickling now works with ``__slots__`` classes.
1921
`#81 <https://github.com/hynek/attrs/issues/81>`_
20-
- ``attr.assoc`` now works with ``__slots__`` classes.
22+
- ``attr.assoc()`` now works with ``__slots__`` classes.
2123
`#84 <https://github.com/hynek/attrs/issues/84>`_
22-
- The tuple returned by ``attr.fields`` now also allows to access the ``Attribute`` instances by name.
24+
- The tuple returned by ``attr.fields()`` now also allows to access the ``Attribute`` instances by name.
2325
Yes, we've subclassed ``tuple`` so you don't have to!
2426
Therefore ``attr.fields(C).x`` is equivalent to the deprecated ``C.x`` and works with ``__slots__`` classes.
2527
`#88 <https://github.com/hynek/attrs/issues/88>`_
@@ -43,7 +45,7 @@ Deprecations:
4345
^^^^^^^^^^^^^
4446

4547
- Accessing ``Attribute`` instances on class objects is now deprecated and will stop working in 2017.
46-
If you need introspection please use the ``__attrs_attrs__`` attribute or the ``attr.fields`` function that carry them too.
48+
If you need introspection please use the ``__attrs_attrs__`` attribute or the ``attr.fields()`` function that carry them too.
4749
In the future, the attributes that are defined on the class body and are usually overwritten in your ``__init__`` method are simply removed after ``@attr.s`` has been applied.
4850

4951
This will remove the confusing error message if you write your own ``__init__`` and forget to initialize some attribute.
@@ -56,15 +58,15 @@ Deprecations:
5658
Changes:
5759
^^^^^^^^
5860

59-
- ``attr.asdict``\ 's ``dict_factory`` arguments is now propagated on recursion.
61+
- ``attr.asdict()``\ 's ``dict_factory`` arguments is now propagated on recursion.
6062
`#45 <https://github.com/hynek/attrs/issues/45>`_
61-
- ``attr.asdict``, ``attr.has`` and ``attr.fields`` are significantly faster.
63+
- ``attr.asdict()``, ``attr.has()`` and ``attr.fields()`` are significantly faster.
6264
`#48 <https://github.com/hynek/attrs/issues/48>`_
6365
`#51 <https://github.com/hynek/attrs/issues/51>`_
6466
- Add ``attr.attrs`` and ``attr.attrib`` as a more consistent aliases for ``attr.s`` and ``attr.ib``.
6567
- Add ``frozen`` option to ``attr.s`` that will make instances best-effort immutable.
6668
`#60 <https://github.com/hynek/attrs/issues/60>`_
67-
- ``attr.asdict`` now takes ``retain_collection_types`` as an argument.
69+
- ``attr.asdict()`` now takes ``retain_collection_types`` as an argument.
6870
If ``True``, it does not convert attributes of type ``tuple`` or ``set`` to ``list``.
6971
`#69 <https://github.com/hynek/attrs/issues/69>`_
7072

@@ -95,7 +97,7 @@ Changes:
9597
- Allow the case of initializing attributes that are set to ``init=False``.
9698
This allows for clean initializer parameter lists while being able to initialize attributes to default values.
9799
`#32 <https://github.com/hynek/attrs/issues/32>`_
98-
- ``attr.asdict`` can now produce arbitrary mappings instead of Python ``dict``\ s when provided with a ``dict_factory`` argument.
100+
- ``attr.asdict()`` can now produce arbitrary mappings instead of Python ``dict``\ s when provided with a ``dict_factory`` argument.
99101
`#40 <https://github.com/hynek/attrs/issues/40>`_
100102
- Multiple performance improvements.
101103

docs/api.rst

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,19 @@ Helpers
161161
{'y': {'y': 3, 'x': 2}, 'x': 1}
162162

163163

164+
.. autofunction:: attr.astuple
165+
166+
For example:
167+
168+
.. doctest::
169+
170+
>>> @attr.s
171+
... class C(object):
172+
... x = attr.ib()
173+
... y = attr.ib()
174+
>>> attr.astuple(C(1,2))
175+
(1, 2)
176+
164177
``attrs`` includes some handy helpers for filtering:
165178

166179
.. autofunction:: attr.filters.include

docs/examples.rst

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -162,8 +162,8 @@ On Python 3 it overrides the implicit detection.
162162

163163
.. _asdict:
164164

165-
Converting to Dictionaries
166-
--------------------------
165+
Converting to Collections Types
166+
-------------------------------
167167

168168
When you have a class with data, it often is very convenient to transform that class into a :class:`dict` (for example if you want to serialize it to JSON):
169169

@@ -210,6 +210,29 @@ For the common case where you want to :func:`include <attr.filters.include>` or
210210
... filter=attr.filters.include(int, attr.fields(C).x))
211211
{'z': 3, 'x': 'foo'}
212212

213+
Other times, all you want is a tuple and ``attrs`` won't let you down:
214+
215+
.. doctest::
216+
217+
>>> import sqlite3
218+
>>> import attr
219+
>>> @attr.s
220+
... class Foo:
221+
... a = attr.ib()
222+
... b = attr.ib()
223+
>>> foo = Foo(2, 3)
224+
>>> with sqlite3.connect(":memory:") as conn:
225+
... c = conn.cursor()
226+
... c.execute("CREATE TABLE foo (x INTEGER PRIMARY KEY ASC, y)") #doctest: +ELLIPSIS
227+
... c.execute("INSERT INTO foo VALUES (?, ?)", attr.astuple(foo)) #doctest: +ELLIPSIS
228+
... foo2 = Foo(*c.execute("SELECT x, y FROM foo").fetchone())
229+
<sqlite3.Cursor object at ...>
230+
<sqlite3.Cursor object at ...>
231+
>>> foo == foo2
232+
True
233+
234+
235+
213236

214237
Defaults
215238
--------

src/attr/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
from ._funcs import (
44
asdict,
55
assoc,
6+
astuple,
67
has,
78
)
89
from ._make import (
@@ -46,6 +47,7 @@
4647
"Factory",
4748
"NOTHING",
4849
"asdict",
50+
"astuple",
4951
"assoc",
5052
"attr",
5153
"attrib",

src/attr/_funcs.py

Lines changed: 73 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ def asdict(inst, recurse=True, filter=None, dict_factory=dict,
2525
example, to produce ordered dictionaries instead of normal Python
2626
dictionaries, pass in ``collections.OrderedDict``.
2727
:param bool retain_collection_types: Do not convert to ``list`` when
28-
encountering an attribute which is type ``tuple`` or ``set``. Only
28+
encountering an attribute whose type is ``tuple`` or ``set``. Only
2929
meaningful if ``recurse`` is ``True``.
3030
3131
:rtype: return type of *dict_factory*
@@ -67,6 +67,78 @@ def asdict(inst, recurse=True, filter=None, dict_factory=dict,
6767
return rv
6868

6969

70+
def astuple(inst, recurse=True, filter=None, tuple_factory=tuple,
71+
retain_collection_types=False):
72+
"""
73+
Return the ``attrs`` attribute values of *inst* as a tuple.
74+
75+
Optionally recurse into other ``attrs``-decorated classes.
76+
77+
:param inst: Instance of an ``attrs``-decorated class.
78+
:param bool recurse: Recurse into classes that are also
79+
``attrs``-decorated.
80+
:param callable filter: A callable whose return code determines whether an
81+
attribute or element is included (``True``) or dropped (``False``). Is
82+
called with the :class:`attr.Attribute` as the first argument and the
83+
value as the second argument.
84+
:param callable tuple_factory: A callable to produce tuples from. For
85+
example, to produce lists instead of tuples.
86+
:param bool retain_collection_types: Do not convert to ``list``
87+
or ``dict`` when encountering an attribute which type is
88+
``tuple``, ``dict`` or ``set``. Only meaningful if ``recurse`` is
89+
``True``.
90+
91+
:rtype: return type of *tuple_factory*
92+
93+
:raise attr.exceptions.NotAnAttrsClassError: If *cls* is not an ``attrs``
94+
class.
95+
96+
.. versionadded:: 16.2.0
97+
"""
98+
attrs = fields(inst.__class__)
99+
rv = []
100+
retain = retain_collection_types # Very long. :/
101+
for a in attrs:
102+
v = getattr(inst, a.name)
103+
if filter is not None and not filter(a, v):
104+
continue
105+
if recurse is True:
106+
if has(v.__class__):
107+
rv.append(astuple(v, recurse=True, filter=filter,
108+
tuple_factory=tuple_factory,
109+
retain_collection_types=retain))
110+
elif isinstance(v, (tuple, list, set)):
111+
cf = v.__class__ if retain is True else list
112+
rv.append(cf([
113+
astuple(j, recurse=True, filter=filter,
114+
tuple_factory=tuple_factory,
115+
retain_collection_types=retain)
116+
if has(j.__class__) else j
117+
for j in v
118+
]))
119+
elif isinstance(v, dict):
120+
df = v.__class__ if retain is True else dict
121+
rv.append(df(
122+
(
123+
astuple(
124+
kk,
125+
tuple_factory=tuple_factory,
126+
retain_collection_types=retain
127+
) if has(kk.__class__) else kk,
128+
astuple(
129+
vv,
130+
tuple_factory=tuple_factory,
131+
retain_collection_types=retain
132+
) if has(vv.__class__) else vv
133+
)
134+
for kk, vv in iteritems(v)))
135+
else:
136+
rv.append(v)
137+
else:
138+
rv.append(v)
139+
return rv if tuple_factory is list else tuple_factory(rv)
140+
141+
70142
def has(cls):
71143
"""
72144
Check whether *cls* is a class with ``attrs`` attributes.

src/attr/exceptions.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,9 @@ class FrozenInstanceError(AttributeError):
66
A frozen/immutable instance has been attempted to be modified.
77
88
It mirrors the behavior of ``namedtuples`` by using the same error message
9-
and subclassing :exc:`AttributeError``.
9+
and subclassing :exc:`AttributeError`.
10+
11+
.. versionadded:: 16.1.0
1012
"""
1113
msg = "can't set attribute"
1214
args = [msg]

0 commit comments

Comments
 (0)