-
-
Notifications
You must be signed in to change notification settings - Fork 321
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[Q] Provider override not working with subcontainers #851
Comments
Nested containers is an area we can improve. In your particular case, proper incantation would be I've adapted your example to be a standalone runnable test, it should work as you expect: test_di_issue851.pyfrom dataclasses import dataclass
from enum import StrEnum, auto
from unittest.mock import AsyncMock
import pytest
from dependency_injector import containers, providers
from dependency_injector.wiring import Provide, inject
class Health(StrEnum):
OK = auto()
FAILING = auto()
@dataclass
class HealthCheckResponse:
app: Health
db: Health
class PostgresDatabase:
async def health(self) -> Health:
return Health.OK
class UtilsContainer(containers.DeclarativeContainer):
database = providers.Factory(PostgresDatabase)
class MainContainer(containers.DeclarativeContainer):
utils_package = providers.Container(UtilsContainer)
class RootContainer(containers.DeclarativeContainer):
wiring_config = containers.WiringConfiguration(modules=[__name__])
main_package = providers.Container(MainContainer)
@inject
async def get_health_status(
database: PostgresDatabase = Provide["main_package.utils_package.database"],
) -> HealthCheckResponse:
return HealthCheckResponse(
app=Health.OK,
db=await database.health(),
)
@pytest.fixture
def container(request):
"""Creates container"""
container = RootContainer()
container.init_resources()
overrides = request.param
generators = []
for override_fn in overrides:
generators.append(override_fn(container))
for generator in generators:
next(generator)
yield container
for generator in generators:
try:
next(generator)
except StopIteration:
pass
container.unwire()
def unhealthy_database(container):
mocked_db = AsyncMock(spec=PostgresDatabase)
mocked_db.health.return_value = Health.FAILING
with container.main_package.utils_package.database.override(mocked_db):
yield
@pytest.mark.asyncio
@pytest.mark.parametrize("container", [[unhealthy_database]], indirect=True)
async def test_unhealthy(container):
health_status = await get_health_status()
assert health_status.app == Health.OK
assert health_status.db == Health.FAILING |
Thank you so much for this. I would like to wrap my brain around how all this works. The doc is excellent in going over most usage scenarios, but I feel like it lacks in the deep end territory. Do you happen to have a resource to point me towards, or is reading code the only way for now? We are trying to use this as our DI "framework" at work and it will get hard to advocate for it if nobody knows how it works. Again, thank you a ton for answering both of my posts. |
Unfortunately, this. Also it is worth checking the issues page, people did figure out some common use cases not outlined in the docs. The good news - past few weeks I've been working on updating docs/examples based on few recent changes + my experience using DI at scale. At my current place we have 50+ Python microservices of various sizes, majority of them use DI. The reason I've volunteered to assist @rmk135 in maintenance to begin with, as its usage is 100% my initiative. The bad news is - I have a limited time to work on it too.
NP, feel free to ask more if needed. I'll consolidate the knowledge eventually. |
Thanks, I think I'll take you up on that. Sorry 😅 You mentioned using And one that's on topic - The example works, but only if i wire the root container to the module where I use it. So I think I have the pattern. In tests I need to instantiate and override using the exact path that got used to wire the container to the modules I'm testing. |
Wiring is a monkey-patch level of sorcery, which you rarely want in your code (for sanity reasons). Think of DI as a glue to assemble all the components together. By adding explicit So the basic pattern is to go full java-style constructor injection. I.e. provide every dependency through the As a bonus of such approach, you:
If I got you right, this is what we configured it to do so.
As I've mentioned before, nesting containers could do better. In my projects we never call |
Well, your approach - having even your services wired up with their dependencies in the Container in my mind now makes your API layer dependent on injects, or on having a reference to the container instance. For now, I am opting to test out the opposite way to your approach. I am going to inject like this You'll probably know soon enough when this approach crashes and burns. 😅 |
You're absolutely right, but there is a catch. Our views looks like this: views.py
We're trying to practice CQRS, so "command" looks not so different from example above (queries). Our views have almost no logic, aside from schema validation (which is done mostly by Pydantic already). I cannot share the specifics, but we were heavily inspired by the Cosmic Python book, I recommend you checking it too. It's quite difficult to do it right the first time, you might want to experiment with something simpler like Clean Architecture (in the example above you'll be injecting a "use case" instead of query or command). We nest our containers through Container provider, btw. There is no proper documentation page for it, but there are few miniapp examples. Maybe this will help.
I've seen so much |
Your API layer looks exactly like what we set out to do. Our old one is a mess and we dislike it. |
It's a bit tricky to cook DI for testing, but here's setup that works for me for non-unit tests: https://gist.github.com/ZipFile/7b9911c3f0c57c7d112725618a596464 |
Thank you, I'll reference this when I get stuck doing these kinds of tests. So far, I have my service test without DI and moving forward. I burned an ungodly amount of time on this. Without you, I'd probably be forced to give up. |
Hi, so again, my nested structure has come back to bite me. These are integration tests using httpx, my limited understanding is it "simulates" a running web server while internally using just the FastAPI object It seems like the injection which does not specify the full path attempts to instantiate a whole new PrinterContainer which depends on MainContainer and so it creates that too and the two identical containers coexist. I am unable to see the actual init calls as it all happens in some dynamic containers I cannot touch with my lowly human code. More interestingly, when another endpoint needs the injection, the structure does not seem to be created a third time. I printed them using this code:
Nice, everything exists twice. The RootContainer seems to exist just once and some are rooted in MainContainer instead. Doing it the full path way works, but the same duplicated DynamicContainer objects are printed Do you happen to know what do I keep unleashing upon myself? Like, what actually happens? |
For now, I am opting for path aware containers. You give the class the dependency name and it gives you the full path, so re-structuring would mean one path change per container.
but I am eager to get rid of this if anything else is, or becomes available |
The recommended way aged poorly, unfortunately. It also makes Here's the missing part from the gist I shared before: containers.py
class FeatureFlagsContainer(DeclarativeContainer):
api_key: Dependency[str] = Dependency(default="")
enabled: Singleton[bool] = Singleton(bool, environment_key)
cache_ttl: Dependency[float] = Dependency(default=120)
cache: Singleton[FFCache] = Singleton(FFCache, cache_ttl=cache_ttl)
client: Factory[FFClient] = Factory(FFClient, cache=cache, api_key=api_key)
class MainContainer(DeclarativeContainer):
__self__ = Self()
config: Configuration = Configuration()
lifespan = Singleton(Lifespan, __self__)
fastapi_app = Factory(
make_fastapi_app,
lifespan=lifespan ,
title=config.misc.title,
...
)
_configure_loggging: Resource[None] = Resource(
logging_init,
log_config_file_path=config.logging.config_file_path,
log_level=config.logging.level,
log_config_name=config.logging.name,
)
ff = Container(
FeatureFlagsContainer, # Used as Provide["ff.client"] in code.
api_key=config.ff.api_key,
cache_ttl=config.ff.cache_ttl,
)
... application.pyHere exists everything FastAPI-related to avoid polluting other modules with API-layer stuff. It is separated purely for organizational reasons. def make_fastapi_app(lifespan: Lifespan, title: str) -> FastAPI:
app = FastAPI(
title=title,
lifespan=lifespan,
)
app.include_router(...)
return app asgi.pyThis is a special file for def build_app() -> FastAPI:
container = MainContainer()
settings = Settings()
container.config.from_dict(settings.model_dump())
return container.fastapi_app() __main__.pyThis is actual web server launcher meant to be run as import os
import uvicorn
def run() -> None:
uvicorn.run(
"myproject.asgi:build_app",
factory=True,
port=8000,
host="0.0.0.0",
reload=os.environ.get("ENVIRONMENT") == "local",
log_config=None, # this tells uvicorn to not to configure logging
)
if __name__ == "__main__":
run() conftest.pySelf-explanatory. Tweak @fixture
def fastapi_app(
container: Container[MainContainer],
db_session: AsyncSession,
) -> FastAPI:
return container.fastapi_app() # type: ignore[no-any-return]
@fixture
def client(fastapi_app: FastAPI) -> AsyncClient:
return AsyncClient(
transport=ASGITransport(app=fastapi_app),
base_url="http://test",
) I cannot answer what exactly is wrong with your setup without looking at the code, but hope my DI cooking recipe works for you. |
Sorry for the second post today, i am kinda at the end of my ropes here
I would be so happy if someone manages to get me unstuck.
I have three levels of containers. Root, Main and module specific
In component/unit tests when I want to override a provider, it only works if I instantiate the module specific container like UtilsContainer and orverride its database provider
If I instead make an instance of RootContainer and do
with container.main_package.utils_package.database.override(mocked_db):
it does not work and the original container is used
I placed a raise statement before my RootContainer init in the main code. Does not get triggered, so that instance does not get created from a spurious import somewhere
I am trying to do this without getting a reference to the root container, instead hoping that I can create a new instance in my test, override there and have that injected in whatever I call from the test
My service does not get the container reference, it injects like so:
database: PostgresDatabase = Provide[UtilsContainer.database]
conftest.py
test_utilities.py
I am open to distilling this into a minimal example, but I fear I am missing something simple.
Thank you
The text was updated successfully, but these errors were encountered: