python-dependency-injector
python-dependency-injector copied to clipboard
How to avoid circular import when using dependency injection?
Help, pls! I really don't understand how I can integrate python-dependency-injector
in my project ππ
When using a container in my application, cyclical dependencies that look like: Container -> core -> services -> Container
. And I really don't understand how I should change the structure of my application to avoid this.
For example, I have the following Flask application structure (usually core
, extensions
and service
are packages):
app
β __init__.py # create_app
β containers.py # Container
β core.py # reused components (BaseModel, ...) that I want to use in any part of the App (services, api, ...)
β extensions.py # SQLAlchemy, HTTPTokenAuth, Keycloak, ...
β
ββββapi # here, wiring works great
β ...
β
ββββservices
service.py # Models, Repositories, UseCases for this service
...
__init__.py
__init__.py
contains just the App factory:
from apiflask import APIFlask
from app import containers
def create_app() -> APIFlask:
app = APIFlask(__name__)
...
containers.init_app(app)
...
return app
In the container in containers.py
I want to store both extensions and services:
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)
...
In core.py
, I keep a template code that I want to reuse from anywhere in the application. This code often requires the use of extension objects (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)
...
Using string literals doesn't help!
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
...
With this approach, there is an obvious cyclical dependence: 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)
What is the best application structure that can save me from this error? Help please ππ
I have a similar project where I have Flask with SQLAlchemy and a logging service.
our template code that we use throughout our project is broken apart into specific files, so a BaseModel is saved in the database.model directory because that's the only place it needs to be.
We use controllers for our views where the inject happens.
Inject happens here
_________ _____________ ___________
| | | | | |
| Views | ----> | Controller | ----> | Services |
|_______| |____________| |__________|
So our services are provided to our controllers and our controllers are never required by each other and our views have nothing else to call than the controller specific to that, thus no circular dependencies.
@bmgarness, Thanks for the answer,
But I didn't fully understand. Don't your Services import and use Models (which are inherited from BaseModel)? Or are the Models in the Services you use through dependency injection?
db = SQLAlchemy() # must be initialized in the container
...
db.Model <- BaseModel <- (Models) <- Services
So the way my app works and uses SQLAlchemy() is by using Session maker and providing that to the database service. My models on the other hand are independent.
try to use string identifiers instead of Container class. example :
@inject
def foo(service: UserService = Provide["services.user"]) -> None:
...
Did anyone find a solution? I also struggle with the same whenever my containers.py
has an object that itself or one of its dependencies is using a Provide[] from the container. When I try with string identifier as @devami suggested the DI no longer works (no error, but the object is not instantiated). I used DI framework in .NET Core and recall it was much simpler - no need to manually wire modules & simple class/method decorators would make it work.