Pull to refresh
38
0
Роман Могилатов @rmk

Разработчик

Send message
Собрал простенький пример:

from dependency_injector import containers, providers


class ApiClient:

    def __init__(self, api_key: str, timeout: int):
        self.api_key = api_key
        self.timeout = timeout


class Service:

    def __init__(self, api_client: ApiClient):
        self.api_client = api_client


class Container(containers.DeclarativeContainer):

    config = providers.Configuration()

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

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


if __name__ == '__main__':
    container = Container()
    container.config.from_yaml('config.yml')

    service = container.service()

    assert isinstance(service.api_client, ApiClient)
Согласен. Автоматическое связывание по именам работает плохо.

Я придерживаюсь явного декларативного подхода.
В открытом доступе есть вот такой пример:

"""Application containers module."""

from dependency_injector import containers, providers

from newsfeed import core, infrastructure, domainmodel, webapi


class Core(containers.DeclarativeContainer):
    """Core container."""

    config = providers.Configuration('core')

    configure_logging = providers.Callable(
        core.log.configure_logging,
        level=config.log_level,
    )

    configure_event_loop = providers.Callable(
        core.loop.configure_event_loop,
        enable_uvloop=config.enable_uvloop,
    )


class Infrastructure(containers.DeclarativeContainer):
    """Infrastructure container."""

    config = providers.Configuration('infrastructure')

    event_queue = providers.Singleton(
        infrastructure.event_queues.InMemoryEventQueue,
        config=config.event_queue,
    )

    event_storage = providers.Singleton(
        infrastructure.event_storages.RedisEventStorage,
        config=config.event_storage,
    )

    subscription_storage = providers.Singleton(
        infrastructure.subscription_storages.RedisSubscriptionStorage,
        config=config.subscription_storage,
    )


class DomainModel(containers.DeclarativeContainer):
    """Domain model container."""

    config = providers.Configuration('domainmodel')

    infra: Infrastructure = providers.DependenciesContainer()

    # Common

    newsfeed_id_specification = providers.Singleton(
        domainmodel.newsfeed_id.NewsfeedIDSpecification,
        max_length=config.newsfeed_id_length,
    )

    # Subscription

    subscription_factory = providers.Factory(
        domainmodel.subscription.SubscriptionFactory,
        cls=domainmodel.subscription.Subscription,
    )

    subscription_specification = providers.Singleton(
        domainmodel.subscription.SubscriptionSpecification,
        newsfeed_id_specification=newsfeed_id_specification,
    )

    subscription_repository = providers.Singleton(
        domainmodel.subscription.SubscriptionRepository,
        factory=subscription_factory,
        storage=infra.subscription_storage,
    )

    subscription_service = providers.Singleton(
        domainmodel.subscription.SubscriptionService,
        factory=subscription_factory,
        specification=subscription_specification,
        repository=subscription_repository,
    )

    # Event

    event_factory = providers.Factory(
        domainmodel.event.EventFactory,
        cls=domainmodel.event.Event,
    )

    event_specification = providers.Singleton(
        domainmodel.event.EventSpecification,
        newsfeed_id_specification=newsfeed_id_specification,
    )

    event_repository = providers.Singleton(
        domainmodel.event.EventRepository,
        factory=event_factory,
        storage=infra.event_storage,
    )

    event_dispatcher_service = providers.Singleton(
        domainmodel.event_dispatcher.EventDispatcherService,
        event_factory=event_factory,
        event_specification=event_specification,
        event_queue=infra.event_queue,
    )

    event_processor_service = providers.Singleton(
        domainmodel.event_processor.EventProcessorService,
        event_queue=infra.event_queue,
        event_factory=event_factory,
        event_repository=event_repository,
        subscription_repository=subscription_repository,
    )


class WebApi(containers.DeclarativeContainer):
    """Web API container."""

    config = providers.Configuration('webapi')

    domain: DomainModel = providers.DependenciesContainer()

    web_app = providers.Factory(
        webapi.app.create_web_app,
        base_path=config.base_path,
        routes=[
            # Subscriptions
            webapi.app.route(
                method='GET',
                path='/newsfeed/{newsfeed_id}/subscriptions/',
                handler=providers.Coroutine(
                    webapi.handlers.subscriptions.get_subscriptions_handler,
                    subscription_service=domain.subscription_service,
                ),
            ),
            webapi.app.route(
                method='POST',
                path='/newsfeed/{newsfeed_id}/subscriptions/',
                handler=providers.Coroutine(
                    webapi.handlers.subscriptions.post_subscription_handler,
                    subscription_service=domain.subscription_service,
                ),
            ),
            webapi.app.route(
                method='DELETE',
                path='/newsfeed/{newsfeed_id}/subscriptions/{subscription_id}/',
                handler=providers.Coroutine(
                    webapi.handlers.subscriptions.delete_subscription_handler,
                    subscription_service=domain.subscription_service,
                ),
            ),
            webapi.app.route(
                method='GET',
                path='/newsfeed/{newsfeed_id}/subscribers/subscriptions/',
                handler=providers.Coroutine(
                    webapi.handlers.subscriptions.get_subscriber_subscriptions_handler,
                    subscription_service=domain.subscription_service,
                ),
            ),

            # Events
            webapi.app.route(
                method='GET',
                path='/newsfeed/{newsfeed_id}/events/',
                handler=providers.Coroutine(
                    webapi.handlers.events.get_events_handler,
                    event_repository=domain.event_repository,
                ),
            ),
            webapi.app.route(
                method='POST',
                path='/newsfeed/{newsfeed_id}/events/',
                handler=providers.Coroutine(
                    webapi.handlers.events.post_event_handler,
                    event_dispatcher_service=domain.event_dispatcher_service,
                ),
            ),
            webapi.app.route(
                method='DELETE',
                path='/newsfeed/{newsfeed_id}/events/{event_id}/',
                handler=providers.Coroutine(
                    webapi.handlers.events.delete_event_handler,
                    event_dispatcher_service=domain.event_dispatcher_service,
                ),
            ),

            # Miscellaneous
            webapi.app.route(
                method='GET',
                path='/status/',
                handler=providers.Coroutine(
                    webapi.handlers.misc.get_status_handler,
                ),
            ),
            webapi.app.route(
                method='GET',
                path='/docs/',
                handler=providers.Coroutine(
                    webapi.handlers.misc.get_openapi_schema_handler,
                    base_path=config.base_path,
                ),
            ),
        ],
    )

Тут используется несколько контейнеров.

Сборка выглядит вот так:

class Application:
    """Application."""

    class Containers:
        """Application containers."""

        core = Core
        infrastructure = Infrastructure
        domainmodel = DomainModel
        webapi = WebApi

    def __init__(self, config: Dict[str, Any]):
        """Initialize application."""
        self.config = config

        self.core: Core = self.Containers.core()
        self.core.config.override(self.config.get(self.core.config.get_name()))

        self.infrastructure: Infrastructure = self.Containers.infrastructure()
        self.infrastructure.config.override(self.config.get(self.infrastructure.config.get_name()))

        self.domainmodel: DomainModel = self.Containers.domainmodel()
        self.domainmodel.config.override(self.config.get(self.domainmodel.config.get_name()))
        self.domainmodel.infra.override(self.infrastructure)

        self.webapi: WebApi = self.Containers.webapi()
        self.webapi.config.override(self.config.get(self.webapi.config.get_name()))
        self.webapi.domain.override(self.domainmodel)

        self.processor_tasks: List[asyncio.Task[None]] = []

    def main(self) -> None:
        """Run application."""
        self.core.configure_logging()
        self.core.configure_event_loop()

        web_app: web.Application = self.webapi.web_app()

        web_app.on_startup.append(self._start_background_tasks)
        web_app.on_cleanup.append(self._cleanup_background_tasks)

        web.run_app(web_app, port=int(self.webapi.config.port()), print=None)

Это немного устаревший вариант. Для такой сборки сейчас есть специальный провайдер Container. Он автоматизирует то, что написано в __init__().

Проект находится тут — Newsfeed.
Понимаю. Постараюсь объяснить свою точку зрения.

Dependency Injector — это про 2 вещи:

1. Dependency injection — это хорошо.

Почему? Decoupling со всеми вытекающими обстоятельствами: гибкость, тестирование и т. д.

2. Указывать зависимости явно — тоже хорошо.

Приведу примеры от обратного.

Вариант №1. В Dependency Injector можно реализовать метод container.autowire(). Это метод сможет с высокой точностью связать все компоненты приложения без вас. Контейнер будет выглядеть следующим образов:

class ApplicationContainer(containers.DeclarativeContainer):

    config = providers.Configuration()

    configure_logging = providers.Callable(logging.basicConfig)

    http_client = providers.Factory(http.HttpClient)

    example_monitor = providers.Factory(monitors.HttpMonitor)

    httpbin_monitor = providers.Factory(monitors.HttpMonitor)

    dispatcher = providers.Factory(dispatcher.Dispatcher)


Много ли можно сказать о приложении по такому контейнеру? Почти ничего.

Вариант №2. Отказаться от наличия контейнера и добавить магию в стиле декоратора @inject:

from dependency_injector import inject

@inject
class Dispatcher:

    def __init__(self, monitors: List[Monitor]) -> None:
        ...

@inject
class HttpMonitor(Monitor):

    def __init__(
            self,
            http_client: HttpClient,
            options: Dict[str, Any],
    ) -> None:
        ...


В таком случае ваш код будет прочно привязан к dependency injection фреймворку. Как подключать сторонние модули..? Как собрать один и тот же класс с разными параметрами (пример с HttpMonitor из данного примера)..? Для простых случаев удобно, в более сложных — фреймворк наложит прилично ограничений.

В первой версии Dependency Injector декоратор @inject был. Убрал его специально. Считаю, что код должен жить отдельно от фреймворка. Это главный принцип Dependency Injector. Он не загрязняет ваш код магическими декораторами и правилами. Вместо этого он накладывается поверх на любой код, написанный по принципу dependency injection.

Про pytest

Фикстуры в pytest — удобная штука. Зависимости там тоже указываются явно. Если переписать контейнер из этого примера, то получится вот так:

@fixture
def config():
    return providers.Configuration()


@fixture
def configure_logging(config):
    return logging.basicConfig(
        stream=sys.stdout,
        level=config.log.level,
        format=config.log.format,
    )


@fixture
def http_client(config):
    return http.HttpClient()


@fixture
def example_monitor(http_client, config):
    return monitors.HttpMonitor(
        http_client=http_client,
        options=config.monitors.example,
    )


@fixture
def httpbin_monitor(http_client, config):
    return monitors.HttpMonitor(
        http_client=http_client,
        options=config.monitors.httpbin,
    )

@fixture
def dispatcher(example_monitor, httpbin_monitor):
    return dispatcher.Dispatcher(
        monitors=[
            example_monitor,
            httpbin_monitor,
        ],
    )

В целом тоже самое, но писать больше.
Явное лучше неявного — это классный принцип. Он здесь полностью реализован.

1) Самостоятельно определять порядок создания обьектов. Ваш фреймворк этого не делает и требует явного описания порядка

Это как раз про «явное лучше неявного». В Python многие библиотеки при инициализации основных классов используют *args, **kwargs + документацию. Интроспекцию на этом не построишь. Отказался от этого в Dependency Injector специально.

2) Управлять временем жизни обьекта. Условно есть обьект представляющий собой транзакцию в БД время жизни которого — запрос, есть обьект соединениу к базе данных, который живет постоянно. В таком случае DI фреймворк управлял бы выделением соединений из пула и созданием временных обьектов и т.д. Ваш фреймворк насколько я понял такого тоже не умеет.

Из коробки умеет базовые вещи в плане управления временем жизни: Singleton, ThreadLocalSingleton, ThreadSafeSingleton.

Еще из коробки умеет собирать сложные графы объектов и добавлять некоторую вариативность на базе конфигурации.

Кому удобнее? Вам как автору фреймворка или вчерашнему джуну который его первый раз видит?

Все когда-то все видели в первый раз. Вопрос популярности. Dependency Injector не новый фреймворк. Его скачивают с PyPi 200 тыс. раз в месяц.

В чем профит описания структуры без создания объектов? Для того чтобы понять структуру приложения разработчик читает его код, в процессе чтения кода объекты не создаются. В процессе работы приложения обьекты все равно будут созданы и без разницы произойдет это в функции create_app или в контейнере.

На мой взгляд удобнее читать такой код:

Объект1

Объект2
    - зависит от:
        - Объект1

запуск_приложения()
    - сделать_что-то_0()
    - сделать_что-то_1()

чем такой:

сделать_что-то_0()

Объект1

сделать_что-то_1()

Объект2
    - зависит от:
        - Объект1

Для меня стало очевидно, что удобнее разделять. «Можно ли написать все в одном файле?», «Можно ли в html шаблоне написать sql запрос?» — можно, но лучше так не делать.
Справедливости ради я так и не понял при чём тут asyncio\flask к DI, ну да ладно

Это руководство о том, как построить приложение используя модуль asyncio и применяя принцип dependency injection.

Dependency Injector не привязан к asyncio\flask. Его можно использовать отдельно.
1) все объекты создаются на этапе инициализации, т.е. это абсолютно тоже самое что руками написать код создания объектов.

Объекты на этапе инициализации не создаются. ApplicationContainer декларативный, при его описании не создается ни одного объекта.

2) все эти объекты в общем случае существуют все время выполнения программы и соответственно ApplicationContainer никак не управляет их временем жизни.

ApplicationContainer управляет временем жизни. Фабрики создают объекты при обращении к ним и передают зависимости в создаваемый объект. Если зависимость — другая фабрика, она тоже создаст объект. Таким образом соединяя провайдеры можно описывать сложные графы. В модуле providers есть другие провайдеры Singleton, ThreadLocalSingleton, ThreadSafeSingleton, и т. д.
абсолютно непонятно зачем вообще это все нужно и какую вообще проблему решает DI

Это классный вопрос. Специально его не затрагивал. Хотел сделать практическое руководство.

Про пользу dependency injection. Я относился к нему пренебрежительно пока в 2014 на себе не почувствовал его магическую силу. Это случилось при рефакторинге крупной легаси платформы. Было много запутанного кода. Мы применили DI. Когда применяешь DI все связи становятся явными. Если когда-то пробовали вышивать, это как заглянуть на обратную сторону рисунка. Структура приложения вырисовывается сама собой. Она отделяется от runtime части и с ней становится удобно работать. После применения DI все стало прозрачно и просто. В тот момент я осознал силу подхода.

Так и появился Dependency Injector.

Если интересно, напишу отдельную статью.

PS: Подход неинтуитивный, нужно привыкнуть. Плюс это инвестиция в будущее: когда пишешь приложение и добавляешь все в контейнер — кажется избыточно. Когда возвращаешься через время чтобы что-то поменять — рад тому, что контейнер есть.

что мешает сделать тоже самое в функции create_app и без всякого фреймворка? Результат будет одинаковый.

Мой фреймворк позволяет описать всю структуру приложения в декларативном стиле не создав ни одного объекта. В таком виде со структурой удобнее всего работать.

Тоже самое можно сделать и без него, только у вас получится такой же фреймворк. Его нужно будет протестировать, оптимизировать, настроить сборку 44 wheels для всех версий Python и ОС, поддерживать, фиксить баги, написать документацию, примеры и это руководство. Для моего фреймворка я это все уже сделал.

Писать все в функцию create_app() неправильно. Это смешивание декларативной и runtime частей. Такие приложения тяжелее понимать.
Все верно.

Модуль containers — Python код, который транслирован в C c помощью Cython. Есть несколько особенностей: несколько mapping'ов на модуль providers, несколько cdef приведений типов и несколько cpdef функций.

В планах перевести модуль containers в более глубокую Cython типизацию после прекращения поддержки Python 2. Пока не получается из-за применения метакласса.
Очень согласен!

Скорее всего такое представление (управленец — человек первого сорта, инженер — второго) складывается из-за того, что большинство людей не видят развитие себя в техническом плане. И получается, что вместо хорошего системного архитектора появляется плохой тим лид.
Скажите, а есть ли какие-нибудь фичи по интеграции с Cython?
Мне одному кажется, что тут ограничений больше, чем пользы?
Скажите, а как обстоят у вас дела с контролем доступа?

Т. е., если я хочу показывать видео только тем, кому разрешает моя бизнес логика. Вариант с секретными ссылками не рассматриваем. Хотелось бы сделать честную проверку, а именно, перед началом показа видео спросить мою систему, можно ли вот этому пользователю (нужные его куки) предоставить права на просмотр вещания?
А я считаю, что как раз таки оверинжининг встречается очень часто, и это большое зло. Он создает излишнюю неоправданную сложность. Что касается поста, то этот код спокойно можно переписать в структурном стиле, его суть не изменится.
Я надеюсь, unit тесты есть?
Ну так, zf еще год назад подобное умел.
В самую точку! Превосходно!

Information

Rating
Does not participate
Location
Днепропетровская обл., Украина
Date of birth
Registered
Activity