Как избежать циклического импорта при использовании контейнеров 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 шт):
Попробуйте заменить все конструкции 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