cattrs icon indicating copy to clipboard operation
cattrs copied to clipboard

Using `typing.Any` to register a structure hook for a generic type is not working.

Open diefans opened this issue 5 years ago • 1 comments

  • 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.

diefans avatar May 19 '20 10:05 diefans

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) .

raabf avatar Aug 03 '20 10:08 raabf