Skip to content

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

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
diefans opened this issue May 19, 2020 · 1 comment

Comments

@diefans
Copy link

diefans commented May 19, 2020

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

@raabf
Copy link
Contributor

raabf commented Aug 3, 2020

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants