Drive your career as React Developer with Symphony Solutions!
×Закрыть

Dependency Injector 4.0 — упрощенная интеграция с другими Python фреймворками

Привет,

Я выпустил новую мажорную версию Dependency Injector.

Основная фича этой версии — связывание (wiring). Она позволяет делать инъекции в функции и методы без затягивания их в контейнер.

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()  # <-- dependency is injected automatically

    with container.api_client.override(mock.Mock()):
        main()  # <-- overridden dependency is injected automatically

Когда вызывается функция main() зависимость Service собирается и передается автоматически.

При тестировании вызывается container.api_client.override() чтобы заменить API клиент на мок. При вызове main() зависимость Service будет собираться с моком.

Новая фича упрощает использование Dependency Injector’а с другими Python фреймворками.

Как связывание помогает интеграции с другими фреймворками?

Связывание дает возможность делать точные инъекции независимо от структуры приложения. В отличии от 3-ей версии для внедрения зависимости не нужно затягивать функцию или класс в контейнер.

Пример с 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()

Другие примеры:

Как работает связывание?

Для того чтобы применять связывание нужно:

  • Разместить маркеры в коде. Маркер вида Provide[Container.bar] указывается как дефолтное значение аргумента функции или метода. Маркеры нужны чтобы указать что и куда внедрять.
  • Связать контейнер с маркерами в коде. Для этого нужно вызвать метод container.wire(modules=[...], packages=[...]) и указать модули или пакеты, в которых есть маркеры.
  • Использовать функции и методы как обычно. Фреймворк подготовит и внедрит нужные зависимости автоматически.

Связывание работает на базе интроспекции. При вызове container.wire(modules=[...], packages=[...]) фреймворк пройдется по всем функциям и методам в этих пакетах и модулях и изучит их дефолтные параметры. Если дефолтным параметром будет маркер, то такая функция или метод будут пропатчены декоратором внедрения зависимостей. Этот декоратор при вызове подготавливает и внедряет зависимости вместо маркеров в оригинальную функцию.

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


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

foo()  # <--- Argument "bar" is injected

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

Больше про связывание можно узнать тут.

Совместимость?

Версия 4.0 совместима с версиями 3.х.

Интеграционные модули ext.flask и ext.aiohttp задеприкечены в пользу связывания.
При использовании фреймворк будет выводить предупреждение и рекомендовать перейти на связывание.

Полный список изменений можно найти тут.

Что дальше?

  • Загляни на Github проекта
  • Загляни в документацию
  • Попробуй использовать Dependency Injector на проекте
  • Хочешь применить в своем проекте и нужна помощь? Пиши в личку — помогу советом и код ревью.

PS: Прошлый пост про Dependency Injector тут.

LinkedIn
Допустимые теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter
Допустимые теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter

Ваш чудесный DI фреймворк как-то управляет ресурсами или просто подставляет аргументы в конструкторы?

Если под управлением ресурсами имеется ввиду время жизни объектов, то да — за счет разных провайдеров:

  • Factory — всегда создает новый объект
  • Singleton — создает объект единожды и переиспользует при последующих обращениях
  • ThreadLocalSingleton — создает объект единожды для каждого потока
  • Selector — выбирает что создать на основе значения конфигурации

Про провайдеры можно почитать тут.

Про другие фичи можно почитать тут.

А можно ли делать одни провайдеры зависимыми от других?

Да. Вот как выглядит контейнер побольше:

"""Containers module."""

import logging.config
import sqlite3

import boto3
from dependency_injector import containers, providers

from . import services


class Container(containers.DeclarativeContainer):

    config = providers.Configuration()

    configure_logging = providers.Callable(
        logging.config.fileConfig,
        fname='logging.ini',
    )

    # Gateways

    database_client = providers.Singleton(
        sqlite3.connect,
        config.database.dsn,
    )

    s3_client = providers.Singleton(
        boto3.client,
        service_name='s3',
        aws_access_key_id=config.aws.access_key_id,
        aws_secret_access_key=config.aws.secret_access_key,
    )

    # Services

    user_service = providers.Factory(
        services.UserService,
        db=database_client,
    )

    auth_service = providers.Factory(
        services.AuthService,
        db=database_client,
        token_ttl=config.auth.token_ttl.as_int(),
    )

    photo_service = providers.Factory(
        services.PhotoService,
        db=database_client,
        s3=s3_client,
    )

Factory провайдер photo_service зависит от от Singleton-провайдеров s3_client и database_client и т. д.

При вызове провайдер photo_service будет всегда создавать новый объект, а в качестве зависимостей будут передаваться единожды созданные объекты провайдеров database_client и s3_client.

Подписаться на комментарии