Skip to content

Injection of class instances as self #382

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: develop
Choose a base branch
from

Conversation

tuukkamustonen
Copy link

@tuukkamustonen tuukkamustonen commented Mar 1, 2025

Could it support injection of class instances, for OOP design in the endpoints? E.g. FastAPI:

router = APIRouter(router_class=DishkaRouter)

@dataclass
class SomeController:
    dep: Dependency

    @router.get("/")
    async def get(self):
        print(self)

This trigger the creation/injection of the wrapping class as self, and allow defining dependencies per class (instance) rather than for each endpoint separately.

It's a bit hacky now, and even breaks some tests, so proper PR would need to be created if you're open to the idea?

@Tishka17
Copy link
Member

Tishka17 commented Mar 1, 2025

Does fastapi has built-in support of such style?

@tuukkamustonen
Copy link
Author

tuukkamustonen commented Mar 1, 2025

No, it only supports functions.

fastapi-utils provides something similar though, with @cbv decorator. See https://fastapi-utils.davidmontague.xyz/user-guide/class-based-views/

There are also other similar(ish) libs/snippets out there, so I believe there's a group of devs who want the same.

@Tishka17
Copy link
Member

Tishka17 commented Mar 1, 2025

I do not think we should provide new style of using web frameworks - it is out of scope. Though we need to be compatible. The idea is interesting, I'll keep this open for a while to think about it.

@Tishka17 Tishka17 added the to clarify Needs information or coordination with other issues label Mar 1, 2025
@tuukkamustonen
Copy link
Author

tuukkamustonen commented Mar 1, 2025

Yeah, I understand your viewpoint.

I browsed through FastAPI issues, and they have been requested many times and again and again.

The ticket with most attention is fastapi/fastapi#270.

It could a nice selling point for Dishka - and imo it just makes sense. I mean, everything else can be a class/instance, why not the endpoint.

Being able to integrate a separate DI system, that you can also use in background workers, scripts, and not just in APIs is important for larger projects. You cannot re-use FastAPI injection system outside a FastAPI app (except maybe via https://github.com/Lancetnik/FastDepends which you're probably familiar with), but that has FastAPI DI system annoyances and limitations, and I'm not sure how mature it is, etc.

So you end up wanting something like Dishka, to follow similar OOP model (if that's what you do) also for the API.

Most of the snippets out there for "class based views/controllers" are just syntactical sugar and require eager initialization of classes, e.g.:

class SurveyController(Controller):

    def __init__(self, survey_service: SurveyService) -> None:
        super().__init__(router)
        self.survey_service = survey_service

    @router.post('/', status_code=201)
    async def create(self, request: Request):
        return await self.survey_service.create(await request.json())

def build_app():
    ...
    survey_controller = SurveyController(survey_service)
    app.include_router(survey_controller)

(Taken from fastapi/fastapi#8318 (comment).)

One of the problem with above is that you end up eagerly creating all the "controller" classes during app startup, and that of course requires recursive initialization of all the dependencies, too. In any larger project, that can come with a serious computation/IO/setup/whatever expense, and at least in tests when you're usually testing a single (or a few) endpoints at a time, it doesn't make sense to trigger the whole application setup each time.

I think you implemented this, and then pinged in fastapi/fastapi#270 you'd get some good attention and new users 😸

@tuukkamustonen
Copy link
Author

Hey just a status update, I modified the code to be a standalone extension, which doesn't require a change in the Dishka internals:

class DishkaRoute(APIRoute):
    def __init__(
        self,
        path: str,
        endpoint: Callable[..., Any],
        **kwargs: Any,
    ) -> None:
        wrapper_endpoint = inject(endpoint)

        sig = signature(endpoint)
        if "self" in sig.parameters and sig.parameters["self"].annotation is Parameter.empty:
            wrapper_endpoint = self._wrap_endpoint(endpoint, wrapper_endpoint)

        super().__init__(path, wrapper_endpoint, **kwargs)

    def _wrap_endpoint(
        self,
        orig_endpoint: Callable[..., Any],
        endpoint: Callable[..., Any],
    ) -> Callable[..., Any]:
        sig = inspect.signature(endpoint)
        hints = typing.get_type_hints(endpoint, include_extras=True)

        async def depend_self(request: Request) -> Any:
            if not (self_class := getattr(depend_self, "__self_class", None)):
                self_class = inspect._findclass(orig_endpoint)  # type: ignore[attr-defined]  # noqa: SLF001
                setattr(depend_self, "__self_class", self_class)

            return await request.state.dishka_container.get(self_class)

        self_class_type = NewType("self_class_type", type)  # I guess this is okay(?)
        new_hint = Annotated[self_class_type, Depends(depend_self)]

        endpoint.__signature__ = sig.replace(  # type: ignore[attr-defined]  # false positive?
            parameters=[
                sig.parameters["self"].replace(annotation=new_hint),
                *(parameter for parameter in sig.parameters.values() if parameter.name != "self"),
            ]
        )
        endpoint.__annotations__ = {**hints, "self": new_hint}

        return endpoint

This is FastAPI specific now, as it uses Depends instead wrapping a function on top of it.

orig_endpoint is passed (and not the modified endpoint) because there's something weird with the qualname(?) of the adjusted function - it points to dishka.something.something although it should be set to the original one in Dishka code. inspect._findclass fails with the modified endpoint. Not sure about that.

@Tishka17 Tishka17 moved this to Backlog in Dishka kanban Apr 13, 2025
@Tishka17 Tishka17 removed this from Dishka kanban Apr 13, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
to clarify Needs information or coordination with other issues
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants