Как избежать циклического импорта при использовании контейнеров python-dependency-injection в проекте на Flask?

python-dependency-injector - популярная библиотека для реализации внедрения зависимостей (dependency injection) в python.

Но я не понимаю, как использовать python-dependency-injector в моем проекте ??

Всякий раз когда я пытаюсь использовать Container в своем приложении, я сталкиваюсь с циклическими зависимостями. Пример: Container -> core -> services -> Container. У меня нет идей, как я должен поменять структуру своего проекта, чтобы избежать этого.

Например, у меня есть следующая структура приложения на Flask (обычно core, extensions и service являются пакетами):

app
│   __init__.py         # create_app
│   containers.py       # Container
│   core.py             # переиспользуемые компоненты (BaseModel, ...), которые я хочу использовать в любой части приложения (services, api, ...)
│   extensions.py       # SQLAlchemy, HTTPTokenAuth, Keycloak, ...
│
├───api                 # здесь wiring инъекции работают отлично
│       ...
│
└───services
        service.py      # Models, Repositories, UseCases этого сервиса
        ...
        __init__.py

__init__.py содержит только фабрику приложения:

from apiflask import APIFlask

from app import containers


def create_app() -> APIFlask:
    app = APIFlask(__name__)
    ...
    containers.init_app(app)
    ...
    return app

В контейнере в containers.py я хочу хранить и extension'ы и сервисы:

from apiflask import APIFlask, HTTPTokenAuth
from dependency_injector import containers, providers

from _issue.extensions import SQLAlchemy, Keycloak
from _issue.services.service import ModelRepository, ModelUseCase

CONTAINER_CONFIG = {}


class Container(containers.DeclarativeContainer):
    wiring_config = containers.WiringConfiguration(packages=['app.api'], modules=['app.core'])
    config = providers.Configuration(strict=True)

    db = providers.Singleton(  # extension
        SQLAlchemy,
        ...
    )

    auth = providers.Singleton(  # extension
        HTTPTokenAuth,
        ...
    )

    keycloak = providers.Singleton(  # extension
        Keycloak,
        ...
    )

    model_repo = providers.Factory(  # service
        ModelRepository,
        session=db.provided.session
    )

    model_use_case = providers.Factory(  # service
        ModelUseCase,
        repository=model_repo
    )


def init_app(app: APIFlask) -> None:
    container = Container()
    container.config.from_dict(CONTAINER_CONFIG)

    app.container = container

    db = container.db()
    db.init_app(app)
    ...

В core.py содержится шаблонный код, который я хочу переиспользовать в любой части своего приложения. Данный код требует объекты extension'ов (auth, db, ...):

from dependency_injector.wiring import Provide
from flask_sqlalchemy import SQLAlchemy

from _issue.containers import Container
from _issue.extensions import Keycloak, HTTPTokenAuth

db: SQLAlchemy = Provide[Container.db]  # Provide['db'] -> Provide object
auth: HTTPTokenAuth = Provide[Container.auth]  # Provide['auth'] -> Provide object


class BaseModel(db.Model):
    __abstract__ = True
    ...


@auth.verify_token
def verify_token(token: str, keycloak: Keycloak = Provide[Container.keycloak]):  # Provide['keycloak'] -> Provide object
    userinfo = keycloak.userinfo(token)
    ...

Использование строковых литералов в Provide не помогает!

extensions.py

import typing as t

from apiflask import HTTPTokenAuth
from flask_sqlalchemy import SQLAlchemy

__all__ = ['HTTPTokenAuth', 'SQLAlchemy', 'Keycloak']


class Keycloak:
    def userinfo(self, token: str) -> dict[str, t.Any]: ...

services/service.py

import typing as t
from abc import ABC

from sqlalchemy.orm import Session

from _issue.core import BaseModel


class BaseRepository(ABC):

    def __init__(self, session: Session, model: t.Type[BaseModel]):
        self._session = session
        self._model = model


class Model(BaseModel):
    ...


class ModelRepository(BaseRepository):

    def __init__(self, session: Session):
        super(ModelRepository, self).__init__(session, Model)


class ModelUseCase:

    def __init__(self, repository: ModelRepository):
        self._repository = repository

    ...

При таком подходе возникает очевидная циклическая зависимость: Container -> BaseModel -> ModelRepository -> Container

from app import containers
...
from app import containers
...
from app.services.service_1 import ModelRepository, ModelUseCase
...
from app.core import BaseModel
...
from app.containers import Container
...
ImportError: cannot import name 'Container' from partially initialized module 'app.containers' (most likely due to a circular import) (...\app\containers.py)

Как я должен переструктурировать свое приложения, чтобы избежать данной ошибки? Помогите, пожалуйста ??


Ответы (1 шт):

Автор решения: Prosto_Oleg

Попробуйте заменить все конструкции from ... import ..., на которые ругается пайтон, на import <filename> [as some alias]

import app.containers as containers  # Импорт
... 
container = containers.Container() # Использование 

Проверка

файл a.py:

from b import b_func

def a_func():
    print("Hello from a module")


def some_func():
    b_func()

Файл b.py:

from a import a_func

def b_func():
    print("Hello from b module")
    a_func()

Файл main.py:

from b import b_func

b_func()

Ошибка:

ImportError: cannot import name 'b_func' from partially initialized module 'b' (most likely due to a circular import) (/home/oleg/pet/Tests/b.py)

Замена в файла a.py на :

import b as some_alias

def a_func():
    print("Hello from a module")


def some_func():
    some_alias.b_func()

Вывод в консоль:

Hello from b module
Hello from a module
→ Ссылка