- cattrs version: 1.10.0
- Python version: 3.7
- Operating System: Linux 5.16.1-1 OpenSUSE Thumbleweed
There is an exception AttributeError: type object 'CLASS' has no attribute '__parameters__' when you try to structure an attrs Class which inherts typing.CLASS
Reproduce
Have a look at this example where CLASS is Hashable:
import attr
import typing
import cattrs
import collections
converter = cattrs.GenConverter()
@attr.define
class A(collections.abc.Hashable): # typing.Hashable should be preferred over collections.abc.Hashable
attr1: str = ''
attr2: int = 0
def __hash__(self) -> int:
return hash(self.attr2)
@attr.define
class B(typing.Hashable):
attr1: str = ''
attr2: int = 0
def __hash__(self) -> int:
return hash(self.attr2)
def main() -> None:
converter.structure(dict(attr1='hello', attr2=42), A) # OK
converter.structure(dict(attr1='hello', attr2=42), B) # AttributeError: type object 'Hashable' has no attribute '__parameters__'
if __name__ == '__main__':
main()
Output
Traceback (most recent call last):
File "/home/raabf/sl/demo.py", line 40, in <module>
main()
File "/home/raabf/sl/demo.py", line 37, in main
converter.structure(dict(attr1='hello', attr2=42), B) # AttributeError: type object 'Hashable' has no attribute '__parameters__'
File "/home/raabf/.pyenv/versions/sl37/lib/python3.7/site-packages/cattr/converters.py", line 300, in structure
return self._structure_func.dispatch(cl)(obj, cl)
File "/home/raabf/.pyenv/versions/sl37/lib/python3.7/site-packages/cattr/dispatch.py", line 49, in _dispatch
return self._function_dispatch.dispatch(cl)
File "/home/raabf/.pyenv/versions/sl37/lib/python3.7/site-packages/cattr/dispatch.py", line 126, in dispatch
return handler(typ)
File "/home/raabf/.pyenv/versions/sl37/lib/python3.7/site-packages/cattr/converters.py", line 745, in gen_structure_attrs_fromdict
**attrib_overrides,
File "/home/raabf/.pyenv/versions/sl37/lib/python3.7/site-packages/cattr/gen.py", line 224, in make_dict_structure_fn
mapping = _generate_mapping(base, mapping)
File "/home/raabf/.pyenv/versions/sl37/lib/python3.7/site-packages/cattr/gen.py", line 195, in _generate_mapping
for p, t in zip(get_origin(cl).__parameters__, get_args(cl)):
AttributeError: type object 'Hashable' has no attribute '__parameters__'
Note that in the above code snippet I use typing.Hashable just as an example, the same exception occurs if you inherit something other from typing.*, such as typing.Reversable or typing.Iterable.
Offending Part
The offending part in cattrs is:
https://github.com/python-attrs/cattrs/blob/v1.10.0/src/cattr/gen.py#L206-L225
def make_dict_structure_fn(
cl: Type[T],
converter: "Converter",
_cattrs_forbid_extra_keys: bool = False,
_cattrs_use_linecache: bool = True,
_cattrs_prefer_attrib_converters: bool = False,
**kwargs,
) -> Callable[[Mapping[str, Any]], T]:
"""Generate a specialized dict structuring function for an attrs class."""
mapping = {}
if is_generic(cl):
base = get_origin(cl)
mapping = _generate_mapping(cl, mapping)
cl = base
for base in getattr(cl, "__orig_bases__", ()):
if is_generic(base) and not str(base).startswith("typing.Generic"):
mapping = _generate_mapping(base, mapping)
break
which again calls
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)):
if isinstance(t, TypeVar):
continue
mapping[p.__name__] = t
if not mapping:
return old_mapping
return mapping
where __parameters__ is accessed. During the exception cl is typing.Hashable, which resolves to collections.abc.Hashable by get_origin(cl).
Reason
The use of typing.Hashable is correct. As far as I understand, during static analyisis from tools such as mypy it acts as type checking, during runtime it is resolved to collections.abc.Hashable where the class is implemented.
I personally rely on inheriting from typing.Hashable instead of collections.abc.Hashable, or else mypy would do wrong type checking.
I do not understand what cattrs is doing, so I just can make some guesses:
- The resolving from
typing.Hashable to collections.abc.Hashable at runtime (i.e. typing.Hashable has collections.abc.Hashable as its origin) confuses cattrs.
- Some check before fails and
_generate_mapping should have been never called with typing.Hashable
- In general you cannot assume that all origins have the
__parameters__ attribute, i.e. use getattr(get_origin(cl), '__parameters__', ()) instead.
Reference for getting parameters
The library typing_inspect has an get_parameters function to get __parameters__. The function is a bit longer and handles some corner cases , I do not know if this helps with problem reported in this issue here or can be utilized here.
There is an exception
AttributeError: type object 'CLASS' has no attribute '__parameters__'when you try to structure an attrs Class which inhertstyping.CLASSReproduce
Have a look at this example where
CLASSisHashable:Output
Note that in the above code snippet I use
typing.Hashablejust as an example, the same exception occurs if you inherit something other fromtyping.*, such astyping.Reversableortyping.Iterable.Offending Part
The offending part in cattrs is:
https://github.com/python-attrs/cattrs/blob/v1.10.0/src/cattr/gen.py#L206-L225
which again calls
where
__parameters__is accessed. During the exceptionclistyping.Hashable, which resolves tocollections.abc.Hashablebyget_origin(cl).Reason
The use of
typing.Hashableis correct. As far as I understand, during static analyisis from tools such asmypyit acts as type checking, during runtime it is resolved tocollections.abc.Hashablewhere the class is implemented.I personally rely on inheriting from
typing.Hashableinstead ofcollections.abc.Hashable, or else mypy would do wrong type checking.I do not understand what cattrs is doing, so I just can make some guesses:
typing.Hashabletocollections.abc.Hashableat runtime (i.e.typing.Hashablehascollections.abc.Hashableas its origin) confuses cattrs._generate_mappingshould have been never called withtyping.Hashable__parameters__attribute, i.e. usegetattr(get_origin(cl), '__parameters__', ())instead.Reference for getting parameters
The library
typing_inspecthas anget_parametersfunction to get__parameters__. The function is a bit longer and handles some corner cases , I do not know if this helps with problem reported in this issue here or can be utilized here.