-
-
Notifications
You must be signed in to change notification settings - Fork 6.1k
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
Comments
Seems there are a couple questions here:
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. |
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
Yeah, but that's just unnecessary boilerplate, like writing getter methods with 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(?)
Gotta love this typo! There's truth in it, too - FastAPI provides so much already, a chubby fellow for sure. |
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 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 |
@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 |
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 @sm-Fifteen In the comment you linked to, you use 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 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! |
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()
You seem to be looking for Parametrized Dependencies instead of scoped dependencies. |
Sorry for late reply!
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.
Ok, this is good clarification (though "most cases" sounds vague, but I'll read the docs).
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 |
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.
Ok, let's back up a little, I get the feeling we might have both lost track of the original issue a little, here:
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. 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)):
...
``` |
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 |
@tuukkamustonen This may be what you're looking for: https://gist.github.com/selimb/c8ade7215a8344f4011968126c8047c6 As you can see from The only downside is that dependencies decorated with 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. |
@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 |
i think you need this https://python-dependency-injector.ets-labs.org/examples/fastapi.html |
This issue was moved to a discussion.
You can continue the conversation there. Go to discussion →
To inject services/DAOs/similar with FastAPI, I guess I should use
Depends
(because it allows to replace the dependencies viaapp.dependency_overrides
when testing):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 considerconfig: 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:
That gives routes access to the service instances created in
build_app()
(so I don't need to look them up viacurrent_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.
The text was updated successfully, but these errors were encountered: