Skip to content

Commit ff8cd94

Browse files
raabfTinche
authored andcommitted
Fix __parameters__ access in gen._generate_mapping
There are Generic types in the typing modules from which you can inherit in your own classes which do not have an __parameters__ attribute, such classes are now ignored making gen._generate_mapping effectively a no-op in case the class do not have an __parameters__ attribute. As https://github.com/ilevkivskyi/typing_inspect/blob/8f6aa2075ba448ab322def454137e7c59b9b302d/typing_inspect.py#L405 is showing there are also cases where __parameters__ could be None, so I test for both cases, that it is None or that it does not exist. See Also: #217
1 parent a77b5dd commit ff8cd94

File tree

2 files changed

+72
-1
lines changed

2 files changed

+72
-1
lines changed

src/cattrs/gen.py

+11-1
Original file line numberDiff line numberDiff line change
@@ -204,7 +204,17 @@ def _generate_mapping(
204204
cl: Type, old_mapping: Dict[str, type]
205205
) -> Dict[str, type]:
206206
mapping = {}
207-
for p, t in zip(get_origin(cl).__parameters__, get_args(cl)):
207+
208+
# To handle the cases where classes in the typing module are using
209+
# the GenericAlias structure but aren’t a Generic and hence
210+
# end up in this function but do not have an `__parameters__`
211+
# attribute. These classes are interface types, for example
212+
# `typing.Hashable`.
213+
parameters = getattr(get_origin(cl), "__parameters__", None)
214+
if parameters is None:
215+
return old_mapping
216+
217+
for p, t in zip(parameters, get_args(cl)):
208218
if isinstance(t, TypeVar):
209219
continue
210220
mapping[p.__name__] = t

tests/test_converter_inheritance.py

+61
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import collections
2+
import typing
13
from typing import Type
24

35
import attr
@@ -43,3 +45,62 @@ class B(A):
4345

4446
# This should still work, but using the new hook instead.
4547
assert converter.structure({"i": 1}, B) == B(2)
48+
49+
50+
@pytest.mark.parametrize("converter_cls", [Converter, GenConverter])
51+
@pytest.mark.parametrize(
52+
"typing_cls", [typing.Hashable, typing.Iterable, typing.Reversible]
53+
)
54+
def test_inherit_typing(converter_cls: Type[Converter], typing_cls):
55+
"""Stuff from typing.* resolves to runtime to collections.abc.*.
56+
57+
Hence, typing.* are of a special alias type which we want to check if
58+
cattrs handles them correctly.
59+
"""
60+
converter = converter_cls()
61+
62+
@attr.define
63+
class A(typing_cls):
64+
i: int = 0
65+
66+
def __hash__(self):
67+
return hash(self.i)
68+
69+
def __iter__(self):
70+
return iter([self.i])
71+
72+
def __reversed__(self):
73+
return iter([self.i])
74+
75+
assert converter.structure({"i": 1}, A) == A(i=1)
76+
77+
78+
@pytest.mark.parametrize("converter_cls", [Converter, GenConverter])
79+
@pytest.mark.parametrize(
80+
"collections_abc_cls",
81+
[
82+
collections.abc.Hashable,
83+
collections.abc.Iterable,
84+
collections.abc.Reversible,
85+
],
86+
)
87+
def test_inherit_collections_abc(
88+
converter_cls: Type[Converter], collections_abc_cls
89+
):
90+
"""As extension of test_inherit_typing, check if collections.abc.* work."""
91+
converter = converter_cls()
92+
93+
@attr.define
94+
class A(collections_abc_cls):
95+
i: int = 0
96+
97+
def __hash__(self):
98+
return hash(self.i)
99+
100+
def __iter__(self):
101+
return iter([self.i])
102+
103+
def __reversed__(self):
104+
return iter([self.i])
105+
106+
assert converter.structure({"i": 1}, A) == A(i=1)

0 commit comments

Comments
 (0)