ets-labs / python-dependency-injector

Dependency injection framework for Python
https://python-dependency-injector.ets-labs.org/
BSD 3-Clause "New" or "Revised" License
4.01k stars 306 forks source link

django.core.exceptions.AppRegistryNotReady: Apps aren't loaded yet. #466

Open vuhi opened 3 years ago

vuhi commented 3 years ago

Hi,

Thank you so much for a wonderful project. I am using dependency injection in a Django side project of mine.

I notice a big problem with the way the container initialize. As you state in the example section for Django, we initiate the container in __init__.py at project level.

from .di_containers import DIContainer
from . import settings

di_container = DIContainer() -> create dependency before other app load
...

https://python-dependency-injector.ets-labs.org/examples/django.html

However, if one of your provider (Ex: UserService needs User model) requires model to do some works, django will throw django.core.exceptions.AppRegistryNotReady: Apps aren't loaded yet.

I am not so sure how to solve it? I am using Singleton in my container (required). Not sure if it that the reason?

class DIContainer(containers.DeclarativeContainer):
    user_service = providers.Singleton(UserService)
from api_core.apps.user.models import User <- error cause by this import

@inject
class UserService:
    def __init__(self):
        self.test = 'test'

Update: I solve the problem with local import. But still I have to import locally multiple places in different function. The main root cause still because of initialization of the container in __init__.py at project level. Let say we move it to a custom app, which register at the end of INSTALLED_APP list, we still have a problem how to wire them to the other app. I don't know if you familiar with ReactiveX. I think this package can help solve the problem of wiring https://github.com/ReactiveX/RxPY. We could have an subscriber at each AppConfig in def ready: .... Then when we initiate the container, it will trigger a signal to tell them, ok it is safe for you guys to wire now ?

rmk135 commented 3 years ago

Hey @vuhi ,

Yeah, that's a relevant problem. I think that I didn't hit it in the example cause I didn't use any models. I recall that django models can't be imported before django intialization is finished.

This definitely needs to have a solution.

I don't know if you familiar with ReactiveX

No, I'm not familiar with it. I'll take a look for sure.

Also I'll take a look on writing a custom app that could twist in django and dependency injector initialization properly.


Huge thanks for bringing this up!

vuhi commented 3 years ago

Hi, @rmk135

I finally make it works. I am pretty new to python & Django. I am not sure how Django handle import models & stuff. Here is my work around:

I create an django app call ioc. It contains a file to declare container class, in my case is: di_containers.py.

> experiment  (view app)
    > __init__.py
    > apps.py ( wiring dependencies here)
    > urls.py
    > views.py 
> core
    > db
        > user.py (just a way to separate model class to each file)
        > book.py
    > models.py ( important, all models need to import back to this file)
    > apps.py  ( optionals, can be use to wire dependencies as old example )
    > ...
> ioc
    > __init__.py ( optionals, can put anything in here )
    > apps.py  ( optionals )
    > di_containers.py ( important, declare container class & initialize it

ioc/di_containers.py

from path.to.import.model import User <-- test if we could import model

class DIContainer(containers.DeclarativeContainer):
    config = providers.Configuration()
    google_oauth = providers.Factory(GoogleOAuth)
    facebook_oauth = providers.Factory(FaceBookOAuth)

    auth_service = providers.Factory(AuthService)
    token_service = providers.Factory(AccessToken)
    user_service = providers.Singleton(User) # <-- test if we could use model

di_container: DIContainer = DIContainer()
di_container.config.from_dict({'CONFIG': settings.CONFIG})
...

I register the app at the end of INSTALLED_APPS list. All of my model I put in a separate django app call core

settings.py

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'rest_framework',
    'api_core.apps.core', <-- contains all models
    'api_core.apps.experiment',  <-- no model just view
    ...
    'api_core.apps.ioc', <-- at the end
]

experiment/apps.py

    def ready(self):
        from . import views
        from path.to.ioc.di_containers import di_container

        # very important step to inject the dependencies in DI to each module
        # wire will automatically inject dependencies to @inject
        di_container.wire(modules=[views])
]

experiment/views.py

@api_view(['GET'])
@authentication_classes([JWTTokenAuthentication])
@permission_classes([IsAuthenticated])
@inject
def protected_ping(request: Request, user_service: User = Provide['user_service']):
    print(user_service.__class__.service.all()[0]) -> success print to console
    user = {'id': request.user.id, 'email': request.user.email}
    return SuccessRes('Yeah!!. This route was protected behind jwt token!', {'user': user})

PS:

I don't understand how the wire process work. I do have a question on that.

What if I override a service inside the container in one of the view some time after the wiring process (which happen once in apps.py)

For example:

Thank you,

renanivo commented 2 years ago

Thanks, @vuhi.

I got here because I was having the same issue. Your fix worked for me. Is there anybody working on updating the docs?

stevenengland commented 1 year ago

In my latest experience there is no need to put all the models to an core app etc. The problematic part from the Django example seems to be this portion:

# githubnavigator/__init__.py
from .containers import Container # <--- here is the problematic part if containers.py imports/references models
from . import settings

container = Container()
container.config.from_dict(settings.__dict__)

Because the __init__.py code is called early you cannot reference/import models inside githubnavigator/containers.py. But if you move the code snippet from the __init__.py file to web/apps.py and githubnavigator/containers.py then you have the chance to influence the point in time when the container and all its dependencies are created and then you can use Djangos def ready() (Link) override to import everything (especiallly if it is related to models):

# web/apps.py
from django.apps import AppConfig

from githubnavigator import container

class WebConfig(AppConfig):
    name = "web"

    def ready(self):
        from githubnavigator import container # <-- import the container here so further imports inside container.py like models can succeed
        container.wire(modules=[".views"])
# githubnavigator/containers.py
from dependency_injector import containers, providers
from github import Github

from . import services

class Container(containers.DeclarativeContainer):

    config = providers.Configuration()

    github_client = providers.Factory(
        Github,
        login_or_token=config.GITHUB_TOKEN,
        timeout=config.GITHUB_REQUEST_TIMEOUT,
    )

    search_service = providers.Factory(
        services.SearchService,
        github_client=github_client,
    )

container = Container() # <-- create the container here
container.config.from_dict(settings.__dict__)

Disclaimer: I am still learning Django and I am still bootstrapping my project so I can't tell something about long term issues with this approach but so far this was what made my project work again.

@rmk135 Can you confirm that and maybe change the Django example?