Dependency Injector 4.0 - Simplified Integration with Other Python Frameworks





Hello, Habr! I have released a new major version of Dependency Injector .



The main feature of this version is wiring. It allows you to inject functions and methods without dragging them into a container.



from dependency_injector import containers, providers
from dependency_injector.wiring import Provide


class Container(containers.DeclarativeContainer):

    config = providers.Configuration()

    api_client = providers.Singleton(
        ApiClient,
        api_key=config.api_key,
        timeout=config.timeout.as_int(),
    )

    service = providers.Factory(
        Service,
        api_client=api_client,
    )


def main(service: Service = Provide[Container.service]):
    ...


if __name__ == '__main__':
    container = Container()
    container.config.api_key.from_env('API_KEY')
    container.config.timeout.from_env('TIMEOUT')
    container.wire(modules=[sys.modules[__name__]])

    main()  # <--   

    with container.api_client.override(mock.Mock()):
        main()  # <--    


When the function is called, the main()dependency Serviceis collected and passed automatically.



During testing, it is called container.api_client.override()to replace the API client with a mock. When called, the main()dependency Servicewill collect mock.



The new feature makes it easier to use the Dependency Injector with other Python frameworks.



How does linking help integrate with other frameworks?



Binding enables precise injection regardless of the application structure. Unlike version 3, dependency injection does not require pulling a function or class into a container.



Example with Flask:



import sys

from dependency_injector import containers, providers
from dependency_injector.wiring import Provide
from flask import Flask, json


class Service:
    ...


class Container(containers.DeclarativeContainer):

    service = providers.Factory(Service)


def index_view(service: Service = Provide[Container.service]) -> str:
    return json.dumps({'service_id': id(service)})


if __name__ == '__main__':
    container = Container()
    container.wire(modules=[sys.modules[__name__]])

    app = Flask(__name__)
    app.add_url_rule('/', 'index', index_view)
    app.run()


Other examples:





How does binding work?



In order to apply binding you need:



  • Place markers in code. The view marker is Provide[Container.bar]specified as the default value of the function or method argument. Markers are needed to indicate what and where to embed.
  • Associate a container with markers in code. To do this, you need to call the container.wire (modules = [...], packages = [...]) method and specify the modules or packages that have markers.
  • Use functions and methods as usual. The framework will prepare and inject the required dependencies automatically.


The binding works on the basis of introspection. When called, the container.wire(modules=[...], packages=[...])framework will go through all the functions and methods in these packages and modules and examine their default parameters. If the default parameter is a marker, then such a function or method will be patched by the dependency injection decorator. This decorator, when called, prepares and injects dependencies instead of markers into the original function.



def foo(bar: Bar = Provide[Container.bar]):
    ...


container = Container()
container.wire(modules=[sys.modules[__name__]])

foo()  # <---  "bar"  

#    :
foo(bar=container.bar())


Learn more about linking here .



Compatibility?



Version 4.0 is compatible with versions 3.x.



Integration modules ext.flaskand are ext.aiohttppinned in favor of bundling.

When used, the framework will display a warning and recommend switching to linking.



A complete list of changes can be found here .



What's next?






All Articles