Fastapi: How to achieve nested parameterized dependencies?

Created on 30 Jun 2020  路  21Comments  路  Source: tiangolo/fastapi

First check

  • [x] I added a very descriptive title to this issue.
  • [x] I used the GitHub search to find a similar issue and didn't find it.
  • [x] I searched the FastAPI documentation, with the integrated search.
  • [x] I already searched in Google "How to X in FastAPI" and didn't find any information.
  • [x] I already read and followed all the tutorial in the docs and didn't find an answer.
  • [x] I already checked if it is not related to FastAPI but to Pydantic.
  • [x] I already checked if it is not related to FastAPI but to Swagger UI.
  • [x] I already checked if it is not related to FastAPI but to ReDoc.

Example

Given the follow minimal working example, where Test1 and Test2 are parameterized dependencies (as described in https://fastapi.tiangolo.com/advanced/advanced-dependencies/). One parameter of Test2 should be forwarded to Test1 which should be injected into Test2.:

import uvicorn
from fastapi import Depends, FastAPI
app = FastAPI()

class Test1():

    def __init__(self, t1) -> None:
        self.t1 = t1

    def __call__(self):
        return self.t1

class Test2():

    def __init__(self, t2) -> None:
        self.t2 = t2

    def __call__(self):
        return Depends(Test1(t1=self.t2))

@app.get("/test/")
def test_call(test: str = Depends(Test2(t2='test'))):
    return test


if __name__ == "__main__":
    uvicorn.run("main:app", host="127.0.0.1", port=8000, reload=True, debug=True)

Description

It is not possible to include Test1 as dependency in the call parameters of Test2 since there wouldn't be any access to the parameter of Test1.
Directly calling Depends as in the example above also does not work since there does not seem to be any dependency injection at this level.

How to achieve this kind of nesting? Is this simply not possible at the moment? If not, I would like to add this as feature request.

Environment

  • OS: Windows:
  • FastAPI Version 0.58.0
  • Python version: 3.7
question

Most helpful comment

It doesn't take much work to get your example working:


from typing import Callable, TypeVar
from fastapi.testclient import TestClient
from fastapi import FastAPI, Request, Query
from fastapi.params import Depends
from rarbg_local.singleton import get

app = FastAPI()


class ParameterizedDepends(Depends):
    def __init__(self, dependency) -> None:
        self.real_dependency = dependency
        super().__init__(self.__call__)

    async def __call__(self, request: Request):
        async def internal():
            param = yield
            yield await get(
                request.app,
                lambda item=Depends(self.real_dependency(param)): item,
                request,
            )

        value = internal()
        await value.asend(None)
        return value


class Test1:
    def __init__(self, t1) -> None:
        self.t1 = t1

    def __call__(self, request: Request, orderId: str = Query(...)):
        return self.t1


class Test2:
    def __init__(self, t2) -> None:
        self.t2 = t2

    async def __call__(self, test1=ParameterizedDepends(Test1)):
        return await test1.asend(self.t2)


@app.get('/')
def thing(t=Depends(Test2('thing'))):
    pass


tc = TestClient(app)
tc.get('/', params={'orderId': '1'})

All 21 comments

you could add a Test1 instance in Test2.__init__
you could get an instance as input like : Depends(Test2(t2='test2', test1=Test1(t1='test1')))
or you can create one in __init__ like:

class Test2():

    def __init__(self, t2) -> None:
        self.t2 = t2
        self.test1=Test1(t1=t2)

@ihakh This does not provide any dependency injection to the __call__ method of Test1. Getting this to work with dependency injections is the point of this question / feature request.

Hey @Spenhouet, thanks for the interest.

I think I'm having a bit of trouble understanding the use case. I think I can imagine simple ways to solve it without requiring support for that, but I'm probably missing details of your use case.

For example, this seems to work:

import uvicorn
from fastapi import Depends, FastAPI

app = FastAPI()


class Test1:
    def __init__(self, t1) -> None:
        self.t1 = t1

    def __call__(self):
        return self.t1


class Test2:
    def __init__(self, t2) -> None:
        self.t2 = t2

    def __call__(self):
        return Test1(t1=self.t2)()


@app.get("/test/")
def test_call(test: str = Depends(Test2(t2="test"))):
    return test


if __name__ == "__main__":
    uvicorn.run("main:app", host="127.0.0.1", port=8000, reload=True, debug=True)

It seems my issue left out a crucial detail which must have seemed obvious to me when I created that issue.

Test1 and Test2 should be provided with dependency injection. Test1 should not be a simple class as in your code example.

The crucial detail I missed to specifically mention is that I would like to have dependency injection available in the call method of Test1 while also having it parameterized via Test2.

For example, the call method of Test1 should have parameters like request available:

class Test1():

    def __init__(self, t1) -> None:
        self.t1 = t1

    def __call__(self, request: Request):
        return self.t1

My use case gets more clear when looking at the following syntactically wrong example of what I would have liked to do:

class Test2():

    def __init__(self, t2) -> None:
        self.t2 = t2

    def __call__(self, test1 = Depends(Test1(t1=self.t2))):
        return test1

But this is not possible in python since self isn't available at this point.

My thought process was that I might be able to perform the dependency injection via the Depends wrapper outside the parameters directly in the code.

def __call__(self):
        test1 = Depends(Test1(t1=self.t2))
        return test1

But this does not execute as expected. This just returns an object but does not actually execute the call method of Test1 like it is done when it is included as parameter.

Could you do something like this instead?


    def __call__(self, test1 = Depends(Test1())):
        test1.t1 = self.t2
        return test1

Without knowing your actual use case, it's difficult to suggest possible solutions

@Mause No, that would not make sense since the call method of Test1 would then be called before t1 would be assigned.

Ok, how about something like this?


from fastapi.testclient import TestClient
from fastapi import Depends, FastAPI, Request
from rarbg_local.singleton import get

app = FastAPI()


class Test1:
    def __init__(self, t1) -> None:
        self.t1 = t1

    def __call__(self, request: Request):
        return self.t1


class Test2:
    def __init__(self, t2) -> None:
        self.t2 = t2

    async def __call__(self, request: Request):
        test1 = await get(
            request.app, lambda item=Depends(Test1(self.t2)): item, request
        )
        return test1


@app.get('/')
def thing(t=Depends(Test2('thing'))):
    pass


tc = TestClient(app)
tc.get('/')

With the missing piece here: https://github.com/Mause/media/blob/master/rarbg_local/singleton.py#L14

I will try to give an additional more complete and more clear explanation.

Starting with the scenario where you have nested dependencies:

import uvicorn
from fastapi import Depends, FastAPI, Request

app = FastAPI()


def sub_dependency(request: Request) -> str:
    return request.method


def main_dependency(sub_dependency_value: str = Depends(sub_dependency)) -> str:
    return sub_dependency_value


@app.get("/test/")
def test_endpoint(test: str = Depends(main_dependency)):
    return test


if __name__ == "__main__":
    uvicorn.run("main:app", host="127.0.0.1", port=8000, reload=True, debug=True)

Notice how sub_dependency can use dependency injection to get for example the request object.

Dependencies can also be written like classes instead of methods. The above example can be written like this:

import uvicorn
from fastapi import Depends, FastAPI, Request

app = FastAPI()


class SubDependency():

    def __init__(self) -> None:
        pass

    def __call__(self, request: Request) -> str:
        return request.method


class MainDependency():

    def __init__(self) -> None:
        pass

    def __call__(self, sub_dependency_value: str = Depends(sub_dependency)) -> str:
        return sub_dependency_value


@app.get("/test/")
def test_endpoint(test: str = Depends(MainDependency())):
    return test


if __name__ == "__main__":
    uvicorn.run("main:app", host="127.0.0.1", port=8000, reload=True, debug=True)

Notice that nothing changed. It is just a different syntax.

Now you can have parameterized dependencies. This allows you to have some generic dependency which changes its function based on the provided parameter. For example lets add a parameter type to MainDependency:

import uvicorn
from fastapi import Depends, FastAPI, Request

app = FastAPI()


class SubDependency():

    def __init__(self) -> None:
        pass

    def __call__(self, request: Request) -> str:
        return request.method


class MainDependency():

    def __init__(self, type: str) -> None:
        self.type = type

    def __call__(self, sub_dependency_value: str = Depends(SubDependency())) -> str:
        return self.type + sub_dependency_value


@app.get("/test/")
def test_endpoint(test: str = Depends(MainDependency(type="Customer"))):
    return test


if __name__ == "__main__":
    uvicorn.run("main:app", host="127.0.0.1", port=8000, reload=True, debug=True)

Now image I already have a nested dependency structure but to reduce redundancy I would like to have a more generic parameterized sub dependency. For this I would need to pass the parameter through the main dependency. That is what I can't solve at the moment.

My code are just minimal examples of the issue at hand. There can be much deeper nesting than just main and sub as in the example above. I don't want every hosting dependency to provide / carry all parameters which the sub dependency needs. That would miss the point of having dependency injection. The usage of the request for the injection or the parameter type is also just an example. I would like to have full dependency injection access on the sub dependency.

I'm afraid you haven't given an example that can't be solved with the method I supplied above - perhaps you'd like to give a concrete example rather than a theoretical one?

It seems it is still not clear. This is not a theoretical example. This is actually a very practical example. I stated above why your suggestions do not work. I don't see how your example provides dependency injection. You are simply passing along the parameters. That is not dependency injection. Maybe we have a different understanding of a what a injector is supposed to do.

I can't make it more clear since this would simply require me to solve the problem which most probably is not possible at the moment. That is why this is a feature request and not a bug report.

Well, I'm actually not aware of any dependency injection systems that support what you are asking for. Perhaps you could show us one that has the feature you're asking for and we could look at implementing it?

I partially misunderstood what your code example above did. I did not test it before since I did not have your code (singleton.py).

async def __call__(self, request: Request):
        test1 = await get(request.app, lambda item=Depends(Test1(self.t2)): item, request)

I thought you were just passing in the request object which was required in the sub dependency of my example and therefore didn't expect it to perform dependency injection on everything else - hence my statement "You are simply passing along the parameters.".

But your code does actually resolve the dependency injection. That is what I would have expected of Depends to do on its own. I also think it would be nicer if passing request wouldn't be necessary.

async def __call__(self, request: Request):
        test1 = Depends(Test1(self.t2))

One thing I thought of that would make it a bit nicer would be:

# As part of fastapi:

class ParameterizedDepends:

    def __init__(self, dependency) -> None:
        self.dependency = dependency

    async def __call__(self, request: Request):
        param = yield
        yield await get(request.app, lambda item=Depends(self.dependency(param)): item, request)
# Usage example:

class Test1:

    def __init__(self, t1) -> None:
        self.t1 = t1

    def __call__(self, request: Request, orderId: str = Query(...)):
        return self.t1


class Test2:

    def __init__(self, t2) -> None:
        self.t2 = t2

    async def __call__(self, test1=ParameterizedDepends(Test1)):
        return test1.send(self.t2)

But this doesn't exactly work as expected. ParameterizedDepends is not picked up the same way as Depends.
Providing a functionality like this would already improve the situation.

One issue that remains is that the init of the dependencies it not called during start up but on call. This defeats any caching etc. happing there.

But again, passing on the parameter through the nesting seems to be not possible by the way python works. Not sure if this is even doable.

The best thing would be if

def __call__(self, test1=Depends(Test1)):
        return test1

could just pass along the parameter in some way. I currently have no idea on a possible design or technical solution.

Having something that allows the deliberate execution of Depends as you proposed would already be an improvement. It would at least enable the dependency injection for parameterized sub dependencies which currently isn't possible.

It doesn't take much work to get your example working:


from typing import Callable, TypeVar
from fastapi.testclient import TestClient
from fastapi import FastAPI, Request, Query
from fastapi.params import Depends
from rarbg_local.singleton import get

app = FastAPI()


class ParameterizedDepends(Depends):
    def __init__(self, dependency) -> None:
        self.real_dependency = dependency
        super().__init__(self.__call__)

    async def __call__(self, request: Request):
        async def internal():
            param = yield
            yield await get(
                request.app,
                lambda item=Depends(self.real_dependency(param)): item,
                request,
            )

        value = internal()
        await value.asend(None)
        return value


class Test1:
    def __init__(self, t1) -> None:
        self.t1 = t1

    def __call__(self, request: Request, orderId: str = Query(...)):
        return self.t1


class Test2:
    def __init__(self, t2) -> None:
        self.t2 = t2

    async def __call__(self, test1=ParameterizedDepends(Test1)):
        return await test1.asend(self.t2)


@app.get('/')
def thing(t=Depends(Test2('thing'))):
    pass


tc = TestClient(app)
tc.get('/', params={'orderId': '1'})

@Mause Nice!

This can probably even further improved by removing the need to async and by hiding the asend to a function call like test1(self.t2).

This would allow a relatively clean solution.

@tiangolo Might this be something that can be integrated like this or in a similar form? Currently there is no build in way to use nested parameterized dependencies with dependency injection. The main benefit is a cleaner code with less redundancy (more code reuse). The code necessary for the solution above is a total overkill for a "workaround" like solution. We would never put something like this in our code. But if there would be a build in method like ParameterizedDepends, we for sure would use it.
The solution above does enable dependency injection in nested parameterized dependencies. The only draw back is that init methods of the parameterized sub dependencies are executed at call time and not at application start. I currently don't see a way how this could be solved since it seems to clash with how python works in its self.

Honestly, I'm yet to see a reasonable use case for such a feature? It seems fairly arbitrary, and hence unlikely to be included in fastapi's core.

The examples you have given so far as well don't seem real world to me either - perhaps you can give an example from your own problem domain?

In the end, there is nothing stopping you from taking the above code and creating and publishing a package to pypi for your own use, or simply having it as a module in your own code.

I don't really get what you are asking for. To me your question sounds like "why do you need parameterized dependencies?". If they do not have a reasonable use case, why did you include them in fastapi?

The issue here is simply that currently parameterized dependencies do not work as sub dependencies. This really limits their usability. For us parameterized dependencies are very useful, that's probably why you included them in fastapi in the first place.

That is indeed my question - parameterized subdependencies do not currently exist in fastapi, please help us understand why you need them?

So you are saying that there is a use case for parameterized dependencies but not for parameterized subdependencies? Why do you think the use case of parameterized dependencies only applies for the first level?

For us this use case has nothing todo with the level they are in. There are use cases for parameterized dependencies (e.g. for generalized dependencies). We simply want to be able to use them at any level. I don't get why that should be unreasonable to expect.

Please understand that fastapi has no support for parameterized dependencies at all - that feature exists purely by coincidence, because python allows you to define classes that fastapi considers functions, and which hence work with the Depends functionality.

All I ask is that you demonstrate your need.

I didn't know, thanks for the hint. Since there is a specific documentation page for parameterized dependencies (https://fastapi.tiangolo.com/advanced/advanced-dependencies/#parameterized-dependencies) I was expecting it to be a "feature" of fastapi. I'm aware of the technical detail.

One example: We have a dependency that verifies JWTs. Now we have different type of JWTs (customer and product) and they also connect to a respective database table which the content of the JWT is authenticated against. But the general logic is always the same. In comes a JWT and as input to a method (output of a dependency) we would like to have the database object (i.e. customer or product object). Now we need to only provide information like the header field to read and the database table to use to these generic verification and authentication methods. These methods should also independently be useable. That requires a nested parameterized dependency.

I mean this

import uvicorn
from fastapi import Depends, FastAPI, Request

app = FastAPI()


class SubDependency():

    def __init__(self) -> None:
        pass

    aync def __call__(self, request: Request) -> str:
        return request.method


class MainDependency():

    def __init__(self, sub) -> None:
        self.sub=sub

    async def __call__(self, request: Request) -> str:
        return self.sub(request)


@app.get("/test/")
def test_endpoint(test: str = Depends(MainDependency())):
    return test


if __name__ == "__main__":
    uvicorn.run("main:app", host="127.0.0.1", port=8000, reload=True, debug=True)
Was this page helpful?
0 / 5 - 0 ratings

Related issues

devtud picture devtud  路  3Comments

tsdmrfth picture tsdmrfth  路  3Comments

zero0nee picture zero0nee  路  3Comments

KoduIsGreat picture KoduIsGreat  路  3Comments

kkinder picture kkinder  路  3Comments