-
-
Notifications
You must be signed in to change notification settings - Fork 129
Description
Hello,
since the latest cattrs v25.1.0, I am experiencing an AttributeError: type object CLASS has no attribute '__parameters__' when trying to structure attrs classes that are subclasses of one of collections.abc or typing generic alias types (e.g. Mapping[str, int])
For example:
from collections.abc import Mapping
from attrs import define, fields
from cattrs import Converter
from cattrs.gen import make_dict_structure_fn
@define
class Point(Mapping[str, int]):
x: int = 0
y: int = 0
def __getitem__(self, key):
try:
return getattr(self, key)
except AttributeError:
raise KeyError(key) from None
def __iter__(self):
return (f.name for f in fields(self.__class__))
def __len__(self):
return len(fields(self.__class__))
converter = Converter()
converter.register_structure_hook(
Point,
make_dict_structure_fn(Point, converter, _cattrs_forbid_extra_keys=True),
)
converter.structure({"x": 2, "y": 3}, Point)I get the following error:
Traceback (most recent call last):
File "<python-input-3>", line 23, in <module>
make_dict_structure_fn(Point, converter, _cattrs_forbid_extra_keys=True),
~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/Users/clupo/oss/cattrs/src/cattrs/gen/__init__.py", line 728, in make_dict_structure_fn
mapping = generate_mapping(base, mapping)
File "/Users/clupo/oss/cattrs/src/cattrs/gen/_generics.py", line 39, in generate_mapping
parameters = origin.__parameters__
^^^^^^^^^^^^^^^^^^^^^
AttributeError: type object 'Mapping' has no attribute '__parameters__
If instead of using a generic base class, I define my example Point class above simply as class Point(Mapping) without subscripting Mapping[str, int] then the cattrs structuring works.
The error did not occur with the previous cattrs release (v24.1.3) so it looks like a regression.
Note this is exactly the same error as the one identified in this old issue:
#217
That issue was fixed with this PR #221
However it looks like the current code in cattrs.gen._generics.generate_mapping no longer handles the absence of __parameters__ attribute which is not present in classes from collections.abc (generic alias types).
You do have a test that confirms structuring attrs-classes that in turn subclass from typing/collections.abc classes works, see:
cattrs/tests/test_converter_inheritance.py
Lines 44 to 88 in 0bb472a
| @pytest.mark.parametrize("typing_cls", [Hashable, Iterable, Reversible]) | |
| def test_inherit_typing(converter: BaseConverter, 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. | |
| """ | |
| @define | |
| class A(typing_cls): # pragma: nocover | |
| 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( | |
| "collections_abc_cls", | |
| [collections.abc.Hashable, collections.abc.Iterable, collections.abc.Reversible], | |
| ) | |
| def test_inherit_collections_abc(converter: BaseConverter, collections_abc_cls): | |
| """As extension of test_inherit_typing, check if collections.abc.* work.""" | |
| @define | |
| class A(collections_abc_cls): # pragma: nocover | |
| 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) |
However in those tests you are only testing with non-generic types.
If I modify the test case by adding e.g. Iterable[int] to the parametrized typing_cls, then the test fails with the same AttributeError: type object 'Iterable' has no attribute '__parameters__'.
I bisected the regression in this specific commit that was meant to "drop dead code", but it turns out this wasn't really "dead":
2fe721e