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
5 changes: 5 additions & 0 deletions HISTORY.rst
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,11 @@ History
* ``attrs`` and dataclass structuring is now ~25% faster.
* Fix an issue structuring bare ``typing.List`` s on Pythons lower than 3.9.
(`#209 <https://github.com/python-attrs/cattrs/issues/209>`_)
* Fix structuring of non-parametrized containers like ``list/dict/...`` on Pythons lower than 3.9.
(`https://github.com/python-attrs/cattrs/issues/218`)
* Fix structuring bare ``typing.Tuple`` on Pythons lower than 3.9.
(`https://github.com/python-attrs/cattrs/issues/218`)


1.10.0 (2022-01-04)
-------------------
Expand Down
23 changes: 10 additions & 13 deletions src/cattr/_compat.py
Original file line number Diff line number Diff line change
Expand Up @@ -161,21 +161,18 @@ def is_mapping(type):
and issubclass(type.__origin__, TypingMapping)
)

bare_list_args = List.__args__
bare_seq_args = TypingSequence.__args__
bare_mapping_args = TypingMapping.__args__
bare_dict_args = Dict.__args__
bare_mutable_seq_args = TypingMutableSequence.__args__
bare_generic_args = {
List.__args__,
TypingSequence.__args__,
TypingMapping.__args__,
Dict.__args__,
TypingMutableSequence.__args__,
Tuple.__args__,
None, # non-parametrized containers do not have `__args__ attribute in py3.7-8
}

def is_bare(type):
args = type.__args__
return (
args == bare_list_args
or args == bare_seq_args
or args == bare_mapping_args
or args == bare_dict_args
or args == bare_mutable_seq_args
)
return getattr(type, "__args__", None) in bare_generic_args

def is_counter(type):
return (
Expand Down
6 changes: 4 additions & 2 deletions src/cattrs/gen.py
Original file line number Diff line number Diff line change
Expand Up @@ -412,8 +412,10 @@ def make_iterable_unstructure_fn(cl: Any, converter, unstructure_to=None):

fn_name = "unstructure_iterable"

# Let's try fishing out the type args.
if getattr(cl, "__args__", None) is not None:
# Let's try fishing out the type args
# Unspecified tuples have `__args__` as empty tuples, so guard
# against IndexError.
if getattr(cl, "__args__", None) not in (None, ()):
Comment thread
bibajz marked this conversation as resolved.
type_arg = cl.__args__[0]
# We don't know how to handle the TypeVar on this level,
# so we skip doing the dispatch here.
Expand Down
103 changes: 65 additions & 38 deletions tests/metadata/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,12 @@
Any,
Callable,
Dict,
FrozenSet,
List,
MutableSequence,
MutableSet,
Sequence,
Set,
Tuple,
Type,
TypeVar,
Expand Down Expand Up @@ -98,16 +101,25 @@ def simple_typed_attrs(
| int_typed_attrs(defaults)
| str_typed_attrs(defaults)
| float_typed_attrs(defaults)
| frozenset_typed_attrs(defaults, legacy_types_only=True)
| homo_tuple_typed_attrs(defaults, legacy_types_only=True)
)
if not for_frozen:
res = (
res
| dict_typed_attrs(defaults, allow_mutable_defaults)
| mutable_seq_typed_attrs(defaults, allow_mutable_defaults)
| seq_typed_attrs(defaults, allow_mutable_defaults)
| mutable_seq_typed_attrs(
defaults, allow_mutable_defaults, legacy_types_only=True
)
| seq_typed_attrs(
defaults, allow_mutable_defaults, legacy_types_only=True
)
| list_typed_attrs(
defaults, allow_mutable_defaults, legacy_types_only=True
)
| set_typed_attrs(
defaults, allow_mutable_defaults, legacy_types_only=True
)
)
else:
res = (
Expand Down Expand Up @@ -282,6 +294,7 @@ def dict_typed_attrs(
"""
Generate a tuple of an attribute and a strategy that yields dictionaries
for that attribute. The dictionaries map strings to integers.
The generated dict types are what's expected to be used on pre-3.9 Pythons.
"""
default = attr.NOTHING
val_strat = dictionaries(keys=text(), values=integers())
Expand All @@ -291,12 +304,8 @@ def dict_typed_attrs(
default = Factory(lambda: default_val)
else:
default = default_val
return (
attr.ib(
type=Dict[str, int] if draw(booleans()) else Dict, default=default
),
val_strat,
)
type = draw(sampled_from([Dict[str, int], Dict, dict]))
return (attr.ib(type=type, default=default), val_strat)


@composite
Expand All @@ -318,15 +327,16 @@ def new_dict_typed_attrs(draw, defaults=None, allow_mutable_defaults=True):
else:
default = default_val

type = (
dict[str, int] if draw(booleans()) else dict
) # We also produce bare dicts.

return (attr.ib(type=type, default=default), val_strat)
return (attr.ib(type=dict[str, int], default=default), val_strat)


@composite
def set_typed_attrs(draw, defaults=None, allow_mutable_defaults=True):
def set_typed_attrs(
draw: DrawFn,
defaults=None,
allow_mutable_defaults=True,
legacy_types_only=False,
):
"""
Generate a tuple of an attribute and a strategy that yields sets
for that attribute. The sets contain integers.
Expand All @@ -341,19 +351,21 @@ def set_typed_attrs(draw, defaults=None, allow_mutable_defaults=True):
default = default_val
else:
default = default_val
return (
attr.ib(
type=set[int]
if draw(booleans())
else (AbcSet[int] if draw(booleans()) else AbcMutableSet[int]),
default=default,
),
val_strat,

type = draw(
sampled_from(
[set, set[int], AbcSet[int], AbcMutableSet[int]]
if not legacy_types_only
else [set, Set[int], MutableSet[int]]
)
)
return (attr.ib(type=type, default=default), val_strat)


@composite
def frozenset_typed_attrs(draw, defaults=None):
def frozenset_typed_attrs(
draw: DrawFn, defaults=None, legacy_types_only=False
):
"""
Generate a tuple of an attribute and a strategy that yields frozensets
for that attribute. The frozensets contain integers.
Expand All @@ -362,7 +374,14 @@ def frozenset_typed_attrs(draw, defaults=None):
val_strat = frozensets(integers())
if defaults is True or (defaults is None and draw(booleans())):
default = draw(val_strat)
return (attr.ib(type=frozenset[int], default=default), val_strat)
type = draw(
sampled_from(
[frozenset[int], frozenset, FrozenSet[int], FrozenSet]
if not legacy_types_only
else [frozenset, FrozenSet[int], FrozenSet]
)
)
return (attr.ib(type=type, default=default), val_strat)


@composite
Expand Down Expand Up @@ -390,9 +409,9 @@ def list_typed_attrs(
attr.ib(
type=draw(
sampled_from(
[List, List[float]]
if legacy_types_only
else [list[float], List[float], List]
[list[float], list, List[float], List]
if not legacy_types_only
else [List, List[float], list]
)
),
default=default,
Expand All @@ -402,10 +421,12 @@ def list_typed_attrs(


@composite
def seq_typed_attrs(draw, defaults=None, allow_mutable_defaults=True):
def seq_typed_attrs(
draw, defaults=None, allow_mutable_defaults=True, legacy_types_only=False
):
"""
Generate a tuple of an attribute and a strategy that yields lists
for that attribute. The lists contain floats.
for that attribute. The lists contain integers.
"""
default_val = attr.NOTHING
val_strat = lists(integers())
Expand All @@ -420,17 +441,17 @@ def seq_typed_attrs(draw, defaults=None, allow_mutable_defaults=True):

return (
attr.ib(
type=Sequence[int]
if not is_39_or_later or draw(booleans())
else AbcSequence[int],
type=AbcSequence[int] if not legacy_types_only else Sequence[int],
default=default,
),
val_strat,
)


@composite
def mutable_seq_typed_attrs(draw, defaults=None, allow_mutable_defaults=True):
def mutable_seq_typed_attrs(
draw, defaults=None, allow_mutable_defaults=True, legacy_types_only=False
):
"""
Generate a tuple of an attribute and a strategy that yields lists
for that attribute. The lists contain floats.
Expand All @@ -448,17 +469,17 @@ def mutable_seq_typed_attrs(draw, defaults=None, allow_mutable_defaults=True):

return (
attr.ib(
type=MutableSequence[float]
if not is_39_or_later
else AbcMutableSequence[float],
type=AbcMutableSequence[float]
if not legacy_types_only
else MutableSequence[float],
default=default,
),
val_strat,
)


@composite
def homo_tuple_typed_attrs(draw, defaults=None):
def homo_tuple_typed_attrs(draw, defaults=None, legacy_types_only=False):
"""
Generate a tuple of an attribute and a strategy that yields homogenous
tuples for that attribute. The tuples contain strings.
Expand All @@ -469,7 +490,13 @@ def homo_tuple_typed_attrs(draw, defaults=None):
default = draw(val_strat)
return (
attr.ib(
type=tuple[str, ...] if draw(booleans()) else Tuple[str, ...],
type=draw(
sampled_from(
[tuple[str, ...], tuple, Tuple, Tuple[str, ...]]
if not legacy_types_only
else [tuple, Tuple, Tuple[str, ...]]
)
),
default=default,
),
val_strat,
Expand Down