Using `typing.Any` to register a structure hook for a generic type is not working.
- cattrs version: 1.0.0
- Python version: 3.8.2
- Operating System: archlinux
Description
import typing
import attr
import cattr
import typing_inspect
T = typing.TypeVar("T")
class GenericList(typing.List[T]):
...
def _structure_generic_list(d, t):
(conv,) = typing_inspect.get_args(t)
return list(map(conv, d.split(",")))
# this is ignored
cattr.register_structure_hook(GenericList[typing.Any], _structure_generic_list)
# this works
cattr.register_structure_hook(GenericList[str], _structure_generic_list)
cattr.register_structure_hook(GenericList[int], _structure_generic_list)
@attr.s(auto_attribs=True)
class Params:
some_words: GenericList[str]
some_ids: GenericList[int]
def test_structure_generic_list():
src = {"some_words": "foo,bar", "some_ids": "123,456"}
params = cattr.structure(src, Params)
assert params == Params(some_words=["foo", "bar"], some_ids=[123, 456])
Using typing.Any to register a structure hook for a generic type is not working.
The reason is that typing.Any is not a class in that way. It actually just an alias (from the typing module):
Any = _SpecialForm('Any', doc='…')
And _SpecialForm is not the super class of any other class. If you do a subclass check, it will fail always:
>>> import typing
>>> issubclass(bool, typing.Any)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "/home/raabf/.zplug/repos/pyenv/pyenv/versions/3.7.7/lib/python3.7/typing.py", line 338, in __subclasscheck__
raise TypeError(f"{self} cannot be used with issubclass()")
TypeError: typing.Any cannot be used with issubclass()
Or in other words, typing.Any can be only used for static type checks, but cattr.register_structure_hook() resolves the correct registered function by run-time type checking (issubclass/isinstance must succeed), hence it can never work.
Probably, registering typing.Any is a bad idea anyway, since it is a very generic type and hence unspecified to which type is should be constructed.
For your case you have two alternatives.
First, just define a class which purpose is to decide between specific types, for example:
import abc
class IntOrStrType(metaclass=abc.ABCMeta):
pass
def _structure_int_or_str(d, t):
try:
return int(d)
except AttributeError:
return str(d)
# Register the types is not required by cattr, but nice if you want to do check afterwards if the constructed value is a subtype of the attr field (for example some_words/some_ids in your example).
IntOrStrType.register(int)
IntOrStrType.register(str)
issubclass(int, IntOrStrType) # True
issubclass(str, IntOrStrType) # True
cattr.register_structure_hook(IntOrStrType, _structure_int_or_str)
Then just use cattr.structure(some_input, IntOrStrType). This would also work with your generic types, but for generics there is an even better solution.
Second, if you want to have a single function for a generic class such as GenericList regardless which specific type it has, you can register the generic class as following:
cattr.register_structure_hook_func(
# the default value (here `bool`) must be something so that the expression is `False` (i.e. not a subclass of GenericList). Could
# be also replaced by an `if hasattr(cls, '__origin__') …` but it is shorter and faster that way.
# The object has a __origin__ attribute if it us used as `Class[TYPE]`, then __origin__ will point to `Class`. This
# test is secure enough since it is not only tested that the class has the attribute, but that it is also
# a subclass of GenericList, which is the class we want to support with this converter.
lambda cls: issubclass(getattr(cls, '__origin__', bool), GenericList),
_structure_generic_list,
)
This will call _structure_generic_list always when GenericList or one of its subtypes is used, and the type T of the generic does not matter any more (so you can still use GenericList[str], GenericList[int], … as your attr type) .