Skip to content

How to avoid circular import when using dependency injection? #607

Open
@belo4ya

Description

@belo4ya

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 😔😭

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions