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
1 change: 1 addition & 0 deletions changelog.d/1463.change.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
The performance of `attrs.asdict()` has been improved by 45–260%.
39 changes: 32 additions & 7 deletions src/attr/_funcs.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,23 @@
from .exceptions import AttrsAttributeNotFoundError


_ATOMIC_TYPES = frozenset(
{
type(None),
bool,
int,
float,
str,
complex,
bytes,
type(...),
type,
range,
property,
}
)


def asdict(
inst,
recurse=True,
Expand Down Expand Up @@ -71,7 +88,10 @@ def asdict(
v = value_serializer(inst, a, v)

if recurse is True:
if has(v.__class__):
value_type = type(v)
if value_type in _ATOMIC_TYPES:
rv[a.name] = v
elif has(value_type):
rv[a.name] = asdict(
v,
recurse=True,
Expand All @@ -80,8 +100,8 @@ def asdict(
retain_collection_types=retain_collection_types,
value_serializer=value_serializer,
)
elif isinstance(v, (tuple, list, set, frozenset)):
cf = v.__class__ if retain_collection_types is True else list
elif issubclass(value_type, (tuple, list, set, frozenset)):
cf = value_type if retain_collection_types is True else list
items = [
_asdict_anything(
i,
Expand All @@ -101,7 +121,7 @@ def asdict(
# Workaround for TypeError: cf.__new__() missing 1 required
# positional argument (which appears, for a namedturle)
rv[a.name] = cf(*items)
elif isinstance(v, dict):
elif issubclass(value_type, dict):
df = dict_factory
rv[a.name] = df(
(
Expand Down Expand Up @@ -142,7 +162,12 @@ def _asdict_anything(
"""
``asdict`` only works on attrs instances, this works on anything.
"""
if getattr(val.__class__, "__attrs_attrs__", None) is not None:
val_type = type(val)
if val_type in _ATOMIC_TYPES:
rv = val
if value_serializer is not None:
rv = value_serializer(None, None, rv)
elif getattr(val_type, "__attrs_attrs__", None) is not None:
# Attrs class.
rv = asdict(
val,
Expand All @@ -152,7 +177,7 @@ def _asdict_anything(
retain_collection_types=retain_collection_types,
value_serializer=value_serializer,
)
elif isinstance(val, (tuple, list, set, frozenset)):
elif issubclass(val_type, (tuple, list, set, frozenset)):
if retain_collection_types is True:
cf = val.__class__
elif is_key:
Expand All @@ -173,7 +198,7 @@ def _asdict_anything(
for i in val
]
)
elif isinstance(val, dict):
elif issubclass(val_type, dict):
df = dict_factory
rv = df(
(
Expand Down
16 changes: 16 additions & 0 deletions tests/test_funcs.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,22 @@ def test_recurse(self, C, dict_class):
C(C(1, 2), C(3, 4)), dict_factory=dict_class
)

def test_non_atomic_types(self, C):
"""
Non-atomic types that don't have special treatment for are serialized
and the types are retained.
"""

class Int(int):
pass

c = C(Int(10), [Int(1), 2])
expected = {"x": 10, "y": [1, 2]}

assert expected == asdict(c)
assert type(asdict(c)["x"]) is Int
assert type(asdict(c)["y"][0]) is Int

def test_nested_lists(self, C):
"""
Test unstructuring deeply nested lists.
Expand Down