I'm trying to implement a factory design pattern (python 3.12+) and I'm having some trouble keeping my factory signature correct. Maybe what I'm trying to do is not conventional and I should look for an other design pattern.
Let's take this code example :
from typing import ParamSpec, TypeVar
P = ParamSpec("P")
T = TypeVar("T")
class Base:
@classmethod
def factory(cls, *args:P.args, **kwargs:P.kwargs):
def f(parent):
return cls(parent, *args, **kwargs)
return f
class CustomA(Base):
def __init__(self, parent:str, arg1:int, arg2:int):
print(f"Instantiate CustomA with arg {parent}, {arg1}, {arg2}")
#Ideally I would like this method signature to match the CustomA constructor signature
#without the parent argument
test = CustomA.factory(1, 2)
print(test) #Does correctly return a method
a = test("parent")
print(a) # That when is called return the object correctly instantiate
In my use case I would like to partially prepare a method call to instantiate an object while missing a parameter for that. Later I would complete that partial call to actually instantiate the object. In order to do so I implemented a factory builder method in the parent class that would allow me to easily implement that feature in all my child class. The code above is working fine, but I lose the signature and therefore IDE IntelliSense for the subclass constructor.
As you can see I tried to use ParamSpec and TypeVar but I don't really understand how they work. Maybe a different approach using the new method, or a decorator would be more suitable ? I didn't had success so far with those other approach.
You need two more steps to make it work: first, bind ParamSpec
to constructor arguments, and second, extract the first arg from the signature. The former can be done by reinterpreting current class (cls
) as a Callable
- remember that type[Something]
is very similar to type of its constructor. The latter can be done using Concatenate
(3.10+, import from typing_extensions
on older versions). So the final attempt might look like this:
from typing import Callable, Concatenate, ParamSpec, TypeVar
P = ParamSpec("P")
T = TypeVar("T")
R = TypeVar("R")
class Base:
@classmethod
def factory(cls: Callable[Concatenate[T, P], R], *args: P.args, **kwargs: P.kwargs) -> Callable[[T], R]:
def f(parent: T) -> R:
return cls(parent, *args, **kwargs)
return f
class CustomA(Base):
def __init__(self, parent: str, arg1: int, arg2: int) -> None:
print(f"Instantiate CustomA with arg {parent}, {arg1}, {arg2}")
#Ideally I would like this method signature to match the CustomA constructor signature
#without the parent argument
test = CustomA.factory(1, 2)
print(test) #Does correctly return a method
a = test("parent")
print(a) # That when is called return the object correctly instantiate
and it passes mypy
cleanly. To satisfy pyright
, you will also need to define __init__
on Base
with at least one argument (like def __init__(self, parent: str) -> None: pass
), or it will consider Callable
with at least one arg (required by Concatenate
) incompatible with cls
.