From c6fbec9637032114a7c9223b6d676eb3ac7add5d Mon Sep 17 00:00:00 2001 From: Fabian Kochem Date: Tue, 23 Aug 2016 13:29:17 +0200 Subject: [PATCH] Optionally retain collection types (fixes #69) --- CHANGELOG.rst | 3 +++ src/attr/_funcs.py | 27 +++++++++++++++------------ tests/test_funcs.py | 12 ++++++++++++ 3 files changed, 30 insertions(+), 12 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index e9e9cc5d370d7e4..3020857460a1441 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -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 `_ +- ``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 `_ ---- diff --git a/src/attr/_funcs.py b/src/attr/_funcs.py index e76ab9472f5c08d..3bc4ab89893ee2b 100644 --- a/src/attr/_funcs.py +++ b/src/attr/_funcs.py @@ -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() @@ -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(( diff --git a/tests/test_funcs.py b/tests/test_funcs.py index cfd422334143d47..1216e431f4aa25d 100644 --- a/tests/test_funcs.py +++ b/tests/test_funcs.py @@ -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): """