python-dependency-injector icon indicating copy to clipboard operation
python-dependency-injector copied to clipboard

How to avoid circular import when using dependency injection?

Open belo4ya opened this issue 2 years ago β€’ 5 comments

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 πŸ˜”πŸ˜­

belo4ya avatar Jul 11 '22 22:07 belo4ya

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 avatar Jul 28 '22 15:07 bmgarness

@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

belo4ya avatar Aug 01 '22 16:08 belo4ya

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.

bmgarness avatar Aug 03 '22 06:08 bmgarness

try to use string identifiers instead of Container class. example :

@inject
def foo(service: UserService = Provide["services.user"]) -> None:
    ...

devamin avatar Sep 01 '22 23:09 devamin

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.

benm5678 avatar Mar 09 '24 21:03 benm5678