Skip to content

Commit 7c67730

Browse files
authored
Fix structuring of attrs class that inherit from typing/collections.abc generic type aliases (#655)
* [test_converter_inheritance] test inheriting from generic collections.abc types still works This reproduces issue #654: AttributeError: type object 'Iterable' has no attribute '__parameters__' * [_generics] handle parametrized types from collections.abc without __parameters__ attributes Reverts 2fe721e
1 parent 0bb472a commit 7c67730

File tree

3 files changed

+20
-3
lines changed

3 files changed

+20
-3
lines changed

HISTORY.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,11 @@ The third number is for emergencies when we need to start branches for older rel
1111

1212
Our backwards-compatibility policy can be found [here](https://github.com/python-attrs/cattrs/blob/main/.github/SECURITY.md).
1313

14+
## 25.1.1 (UNRELEASED)
15+
16+
- Fixed `AttributeError: no attribute '__parameters__'` while structuring attrs classes that inherit from parametrized generic aliases from `collections.abc`.
17+
([#654](https://github.com/python-attrs/cattrs/issues/654) [#655](https://github.com/python-attrs/cattrs/pull/655))
18+
1419
## 25.1.0 (2025-05-31)
1520

1621
- **Potentially breaking**: The converters raise {class}`StructureHandlerNotFoundError` more eagerly (on hook creation, instead of on hook use).

src/cattrs/gen/_generics.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,14 @@ def generate_mapping(cl: type, old_mapping: dict[str, type] = {}) -> dict[str, t
3636
origin = get_origin(cl)
3737

3838
if origin is not None:
39-
parameters = origin.__parameters__
39+
# To handle the cases where classes in the typing module are using
40+
# the GenericAlias structure but aren't a Generic and hence
41+
# end up in this function but do not have an `__parameters__`
42+
# attribute. These classes are interface types, for example
43+
# `typing.Hashable`.
44+
parameters = getattr(get_origin(cl), "__parameters__", None)
45+
if parameters is None:
46+
return dict(old_mapping)
4047

4148
for p, t in zip(parameters, get_args(cl)):
4249
if isinstance(t, TypeVar):

tests/test_converter_inheritance.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ class B(A):
4141
assert converter.structure({"i": 1}, B) == B(2)
4242

4343

44-
@pytest.mark.parametrize("typing_cls", [Hashable, Iterable, Reversible])
44+
@pytest.mark.parametrize("typing_cls", [Hashable, Iterable, Reversible, Iterable[int]])
4545
def test_inherit_typing(converter: BaseConverter, typing_cls):
4646
"""Stuff from typing.* resolves to runtime to collections.abc.*.
4747
@@ -67,7 +67,12 @@ def __reversed__(self):
6767

6868
@pytest.mark.parametrize(
6969
"collections_abc_cls",
70-
[collections.abc.Hashable, collections.abc.Iterable, collections.abc.Reversible],
70+
[
71+
collections.abc.Hashable,
72+
collections.abc.Iterable,
73+
collections.abc.Reversible,
74+
collections.abc.Iterable[int],
75+
],
7176
)
7277
def test_inherit_collections_abc(converter: BaseConverter, collections_abc_cls):
7378
"""As extension of test_inherit_typing, check if collections.abc.* work."""

0 commit comments

Comments
 (0)