diff --git a/HISTORY.rst b/HISTORY.rst index 2523469f..f7a3e6a4 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -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 `_) +* 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) ------------------- diff --git a/src/cattr/_compat.py b/src/cattr/_compat.py index 4bcd2c34..77c68e9b 100644 --- a/src/cattr/_compat.py +++ b/src/cattr/_compat.py @@ -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 ( diff --git a/src/cattrs/gen.py b/src/cattrs/gen.py index aedc4abf..c025269b 100644 --- a/src/cattrs/gen.py +++ b/src/cattrs/gen.py @@ -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, ()): type_arg = cl.__args__[0] # We don't know how to handle the TypeVar on this level, # so we skip doing the dispatch here. diff --git a/tests/metadata/__init__.py b/tests/metadata/__init__.py index a557912f..51c91edd 100644 --- a/tests/metadata/__init__.py +++ b/tests/metadata/__init__.py @@ -11,9 +11,12 @@ Any, Callable, Dict, + FrozenSet, List, MutableSequence, + MutableSet, Sequence, + Set, Tuple, Type, TypeVar, @@ -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 = ( @@ -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()) @@ -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 @@ -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. @@ -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. @@ -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 @@ -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, @@ -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()) @@ -420,9 +441,7 @@ 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, @@ -430,7 +449,9 @@ def seq_typed_attrs(draw, defaults=None, allow_mutable_defaults=True): @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. @@ -448,9 +469,9 @@ 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, @@ -458,7 +479,7 @@ def mutable_seq_typed_attrs(draw, defaults=None, allow_mutable_defaults=True): @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. @@ -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,