From 98e6cb834e3c45fad9d3e6bf3667403a2b1cad38 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tin=20Tvrtkovi=C4=87?= Date: Thu, 3 Feb 2022 18:48:00 +0100 Subject: [PATCH 1/7] Rework tests to test for bare collections on 3.7/3.8 --- tests/metadata/__init__.py | 60 +++++++++++++++++++++++--------------- 1 file changed, 36 insertions(+), 24 deletions(-) diff --git a/tests/metadata/__init__.py b/tests/metadata/__init__.py index a557912f..f4124766 100644 --- a/tests/metadata/__init__.py +++ b/tests/metadata/__init__.py @@ -11,6 +11,7 @@ Any, Callable, Dict, + FrozenSet, List, MutableSequence, Sequence, @@ -98,6 +99,7 @@ def simple_typed_attrs( | int_typed_attrs(defaults) | str_typed_attrs(defaults) | float_typed_attrs(defaults) + | frozenset_typed_attrs(defaults, legacy_types_only=True) ) if not for_frozen: res = ( @@ -108,6 +110,9 @@ def simple_typed_attrs( | 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 +287,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 +297,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 +320,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 +344,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, AbcSet[int], AbcMutableSet[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 +367,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 +402,9 @@ def list_typed_attrs( attr.ib( type=draw( sampled_from( - [List, List[float]] + [List, List[float], list] if legacy_types_only - else [list[float], List[float], List] + else [list[float], list, List[float], List] ) ), default=default, From bbe04425e5cdd3c212a75f38509b4ad17273354e Mon Sep 17 00:00:00 2001 From: bibajz Date: Thu, 3 Feb 2022 21:27:07 +0100 Subject: [PATCH 2/7] Add tests for bare tuples and sequences to work in py3.7/8 --- tests/metadata/__init__.py | 49 +++++++++++++++++++++++++------------- 1 file changed, 32 insertions(+), 17 deletions(-) diff --git a/tests/metadata/__init__.py b/tests/metadata/__init__.py index f4124766..51c91edd 100644 --- a/tests/metadata/__init__.py +++ b/tests/metadata/__init__.py @@ -14,7 +14,9 @@ FrozenSet, List, MutableSequence, + MutableSet, Sequence, + Set, Tuple, Type, TypeVar, @@ -100,13 +102,18 @@ def simple_typed_attrs( | 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 ) @@ -349,7 +356,7 @@ def set_typed_attrs( sampled_from( [set, set[int], AbcSet[int], AbcMutableSet[int]] if not legacy_types_only - else [set, AbcSet[int], AbcMutableSet[int]] + else [set, Set[int], MutableSet[int]] ) ) return (attr.ib(type=type, default=default), val_strat) @@ -402,9 +409,9 @@ def list_typed_attrs( attr.ib( type=draw( sampled_from( - [List, List[float], list] - if legacy_types_only - else [list[float], list, List[float], List] + [list[float], list, List[float], List] + if not legacy_types_only + else [List, List[float], list] ) ), default=default, @@ -414,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()) @@ -432,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, @@ -442,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. @@ -460,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, @@ -470,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. @@ -481,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, From 698a71cbfa499eb1850205eeab465636c37a207c Mon Sep 17 00:00:00 2001 From: bibajz Date: Thu, 3 Feb 2022 21:28:12 +0100 Subject: [PATCH 3/7] Fix py37-38 structuring of mixed typing/regular generics --- HISTORY.rst | 2 ++ src/cattr/_compat.py | 4 +++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/HISTORY.rst b/HISTORY.rst index 2523469f..ba7d18c2 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -7,6 +7,8 @@ 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 bare generics 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..2b26c9e5 100644 --- a/src/cattr/_compat.py +++ b/src/cattr/_compat.py @@ -168,13 +168,15 @@ def is_mapping(type): bare_mutable_seq_args = TypingMutableSequence.__args__ def is_bare(type): - args = type.__args__ + # Lower-cased generics in 3.7-8 do not have `__args__` attribute. + args = getattr(type, "__args__", None) 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 + or args is None ) def is_counter(type): From bf6dc2c9ac3e0a22dd186d3025ab70301137e62a Mon Sep 17 00:00:00 2001 From: bibajz Date: Thu, 3 Feb 2022 22:44:35 +0100 Subject: [PATCH 4/7] Add forgotten is_bare check for unspecified Tuple * Tuple.__args__ is equal to (), which is not covered by cases already listed --- src/cattr/_compat.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/cattr/_compat.py b/src/cattr/_compat.py index 2b26c9e5..e2106c14 100644 --- a/src/cattr/_compat.py +++ b/src/cattr/_compat.py @@ -166,6 +166,7 @@ def is_mapping(type): bare_mapping_args = TypingMapping.__args__ bare_dict_args = Dict.__args__ bare_mutable_seq_args = TypingMutableSequence.__args__ + bare_tuple_args = Tuple.__args__ def is_bare(type): # Lower-cased generics in 3.7-8 do not have `__args__` attribute. @@ -176,6 +177,7 @@ def is_bare(type): or args == bare_mapping_args or args == bare_dict_args or args == bare_mutable_seq_args + or args == bare_tuple_args or args is None ) From 9eb53502ac9f8f92699032cbf8556caed2729e42 Mon Sep 17 00:00:00 2001 From: bibajz Date: Thu, 3 Feb 2022 22:54:34 +0100 Subject: [PATCH 5/7] Fix unstructuring unannotated typing.Tuples --- src/cattrs/gen.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) 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. From 9b18974df8611b3c06e18a6f7628158cd7adfa57 Mon Sep 17 00:00:00 2001 From: bibajz Date: Fri, 4 Feb 2022 14:28:52 +0100 Subject: [PATCH 6/7] Reword HISTORY.rst entry, add a note about Tuple structuring --- HISTORY.rst | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/HISTORY.rst b/HISTORY.rst index ba7d18c2..f7a3e6a4 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -7,8 +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 bare generics on Pythons lower than 3.9. +* 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) ------------------- From 312cc2cd2f137ce9bb6538a116c0500f6d8b1d64 Mon Sep 17 00:00:00 2001 From: bibajz Date: Fri, 4 Feb 2022 14:35:10 +0100 Subject: [PATCH 7/7] Refactor is_bare for py3.7-8 --- src/cattr/_compat.py | 27 ++++++++++----------------- 1 file changed, 10 insertions(+), 17 deletions(-) diff --git a/src/cattr/_compat.py b/src/cattr/_compat.py index e2106c14..77c68e9b 100644 --- a/src/cattr/_compat.py +++ b/src/cattr/_compat.py @@ -161,25 +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_tuple_args = Tuple.__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): - # Lower-cased generics in 3.7-8 do not have `__args__` attribute. - args = getattr(type, "__args__", None) - 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 - or args == bare_tuple_args - or args is None - ) + return getattr(type, "__args__", None) in bare_generic_args def is_counter(type): return (