How to type hint a class wrapper that checks for constructor signature?

I’m trying to write a function that “cache” the parameters used to build some classes, and may be applied to different classes.
However I’m not sure hot to properly typecheck them. I tried to use the ParamSpec and a class Protocol to try to capture the required __init__ signature, but I think I’m missing something.

On mypy Playground you can find a mypy gist, and, at the end, I am unable to get a type checker error for a wrong usage of the function (witch is my goal)

(for reference this is the snippet)

from typing import Generic
from typing import ParamSpec
from typing import Protocol
from typing import reveal_type

P0 = ParamSpec('P0')


class T1(Generic[P0], Protocol):
    def __init__(self, *args: P0.args, **kwargs: P0.kwargs) -> None: ...


class T2(Generic[P0], Protocol):
    def __call__(self, cls: type[T1[P0]]) -> T1[P0]: ...


class C:
    def __init__(self, foo: int, bar: str) -> None: ...


class D:
    def __init__(self) -> None: ...


def factory_builder[**P](*args: P.args, **kwargs: P.kwargs) -> T2[P]:
    def factory(cls: type[T1[P]]) -> T1[P]:
        return cls(*args, **kwargs)

    return factory


factory = factory_builder(1, bar='xxx')
reveal_type(factory)
c = factory(C)  # ==> c == C(1, bar="xxx")
reveal_type(c)
d = factory(D)  # error
reveal_type(d)

After c = factory(C), is the T1[P] of the global factory function bound to C? Or is T1 still generic, and can be rebound, even though P in it is fixed by the call to factory_builder?

I’m not sure to follow the question.
the goal is that, as factory has been built via factory_builder(1, bar='xxx'), it can be called only with classes that have the same constructor signature (as C, for example, because it accept foo: int, bar: str)

I think I’ve achieved it with a simplified(?) typing structure

from typing import TYPE_CHECKING
from typing import Protocol

if TYPE_CHECKING:
    from collections.abc import Callable


class T[**P](Protocol):
    def __init__(self, *args: P.args, **kwargs: P.kwargs) -> None: ...


def factory_builder[**P](*args: P.args, **kwargs: P.kwargs) -> 'Callable[[Callable[P, T[P]]], T[P]]':
    def factory(cls: 'Callable[P, T[P]]') -> T[P]:
        return cls(*args, **kwargs)

    return factory

but it’s not clear to me why it didn’t work with classes protocol

1 Like