Creating a wrapper for dependency injection (mypy issue)
Closed this issue · 5 comments
Hi!
To give some context first, I'm currently trying to use antidote in a web backend made by FastAPI, and i'm trying to couple it's dependency injection system, with the one from antidote. I'm doing it, as the DI from FastAPI is limited to injecting into routes only, which is not enough in my case.
I did try to create a wrapper, that would allow me to couple those systems, but mypy does not like my implementation:
from typing import Type, TypeVar
from antidote import world
from fastapi import Depends
T = TypeVar("T")
class InjectService:
def __init__(self, service_type: Type[T]):
self.service_type = service_type
def __call__(self) -> T:
return world.get(self.service_type)
async def example_route(service: IUserService = Depends(InjectService(IUserService))) -> None:
pass
When trying to use the InjectService
class, it shows the following error:
Only concrete class can be given where "Type[IUserService]" is expected mypy(error)
The issue does not seem to be related to FastAPI, as i'm just not able to create a definition, that would allow me to pass a class with @abstractmethod
, even without TypeVar. Even replacing the Type[T]
with Type[Any]
does not allow me to pass the class.
Honestly, even though i tried, i cannot find out how you managed to create a type definition for world.get
that does not make mypy scream at me when using a class with @abstractmethod
:(
To give some more context, i would like to pass some data from fastapi dependencies (which would be in the __call__
method), to the injected service. For example the user that made the request, which would allow me to pass all the services that user, without the need to do that manually every time.
Is there maybe some way that would allow me to create such a wrapper? I did go through the docs, but i did not see anything that could help me. Thanks in advance :)
FWIW, @Finistere talked me through something similar in a discussion post
Also, that's a cool idea for the project. I know there's another project seeking to do an alternate DI for FastAPI.
Hi!
Protocols are complex to use properly with @interface
to be honest. Functions do not accept generic parameters so Antidote has to pass through __getitem__
with Type[T]
to mimic that behavior. But the latter doesn't accept protocols as you've seen.
I played a bit with it, but all solutions have their own problems:
- Just use
Any
, like FastAPI. - Either cast explicitly to
Type[IUserService]
or use an abstract class. - Hacky use of class generics. Not recommended.
from abc import ABC
from typing import Any, cast, Generic, Type, TypeVar
from fastapi import Depends
from typing_extensions import Protocol
from antidote import world
T = TypeVar("T")
class AbstractUserService(ABC):
...
class IUserService(Protocol):
...
# FastAPI Depends returns Any in all case, so whatever typing we may do it will be silently ignored.
def depends(dependency: object) -> Any:
return Depends(lambda: world.get[object](dependency))
def typed_depends(dependency: Type[T]) -> T:
return cast(T, Depends(lambda: world.get(dependency)))
class DependencyOf(Generic[T]):
def param(self) -> T:
# This is NOT part of the public API of Python
dependency: object = get_args(self.__orig_class__)[0] # type: ignore
return cast(T, Depends(lambda: world.get[object](dependency)))
async def example_route(service: IUserService = depends(IUserService)) -> None:
pass
async def example_route_v2(service: IUserService = typed_depends(AbstractUserService)
) -> None:
pass
async def example_route_v2b(service: IUserService = typed_depends(cast(Type[IUserService], IUserService))
) -> None:
pass
async def example_route_v3(service: IUserService = DependencyOf[IUserService]().param()) -> None:
pass
Unfortunately there isn't a lot Antidote can do here.
Unfortunately there isn't a lot Antidote can do here.
Well, not entirely true, with pure antidote you can use inject.me()
which circumvents that problem. But with FastAPI I didn't see an obvious way to use it.
@inject
async def example_route(service: IUserService = inject.me()) -> None:
pass
inject.me()
also returns Any
, but it relies on the type hints to determine the dependency, so it's as safe as you can be in Python.
Well, so Any
it is :(
It's actually kinda sad to be honest. I did some more research on that topic, and i encountered some opened issues in mypy related to that topic: mypy #4717, mypy #1843, mypy #5374.
Seems that people from mypy think, that this use case is not common enough, which forces most of DI libraries for python to create some workarounds, or just don't support it at all, for example Injector #134.
I've tried to find some ways to implement some kind of workaround for my use case, but as you said, there no perfect solution at the moment, I guess I will have to use Any
at the moment.
Thanks anyway for your help, it did give me some new ideas how to tackle this problem, just unsafe ones (that Any
in this place will haunt me).
Closing the issue, as it's something more related to mypy than antidote.