From cbe99f4c5797b487961b8dacaa2c1064d751e74a Mon Sep 17 00:00:00 2001 From: Alexander Pushkov Date: Tue, 5 Dec 2023 12:06:23 +0300 Subject: [PATCH 01/10] Add support for using asynccontextmanager as a dependency --- fastapi/dependencies/utils.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/fastapi/dependencies/utils.py b/fastapi/dependencies/utils.py index 4e88410a5..14ec7f4d1 100644 --- a/fastapi/dependencies/utils.py +++ b/fastapi/dependencies/utils.py @@ -598,7 +598,13 @@ async def solve_dependencies( elif is_coroutine_callable(call): solved = await call(**sub_values) else: - solved = await run_in_threadpool(call, **sub_values) + stack = request.scope.get("fastapi_astack") + assert isinstance(stack, AsyncExitStack) + called = run_in_threadpool(call, **sub_values) + if hasattr(called, "__aenter__"): + solved = await stack.enter_async_context(called) + else: + solved = await called if sub_dependant.name is not None: values[sub_dependant.name] = solved if sub_dependant.cache_key not in dependency_cache: From 04f7fdec9e74c8486998affe7899e56299466a4b Mon Sep 17 00:00:00 2001 From: Alexander Pushkov Date: Tue, 5 Dec 2023 12:06:28 +0300 Subject: [PATCH 02/10] Fix run_in_threadpool call, add support for sync context managers as well --- fastapi/dependencies/utils.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/fastapi/dependencies/utils.py b/fastapi/dependencies/utils.py index 14ec7f4d1..4021f212c 100644 --- a/fastapi/dependencies/utils.py +++ b/fastapi/dependencies/utils.py @@ -600,11 +600,14 @@ async def solve_dependencies( else: stack = request.scope.get("fastapi_astack") assert isinstance(stack, AsyncExitStack) - called = run_in_threadpool(call, **sub_values) + called = await run_in_threadpool(call, **sub_values) if hasattr(called, "__aenter__"): solved = await stack.enter_async_context(called) + elif hasattr(called, "__enter__"): + cm = contextmanager_in_threadpool(called) + solved = await stack.enter_async_context(cm) else: - solved = await called + solved = called if sub_dependant.name is not None: values[sub_dependant.name] = solved if sub_dependant.cache_key not in dependency_cache: From 4c0b37854f4a7a893af9e2442ab1b0c7d87bda69 Mon Sep 17 00:00:00 2001 From: Alexander Pushkov Date: Tue, 5 Dec 2023 12:42:43 +0300 Subject: [PATCH 03/10] =?UTF-8?q?=E2=9C=8F=EF=B8=8F=20Add=20documentation?= =?UTF-8?q?=20on=20using=20context=20managers=20as=20dependencies?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../dependencies/dependencies-with-yield.md | 40 +++++++++++-------- docs_src/dependencies/tutorial010.py | 6 +-- docs_src/dependencies/tutorial010_ctx.py | 9 +++++ .../dependencies/tutorial010_ctx_usage.py | 3 ++ 4 files changed, 39 insertions(+), 19 deletions(-) create mode 100644 docs_src/dependencies/tutorial010_ctx.py create mode 100644 docs_src/dependencies/tutorial010_ctx_usage.py diff --git a/docs/en/docs/tutorial/dependencies/dependencies-with-yield.md b/docs/en/docs/tutorial/dependencies/dependencies-with-yield.md index fe18f1f1d..069732fb3 100644 --- a/docs/en/docs/tutorial/dependencies/dependencies-with-yield.md +++ b/docs/en/docs/tutorial/dependencies/dependencies-with-yield.md @@ -17,6 +17,8 @@ To do this, use `yield` instead of `return`, and write the extra steps after. In fact, FastAPI uses those two decorators internally. + And if you have a context manager already, you can use it as a dependency, too. + ## A database dependency with `yield` For example, you could use this to create a database session and close it after finishing. @@ -222,32 +224,38 @@ When the `with` block finishes, it makes sure to close the file, even if there w 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` +## Context Managers as dependencies + +You don’t have to create a Context Manager to use FastAPI dependencies. + +But sometimes you might want to use a dependency both inside and outside FastAPI. For example, you might want to connect to a database in a DB migration script. You can create a Context Manager manually then: + +```Python +{!../../../docs_src/dependencies/tutorial010_ctx.py!} +``` + +This way you can use it with `Depends()` in your app, and as a regular Context Manager everywhere else: + +```Python +{!../../../docs_src/dependencies/tutorial010_ctx_usage.py!} +``` + + +### Defining Context Managers as classes !!! 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 creating a class with two methods: `__enter__()` and `__exit__()`. +You can also create Context Managers by creating a class with two methods: `__enter__()` and `__exit__()`. -You can also use them inside of **FastAPI** dependencies with `yield` by using -`with` or `async with` statements inside of the dependency function: +Like the Context Managers created with `contextlib`, you can use them inside of **FastAPI** dependencies directly: ```Python hl_lines="1-9 13" {!../../../docs_src/dependencies/tutorial010.py!} ``` -!!! tip - Another way to create a context manager is with: - - * `@contextlib.contextmanager` or - * `@contextlib.asynccontextmanager` - - 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. +!!! note "Technical Details" + Internally, FastAPI calls the dependency function and then checks if the result has either an `__enter__()` or an `__aenter__()` method. If that’s the case, it will treat it as a context manager. diff --git a/docs_src/dependencies/tutorial010.py b/docs_src/dependencies/tutorial010.py index c27f1b170..fcf80e698 100644 --- a/docs_src/dependencies/tutorial010.py +++ b/docs_src/dependencies/tutorial010.py @@ -9,6 +9,6 @@ class MySuperContextManager: self.db.close() -async def get_db(): - with MySuperContextManager() as db: - yield db +@app.get("/") +async def get_root(db: Annotated[DBSession, Depends(MySuperContextManager)]): + ... diff --git a/docs_src/dependencies/tutorial010_ctx.py b/docs_src/dependencies/tutorial010_ctx.py new file mode 100644 index 000000000..71f6aa284 --- /dev/null +++ b/docs_src/dependencies/tutorial010_ctx.py @@ -0,0 +1,9 @@ +from contextlib import asynccontextmanager + +@asynccontextmanager +async def get_db(): + db = DBSession() + try: + yield db + finally: + db.close() diff --git a/docs_src/dependencies/tutorial010_ctx_usage.py b/docs_src/dependencies/tutorial010_ctx_usage.py new file mode 100644 index 000000000..11a3ccf2e --- /dev/null +++ b/docs_src/dependencies/tutorial010_ctx_usage.py @@ -0,0 +1,3 @@ +async def migrate(): + with get_db() as db: + ... From affb3ca01707cf32181211c50b91a4120c8d14dc Mon Sep 17 00:00:00 2001 From: Alexander Pushkov Date: Tue, 5 Dec 2023 12:51:59 +0300 Subject: [PATCH 04/10] Add basic tests for using context managers as dependencies --- tests/test_dependency_contextmanager.py | 43 +++++++++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/tests/test_dependency_contextmanager.py b/tests/test_dependency_contextmanager.py index 03ef56c4d..ce0f56474 100644 --- a/tests/test_dependency_contextmanager.py +++ b/tests/test_dependency_contextmanager.py @@ -1,4 +1,5 @@ from typing import Dict +from contextlib import asynccontextmanager, contextmanager import pytest from fastapi import BackgroundTasks, Depends, FastAPI @@ -8,6 +9,8 @@ app = FastAPI() state = { "/async": "asyncgen not started", "/sync": "generator not started", + "/async_ctxmgr": "asyncgen_ctxmgr not started", + "/sync_ctxmgr": "generator_ctxmgr not started", "/async_raise": "asyncgen raise not started", "/sync_raise": "generator raise not started", "context_a": "not started a", @@ -47,6 +50,20 @@ def generator_state(state: Dict[str, str] = Depends(get_state)): state["/sync"] = "generator completed" +@asynccontextmanager +async def asyncgen_state_ctxmgr(state: Dict[str, str] = Depends(get_state)): + state["/async_ctxmgr"] = "asyncgen_ctxmgr started" + yield state["/async_ctxmgr"] + state["/async_ctxmgr"] = "asyncgen_ctxmgr completed" + + +@contextmanager +def generator_state_ctxmgr(state: Dict[str, str] = Depends(get_state)): + state["/sync_ctxmgr"] = "generator_ctxmgr started" + yield state["/sync_ctxmgr"] + state["/sync_ctxmgr"] = "generator_ctxmgr completed" + + async def asyncgen_state_try(state: Dict[str, str] = Depends(get_state)): state["/async_raise"] = "asyncgen raise started" try: @@ -93,6 +110,16 @@ async def get_sync(state: str = Depends(generator_state)): return state +@app.get("/async_ctxmgr") +async def get_async(state: str = Depends(asyncgen_state_ctxmgr)): + return state + + +@app.get("/sync_ctxmgr") +async def get_sync(state: str = Depends(generator_state_ctxmgr)): + return state + + @app.get("/async_raise") async def get_async_raise(state: str = Depends(asyncgen_state_try)): assert state == "asyncgen raise started" @@ -219,6 +246,22 @@ def test_sync_state(): assert state["/sync"] == "generator completed" +def test_async_ctxmgr_state(): + assert state["/async_ctxmgr"] == "asyncgen_ctxmgr not started" + response = client.get("/async_ctxmgr") + assert response.status_code == 200, response.text + assert response.json() == "asyncgen_ctxmgr started" + assert state["/async_ctxmgr"] == "asyncgen_ctxmgr completed" + + +def test_sync_ctxmgr_state(): + assert state["/sync_ctxmgr"] == "generator_ctxmgr not started" + response = client.get("/sync_ctxmgr") + assert response.status_code == 200, response.text + assert response.json() == "generator_ctxmgr started" + assert state["/sync_ctxmgr"] == "generator_ctxmgr completed" + + def test_async_raise_other(): assert state["/async_raise"] == "asyncgen raise not started" with pytest.raises(OtherDependencyError): From 4ef801a817f7eafa6563072dc2227bfd13e5147a Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 5 Dec 2023 09:52:19 +0000 Subject: [PATCH 05/10] =?UTF-8?q?=F0=9F=8E=A8=20[pre-commit.ci]=20Auto=20f?= =?UTF-8?q?ormat=20from=20pre-commit.com=20hooks?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs_src/dependencies/tutorial010_ctx.py | 1 + tests/test_dependency_contextmanager.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/docs_src/dependencies/tutorial010_ctx.py b/docs_src/dependencies/tutorial010_ctx.py index 71f6aa284..1fd180c8f 100644 --- a/docs_src/dependencies/tutorial010_ctx.py +++ b/docs_src/dependencies/tutorial010_ctx.py @@ -1,5 +1,6 @@ from contextlib import asynccontextmanager + @asynccontextmanager async def get_db(): db = DBSession() diff --git a/tests/test_dependency_contextmanager.py b/tests/test_dependency_contextmanager.py index ce0f56474..80b368bf8 100644 --- a/tests/test_dependency_contextmanager.py +++ b/tests/test_dependency_contextmanager.py @@ -1,5 +1,5 @@ -from typing import Dict from contextlib import asynccontextmanager, contextmanager +from typing import Dict import pytest from fastapi import BackgroundTasks, Depends, FastAPI From 9a192e565ab060f3069ae67cffaa82874c0e22ed Mon Sep 17 00:00:00 2001 From: Alexander Pushkov Date: Tue, 5 Dec 2023 13:00:15 +0300 Subject: [PATCH 06/10] =?UTF-8?q?=F0=9F=8E=A8=20Make=20test=20endpoint=20n?= =?UTF-8?q?ames=20unique?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/test_dependency_contextmanager.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_dependency_contextmanager.py b/tests/test_dependency_contextmanager.py index 80b368bf8..38b79e054 100644 --- a/tests/test_dependency_contextmanager.py +++ b/tests/test_dependency_contextmanager.py @@ -111,12 +111,12 @@ async def get_sync(state: str = Depends(generator_state)): @app.get("/async_ctxmgr") -async def get_async(state: str = Depends(asyncgen_state_ctxmgr)): +async def get_async_ctxmgr(state: str = Depends(asyncgen_state_ctxmgr)): return state @app.get("/sync_ctxmgr") -async def get_sync(state: str = Depends(generator_state_ctxmgr)): +async def get_sync_ctxmgr(state: str = Depends(generator_state_ctxmgr)): return state From b9bb157988d2aa6f1aaed1efdf3496dbd1683cf2 Mon Sep 17 00:00:00 2001 From: Alexander Pushkov Date: Sat, 16 Dec 2023 17:31:26 +0300 Subject: [PATCH 07/10] =?UTF-8?q?=F0=9F=8E=A8=20Ignore=20lint=20errors=20i?= =?UTF-8?q?n=20examples?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pyproject.toml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index e67486ae3..876540b36 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -152,6 +152,8 @@ ignore = [ "docs_src/custom_request_and_route/tutorial002.py" = ["B904"] "docs_src/dependencies/tutorial008_an.py" = ["F821"] "docs_src/dependencies/tutorial008_an_py39.py" = ["F821"] +"docs_src/dependencies/tutorial010_ctx.py" = ["F821"] +"docs_src/dependencies/tutorial010_ctx_usage.py" = ["F821", "F841"] "docs_src/query_params_str_validations/tutorial012_an.py" = ["B006"] "docs_src/query_params_str_validations/tutorial012_an_py39.py" = ["B006"] "docs_src/query_params_str_validations/tutorial013_an.py" = ["B006"] From e2e5ae02167431112f8c9932e25c503c46716920 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 25 Jun 2025 09:43:54 +0000 Subject: [PATCH 08/10] =?UTF-8?q?=F0=9F=8E=A8=20[pre-commit.ci]=20Auto=20f?= =?UTF-8?q?ormat=20from=20pre-commit.com=20hooks?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs_src/dependencies/tutorial010.py | 3 +-- tests/test_dependency_contextmanager.py | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/docs_src/dependencies/tutorial010.py b/docs_src/dependencies/tutorial010.py index fcf80e698..3b0f024bc 100644 --- a/docs_src/dependencies/tutorial010.py +++ b/docs_src/dependencies/tutorial010.py @@ -10,5 +10,4 @@ class MySuperContextManager: @app.get("/") -async def get_root(db: Annotated[DBSession, Depends(MySuperContextManager)]): - ... +async def get_root(db: Annotated[DBSession, Depends(MySuperContextManager)]): ... diff --git a/tests/test_dependency_contextmanager.py b/tests/test_dependency_contextmanager.py index b5c94606f..8849e8cf3 100644 --- a/tests/test_dependency_contextmanager.py +++ b/tests/test_dependency_contextmanager.py @@ -1,5 +1,5 @@ -from contextlib import asynccontextmanager, contextmanager import json +from contextlib import asynccontextmanager, contextmanager from typing import Dict import pytest From da43d27e736b481962134c48656a462e252857f4 Mon Sep 17 00:00:00 2001 From: Yurii Motov Date: Wed, 25 Jun 2025 11:59:59 +0200 Subject: [PATCH 09/10] Fix missed merge conflict --- fastapi/dependencies/utils.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/fastapi/dependencies/utils.py b/fastapi/dependencies/utils.py index db55f9667..4c71b7cf0 100644 --- a/fastapi/dependencies/utils.py +++ b/fastapi/dependencies/utils.py @@ -637,14 +637,12 @@ async def solve_dependencies( elif is_coroutine_callable(call): solved = await call(**solved_result.values) else: - stack = request.scope.get("fastapi_astack") - assert isinstance(stack, AsyncExitStack) called = await run_in_threadpool(call, **solved_result.values) if hasattr(called, "__aenter__"): - solved = await stack.enter_async_context(called) + solved = await async_exit_stack.enter_async_context(called) elif hasattr(called, "__enter__"): cm = contextmanager_in_threadpool(called) - solved = await stack.enter_async_context(cm) + solved = await async_exit_stack.enter_async_context(cm) else: solved = called if sub_dependant.name is not None: From 5c8aceb7e5a460328c47879c7e38ab322408357f Mon Sep 17 00:00:00 2001 From: Motov Yurii <109919500+YuriiMotov@users.noreply.github.com> Date: Wed, 2 Jul 2025 22:07:02 +0200 Subject: [PATCH 10/10] Apply suggestions from code review --- .../tutorial/dependencies/dependencies-with-yield.md | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/docs/en/docs/tutorial/dependencies/dependencies-with-yield.md b/docs/en/docs/tutorial/dependencies/dependencies-with-yield.md index 29c3305f1..a6cf2f305 100644 --- a/docs/en/docs/tutorial/dependencies/dependencies-with-yield.md +++ b/docs/en/docs/tutorial/dependencies/dependencies-with-yield.md @@ -23,7 +23,7 @@ In fact, FastAPI uses those two decorators internally. /// - And if you have a context manager already, you can use it as a dependency, too. +And if you have a context manager already, you can use it as a dependency, too. ## A database dependency with `yield` @@ -242,21 +242,17 @@ When the `with` block finishes, it makes sure to close the file, even if there w When you create a dependency with `yield`, **FastAPI** will internally create a context manager for it, and combine it with some other related tools. -## Context Managers as dependencies +### Context Managers as dependencies You don’t have to create a Context Manager to use FastAPI dependencies. But sometimes you might want to use a dependency both inside and outside FastAPI. For example, you might want to connect to a database in a DB migration script. You can create a Context Manager manually then: -```Python -{!../../../docs_src/dependencies/tutorial010_ctx.py!} -``` +{* ../../docs_src/dependencies/tutorial010_ctx.py *} This way you can use it with `Depends()` in your app, and as a regular Context Manager everywhere else: -```Python -{!../../../docs_src/dependencies/tutorial010_ctx_usage.py!} -``` +{* ../../docs_src/dependencies/tutorial010_ctx_usage.py *} ### Defining Context Managers as classes