reagento / dishka

Cute DI framework with agreeable API and everything you need
https://dishka.readthedocs.io
Apache License 2.0
383 stars 38 forks source link
dependency-injection dependency-injector di di-container di-framework ioc-container python

Dishka (from russian "cute DI")

PyPI version Supported versions downloads license GitHub Actions Workflow Status Doc Telegram

Cute DI framework with scopes and agreeable API.

šŸ“š Documentation

Purpose

This library is targeting to provide only an IoC-container but make it really useful. If you are tired manually passing objects to create others objects which are only used to create more objects - we have a solution. Not all project require an IoC-container, but check what we have.

Unlike other instruments we are not trying to solve tasks not related to dependency injection. We want to keep DI in place, not soiling you code with global variables and additional specifiers in all places.

Main ideas:

See more in technical requirements

Quickstart

  1. Install dishka
pip install dishka
  1. Create Provider instance. It is only used to setup all factories providing your objects.
from dishka import Provider

provider = Provider()
  1. Register functions which provide dependencies. Do not forget to place correct typehints for parameters and result. We use scope=Scope.APP for dependencies which are created only once in application lifetime, and scope=Scope.REQUEST for those which should be recreated for each processing request/event/etc.
from dishka import Provider, Scope

def get_a() -> A:
   return A()

def get_b(a: A) -> B:
   return B(a)

provider = Provider()
provider.provide(get_a, scope=Scope.APP)
provider.provide(get_b, scope=Scope.REQUEST)

This can be also rewritten using classes:

from dishka import provide, Provider, Scope

class MyProvider(Provider):
  @provide(scope=Scope.APP)
  def get_a(self) -> A:
     return A()

  @provide(scope=Scope.REQUEST)
  def get_b(self, a: A) -> B:
     return B(a)

provider = MyProvider()
  1. Create Container instance passing providers, and step into APP scope. Container holds dependencies cache and is used to retrieve them. Here, you can use .get method to access APP-scoped dependencies:
from dishka import make_container
container = make_container(provider)  # it has Scope.APP
a = container.get(A)  # `A` has Scope.APP, so it is accessible here
  1. You can enter and exit REQUEST scope multiple times after that:
from dishka import make_container
container = make_container(provider)
with container() as request_container:
    b = request_container.get(B)  # `B` has Scope.REQUEST
    a = request_container.get(A)  # `A` is accessible here too

with container() as request_container:
    b = request_container.get(B)  # another instance of `B`
    a = request_container.get(A)  # the same instance of `A`
  1. Close container in the end:
container.close()
  1. If you are using supported framework add decorators and middleware for it.
from dishka.integrations.fastapi import (
    FromDishka, inject, setup_dishka,
)

@router.get("/")
@inject
async def index(a: FromDishka[A]) -> str:
    ...

...

setup_dishka(container, app)

Concepts

Dependency is what you need for some part of your code to work. They are just object which you do not create in place and probably want to replace some day. At least for tests. Some of them can live while you application is running, others are destroyed and created on each request. Dependencies can depend on other objects, which are their dependencies.

Scope is a lifespan of a dependency. Standard scopes are (without skipped ones):

APP -> REQUEST -> ACTION -> STEP.

You decide when to enter and exit them, but it is done one by one. You set a scope for your dependency when you configure how to create it. If the same dependency is requested multiple time within one scope without leaving it, then by default the same instance is returned.

If you are developing web application, you would enter APP scope on startup, and you would REQUEST scope in each HTTP-request.

You can provide your own Scopes class if you are not satisfied with standard flow.

Container is what you use to get your dependency. You just call .get(SomeType) and it finds a way to get you an instance of that type. It does not create things itself, but manages their lifecycle and caches. It delegates objects creation to providers which are passed during creation.

Provider is a collection of functions which really provide some objects. Provider itself is a class with some attributes and methods. Each of them is either result of provide, alias or decorate. They can be used as provider methods, functions to assign attributes or method decorators.

@provide can be used as a decorator for some method. This method will be called when corresponding dependency has to be created. Name of the method is not important: just check that it is different form other Provider attributes. Type hints do matter: they show what this method creates and what does it require. All method parameters are treated as other dependencies and created using container.

If provide is used with some class then that class itself is treated as a factory (__init__ is analyzed for parameters). But do not forget to assign that call to some attribute otherwise it will be ignored.

Component - is an isolated group of providers within the same container identified by a string. When dependency is requested it is searched only within the same component as its dependant, unless it is declared explicitly.

This allows you to have multiple parts of application build separately without need to think if the use same types.

Tips

class MyProvider(Provider):
   @provide(scope=Scope.APP)
   async def get_a(self) -> A:
      return A()

container = make_async_container(MyProvider())
a = await container.get(A)
from dishka import from_context, Provider, provide, Scope

class MyProvider(Provider):
    scope = Scope.REQUEST

    app = from_context(App, scope=Scope.APP)
    request = from_context(RequestClass)

    @provide
    def get_a(self, request: RequestClass, app: App) -> A:
        ...

container = make_container(MyProvider(), context={App: app})
with container(context={RequestClass: request_instance}) as request_container:
    pass
container = make_container(MyProvider(), OtherProvider())
class MyProvider(Provider):
   scope = Scope.APP

   @provide
   async def get_a(self) -> A:
      return A()