Search code examples
pythongenericspython-typing

How to get the runtime type of a class generic type by using a decorator?


I have a generic class like

from typing import Generic, TypeVar, List

T = TypeVar('T')

@my_decorator
class Foo(Generic[T]):
    pass

f: Foo[int] = Foo()
g: Foo[List[float]] = Foo()

Is there a clean way to get the type annotations of the constructor call Foo[int], Foo[List[float]] in the decorator? I want to do some type checking at runtime.

I'm able do access the constructor call of Foo via the decorator and I even get the code line f: Foo[int] = Foo() of the constructor call by using inspect.stack() and inspect.getsource(my_frame) in a very unpretty way. Then I'm able to get Foo[int] by some string manipulation.

Beside that this is a very dirty way of doing it, it only get the type as a string. But I need the actually type. In this example I could use eval() to "parse" the string and convert it into a type. But that doesn't work for custom classes, like in this example:

from typing import Generic, TypeVar, List

T = TypeVar('T')

class Bar:
    pass

@my_decorator
class Foo(Generic[T]):
    pass

h: Foo[List[Bar]] = Foo()

In this case I cannot use eval() because I have no idea how I can get the proper context. I like to get something like my_file.Foo[typing.List[my_file.Bar]] which would allow me to do type checking at runtime.

So is there any clean way of doing it? Or is there at least a (dirty) way to get the correct context for eval() to "parse" the string?


Solution

  • TL;DR: This is not possible to get the runtime type of a generic inside __init__ but we can get close enough in the CPython implementation

    1. To handle this with only a decorator on your class, you should change your call to h = Foo[List[Bar]]() so that the typehint becomes accessible to the decorator independently from the variable holding the returned object.

    2. This answer indicates that class instances of generic classes have a __orig_class__ attribute available after initialization. This attribute is set after init (see source code).

    Hence, if we write a class decorator, the decorator should modify the source class to basically listen to when the __orig_class__ attribute is set by the runtime. This heavily relies on an undocumented implementation detail though and will probably not work the same way in future versions or other implementations of Python.

    def my_decorator(cls):
        orig_bases = cls.__orig_bases__
        genericType = orig_bases[0]
        class cls2(cls, genericType):
            def __init__(self, *args, **kwargs):
                super(cls2, self).__init__(*args, *kwargs)
            def __setattr__(self, name, value):
                object.__setattr__(self, name, value)
                if name == "__orig_class__":
                    print("Runtime generic type is " + str(get_args(self.__orig_class__)))
        cls2.__orig_bases__ = orig_bases
        return cls2
    

    And then:

    >>> h = Foo[List[Bar]]()
    Runtime generic type is (typing.List[__main__.Bar],)