Search code examples
pythonpython-typingmypy

Python 3.6 type hinting for a function accepting generic class type and instance type of the same generic type


I have a function with the following signature:

def wait_for_namespaced_objects_condition(
    obj_type: Type[NamespacedAPIObject],
    obj_condition_fun: Callable[[NamespacedAPIObject], bool],
) -> List[NamespacedAPIObject]:
...

Important part here are NamespacedAPIObject parameters. This function takes an obj_type as type spec, then creates an object(instance) of that type(class). Then some other objects of that type are added to a list, which is then filtered with obj_condition_fun and returned as a result of type List[NamespacedAPIObject]. This works fine and also evaluates OK with mypy`.

Now, I want to make this function generic, so that in the place of NamespacedAPIObject any subtype of it can be used. My attempt was to do it like this:

T = TypeVar("T", bound=NamespacedAPIObject)

def wait_for_namespaced_objects_condition(
    obj_type: Type[T],
    obj_condition_fun: Callable[[T], bool],
) -> List[T]:

But Type[T] is TypeVar, so it's not the way to go. The question is: what should be the type of obj_type parameter to make this work? I tried Generic[T], which seemed the most reasonable to me, but it doesn't work. If i put there just obj_type: T, mypy evaluates this OK, but it seems to wrong to me. In my opinion this means that obj_type is an instance of a class that is a subtype of NamespacedAPIObject, while what I want to say is that "T is a class variable that represents subtype of NamespacedAPIObject. How to solve this?

[Edit] To explain a little bit better (see comment below) why Type[T] doesn't work for me: in my case all subtype of T implement class method objects(), but when I try to write my code like this:

obj_type.objects()

mypy returns:

pytest_helm_charts/utils.py:36: error: "Type[T]" has no attribute "objects"

Solution

  • But Type[T] is TypeVar, so it's not the way to go.

    No, you are on the right track - TypeVar is definitely the way to go. The problem here is rather in pykube.objects.APIObject class being wrapped in a decorator that mypy cannot deal with yet. Adding type stubs for pykube.objects will resolve the issue. Create a directory _typeshed/pykube and add minimal type stubs for pykube:

    • _typeshed/pykube/__init__.pyi:

      from typing import Any
      
      def __getattr__(name: str) -> Any: ...  # incomplete
      
    • _typeshed/pykube/objects.pyi:

      from typing import Any, ClassVar, Optional
      from pykube.query import Query
      
      def __getattr__(name: str) -> Any: ...  # incomplete
      
      class ObjectManager:
          def __getattr__(self, name: str) -> Any: ...  # incomplete
          def __call__(self, api: Any, namespace: Optional[Any] = None) -> Query: ...
      
      class APIObject:
          objects: ClassVar[ObjectManager]
          def __getattr__(self, name: str) -> Any: ...  # incomplete
      
      class NamespacedAPIObject(APIObject): ...
      

    Now running

    $ MYPYPATH=_typeshed mypy pytest_helm_charts/
    

    resolves obj_type.objects correctly:

    T = TypeVar('T', bound=NamespacedAPIObject)
    
    
    def wait_for_namespaced_objects_condition(obj_type: Type[T]) -> List[T]:
        reveal_type(obj_type.objects)
    

    Output:

    pytest_helm_charts/utils.py:29: note: Revealed type is 'pykube.objects.ObjectManager'