-
Notifications
You must be signed in to change notification settings - Fork 80
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
base: develop
Are you sure you want to change the base?
Conversation
Does fastapi has built-in support of such style? |
No, it only supports functions.
There are also other similar(ish) libs/snippets out there, so I believe there's a group of devs who want the same. |
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. |
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 😸 |
b951cd1
to
9db7dc7
Compare
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
|
Could it support injection of class instances, for OOP design in the endpoints? E.g. FastAPI:
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?