Add dependencies with yield (used as context managers) (#595)

*  Add development/testing dependencies for Python 3.6

*  Add concurrency submodule with contextmanager_in_threadpool

*  Add AsyncExitStack to ASGI scope in FastAPI app call

*  Use async stack for contextmanager-able dependencies

including running in threadpool sync dependencies

*  Add tests for contextmanager dependencies

including internal raise checks when exceptions should be handled and when not

*  Add test for fake asynccontextmanager raiser

* 🐛 Fix mypy errors and coverage

* 🔇 Remove development logs and prints

*  Add tests for sub-contextmanagers, background tasks, and sync functions

* 🐛 Fix mypy errors for Python 3.7

* 💬 Fix error texts for clarity

* 📝 Add docs for dependencies with yield

*  Update SQL with SQLAlchemy tutorial to use dependencies with yield

and add an alternative with a middleware (from the old tutorial)

*  Update SQL tests to remove DB file during the same tests

*  Add tests for example with middleware

as a copy from the tests with dependencies with yield, removing the DB in the tests

* ✏️ Fix typos with suggestions from code review

Co-Authored-By: dmontagu <35119617+dmontagu@users.noreply.github.com>
This commit is contained in:
Sebastián Ramírez
2019-10-09 13:01:58 -05:00
committed by GitHub
parent 380e3731a8
commit 3f9f4a0f8f
19 changed files with 1239 additions and 89 deletions

View File

@@ -1,4 +1,4 @@
!!! danger
!!! warning
This is, more or less, an "advanced" chapter.
If you are just starting with **FastAPI** you might want to skip this chapter and come back to it later.
@@ -22,7 +22,7 @@ Not the class itself (which is already a callable), but an instance of that clas
To do that, we declare a method `__call__`:
```Python hl_lines="10"
{!./src/dependencies/tutorial007.py!}
{!./src/dependencies/tutorial011.py!}
```
In this case, this `__call__` is what **FastAPI** will use to check for additional parameters and sub-dependencies, and this is what will be called to pass a value to the parameter in your *path operation function* later.
@@ -32,7 +32,7 @@ In this case, this `__call__` is what **FastAPI** will use to check for addition
And now, we can use `__init__` to declare the parameters of the instance that we can use to "parameterize" the dependency:
```Python hl_lines="7"
{!./src/dependencies/tutorial007.py!}
{!./src/dependencies/tutorial011.py!}
```
In this case, **FastAPI** won't ever touch or care about `__init__`, we will use it directly in our code.
@@ -42,7 +42,7 @@ In this case, **FastAPI** won't ever touch or care about `__init__`, we will use
We could create an instance of this class with:
```Python hl_lines="16"
{!./src/dependencies/tutorial007.py!}
{!./src/dependencies/tutorial011.py!}
```
And that way we are able to "parameterize" our dependency, that now has `"bar"` inside of it, as the attribute `checker.fixed_content`.
@@ -60,7 +60,7 @@ checker(q="somequery")
...and pass whatever that returns as the value of the dependency in our path operation function as the parameter `fixed_content_included`:
```Python hl_lines="20"
{!./src/dependencies/tutorial007.py!}
{!./src/dependencies/tutorial011.py!}
```
!!! tip

View File

@@ -0,0 +1,153 @@
# Dependencies with `yield`
FastAPI supports dependencies that do some <abbr title='sometimes also called "exit", "cleanup", "teardown", "close", "context managers", ...'>extra steps after finishing</abbr>.
To do this, use `yield` instead of `return`, and write the extra steps after.
!!! tip
Make sure to use `yield` one single time.
!!! info
For this to work, you need to use **Python 3.7** or above, or in **Python 3.6**, install the "backports":
```bash
pip install async-exit-stack async-generator
```
This installs <a href="https://github.com/sorcio/async_exit_stack" target="_blank">async-exit-stack</a> and <a href="https://github.com/python-trio/async_generator" target="_blank">async-generator</a>.
!!! note "Technical Details"
Any function that is valid to use with:
* <a href="https://docs.python.org/3/library/contextlib.html#contextlib.contextmanager" target="_blank">`@contextlib.contextmanager`</a> or
* <a href="https://docs.python.org/3/library/contextlib.html#contextlib.asynccontextmanager" target="_blank">`@contextlib.asynccontextmanager`</a>
would be valid to use as a **FastAPI** dependency.
In fact, FastAPI uses those two decorators internally.
## A database dependency with `yield`
For example, you could use this to create a database session and close it after finishing.
Only the code prior to and including the `yield` statement is executed before sending a response:
```Python hl_lines="2 3 4"
{!./src/dependencies/tutorial007.py!}
```
The yielded value is what is injected into *path operations* and other dependencies:
```Python hl_lines="4"
{!./src/dependencies/tutorial007.py!}
```
The code following the `yield` statement is executed after the response has been delivered:
```Python hl_lines="5 6"
{!./src/dependencies/tutorial007.py!}
```
!!! tip
You can use `async` or normal functions.
**FastAPI** will do the right thing with each, the same as with normal dependencies.
## A dependency with `yield` and `try`
If you use a `try` block in a dependency with `yield`, you'll receive any exception that was thrown when using the dependency.
For example, if some code at some point in the middle, in another dependency or in a *path operation*, made a database transaction "rollback" or create any other error, you will receive the exception in your dependency.
So, you can look for that specific exception inside the dependency with `except SomeException`.
In the same way, you can use `finally` to make sure the exit steps are executed, no matter if there was an exception or not.
```Python hl_lines="3 5"
{!./src/dependencies/tutorial007.py!}
```
## Sub-dependencies with `yield`
You can have sub-dependencies and "trees" of sub-dependencies of any size and shape, and any or all of them can use `yield`.
**FastAPI** will make sure that the "exit code" in each dependency with `yield` is run in the correct order.
For example, `dependency_c` can have a dependency on `dependency_b`, and `dependency_b` on `dependency_a`:
```Python hl_lines="4 12 20"
{!./src/dependencies/tutorial008.py!}
```
And all of them can use `yield`.
In this case `dependency_c`, to execute its exit code, needs the value from `dependency_b` (here named `dep_b`) to still be available.
And, in turn, `dependency_b` needs the value from `dependency_a` (here named `dep_a`) to be available for its exit code.
```Python hl_lines="16 17 24 25"
{!./src/dependencies/tutorial008.py!}
```
The same way, you could have dependencies with `yield` and `return` mixed.
And you could have a single dependency that requires several other dependencies with `yield`, etc.
You can have any combinations of dependencies that you want.
**FastAPI** will make sure everything is run in the correct order.
!!! note "Technical Details"
This works thanks to Python's <a href="https://docs.python.org/3/library/contextlib.html" target="_blank">Context Managers</a>.
**FastAPI** uses them internally to achieve this.
## Context Managers
### What are "Context Managers"
"Context Managers" are any of those Python objects that you can use in a `with` statement.
For example, <a href="https://docs.python.org/3/tutorial/inputoutput.html#reading-and-writing-files" target="_blank">you can use `with` to read a file</a>:
```Python
with open("./somefile.txt") as f:
contents = f.read()
print(contents)
```
Underneath, the `open("./somefile.txt")` returns an object that is a called a "Context Manager".
When the `with` block finishes, it makes sure to close the file, even if there were exceptions.
When you create a dependency with `yield`, **FastAPI** will internally convert it to a context manager, and combine it with some other related tools.
### Using context managers in dependencies with `yield`
!!! warning
This is, more or less, an "advanced" idea.
If you are just starting with **FastAPI** you might want to skip it for now.
In Python, you can create context managers by <a href="https://docs.python.org/3/reference/datamodel.html#context-managers" target="_blank">creating a class with two methods: `__enter__()` and `__exit__()`</a>.
You can also use them with **FastAPI** dependencies with `yield` by using
`with` or `async with` statements inside of the dependency function:
```Python hl_lines="1 2 3 4 5 6 7 8 9 13"
{!./src/dependencies/tutorial010.py!}
```
!!! tip
Another way to create a context manager is with:
* <a href="https://docs.python.org/3/library/contextlib.html#contextlib.contextmanager" target="_blank">`@contextlib.contextmanager`</a> or
* <a href="https://docs.python.org/3/library/contextlib.html#contextlib.asynccontextmanager" target="_blank">`@contextlib.asynccontextmanager`</a>
using them to decorate a function with a single `yield`.
That's what **FastAPI** uses internally for dependencies with `yield`.
But you don't have to use the decorators for FastAPI dependencies (and you shouldn't).
FastAPI will do it for you internally.

View File

@@ -427,21 +427,30 @@ And you would also use Alembic for "migrations" (that's its main job).
A "migration" is the set of steps needed whenever you change the structure of your SQLAlchemy models, add a new attribute, etc. to replicate those changes in the database, add a new column, a new table, etc.
### Create a middleware to handle sessions
### Create a dependency
Now use the `SessionLocal` class we created in the `sql_app/databases.py` file.
!!! info
For this to work, you need to use **Python 3.7** or above, or in **Python 3.6**, install the "backports":
```bash
pip install async-exit-stack async-generator
```
This installs <a href="https://github.com/sorcio/async_exit_stack" target="_blank">async-exit-stack</a> and <a href="https://github.com/python-trio/async_generator" target="_blank">async-generator</a>.
You can also use the alternative method with a "middleware" explained at the end.
Now use the `SessionLocal` class we created in the `sql_app/databases.py` file to create a dependency.
We need to have an independent database session/connection (`SessionLocal`) per request, use the same session through all the request and then close it after the request is finished.
And then a new session will be created for the next request.
For that, we will create a new middleware.
For that, we will create a new dependency with `yield`, as explained before in the section about <a href="https://fastapi.tiangolo.com/tutorial/dependencies/dependencies-with-yield/" target="_blank">Dependencies with `yield`</a>.
A "middleware" is a function that is always executed for each request, and have code before and after the request.
Our dependency will create a new SQLAlchemy `SessionLocal` that will be used in a single request, and then close it once the request is finished.
This middleware (just a function) will create a new SQLAlchemy `SessionLocal` for each request, add it to the request and then close it once the request is finished.
```Python hl_lines="16 17 18 19 20 21 22 23 24"
```Python hl_lines="15 16 17 18 19 20"
{!./src/sql_databases/sql_app/main.py!}
```
@@ -452,21 +461,11 @@ This middleware (just a function) will create a new SQLAlchemy `SessionLocal` fo
This way we make sure the database session is always closed after the request. Even if there was an exception while processing the request.
#### About `request.state`
And then, when using the dependency in a *path operation function*, we declare it with the type `Session` we imported directly from SQLAlchemy.
<a href="https://www.starlette.io/requests/#other-state" target="_blank">`request.state` is a property of each Starlette `Request` object</a>, it is there to store arbitrary objects attached to the request itself, like the database session in this case.
This will then give us better editor support inside the *path operation function*, because the editor will know that the `db` parameter is of type `Session`:
For us in this case, it helps us ensuring a single database session is used through all the request, and then closed afterwards (in the middleware).
### Create a dependency
To simplify the code, reduce repetition and get better editor support, we will create a dependency that returns this same database session from the request.
And when using the dependency in a path operation function, we declare it with the type `Session` we imported directly from SQLAlchemy.
This will then give us better editor support inside the path operation function, because the editor will know that the `db` parameter is of type `Session`.
```Python hl_lines="28 29"
```Python hl_lines="24 32 38 47 53"
{!./src/sql_databases/sql_app/main.py!}
```
@@ -479,22 +478,16 @@ This will then give us better editor support inside the path operation function,
Now, finally, here's the standard **FastAPI** *path operations* code.
```Python hl_lines="32 33 34 35 36 37 40 41 42 43 46 47 48 49 50 51 54 55 56 57 58 61 62 63 64 65"
```Python hl_lines="23 24 25 26 27 28 31 32 33 34 37 38 39 40 41 42 45 46 47 48 49 52 53 54 55"
{!./src/sql_databases/sql_app/main.py!}
```
We are creating the database session before each request, attaching it to the request, and then closing it afterwards.
We are creating the database session before each request in the dependency with `yield`, and then closing it afterwards.
All of this is done in the middleware explained above.
Then, in the dependency `get_db()` we are extracting the database session from the request.
And then we can create the dependency in the path operation function, to get that session directly.
And then we can create the required dependency in the path operation function, to get that session directly.
With that, we can just call `crud.get_user` directly from inside of the path operation function and use that session.
Having this 3-step process (middleware, dependency, path operation) you get better support/checks/completion in all the path operation functions while reducing code repetition.
!!! tip
Notice that the values you return are SQLAlchemy models, or lists of SQLAlchemy models.
@@ -507,7 +500,7 @@ Having this 3-step process (middleware, dependency, path operation) you get bett
### About `def` vs `async def`
Here we are using SQLAlchemy code inside of the path operation function, and, in turn, it will go and communicate with an external database.
Here we are using SQLAlchemy code inside of the path operation function and in the dependency, and, in turn, it will go and communicate with an external database.
That could potentially require some "waiting".
@@ -523,7 +516,7 @@ user = await db.query(User).first()
user = db.query(User).first()
```
Then we should declare the path operation without `async def`, just with a normal `def`, as:
Then we should declare the *path operation functions* and the dependency without `async def`, just with a normal `def`, as:
```Python hl_lines="2"
@app.get("/users/{user_id}", response_model=schemas.User)
@@ -548,8 +541,8 @@ For example, in a background task worker with <a href="http://www.celeryproject.
## Review all the files
Remember you should have a directory named `my_super_project` that contains a sub-directory called `sql_app`.
`sql_app` should have the following files:
`sql_app` should have the following files:
* `sql_app/__init__.py`: is an empty file.
@@ -591,9 +584,6 @@ You can copy this code and use it as is.
In fact, the code shown here is part of the tests. As most of the code in these docs.
You can copy it as is.
Then you can run it with Uvicorn:
```bash
@@ -615,3 +605,51 @@ It will look like this:
<img src="/img/tutorial/sql-databases/image02.png">
You can also use an online SQLite browser like <a href="https://inloop.github.io/sqlite-viewer/" target="_blank">SQLite Viewer</a> or <a href="https://extendsclass.com/sqlite-browser.html" target="_blank">ExtendsClass</a>.
## Alternative DB session with middleware
If you can't use dependencies with `yield` -- for example, if you are not using **Python 3.7** and can't install the "backports" mentioned above for **Python 3.6** -- you can set up the session in a "middleware" in a similar way.
A "middleware" is basically a function that is always executed for each request, with some code executed before, and some code executed after the endpoint function.
### Create a middleware
The middleware we'll add (just a function) will create a new SQLAlchemy `SessionLocal` for each request, add it to the request and then close it once the request is finished.
```Python hl_lines="16 17 18 19 20 21 22 23 24"
{!./src/sql_databases/sql_app/alt_main.py!}
```
!!! info
We put the creation of the `SessionLocal()` and handling of the requests in a `try` block.
And then we close it in the `finally` block.
This way we make sure the database session is always closed after the request. Even if there was an exception while processing the request.
### About `request.state`
<a href="https://www.starlette.io/requests/#other-state" target="_blank">`request.state` is a property of each Starlette `Request` object</a>. It is there to store arbitrary objects attached to the request itself, like the database session in this case.
For us in this case, it helps us ensure a single database session is used through all the request, and then closed afterwards (in the middleware).
### Dependencies with `yield` or middleware
Adding a **middleware** here is similar to what a dependency with `yield` does, with some differences:
* It requires more code and is a bit more complex.
* The middleware has to be an `async` function.
* If there is code in it that has to "wait" for the network, it could "block" your application there and degrade performance a bit.
* Although it's probably not very problematic here with the way `SQLAlchemy` works.
* But if you added more code to the middleware that had a lot of <abbr title="input and output">I/O</abbr> waiting, it could then be problematic.
* A middleware is run for *every* request.
* So, a connection will be created for every request.
* Even when the *path operation* that handles that request didn't need the DB.
!!! tip
It's probably better to use dependencies with `yield` when they are enough for the use case.
!!! info
Dependencies with `yield` were added recently to **FastAPI**.
A previous version of this tutorial only had the examples with a middleware and there are probably several applications using the middleware for database session management.