Skip to content

This issue was moved to a discussion.

You can continue the conversation there. Go to discussion →

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

How to use DI for services/DAOs/similar with app factory #1514

Closed
tuukkamustonen opened this issue Jun 4, 2020 · 12 comments
Closed

How to use DI for services/DAOs/similar with app factory #1514

tuukkamustonen opened this issue Jun 4, 2020 · 12 comments
Labels
question Question or problem question-migrate

Comments

@tuukkamustonen
Copy link

tuukkamustonen commented Jun 4, 2020

To inject services/DAOs/similar with FastAPI, I guess I should use Depends (because it allows to replace the dependencies via app.dependency_overrides when testing):

# app.py
def build_app(config_dir: str):
    ...
    config = yaml.loads(open(config_dir).read())
    service = MyService(config)
    ...
    app.include_router(routers.router)
    return app

# routers.py
router = APIRouter()

@router.get('/')
def endpoint(service: MyService = Depends()):
    ...

But that would generate new instance of service for each request, so I should use Depends(use_cache=False). However, FastAPI would still construct the first instance, and consider config: Dict parameter as body/path/param parameter(?)

So... how can inject the manually created service instance to my routes (and their dependants) now that I'm using an app factory? How can I inject service/DAO dependencies, that have nothing to do with parsing the incoming request?

I think if I didn't use app factory, I could theoretically import service from app... but that would lead to cyclic imports, and things would fail again.

There was related discussion in #504 but I'm not sure I understand what the outcome of that was :/


Background: I've used something like this with Flask:

# app.py
def build_app(config_dir: str):
    ...
    config = yaml.loads(open(config_dir).read())
    service = MyService(config)
    ...
    app.register_blueprint(build_blueprint(service))
    return app

# services.py
class MyService:
    def __init__(self, config: Dict):
        self.config = config
    ...

# blueprints.py
def build_blueprint(service: MyService):
    bp = Blueprint('admin_api', __name__)
    ...
    @bp.route('/')
    def endpoint():
         service.call_something()

    return bp

That gives routes access to the service instances created in build_app() (so I don't need to look them up via current_app or similar). But then, replacing those dependencies in tests would be pretty annoying I guess (but I just haven't had the need for that).

So, it's been sufficient... but I would like something better.

@tuukkamustonen tuukkamustonen added the question Question or problem label Jun 4, 2020
@dbanty
Copy link
Contributor

dbanty commented Jun 4, 2020

Seems there are a couple questions here:

  1. Should you use DI for something which doesn’t depend on other dependencies? Personally I’d say “no”. You can import normally and test by mocking instead and if that’s the main concern.

  2. Where to put the singleton service? How to get it? I’d put it in its own module and have a function which gets it for you. You can make sure it doesn’t load multiple times with lru_cache or a global var. How you get config_dir set initially is up to you, but usually that sort of thing comes from an environment variable. Now I must plug FlexConfig as a way to load config for web apps :D.

To me, one of the greatest benefits of FaatAPI is that most of your logic can be just Python code without having to bend over backwards for the framework. So I’d look toward just solving something with normal Python when possible when you don’t need autodocs, etc.

@tuukkamustonen
Copy link
Author

tuukkamustonen commented Jun 4, 2020

  1. Should you use DI for something which doesn’t depend on other dependencies? Personally I’d say “no”. You can import normally and test by mocking instead and if that’s the main concern.

If the DI mechanism is not recommended for injecting non "path operation" dependencies, should it be documented? (Maybe it is, I need to check again.) If the DI is not supposed to be used for services/DAOs/et.al. then I guess that kinda kills this discussion :)

However...

Maybe there could be an example or suggestion on how to handle these cases (in https://fastapi.tiangolo.com/tutorial/bigger-applications/)? Like one or two suggested approaches. Flask (and probably other web frameworks, too) have the same issue, and for sure there are various solutions (like even Flask-Injector which I've never used). But which one makes most sense? What ticks all the boxes? Huh.

In general, I'd rather inject replacements than monkey-patch them (via mock.patch()). Sure, with python's dynamics you can do a lot of stuff, but oftentimes I prefer a less magical solution, even if it's a bit more verbose. So, if I could use FastAPI's DI easily for "everything", then I just might.

  1. Where to put the singleton service? How to get it? I’d put it in its own module and have a function which gets it for you. You can make sure it doesn’t load multiple times with lru_cache or a global var. How you get config_dir set initially is up to you, but usually that sort of thing comes from an environment variable. Now I must plug FlexConfig as a way to load config for web apps :D.

Yeah, but that's just unnecessary boilerplate, like writing getter methods with @lru_cache to retrieve something. Besides, like you wrote, it doesn't solve the root problem which is getting the config object "injected" into where it is needed.

I want to avoid reading environment variables after initialization, as then the parameter requirement is not visible in function signatures/classes. I am fine overriding settings with env variables, but that should happen when I construct the config. Injecting the config object to then service classes etc. is the problem.

Didn't know about flex-config but now I do. Seems pretty similar to dynaconf maybe(?)

FaatAPI

Gotta love this typo! There's truth in it, too - FastAPI provides so much already, a chubby fellow for sure.

@dbanty
Copy link
Contributor

dbanty commented Jun 4, 2020

So I'm far from the person to decide when something is or is not recommended by the project, I'm just a guy who uses FastAPI every day and tries to answer questions on the internet :P. I'm also feeling rambly today so I'm doing my best to condense some thoughts here.

From what I understand of your question, what you're trying to inject is a global singleton, something you want to instantiate once at startup and access from anywhere. You are going to have to store this singleton somewhere yourself as I do not believe there's any mechanism in FastAPI to register a singleton as a dependency. How / where you store this doesn't seem to be the point of the question, so I'm just going to assume you end up with a function called get_my_service for the sake of discussion.

Aside from the extra verbosity of passing around the instance instead of re-calling the function to get your singleton from where you need it, not a lot changes in the API code. I will say that teaching developers when they should inject functions and when they should just call them is a very blurry line. There's no way to inject everything so inevitably you'll end up with people doing the wrong thing some of the time if DI is just for style and not for required functionality.

The biggest difference comes in testing. Going to assume pytest because that's the most common testing framework.

For each dependency, you're probably going to need to make yourself a yield fixture so it sets the override before the test and removes it after the fact, so a bit more boilerplate. The only other way to be sure you reset your overrides is to recycle your app between every test but that is going to get expensive fast if you're doing a lot of initial setup. Then for each test, you need to include all the fixtures for all the dependencies you're overriding.

If you are importing the function and calling it in your routes (instead of DI), you can use pytest-mock and include the mocker fixture. Then in your test you mocker.patch(my_service.get_my_service) or whatever you call it and pass in the values you need for this particular test. Mocker will auto teardown and cleanup when the test is complete. You then gain all the features of mock, allowing you to assert calling args and such.

@sm-Fifteen
Copy link
Contributor

@tuukkamustonen: The outcome from #504 was that the use of the word "Singleton" was ambiguous and that the conversation should be continued in #617 (since it's about the same thing but defined in less ambiguous terms), but that there is overall a place for lifetime-scoped dependencies. The next release of Starlette is supposed to improve how startup and shutdown events are managed in order to enable handling those in a context-manager style, so there's likely going to be progress on that front in the near future.

In the meantime, you can use the pattern from #726 (comment) to have a handle living in global scope that gets initialized by the startup event and finalized by the shutdown event, and only accessed through a getter function that you can then use as your dependency. If you have access to the app object in the file where that dependency is defined, you can replace global _db_conn with something like app.state._db_conn, to skip having anything in the global scope altogether.

@tuukkamustonen
Copy link
Author

Ok. I've been exploring this topic and running experiments, and well, long story short - the amount of possibilities is astonishing. How else, as like @dbanty said, FastAPI doesn't tie you to anything.

True, a singleton is just a single instance, and the reference to it can be stored... wherever. It could be a IoC container, it could be at module level, it could be custom registry class, or a dict, anything. As long as it's possible to replace/alter it in testing, it's good to go.

About testing, nice pointers by @dbanty, to avoid re-building the application for each test. In general, I experimented with a custom IoC registry and a couple of DI libraries out there, but in the end @mock.patch() and @mock.path.object() were the ones with less boilerplate, and which automatically revert the changes. My Flask-pattern (in the original post) doesn't allow to acquire reference to the injected objects from tests, but the classes (and their methods) can be monkey-patched nevertheless. Not perfect but works if needed. Anyway, I think it's a bad pattern so let's forget about that.

@sm-Fifteen In the comment you linked to, you use Depends() to inject a DB session per thread (acquiring static "lifetime" connection). However, that has nothing to do with path parameters (this seems to be terminology that FastAPI docs use), and it doesn't/shouldn't modify the OpenAPI schema that is generated. So, is it ok to use Depends() here? Yes?

About the signals - yeah, seems nice to have the shutdown signal, though I haven't personally needed it before. The thing with application factory approach is that I prefer passing config_dir: str from outside, rather than reading it from environment variable, which needs to happen if I declare app (and settings) on module level. A trivial thing, just a matter of taste. But here, it seems that things just would be simpler declaring things on module level, so I'll try that.

I agree that this is too broad topic, to be discussed through here. Thank you both for the pointers, I think I have some more experiments, consideration and studying to do!

@sm-Fifteen
Copy link
Contributor

@sm-Fifteen In the comment you linked to, you use Depends() to inject a DB session per thread (acquiring static "lifetime" connection).

Not quite, the DB "engine" (SQLAlchemy's abstraction over a connection/connection pool) becomes a lifetime-scoped dependency, meaning it will be created once at startup, closed at shutdown (which is very important for DB connections) and never instantiated beyond that.

If you look at that part:

async def get_db_conn() -> Database:
    assert _db_conn is not None
    return _db_conn

# This is the part that replaces sessionmaker
async def get_db_sess(db_conn = Depends(get_db_conn)) -> AsyncIterable[Session]:
    sess = Session(bind=db_conn)

    try:
        yield sess
    finally:
        sess.close()

get_db_conn then merely returns the "singleton instance", so to speak. get_db_sess in turns takes that lifetime-scoped DB connection pool as a dependency and creates a session with it, which is request-scoped and will be closed when the response is done being sent.

However, that has nothing to do with path parameters (this seems to be terminology that FastAPI docs use), and it doesn't/shouldn't modify the OpenAPI schema that is generated. So, is it ok to use Depends() here? Yes?

Depends does not alter the parameters for that route in the API definition (unlike Query, Path, Header, Cookie and Body; with Query simply being the default in most cases), unless the function given to Depends also takes such function parameters, which my example does not.

About the signals - yeah, seems nice to have the shutdown signal, though I haven't personally needed it before. The thing with application factory approach is that I prefer passing config_dir: str from outside, rather than reading it from environment variable, which needs to happen if I declare app (and settings) on module level. A trivial thing, just a matter of taste. But here, it seems that things just would be simpler declaring things on module level, so I'll try that.

You seem to be looking for Parametrized Dependencies instead of scoped dependencies.

@tuukkamustonen
Copy link
Author

tuukkamustonen commented Jun 7, 2020

Sorry for late reply!

Not quite, the DB "engine" (SQLAlchemy's abstraction over a connection/connection pool) becomes a lifetime-scoped dependency, meaning it will be created once at startup, closed at shutdown (which is very important for DB connections) and never instantiated beyond that.

Yeah, that's what I meant by DB session per thread (acquiring static "lifetime" connection).. Sure, I should have said per request, so bad phrasing maybe.

Depends does not alter the parameters for that route in the API definition (unlike Query, Path, Header, Cookie and Body; with Query simply being the default in most cases), unless the function given to Depends also takes such function parameters, which my example does not.

Ok, this is good clarification (though "most cases" sounds vague, but I'll read the docs).

You seem to be looking for Parametrized Dependencies instead of scoped dependencies.

I'm not sure how it would help here... like:

# config.py
class Settings(BaseSettings):
    def __init__(conf_dir: str):
        self.conf_dir = conf_dir
    @lru_cache
    def __call__(self):
        pass  # load up settings

settings = Settings('dummy')

# app.py
from config import settings
app = FastAPI()
# do something with settings...

# asgi.py
from config import settings
settings.conf_dir = 'the/actual/path'
from x import app

And then uvicorn asgi:app, sure I guess it would work, but how ugly is that. And same could be accomplished by a dict, similarly manipulated from outside, or env variable, or similar. But maybe I misunderstood...

@sm-Fifteen
Copy link
Contributor

Ok, this is good clarification (though "most cases" sounds vague, but I'll read the docs).

Yeah, "most cases" was intentionnally vague. The default for (I believe) all objects that Pydantic natively supports (and doesn't need to convert to JSON) will be assigned to query parameters unless there's a parameter on your path with that name. Dataclasses, BaseModel subclasses and lists will instead be passed as request body by default. Lists can trivially be changed to be exploded query parameters instead.

You really should read the docs, they're really well-written and have a really nice learning curve.

And then uvicorn asgi:app, sure I guess it would work, but how ugly is that. And same could be accomplished by a dict, similarly manipulated from outside, or env variable, or similar. But maybe I misunderstood...

Ok, let's back up a little, I get the feeling we might have both lost track of the original issue a little, here:

So... how can inject the manually created service instance to my routes (and their dependants) now that I'm using an app factory? How can I inject service/DAO dependencies, that have nothing to do with parsing the incoming request?

# app.py
def build_app(config_dir: str):
    ...
    config = yaml.loads(open(config_dir).read())
    service = MyService(config)
    ...
    app.include_router(routers.router)
    return app

The real problem is having the service you defined within your app builder function accessible from a route that you also mount within your app builder function. include_router does have a depenencies parameter, but as the doc says, "Note that, much like dependencies in path operation decorators, no value will be passed to your path operation function.", so those are for side-effects only.

For things to work with dependency injection, the ressources you inject have to be available from within the cope where your routes are declared, so you need a way to access your service object and/or dependency from the route.py global scope.

# ressources.py
class ServiceWrapper:
    def __init__(self, conf_dir: str):
        config = yaml.loads(open(config_dir).read())
        this.service = MyService(config)

    def __call__(self):
        # Takes no parameters so nothing will be added to your query params
        return this.service

ServiceA = ServiceWrapper('dummy') 
ServiceB = ServiceWrapper('the/actual/path')
```

```py
# routers.py
from ressources import ServiceB 

router = APIRouter()

@router.get('/')
def endpoint(service: MyService = Depends(ServiceB)):
    ...
```

@tuukkamustonen
Copy link
Author

You really should read the docs, they're really well-written and have a really nice learning curve.

I have. But there are lots of them, and you always miss something on the first read. Plus, no docs or reader are perfect. I personally prefer reference-styled, less verbose docs more, but I do agree that FastAPI docs are really really nice. Certainly one major reason why I started exploring it.

I do apprectiate your help here, but I think I am failing to explain the problem. What you pasted is a good example, indeed one way to do things, but unfortunately I don't see it tackle the problem (being able to pass the config dir from "outside"). In any case, the problem is trivial, and not worth iterating on here.

I've gained many insights from this discussion, and trying to (~unit) test the different approaches (with TestClient) gave even more thought on how to not do things. Some day I will get it right :) Thank you both for the guidance!

@selimb
Copy link

selimb commented Jun 20, 2020

@tuukkamustonen This may be what you're looking for:

https://gist.github.com/selimb/c8ade7215a8344f4011968126c8047c6

As you can see from test_fastapi_singleton.py, this is lovely to test: just create the application in the test with whatever config you want, and pass that to TestClient. If you don't want the extra indentation, you could also define a pytest fixture as in test_example_project.py

The only downside is that dependencies decorated with @deps.singleton can't require other dependencies, and instead go through app.state. You could theoretically extend Dependencies to also do dependency injection, and I hope/assume when #617 is resolved that'll be possible, but I didn't want to reinvent that, as I don't have that many dependencies in my project.

PS. Sorry for necroing, once again, but I thought this might give some inspiration to #617 ? I can post my gist there too, if anyone would find it useful.

@adriangb
Copy link
Contributor

adriangb commented Mar 3, 2022

@tuukkamustonen would this work for you?

def build_app(config_dir: str):
    ...
    config = yaml.loads(open(config_dir).read())
    service = MyService(config)
    ...
    app.include_router(routers.router)
    app.dependency_overrides[MyService] = lambda: service
    return app

# routers.py
router = APIRouter()

@router.get('/')
def endpoint(service: MyService = Depends()):
    ...

The trick is to use dependency_overrides like if it were some sort of dependency injection container with which you are registering a "singleton" (by virtue of the lambda always returning the same instance) for the type MyService.

@haydenzhourepo
Copy link

@tiangolo tiangolo changed the title [QUESTION] How to use DI for services/DAOs/similar with app factory How to use DI for services/DAOs/similar with app factory Feb 24, 2023
@tiangolo tiangolo reopened this Feb 28, 2023
Repository owner locked and limited conversation to collaborators Feb 28, 2023
@tiangolo tiangolo converted this issue into discussion #7460 Feb 28, 2023

This issue was moved to a discussion.

You can continue the conversation there. Go to discussion →

Labels
question Question or problem question-migrate
Projects
None yet
Development

No branches or pull requests

7 participants