Open guacs opened 1 year ago
Just for my understanding, where would this be useful or IOW why would the current way (dicts) be limiting one?
Just for my understanding, where would this be useful or IOW why would the current way (dicts) be limiting one?
It's only intended as a nicer API similar to #2422. Also, it'd be nice to add a @provide
decorator so we can do something like the following:
from litestar.di import provide
@provide(name="service", sync_to_thread=False)
def provide_service():
...
app = Litestar(..., dependencies=[provide_service])
I feel like this is a nicer API than what would be needed now:
from litestar.di import Provide
def provide_service():
...
service_provider = Provide(provide_service, sync_to_thread=False)
app = Litestar(..., dependencies={"service": service_provider})
Have you considered injection by type and not by name? Something like:
Litestar(dependencies=[Service, Provide(Service, id="other")])
@get()
def endpoint(my_service: Service, other_instance: Annotated[Service, Provide(id="other")]): ...
Not sure about it, but I don't think I've ever seen a DI framework before which would rely on parameter names?
Not sure about it, but I don't think I've ever seen a DI framework before which would rely on parameter names?
The most popular example in Python of this would probably be pytest:
@pytest.fixture(name="something")
def something_fixture() -> str:
...
def test_something(something: str) -> None:
...
I'd say this is a very common pattern, and it has a few benefits:
As you illustrated in your example, as soon as you're injecting multiple things with the same type, you need an additional identifier (the id
parameter in your example) anyway. You might as well use the name for this right away. This is especially a concern when it comes to scalability. Yes, the Litestar way requires a bit more typing upfront for the very simple cases than would be necessary, but as soon as things start to get more complex, it actually requires less typing, while still being very explicit.
Litestar(dependencies={"my_service": Service, "other_instance": Service})
@get()
def endpoint(my_service: Service, other_instance: Service):
...
The other way to solve this is by injecting via provider identity; FastAPI is an example of this:
def provide_service() -> Service:
...
def endpoint(my_service: Annotated[Service, Depends(provide_service)]):
...
But this then requires you to always have a reference to the provider (not just the provided value type) at the site of injection, introducing a tight coupling which is one of the things DI tries to solve.
Hey thanks for the insight.
Yeah, FastAPI's approach is something I dislike - due to need of each time having to declare where to pull the dependency from (probably also the one of the reasons why you've implemented things differently).
Though, you can make it shorter once you inject the same multipe times:
ServiceDep = Annotated[Service, Depends(provide_service)])
def endpoint(service: ServiceDep): ...
def endpoint1(same_one: ServiceDep): ...
...etc...
The benefit in FastAPI's style is that you can click-navigate in IDE into the callable (provide_service
) and see how/what is actually constructed and returned. In Litestar, you cannot do that, but got traverse up the app layers and look into dependencies
(probably not too much work, though).
But yeah, I understand the idea - having multiple app layers and not having to declare the dependency callable where it gets used.
Probably the cleanest DI interface I've seen on Python is in https://github.com/python-injector/injector (though I haven't actually used it). Copy-paste from their README:
>>> from injector import Injector, inject
>>> class Inner:
... def __init__(self):
... self.forty_two = 42
...
>>> class Outer:
... @inject
... def __init__(self, inner: Inner):
... self.inner = inner
...
>>> injector = Injector()
>>> outer = injector.get(Outer)
>>> outer.inner.forty_two
42
You basically say injector.get(Service)
without any pre-construction, and it builds the instance on-the-fly if one doesn't already exist in cache. Translating to Litestar it would be:
app = Litestar()
def endpoint(whatever_name: Service): ...
...and Service
instance would "just" get constructed. That could be a useful default, because oftentimes we are injecting singletons per class (e.g. UserService
, OrganizationService
, whatever).
For the cases where a single class needs multiple instances something like id
(or qualifier in Spring terms, iirc) could be supported:
app = Litestar()
def special_variant():
return Service(magic_value="something special")
app = Litestar(dependencies=[Provide(special_variant, id="special2")])
@get()
def endpoint(special2: Annotated[Service, Provide(id="special")]): ...
That's actually the same as currently, but with different (and lengthier) syntax? Injection there occurs with type and name combo, instead of just name as currently, so I don't know π
I have a further question: Have you considered making the DI mechanism live outside the application and not within it. To allow exception handlers, middlewares, etc to be injectable, too, and e.g. controllers could be like:
@dataclass
class MyController(Controller):
service: Service # would get injected
@get()
def endpoint(self):
self.service.call_something()
@dataclass
class ExceptionHandler:
service: Service # would get injected
def handle_exception(self, request, exc): ...
app = Litestar(
route_handlers=[MyController],
exception_handlers={Exception: ExceptionHandler}
)
In general, the idea of not having to support DI specifically, but rather relying on DI for the whole app construction. No exact coordinates here, but the idea?
That's actually the same as currently, but with different (and lengthier) syntax? Injection there occurs with type and name combo, instead of just name as currently, so I don't know π
Well that was kind of my point earlier ;) There's different trade-offs to be made, but so far I haven't seen anything convincing to drop the explicit naming style, as it covers the most use cases and edge cases reasonably well. The only thing it cannot do is delivery a good IDE integration for "go to source" natively (pytest e.g. gets around this with a plugin/native support from the IDEs :shrug:)
I have a further question: Have you considered making the DI mechanism live outside the application and not within it. To allow exception handlers, middlewares, etc to be injectable, too, and e.g. controllers could be like:
Yes. While not itemized yet, this is planned and one of the major reasons we want to overhaul our DI for 3.0.
Summary
Allow dependencies for DI to be given like
[provide_service_one, provide_service_two]
instead of{"service_one": provide_service_one, "service_two": provide_service_two}
.The dependencies could be callables or
Provide
instances where the name of the dependency would be the name of the callable by default. To configure the name, aname
field could be provided inProvide
which will be used instead of the callable name, if provided.Basic Example
No response
Drawbacks and Impact
No response
Unresolved questions
No response