Search code examples
pythonpython-3.xtype-hintingmypy

Unbound TypeVar variable in overloaded class


I have this following code which is a simplified version of an entity component system in python:

from __future__ import annotations

from typing import TYPE_CHECKING, TypeVar, overload

if TYPE_CHECKING:
    from collections.abc import Generator


T = TypeVar("T")
T1 = TypeVar("T1")
T2 = TypeVar("T2")


class Registry:
    def __init__(self) -> None:
        self._next_game_object_id = 0
        self._components: dict[type[T], set[int]] = {}
        self._game_objects: dict[int, dict[type[T], T]] = {}

    @overload
    def get_components(
        self,
        component: type[T],
    ) -> Generator[tuple[int, T], None, None]:
        ...

    @overload
    def get_components(
        self,
        component: type[T],
        component_two: type[T1],
    ) -> Generator[tuple[int, T, T1], None, None]:
        ...

    @overload
    def get_components(
        self,
        component: type[T],
        component_two: type[T1],
        component_three: type[T2],
    ) -> Generator[tuple[int, T, T1, T2], None, None]:
        ...

    def get_components(self, *components: type[T]) -> Generator[tuple[int, tuple[T, ...]], None, None]:
        game_object_ids = set.intersection(
            *(self._components[component] for component in components),
        )

        for game_object_id in game_object_ids:
            yield game_object_id, tuple(
                self._game_objects[game_object_id][component]
                for component in components
            )

However, I'm getting some mypy errors which I cannot figure out. One of which says that the TypeVar T is unbound and another says that get_components does not accept all possible arguments of signature 1, 2, and 3. How can I fix these errors?


Solution

  • Possible solution:

    from __future__ import annotations
    
    from typing import TYPE_CHECKING, TypeVar, overload, Generic
    
    if TYPE_CHECKING:
        from collections.abc import Generator
    
    
    T = TypeVar("T")
    T1 = TypeVar("T1")
    T2 = TypeVar("T2")
    
    
    class Registry(Generic[T, T1, T2]):
        def __init__(self) -> None:
            self._next_game_object_id = 0
            self._components: dict[type[T | T1 | T2], set[int]] = {}
            self._game_objects: dict[int, dict[type[T | T1 | T2], T | T1 | T2]] = {}
    
        @overload
        def get_components(
            self,
            __component: type[T],
        ) -> Generator[tuple[int, tuple[T]], None, None]:
            ...
    
        @overload
        def get_components(
            self,
            __component: type[T],
            __component_two: type[T1],
        ) -> Generator[tuple[int, tuple[T, T1]], None, None]:
            ...
    
        @overload
        def get_components(
            self,
            __component: type[T],
            __component_two: type[T1],
            __component_three: type[T2],
        ) -> Generator[tuple[int, tuple[T, T1, T2]], None, None]:
            ...
    
        def get_components(
            self,
            *components: type[T | T1 |T2]
        ) -> Generator[tuple[int, tuple[T | T1 | T2, ...]], None, None]:
            game_object_ids = set.intersection(
                *(self._components[component] for component in components),
            )
    
            for game_object_id in game_object_ids:
                yield game_object_id, tuple(
                    self._game_objects[game_object_id][component]
                    for component in components
                )
    
    

    Explanations:

    1. Inheriting from typing.Generic binds the TypeVars to the class through making it a generic class which depends on those TypeVars (More on Generics)
    2. Generalizing the method definition of get_components is crucial to accept all the different types that can occur through the overloads
    3. wrapping all the T, T, T1 and T, T1, T2 inside a tuple[...] as this ensures consistency with the return annotation of the now generalized method definition
    4. Making the components in the @overloaded methods positional only. This behaviour of *args and @overload seems not very well documented, only through a resolved issue on GitHub