meadsteve / lagom

📦 Autowiring dependency injection container for python 3
https://lagom-di.readthedocs.io/en/latest/
MIT License
286 stars 15 forks source link

@context_dependency_definition is not considered in some cases with FastAPI #254

Open attilavetesi-epam opened 7 months ago

attilavetesi-epam commented 7 months ago

When using FastAPI, the @context_dependency_definition decorator is not considered for constructing the type if the given type is not injected before other injections in the route definition.

Consider the following code:

from typing import Iterator

import uvicorn
from fastapi import FastAPI
from lagom import Container, context_dependency_definition
from lagom.integrations.fast_api import FastApiIntegration
from starlette.responses import PlainTextResponse

class ServiceA:
    def __init__(self, value: str):
        if not value == "correctly constructed":
            raise ValueError("Service A not correctly constructed!")

    def method_a(self):
        return "A result"

class ServiceB:
    def method_b(self):
        return "B result"

class ServiceC:
    def __init__(self, service_b: ServiceB, service_a: ServiceA):  # there are other injections before A, still WORKS
        pass

    def method_c(self):
        return "C result"

container = Container()

@context_dependency_definition(container)
def my_constructor() -> Iterator[ServiceA]:
    try:
        yield ServiceA("correctly constructed")
    finally:
        pass

deps = FastApiIntegration(container, request_context_singletons=[ServiceA])

app = FastAPI()

@app.get("/works1")
async def works1(service_a=deps.depends(ServiceA)):
    a = service_a.method_a()
    return PlainTextResponse(f"A is: {a}")

@app.get("/works2")
async def works2(service_a=deps.depends(ServiceA), service_b=deps.depends(ServiceB)):
    a = service_a.method_a()
    b = service_b.method_b()
    return PlainTextResponse(f"A is: {a}, B is: {b}")

@app.get("/works3")
async def works3(service_c=deps.depends(ServiceC)):
    c = service_c.method_c()
    return PlainTextResponse(f"C is: {c}")

@app.get("/doesnt_work")
async def doesnt_work(service_b=deps.depends(ServiceB), service_a=deps.depends(ServiceA)):  # there are other injections before A, DOESN'T WORK
    a = service_a.method_a()
    b = service_b.method_b()
    return PlainTextResponse(f"A is: {a}, B is: {b}")

uvicorn.run(app)

In the following cases the injection works as expected:

  1. In the FastAPI method: ServiceA is injected as first
  2. Outside of FastAPI method: work in all cases, no matter when ServiceA is injected (Update: not true, see correction below)

Unfortunately in the case when the ServiceA is injected in the FastAPI method, but there are other injections happening beforehand, the following error is thrown, which suggests that the @context_dependency_definition is disregarded and the init of ServiceA is planned to be called directly.

INFO:     Started server process [3572]
INFO:     Waiting for application startup.
INFO:     Application startup complete.
INFO:     Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit)
INFO:     127.0.0.1:53838 - "GET /works1 HTTP/1.1" 200 OK
INFO:     127.0.0.1:53840 - "GET /works2 HTTP/1.1" 200 OK
INFO:     127.0.0.1:53841 - "GET /works3 HTTP/1.1" 200 OK
INFO:     127.0.0.1:53842 - "GET /doesnt_work HTTP/1.1" 500 Internal Server Error
ERROR:    Exception in ASGI application
Traceback (most recent call last):
  File "lagom\container.py", line 391, in _reflection_build_with_err_handling
lagom.exceptions.UnresolvableType: Unable to construct dependency of type str The constructor probably has some unresolvable dependencies: str

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "lagom\container.py", line 392, in _reflection_build_with_err_handling
  File "lagom\container.py", line 406, in _reflection_build
  File "lagom\container.py", line 425, in _infer_dependencies
  File "lagom\container.py", line 265, in resolve
  File "lagom\container.py", line 395, in _reflection_build_with_err_handling
lagom.exceptions.UnresolvableType: Unable to construct dependency of type str The constructor probably has some unresolvable dependencies: str

During handling of the above exception, another exception occurred:

<truncated>

    return container.resolve(dep_type)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "lagom\container.py", line 265, in resolve
  File "lagom\container.py", line 395, in _reflection_build_with_err_handling
lagom.exceptions.UnresolvableType: Unable to construct dependency of type ServiceA The constructor probably has some unresolvable dependencies: ServiceA
attilavetesi-epam commented 7 months ago

Correction: when adding a ServiceD as the first parameter to the /works3 method (after the current ServiceC parameter), the problem can be also reproduced (i.e. ServiceA is not injected correctly).

Summary: seemingly only at the first injection a @context_dependency_definition is recognized, in all other cases (where some other injections are finalized beforehand) the @context_dependency_definition is not detected/used anymore.