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() # <-- зависимость внедряется автоматически
with container.api_client.override(mock.Mock()):
main() # <-- переопределенная зависимость внедряется автоматически
Когда вызывается функция 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() # <--- Аргумент "bar" будет внедрен
# То же что и:
foo(bar=container.bar())
Больше про связывание можно узнать тут.
Совместимость?
Версия 4.0 совместима с версиями 3.х.
Интеграционные модули ext.flask
и ext.aiohttp
задеприкечены в пользу связывания.
При использовании фреймворк будет выводить предупреждение и рекомендовать перейти на связывание.
Полный список изменений можно найти тут.