13g10n
На главную

Symbiont — мой микро-фреймворк для управления зависимостями

Python3 минуты

С огромным запозданием по датам я хочу вам коротко представить небольшой проект, который случайно родился во время работы над собственной системой управления умным домом.

Как можно заметить по датам публикаций в блоге, я отсутствовал более месяца, что связано с болезнями и пережитой операцией. Однако теперь я встал на ноги и снова готов генерировать контент. 🙂

Сразу уточню, что кейс весьма специфический, поэтому скорее всего на очередном вашем проекте больше подойдут библиотеки и фреймворки использующие другую архитектуру (тот же Depends у FastAPI). Однако в моём случае мне показалось удобным использовать именно этот подход.

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

Начнём с установки:

Terminal
poetry add symbiont

Сам модуль состоит из модулей (Module), Injectable (даже пытаться перевести это не стану) и собственно DependencyInjector'а, который занимается логикой инициализации всего этого добра.

Предполагается, что логика ваших приложений разделена на модули, в каждом из которых есть некая часть, которую используют другие части. Не совсем понятно? Давайте на примере:

from symbiont import Module, Injectable, DependencyInjector


class UsersRepository(Injectable):
    ...


class UsersService(Injectable):
    users: UsersRepository
    
    ...


class UsersModule(
    Module,
    providers=[UsersRepository, UsersService]
):
    ...

Здесь у нас знакомая ситуация. Предположим, что UsersRepository содержит в себе работу с БД, которая затем используется в бизнес-логике внутри UsersService (ведь мы инжектим UsersRepository как users). Ну и объединяется всё это в общий модуль UsersModule через параметр providers.

Где-то в другом месте мы создаём наш рутовый модуль, который импортирует модули:

class RootModule(
    Module,
    imports=[UsersModule]
):
    users: UsersService


injector = DependencyInjector()
root = injector.initialize(RootModule)

Инициализация через DependencyInjector создаёт объекты всех модулей, автоматически инициализируя и вставляя Injectable зависимости между ними.

Любой модуль может использовать imports, чтобы импортировать другие модули, что позволяет создать древовидную структуру.

Напоследок покажу небольшую уловку, которая использует children свойство модуля, содержащее все подмодули и Injectable, чтобы вызывать на них переданный метод:

class ExampleModule(
    Module,
    providers=[...]
):
    async def start(self):
        ...

class RootModule(
    Module,
    imports=[ExampleModule, ...]
):
    async def call(self, method: str) -> None:
        await self._waterfall_call(self, method)

    async def _waterfall_call(self, module: Module, attr: str) -> None:
        for submodule in (m for m in module.children if isinstance(m, Module)):
            await self._waterfall_call(submodule, attr)

        if module is not self:
            if method := getattr(module, attr, None):
                await method()

root = DependencyInjector().initialize(RootModule)
root.call('start')

Пользуйтесь с осторожностью, код доступен на GitHub. Любые предложения, исправления и улучшения приветствуются.

PythonDI