Skip to content
Open
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
2 changes: 2 additions & 0 deletions changelog.d/1180.change.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
`attrs.asdict()` now falls back to lists for sets and frozen sets whose
recursive contents become unhashable.
23 changes: 20 additions & 3 deletions src/attr/_funcs.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,22 @@
)


def _make_collection(collection_type, items):
"""
Create a collection from already-converted items.

When retaining a set type, converted attrs instances can become
unhashable dictionaries. In that case, fall back to a list so recursive
serialization can still complete.
"""
try:
return collection_type(items)
except TypeError:
if collection_type in (set, frozenset):
return list(items)
raise


def asdict(
inst,
recurse=True,
Expand Down Expand Up @@ -114,7 +130,7 @@ def asdict(
for i in v
]
try:
rv[a.name] = cf(items)
rv[a.name] = _make_collection(cf, items)
except TypeError:
if not issubclass(cf, tuple):
raise
Expand Down Expand Up @@ -185,7 +201,8 @@ def _asdict_anything(
else:
cf = list

rv = cf(
rv = _make_collection(
cf,
[
_asdict_anything(
i,
Expand All @@ -196,7 +213,7 @@ def _asdict_anything(
value_serializer=value_serializer,
)
for i in val
]
],
)
elif issubclass(val_type, dict):
df = dict_factory
Expand Down
21 changes: 21 additions & 0 deletions tests/test_next_gen.py
Original file line number Diff line number Diff line change
Expand Up @@ -448,6 +448,27 @@ def test_smoke(self):
inst, retain_collection_types=True
)

@pytest.mark.parametrize("collection_type", [set, frozenset])
def test_set_of_instances(self, collection_type):
"""
Sets and frozen sets of attrs instances fall back to lists after recursion.

Since `attrs.asdict` always retains collection types, it used to try
to put the unstructured dictionaries back into a set.
"""

@attrs.frozen
class Foo:
x: int

@attrs.frozen
class Bar:
foos: set[Foo]

assert attrs.asdict(Bar(collection_type([Foo(3)]))) == {
"foos": [{"x": 3}]
}


class TestProps:
"""
Expand Down