Skip to content

Commit

Permalink
Fix __parameters__ access in gen._generate_mapping
Browse files Browse the repository at this point in the history
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:
python-attrs#217
  • Loading branch information
raabf committed Feb 3, 2022
1 parent 989d2d7 commit a527d07
Show file tree
Hide file tree
Showing 2 changed files with 68 additions and 1 deletion.
12 changes: 11 additions & 1 deletion src/cattrs/gen.py
Original file line number Diff line number Diff line change
Expand Up @@ -204,7 +204,17 @@ def _generate_mapping(
cl: Type, old_mapping: Dict[str, type]
) -> Dict[str, type]:
mapping = {}
for p, t in zip(get_origin(cl).__parameters__, get_args(cl)):

# To handle the cases where classes in the typing module are using
# the GenericAlias structure but aren’t a Generic and hence
# end up in this function but do not have an `__parameters__`
# attribute. These classes are interface types, for example
# `typing.Hashable`.
parameters = getattr(get_origin(cl), "__parameters__", None)
if parameters is None:
return old_mapping

for p, t in zip(parameters, get_args(cl)):
if isinstance(t, TypeVar):
continue
mapping[p.__name__] = t
Expand Down
57 changes: 57 additions & 0 deletions tests/test_converter_inheritance.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import collections
import typing
from typing import Type

import attr
Expand Down Expand Up @@ -43,3 +45,58 @@ class B(A):

# This should still work, but using the new hook instead.
assert converter.structure({"i": 1}, B) == B(2)


@pytest.mark.parametrize("converter_cls", [Converter, GenConverter])
@pytest.mark.parametrize(
"typing_cls", [typing.Hashable, typing.Iterable, typing.Reversible]
)
def test_inherit_typing(converter_cls: Type[Converter], typing_cls):
"""Stuff from typing.* resolves to runtime to collections.abc.*.
Hence, typing.* are of a special alias type which we want to check if
cattrs handles them correctly.
"""
converter = converter_cls()

@attr.define
class A(typing_cls):
i: int = 0

def __hash__(self):
return hash(self.i)

def __iter__(self):
return iter([self.i])

def __reversed__(self):
return iter([self.i])

assert converter.structure({"i": 1}, A) == A(i=1)


@pytest.mark.parametrize("converter_cls", [Converter, GenConverter])
@pytest.mark.parametrize(
"collections_abc_cls",
[collections.abc.Hashable, collections.abc.Iterable, collections.abc.Reversible],
)
def test_inherit_collections_abc(
converter_cls: Type[Converter], collections_abc_cls
):
"""As extension of test_inherit_typing, check if collections.abc.* work."""
converter = converter_cls()

@attr.define
class A(collections_abc_cls):
i: int = 0

def __hash__(self):
return hash(self.i)

def __iter__(self):
return iter([self.i])

def __reversed__(self):
return iter([self.i])

assert converter.structure({"i": 1}, A) == A(i=1)

0 comments on commit a527d07

Please sign in to comment.