Skip to content

Commit 3f9f4a0

Browse files
tiangolodmontagu
andauthored
✨ Add dependencies with yield (used as context managers) (fastapi#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>
1 parent 380e373 commit 3f9f4a0

19 files changed

+1238
-88
lines changed

docs/src/dependencies/tutorial007.py

Lines changed: 6 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,6 @@
1-
from fastapi import Depends, FastAPI
2-
3-
app = FastAPI()
4-
5-
6-
class FixedContentQueryChecker:
7-
def __init__(self, fixed_content: str):
8-
self.fixed_content = fixed_content
9-
10-
def __call__(self, q: str = ""):
11-
if q:
12-
return self.fixed_content in q
13-
return False
14-
15-
16-
checker = FixedContentQueryChecker("bar")
17-
18-
19-
@app.get("/query-checker/")
20-
async def read_query_check(fixed_content_included: bool = Depends(checker)):
21-
return {"fixed_content_in_query": fixed_content_included}
1+
async def get_db():
2+
db = DBSession()
3+
try:
4+
yield db
5+
finally:
6+
db.close()

docs/src/dependencies/tutorial008.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
from fastapi import Depends
2+
3+
4+
async def dependency_a():
5+
dep_a = generate_dep_a()
6+
try:
7+
yield dep_a
8+
finally:
9+
dep_a.close()
10+
11+
12+
async def dependency_b(dep_a=Depends(dependency_a)):
13+
dep_b = generate_dep_b()
14+
try:
15+
yield dep_b
16+
finally:
17+
dep_b.close(dep_a)
18+
19+
20+
async def dependency_c(dep_b=Depends(dependency_b)):
21+
dep_c = generate_dep_c()
22+
try:
23+
yield dep_c
24+
finally:
25+
dep_c.close(dep_b)

docs/src/dependencies/tutorial009.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
from fastapi import Depends
2+
3+
4+
async def dependency_a():
5+
dep_a = generate_dep_a()
6+
try:
7+
yield dep_a
8+
finally:
9+
dep_a.close()
10+
11+
12+
async def dependency_b(dep_a=Depends(dependency_a)):
13+
dep_b = generate_dep_b()
14+
try:
15+
yield dep_b
16+
finally:
17+
dep_b.close(dep_a)
18+
19+
20+
async def dependency_c(dep_b=Depends(dependency_b)):
21+
dep_c = generate_dep_c()
22+
try:
23+
yield dep_c
24+
finally:
25+
dep_c.close(dep_b)

docs/src/dependencies/tutorial010.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
class MySuperContextManager:
2+
def __init__(self):
3+
self.db = DBSession()
4+
5+
def __enter__(self):
6+
return self.db
7+
8+
def __exit__(self, exc_type, exc_value, traceback):
9+
self.db.close()
10+
11+
12+
async def get_db():
13+
with MySuperContextManager() as db:
14+
yield db

docs/src/dependencies/tutorial011.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
from fastapi import Depends, FastAPI
2+
3+
app = FastAPI()
4+
5+
6+
class FixedContentQueryChecker:
7+
def __init__(self, fixed_content: str):
8+
self.fixed_content = fixed_content
9+
10+
def __call__(self, q: str = ""):
11+
if q:
12+
return self.fixed_content in q
13+
return False
14+
15+
16+
checker = FixedContentQueryChecker("bar")
17+
18+
19+
@app.get("/query-checker/")
20+
async def read_query_check(fixed_content_included: bool = Depends(checker)):
21+
return {"fixed_content_in_query": fixed_content_included}
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
from typing import List
2+
3+
from fastapi import Depends, FastAPI, HTTPException
4+
from sqlalchemy.orm import Session
5+
from starlette.requests import Request
6+
from starlette.responses import Response
7+
8+
from . import crud, models, schemas
9+
from .database import SessionLocal, engine
10+
11+
models.Base.metadata.create_all(bind=engine)
12+
13+
app = FastAPI()
14+
15+
16+
@app.middleware("http")
17+
async def db_session_middleware(request: Request, call_next):
18+
response = Response("Internal server error", status_code=500)
19+
try:
20+
request.state.db = SessionLocal()
21+
response = await call_next(request)
22+
finally:
23+
request.state.db.close()
24+
return response
25+
26+
27+
# Dependency
28+
def get_db(request: Request):
29+
return request.state.db
30+
31+
32+
@app.post("/users/", response_model=schemas.User)
33+
def create_user(user: schemas.UserCreate, db: Session = Depends(get_db)):
34+
db_user = crud.get_user_by_email(db, email=user.email)
35+
if db_user:
36+
raise HTTPException(status_code=400, detail="Email already registered")
37+
return crud.create_user(db=db, user=user)
38+
39+
40+
@app.get("/users/", response_model=List[schemas.User])
41+
def read_users(skip: int = 0, limit: int = 100, db: Session = Depends(get_db)):
42+
users = crud.get_users(db, skip=skip, limit=limit)
43+
return users
44+
45+
46+
@app.get("/users/{user_id}", response_model=schemas.User)
47+
def read_user(user_id: int, db: Session = Depends(get_db)):
48+
db_user = crud.get_user(db, user_id=user_id)
49+
if db_user is None:
50+
raise HTTPException(status_code=404, detail="User not found")
51+
return db_user
52+
53+
54+
@app.post("/users/{user_id}/items/", response_model=schemas.Item)
55+
def create_item_for_user(
56+
user_id: int, item: schemas.ItemCreate, db: Session = Depends(get_db)
57+
):
58+
return crud.create_user_item(db=db, item=item, user_id=user_id)
59+
60+
61+
@app.get("/items/", response_model=List[schemas.Item])
62+
def read_items(skip: int = 0, limit: int = 100, db: Session = Depends(get_db)):
63+
items = crud.get_items(db, skip=skip, limit=limit)
64+
return items

docs/src/sql_databases/sql_app/main.py

Lines changed: 5 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,6 @@
22

33
from fastapi import Depends, FastAPI, HTTPException
44
from sqlalchemy.orm import Session
5-
from starlette.requests import Request
6-
from starlette.responses import Response
75

86
from . import crud, models, schemas
97
from .database import SessionLocal, engine
@@ -13,20 +11,13 @@
1311
app = FastAPI()
1412

1513

16-
@app.middleware("http")
17-
async def db_session_middleware(request: Request, call_next):
18-
response = Response("Internal server error", status_code=500)
14+
# Dependency
15+
def get_db():
1916
try:
20-
request.state.db = SessionLocal()
21-
response = await call_next(request)
17+
db = SessionLocal()
18+
yield db
2219
finally:
23-
request.state.db.close()
24-
return response
25-
26-
27-
# Dependency
28-
def get_db(request: Request):
29-
return request.state.db
20+
db.close()
3021

3122

3223
@app.post("/users/", response_model=schemas.User)

docs/tutorial/dependencies/advanced-dependencies.md

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
!!! danger
1+
!!! warning
22
This is, more or less, an "advanced" chapter.
33

44
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
2222
To do that, we declare a method `__call__`:
2323

2424
```Python hl_lines="10"
25-
{!./src/dependencies/tutorial007.py!}
25+
{!./src/dependencies/tutorial011.py!}
2626
```
2727

2828
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
3232
And now, we can use `__init__` to declare the parameters of the instance that we can use to "parameterize" the dependency:
3333

3434
```Python hl_lines="7"
35-
{!./src/dependencies/tutorial007.py!}
35+
{!./src/dependencies/tutorial011.py!}
3636
```
3737

3838
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
4242
We could create an instance of this class with:
4343

4444
```Python hl_lines="16"
45-
{!./src/dependencies/tutorial007.py!}
45+
{!./src/dependencies/tutorial011.py!}
4646
```
4747

4848
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")
6060
...and pass whatever that returns as the value of the dependency in our path operation function as the parameter `fixed_content_included`:
6161

6262
```Python hl_lines="20"
63-
{!./src/dependencies/tutorial007.py!}
63+
{!./src/dependencies/tutorial011.py!}
6464
```
6565

6666
!!! tip

0 commit comments

Comments
 (0)