Skip to content
This repository has been archived by the owner on Aug 10, 2024. It is now read-only.

Creating a wrapper for dependency injection (mypy issue) #58

Closed
rschyboll opened this issue Jun 27, 2022 · 5 comments
Closed

Creating a wrapper for dependency injection (mypy issue) #58

rschyboll opened this issue Jun 27, 2022 · 5 comments

Comments

@rschyboll
Copy link

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 :)

@pauleveritt
Copy link

FWIW, @Finistere talked me through something similar in a discussion post

@pauleveritt
Copy link

Also, that's a cool idea for the project. I know there's another project seeking to do an alternate DI for FastAPI.

@Finistere
Copy link
Owner

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.

@Finistere
Copy link
Owner

Finistere commented Jun 28, 2022

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.

@rschyboll
Copy link
Author

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.

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants