Skip to content

Commit

Permalink
Optionally retain collection types (fixes #69)
Browse files Browse the repository at this point in the history
  • Loading branch information
vortec authored and hynek committed Aug 23, 2016
1 parent dd4140e commit c6fbec9
Show file tree
Hide file tree
Showing 3 changed files with 30 additions and 12 deletions.
3 changes: 3 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,9 @@ Changes:
- Add ``attr.attrs`` and ``attr.attrib`` as a more consistent aliases for ``attr.s`` and ``attr.ib``.
- Add ``frozen`` option to ``attr.s`` that will make instances best-effort immutable.
`#60 <https://github.com/hynek/attrs/issues/60>`_
- ``attr.asdict`` now takes ``retain_collection_types`` as an argument.
If ``True``, it does not convert attributes of type ``tuple`` or ``set`` to ``list``.
`#69 <https://github.com/hynek/attrs/issues/69>`_


----
Expand Down
27 changes: 15 additions & 12 deletions src/attr/_funcs.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,29 +6,31 @@
from ._make import Attribute, NOTHING, fields


def asdict(inst, recurse=True, filter=None, dict_factory=dict):
def asdict(inst, recurse=True, filter=None, dict_factory=dict,
retain_collection_types=False):
"""
Return the ``attrs`` attribute values of *inst* as a dict. Optionally
recurse into other ``attrs``-decorated classes.
Return the ``attrs`` attribute values of *inst* as a dict.
:param inst: Instance of an ``attrs``-decorated class.
Optionally recurse into other ``attrs``-decorated classes.
:param inst: Instance of an ``attrs``-decorated class.
:param bool recurse: Recurse into classes that are also
``attrs``-decorated.
:param callable filter: A callable whose return code deteremines whether an
attribute or element is included (``True``) or dropped (``False``). Is
called with the :class:`attr.Attribute` as the first argument and the
value as the second argument.
:param callable dict_factory: A callable to produce dictionaries from. For
:param callable dict_factory: A callable to produce dictionaries from. For
example, to produce ordered dictionaries instead of normal Python
dictionaries, pass in ``collections.OrderedDict``.
:param bool retain_collection_types: Do not convert to ``list`` when
encountering an attribute which is type ``tuple`` or ``set``. Only
meaningful if ``recurse`` is ``True``.
:rtype: :class:`dict`
:rtype: return type of *dict_factory*
.. versionadded:: 16.0.0
*dict_factory*
.. versionadded:: 16.0.0 *dict_factory*
.. versionadded:: 16.1.0 *retain_collection_types*
"""
attrs = fields(inst.__class__)
rv = dict_factory()
Expand All @@ -41,12 +43,13 @@ def asdict(inst, recurse=True, filter=None, dict_factory=dict):
rv[a.name] = asdict(v, recurse=True, filter=filter,
dict_factory=dict_factory)
elif isinstance(v, (tuple, list, set)):
rv[a.name] = [
cf = v.__class__ if retain_collection_types is True else list
rv[a.name] = cf([
asdict(i, recurse=True, filter=filter,
dict_factory=dict_factory)
if has(i.__class__) else i
for i in v
]
])
elif isinstance(v, dict):
df = dict_factory
rv[a.name] = df((
Expand Down
12 changes: 12 additions & 0 deletions tests/test_funcs.py
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,18 @@ def test_lists_tuples(self, container, C):
"y": [{"x": 2, "y": 3}, {"x": 4, "y": 5}, "a"],
} == asdict(C(1, container([C(2, 3), C(4, 5), "a"])))

@given(container=st.sampled_from(SEQUENCE_TYPES))
def test_lists_tuples_retain_type(self, container, C):
"""
If recurse and retain_collection_types are True, also recurse
into lists and do not convert them into list.
"""
assert {
"x": 1,
"y": container([{"x": 2, "y": 3}, {"x": 4, "y": 5}, "a"]),
} == asdict(C(1, container([C(2, 3), C(4, 5), "a"])),
retain_collection_types=True)

@given(st.sampled_from(MAPPING_TYPES))
def test_dicts(self, C, dict_factory):
"""
Expand Down

0 comments on commit c6fbec9

Please sign in to comment.