diff --git a/HISTORY.md b/HISTORY.md index f5b910d8..7bfa83b2 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -11,6 +11,11 @@ The third number is for emergencies when we need to start branches for older rel Our backwards-compatibility policy can be found [here](https://github.com/python-attrs/cattrs/blob/main/.github/SECURITY.md). +## 25.1.1 (UNRELEASED) + +- Fixed `AttributeError: no attribute '__parameters__'` while structuring attrs classes that inherit from parametrized generic aliases from `collections.abc`. + ([#654](https://github.com/python-attrs/cattrs/issues/654) [#655](https://github.com/python-attrs/cattrs/pull/655)) + ## 25.1.0 (2025-05-31) - **Potentially breaking**: The converters raise {class}`StructureHandlerNotFoundError` more eagerly (on hook creation, instead of on hook use). diff --git a/src/cattrs/gen/_generics.py b/src/cattrs/gen/_generics.py index 63d2fb91..069c48c8 100644 --- a/src/cattrs/gen/_generics.py +++ b/src/cattrs/gen/_generics.py @@ -36,7 +36,14 @@ def generate_mapping(cl: type, old_mapping: dict[str, type] = {}) -> dict[str, t origin = get_origin(cl) if origin is not None: - parameters = origin.__parameters__ + # 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 dict(old_mapping) for p, t in zip(parameters, get_args(cl)): if isinstance(t, TypeVar): diff --git a/tests/test_converter_inheritance.py b/tests/test_converter_inheritance.py index 27c68ade..63a5f99a 100644 --- a/tests/test_converter_inheritance.py +++ b/tests/test_converter_inheritance.py @@ -41,7 +41,7 @@ class B(A): assert converter.structure({"i": 1}, B) == B(2) -@pytest.mark.parametrize("typing_cls", [Hashable, Iterable, Reversible]) +@pytest.mark.parametrize("typing_cls", [Hashable, Iterable, Reversible, Iterable[int]]) def test_inherit_typing(converter: BaseConverter, typing_cls): """Stuff from typing.* resolves to runtime to collections.abc.*. @@ -67,7 +67,12 @@ def __reversed__(self): @pytest.mark.parametrize( "collections_abc_cls", - [collections.abc.Hashable, collections.abc.Iterable, collections.abc.Reversible], + [ + collections.abc.Hashable, + collections.abc.Iterable, + collections.abc.Reversible, + collections.abc.Iterable[int], + ], ) def test_inherit_collections_abc(converter: BaseConverter, collections_abc_cls): """As extension of test_inherit_typing, check if collections.abc.* work."""