Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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"
like image 786
piontec Avatar asked Jul 16 '20 14:07

piontec


2 Answers

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'
like image 91
hoefling Avatar answered Sep 29 '22 22:09

hoefling


Why Type[T] is not the way to go? The way I see it, it is quite similar to one of the examples:

However using Type[] and a type variable with an upper bound we can do much better:

U = TypeVar('U', bound=User)
def new_user(user_class: Type[U]) -> U:
    ...

Now when we call new_user() with a specific subclass of User a type checker will infer the correct type of the result:

joe = new_user(BasicUser)  # Inferred type is BasicUser

In case of using classmethod :

from typing import Callable, Type, List, TypeVar

T = TypeVar('T', bound=Base)

class Base:
    @classmethod
    def objects(cls: Type[T]) -> List[T]:
        ...
    
    def run(self):
        ...

class Derived(Base):
    def run(self):
        ...


def foo(d: Derived) -> bool:
    return True


def wait_for_namespaced_objects_condition(
    obj_type: Type[T],
    obj_condition_fun: Callable[[T], bool],
) -> List[T]:
    a = obj_type.objects()
    return a
    
    
wait_for_namespaced_objects_condition(Derived, foo)
like image 40
alex_noname Avatar answered Sep 29 '22 20:09

alex_noname