Search code examples
pythonpython-typingcode-completion

Custom type hint annotation


I just wrote a simple @autowired decorator for Python that instantiate classes based on type annotations.

To enable lazy initialization of the class, the package provides a lazy(type_annotation: (Type, str)) function so that the caller can use it like this:

@autowired
def foo(bla, *, dep: lazy(MyClass)):
   ...

This works very well, under the hood this lazy function just returns a function that returns the actual type and that has a lazy_init property set to True. Also this does not break IDEs' (e.g., PyCharm) code completion feature.

But I want to enable the use of a subscriptable Lazy type use instead of the lazy function.

Like this:

@autowired
def foo(bla, *, dep: Lazy[MyClass]):
   ...

This would behave very much like typing.Union. And while I'm able to implement the subscriptable type, IDEs' code completion feature will be rendered useless as it will present suggestions for attributes in the Lazy class, not MyClass.

I've been working with this code:

class LazyMetaclass(type):
    def __getitem__(lazy_type, type_annotation):
        return lazy_type(type_annotation)

class Lazy(metaclass=LazyMetaclass):
    def __init__(self, type_annotation):
        self.type_annotation = type_annotation

I tried redefining Lazy.__dict__ as a property to forward to the subscripted type's __dict__ but this seems to have no effect on the code completion feature of PyCharm.

I strongly believe that what I'm trying to achieve is possible as typing.Union works well with IDEs' code completion. I've been trying to decipher what in the source code of typing.Union makes it to behave well with code completion features but with no success so far.


Solution

  • For the Container[Type] notation to work you would want to create a user-defined generic type:

    from typing import TypeVar, Generic
    
    T = TypeVar('T')
    
    class Lazy(Generic[T]):
        pass
    

    You then use

    def foo(bla, *, dep: Lazy[MyClass]):
    

    and Lazy is seen as a container that holds the class.

    Note: this still means the IDE sees dep as an object of type Lazy. Lazy is a container type here, holding an object of type MyClass. Your IDE won't auto-complete for the MyClass type, you can't use it that way.

    The notation also doesn't create an instance of the Lazy class; it creates an instance of the (private) typing._GenericAlias class instead. This instance has an attribute __args__ to let you introspect the subscription arguments:

    >>> a = Lazy[str]
    >>> type(a)
    <class 'typing._GenericAlias'>
    >>> a.__args__
    (<class 'str'>,)
    

    but it's better to use the typing.get_args() function; this function handles a few edge cases with specific type hint objects where accessing __args__ would lead to suprising results:

    >>> from typing import get_args
    >>> get_args(Lazy[str])
    (<class 'str'>,)
    

    If all you wanted was to reach into the type annotations at runtime but resolve the name lazily, you could just support a string value:

    def foo(bla, *, dep: 'MyClass'):
    

    This is valid type annotation, and your decorator could resolve the name at runtime by using the typing.get_type_hints() function (at a deferred time, not at decoration time), or by wrapping strings in your lazy() callable at decoration time.

    If lazy() is meant to flag a type to be treated differently from other type hints, then you are trying to overload the type hint annotations with some other meaning, you want to use the Annotated[type_hint, metadata] annotation to attach extra metadata to the type hint for 3rd party use.

    E.g. attaching a Lazy annotation to the type hint would look like this:

    from typing import Annotated
    
    def foo(bla, *, dep: Annotated[MyClass, Lazy]):
    

    Annotated[type_hint, ...] will look like type_hint to type checkers.

    Use get_type_hints(..., include_extras=True) when trying to access such hints from runtime code, otherwise the Annotated objects are stripped. Then, when introspecting Annotated objects, look at the __metadata__ attribute to introspect the extra metadata.