Skip to content
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

✨ Add support for Trio via AnyIO #3372

Merged
merged 24 commits into from
Oct 6, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
6cc34f1
configure strict pytest options
graingert Feb 10, 2021
9f50ab2
open/close files with with
graingert Feb 10, 2021
388b5c9
support trio via anyio with starlette 0.15.0
graingert Jun 14, 2021
33ffaa7
WebSocketDisconnect only happens in websocket_connect context manager
graingert Jun 23, 2021
15b9f76
drop pytest-asyncio dep
graingert Jun 23, 2021
0349712
Merge branch 'configure-strict-pytest' of github.com:graingert/fastap…
graingert Jun 23, 2021
06f543d
Merge branch 'master' into anyio
graingert Jun 29, 2021
3b689b7
Update pyproject.toml
graingert Jul 19, 2021
0622860
bump anyio[trio]
graingert Jul 19, 2021
327cfd0
Merge branch 'master' into anyio
graingert Jul 19, 2021
17ed7ea
increase coverage with pytest.fail() # pragma: no cover
graingert Jul 19, 2021
9b14e4a
drop async_generator and async_exit_stack deps
graingert Jul 20, 2021
7af1e84
drop aiofiles
graingert Jul 20, 2021
0c7195f
Merge branch 'master' into anyio
graingert Jul 24, 2021
ada7c74
Merge branch 'master' of git://github.com/tiangolo/fastapi into anyio
graingert Aug 3, 2021
a539aa6
Merge branch 'master' of git://github.com/tiangolo/fastapi into anyio
graingert Sep 24, 2021
bcfddbe
✏️ Fix typo
tiangolo Oct 6, 2021
702aca3
⏪ Revert import/reexport style in concurrency to simplify maintainabi…
tiangolo Oct 6, 2021
580c65b
♻️ Simplify and inline re-implementation of asyncio.gather()
tiangolo Oct 6, 2021
861f71d
📌 Pin Starlette to the gradual next version, minimum that supports AnyIO
tiangolo Oct 6, 2021
51c5edd
🔀 Merge master
tiangolo Oct 6, 2021
9b52693
🔇 Add pytest filterwarning needed by AnyIO in Python 3.9
tiangolo Oct 6, 2021
96cb5b2
🔀 Merge master
tiangolo Oct 6, 2021
7c19efb
➕ Add contextlib2 as a direct dependency during the migration
tiangolo Oct 6, 2021
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 0 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -442,7 +442,6 @@ Used by Pydantic:
Used by Starlette:

* <a href="https://requests.readthedocs.io" target="_blank"><code>requests</code></a> - Required if you want to use the `TestClient`.
* <a href="https://github.com/Tinche/aiofiles" target="_blank"><code>aiofiles</code></a> - Required if you want to use `FileResponse` or `StaticFiles`.
* <a href="https://jinja.palletsprojects.com" target="_blank"><code>jinja2</code></a> - Required if you want to use the default template configuration.
* <a href="https://andrew-d.github.io/python-multipart/" target="_blank"><code>python-multipart</code></a> - Required if you want to support form <abbr title="converting the string that comes from an HTTP request into Python data">"parsing"</abbr>, with `request.form()`.
* <a href="https://pythonhosted.org/itsdangerous/" target="_blank"><code>itsdangerous</code></a> - Required for `SessionMiddleware` support.
Expand Down
20 changes: 4 additions & 16 deletions docs/en/docs/advanced/async-tests.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,21 +6,9 @@ Being able to use asynchronous functions in your tests could be useful, for exam

Let's look at how we can make that work.

## pytest-asyncio
## pytest.mark.anyio

If we want to call asynchronous functions in our tests, our test functions have to be asynchronous. Pytest provides a neat library for this, called `pytest-asyncio`, that allows us to specify that some test functions are to be called asynchronously.

You can install it via:

<div class="termy">

```console
$ pip install pytest-asyncio

---> 100%
```

</div>
If we want to call asynchronous functions in our tests, our test functions have to be asynchronous. Anyio provides a neat plugin for this, that allows us to specify that some test functions are to be called asynchronously.

## HTTPX

Expand Down Expand Up @@ -66,7 +54,7 @@ $ pytest

## In Detail

The marker `@pytest.mark.asyncio` tells pytest that this test function should be called asynchronously:
The marker `@pytest.mark.anyio` tells pytest that this test function should be called asynchronously:

```Python hl_lines="7"
{!../../../docs_src/async_tests/test_main.py!}
Expand Down Expand Up @@ -97,4 +85,4 @@ that we used to make our requests with the `TestClient`.
As the testing function is now asynchronous, you can now also call (and `await`) other `async` functions apart from sending requests to your FastAPI application in your tests, exactly as you would call them anywhere else in your code.

!!! tip
If you encounter a `RuntimeError: Task attached to a different loop` when integrating asynchronous function calls in your tests (e.g. when using <a href="https://stackoverflow.com/questions/41584243/runtimeerror-task-attached-to-a-different-loop" class="external-link" target="_blank">MongoDB's MotorClient</a>) check out <a href="https://github.com/pytest-dev/pytest-asyncio/issues/38#issuecomment-264418154" class="external-link" target="_blank">this issue</a> in the pytest-asyncio repository.
If you encounter a `RuntimeError: Task attached to a different loop` when integrating asynchronous function calls in your tests (e.g. when using <a href="https://stackoverflow.com/questions/41584243/runtimeerror-task-attached-to-a-different-loop" class="external-link" target="_blank">MongoDB's MotorClient</a>) Remember to instantiate objects that need an event loop only within async functions, e.g. an `'@app.on_event("startup")` callback.
15 changes: 0 additions & 15 deletions docs/en/docs/advanced/extending-openapi.md
Original file line number Diff line number Diff line change
Expand Up @@ -152,21 +152,6 @@ After that, your file structure could look like:
└── swagger-ui.css
```

### Install `aiofiles`

Now you need to install `aiofiles`:


<div class="termy">

```console
$ pip install aiofiles

---> 100%
```

</div>

### Serve the static files

* Import `StaticFiles`.
Expand Down
12 changes: 0 additions & 12 deletions docs/en/docs/advanced/templates.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,18 +20,6 @@ $ pip install jinja2

</div>

If you need to also serve static files (as in this example), install `aiofiles`:

<div class="termy">

```console
$ pip install aiofiles

---> 100%
```

</div>

## Using `Jinja2Templates`

* Import `Jinja2Templates`.
Expand Down
1 change: 0 additions & 1 deletion docs/en/docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -443,7 +443,6 @@ Used by Pydantic:
Used by Starlette:

* <a href="https://requests.readthedocs.io" target="_blank"><code>requests</code></a> - Required if you want to use the `TestClient`.
* <a href="https://github.com/Tinche/aiofiles" target="_blank"><code>aiofiles</code></a> - Required if you want to use `FileResponse` or `StaticFiles`.
* <a href="https://jinja.palletsprojects.com" target="_blank"><code>jinja2</code></a> - Required if you want to use the default template configuration.
* <a href="https://andrew-d.github.io/python-multipart/" target="_blank"><code>python-multipart</code></a> - Required if you want to support form <abbr title="converting the string that comes from an HTTP request into Python data">"parsing"</abbr>, with `request.form()`.
* <a href="https://pythonhosted.org/itsdangerous/" target="_blank"><code>itsdangerous</code></a> - Required for `SessionMiddleware` support.
Expand Down
9 changes: 0 additions & 9 deletions docs/en/docs/tutorial/dependencies/dependencies-with-yield.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,6 @@ 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":

```
pip install async-exit-stack async-generator
```

This installs <a href="https://github.com/sorcio/async_exit_stack" class="external-link" target="_blank">async-exit-stack</a> and <a href="https://github.com/python-trio/async_generator" class="external-link" target="_blank">async-generator</a>.

!!! note "Technical Details"
Any function that is valid to use with:

Expand Down
11 changes: 0 additions & 11 deletions docs/en/docs/tutorial/sql-databases.md
Original file line number Diff line number Diff line change
Expand Up @@ -441,17 +441,6 @@ You can find an example of Alembic in a FastAPI project in the templates from [P

### Create a dependency

!!! info
For this to work, you need to use **Python 3.7** or above, or in **Python 3.6**, install the "backports":

```console
$ pip install async-exit-stack async-generator
```

This installs <a href="https://github.com/sorcio/async_exit_stack" class="external-link" target="_blank">async-exit-stack</a> and <a href="https://github.com/python-trio/async_generator" class="external-link" 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.
Expand Down
14 changes: 0 additions & 14 deletions docs/en/docs/tutorial/static-files.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,6 @@

You can serve static files automatically from a directory using `StaticFiles`.

## Install `aiofiles`

First you need to install `aiofiles`:

<div class="termy">

```console
$ pip install aiofiles

---> 100%
```

</div>

## Use `StaticFiles`

* Import `StaticFiles`.
Expand Down
2 changes: 1 addition & 1 deletion docs_src/async_tests/test_main.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
from .main import app


@pytest.mark.asyncio
@pytest.mark.anyio
async def test_root():
async with AsyncClient(app=app, base_url="http://test") as ac:
response = await ac.get("/")
Expand Down
45 changes: 13 additions & 32 deletions fastapi/concurrency.py
Original file line number Diff line number Diff line change
@@ -1,46 +1,27 @@
from typing import Any, Callable
import sys
from typing import AsyncGenerator, ContextManager, TypeVar

from starlette.concurrency import iterate_in_threadpool as iterate_in_threadpool # noqa
from starlette.concurrency import run_in_threadpool as run_in_threadpool # noqa
from starlette.concurrency import ( # noqa
graingert marked this conversation as resolved.
Show resolved Hide resolved
run_until_first_complete as run_until_first_complete,
)

asynccontextmanager_error_message = """
FastAPI's contextmanager_in_threadpool require Python 3.7 or above,
or the backport for Python 3.6, installed with:
pip install async-generator
"""
if sys.version_info >= (3, 7):
from contextlib import AsyncExitStack as AsyncExitStack
from contextlib import asynccontextmanager as asynccontextmanager
else:
from contextlib2 import AsyncExitStack as AsyncExitStack # noqa
from contextlib2 import asynccontextmanager as asynccontextmanager # noqa


def _fake_asynccontextmanager(func: Callable[..., Any]) -> Callable[..., Any]:
def raiser(*args: Any, **kwargs: Any) -> Any:
raise RuntimeError(asynccontextmanager_error_message)
_T = TypeVar("_T")

return raiser


try:
from contextlib import asynccontextmanager as asynccontextmanager # type: ignore
except ImportError:
try:
from async_generator import ( # type: ignore # isort: skip
asynccontextmanager as asynccontextmanager,
)
except ImportError: # pragma: no cover
asynccontextmanager = _fake_asynccontextmanager

try:
from contextlib import AsyncExitStack as AsyncExitStack # type: ignore
except ImportError:
try:
from async_exit_stack import AsyncExitStack as AsyncExitStack # type: ignore
except ImportError: # pragma: no cover
AsyncExitStack = None # type: ignore


@asynccontextmanager # type: ignore
async def contextmanager_in_threadpool(cm: Any) -> Any:
@asynccontextmanager
async def contextmanager_in_threadpool(
cm: ContextManager[_T],
) -> AsyncGenerator[_T, None]:
try:
yield await run_in_threadpool(cm.__enter__)
except Exception as e:
Expand Down
46 changes: 15 additions & 31 deletions fastapi/dependencies/utils.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import asyncio
import dataclasses
import inspect
from contextlib import contextmanager
from copy import deepcopy
from typing import (
Any,
Callable,
Coroutine,
Dict,
List,
Mapping,
Expand All @@ -17,10 +17,10 @@
cast,
)

import anyio
from fastapi import params
from fastapi.concurrency import (
AsyncExitStack,
_fake_asynccontextmanager,
asynccontextmanager,
contextmanager_in_threadpool,
)
Expand Down Expand Up @@ -266,18 +266,6 @@ def get_typed_annotation(param: inspect.Parameter, globalns: Dict[str, Any]) ->
return annotation


async_contextmanager_dependencies_error = """
FastAPI dependencies with yield require Python 3.7 or above,
or the backports for Python 3.6, installed with:
pip install async-exit-stack async-generator
"""


def check_dependency_contextmanagers() -> None:
if AsyncExitStack is None or asynccontextmanager == _fake_asynccontextmanager:
raise RuntimeError(async_contextmanager_dependencies_error) # pragma: no cover


def get_dependant(
*,
path: str,
Expand All @@ -289,8 +277,6 @@ def get_dependant(
path_param_names = get_path_param_names(path)
endpoint_signature = get_typed_signature(call)
signature_params = endpoint_signature.parameters
if is_gen_callable(call) or is_async_gen_callable(call):
check_dependency_contextmanagers()
dependant = Dependant(call=call, name=name, path=path, use_cache=use_cache)
for param_name, param in signature_params.items():
if isinstance(param.default, params.Depends):
Expand Down Expand Up @@ -452,14 +438,6 @@ async def solve_generator(
if is_gen_callable(call):
cm = contextmanager_in_threadpool(contextmanager(call)(**sub_values))
elif is_async_gen_callable(call):
if not inspect.isasyncgenfunction(call):
# asynccontextmanager from the async_generator backfill pre python3.7
# does not support callables that are not functions or methods.
# See https://github.com/python-trio/async_generator/issues/32
#
# Expand the callable class into its __call__ method before decorating it.
# This approach will work on newer python versions as well.
call = getattr(call, "__call__", None)
cm = asynccontextmanager(call)(**sub_values)
return await stack.enter_async_context(cm)

Expand Down Expand Up @@ -539,10 +517,7 @@ async def solve_dependencies(
solved = dependency_cache[sub_dependant.cache_key]
elif is_gen_callable(call) or is_async_gen_callable(call):
stack = request.scope.get("fastapi_astack")
if stack is None:
raise RuntimeError(
async_contextmanager_dependencies_error
) # pragma: no cover
assert isinstance(stack, AsyncExitStack)
solved = await solve_generator(
call=call, stack=stack, sub_values=sub_values
)
Expand Down Expand Up @@ -697,9 +672,18 @@ async def request_body_to_args(
and lenient_issubclass(field.type_, bytes)
and isinstance(value, sequence_types)
):
awaitables = [sub_value.read() for sub_value in value]
contents = await asyncio.gather(*awaitables)
value = sequence_shape_to_type[field.shape](contents)
results: List[Union[bytes, str]] = []

async def process_fn(
fn: Callable[[], Coroutine[Any, Any, Any]]
) -> None:
result = await fn()
results.append(result)

async with anyio.create_task_group() as tg:
for sub_value in value:
tg.start_soon(process_fn, sub_value.read)
value = sequence_shape_to_type[field.shape](results)

v_, errors_ = field.validate(value, values, loc=loc)

Expand Down
16 changes: 7 additions & 9 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,10 @@ classifiers = [
"Topic :: Internet :: WWW/HTTP",
]
requires = [
"starlette ==0.14.2",
"pydantic >=1.6.2,!=1.7,!=1.7.1,!=1.7.2,!=1.7.3,!=1.8,!=1.8.1,<2.0.0"
"starlette ==0.15.0",
graingert marked this conversation as resolved.
Show resolved Hide resolved
"pydantic >=1.6.2,!=1.7,!=1.7.1,!=1.7.2,!=1.7.3,!=1.8,!=1.8.1,<2.0.0",
# TODO: remove contextlib2 as a direct dependency after upgrading Starlette
"contextlib2 >= 21.6.0; python_version < '3.7'",
]
description-file = "README.md"
requires-python = ">=3.6.1"
Expand All @@ -46,7 +48,6 @@ Documentation = "https://fastapi.tiangolo.com/"
test = [
"pytest >=6.2.4,<7.0.0",
"pytest-cov >=2.12.0,<4.0.0",
"pytest-asyncio >=0.14.0,<0.16.0",
"mypy ==0.910",
"flake8 >=3.8.3,<4.0.0",
"black ==21.9b0",
Expand All @@ -60,11 +61,9 @@ test = [
"orjson >=3.2.1,<4.0.0",
"ujson >=4.0.1,<5.0.0",
"python-multipart >=0.0.5,<0.0.6",
"aiofiles >=0.5.0,<0.8.0",
# TODO: try to upgrade after upgrading Starlette
"flask >=1.1.2,<2.0.0",
"async_exit_stack >=1.0.1,<2.0.0; python_version < '3.7'",
"async_generator >=1.10,<2.0.0; python_version < '3.7'",
"anyio[trio] >=3.2.1,<4.0.0",

# types
"types-ujson ==0.1.1",
Expand All @@ -90,7 +89,6 @@ dev = [
]
all = [
"requests >=2.24.0,<3.0.0",
"aiofiles >=0.5.0,<0.8.0",
# TODO: try to upgrade after upgrading Starlette
"jinja2 >=2.11.2,<3.0.0",
"python-multipart >=0.0.5,<0.0.6",
Expand All @@ -103,8 +101,6 @@ all = [
"orjson >=3.2.1,<4.0.0",
"email_validator >=1.1.1,<2.0.0",
"uvicorn[standard] >=0.12.0,<0.16.0",
"async_exit_stack >=1.0.1,<2.0.0; python_version < '3.7'",
"async_generator >=1.10,<2.0.0; python_version < '3.7'",
]

[tool.isort]
Expand Down Expand Up @@ -148,6 +144,8 @@ junit_family = "xunit2"
filterwarnings = [
"error",
'ignore:"@coroutine" decorator is deprecated since Python 3\.8, use "async def" instead:DeprecationWarning',
# TODO: needed by AnyIO in Python 3.9, try to remove after an AnyIO upgrade
'ignore:The loop argument is deprecated since Python 3\.8, and scheduled for removal in Python 3\.10:DeprecationWarning',
# TODO: if these ignores are needed, enable them, otherwise remove them
# 'ignore:The explicit passing of coroutine objects to asyncio\.wait\(\) is deprecated since Python 3\.8:DeprecationWarning',
# 'ignore:Exception ignored in. <socket\.socket fd=-1:pytest.PytestUnraisableExceptionWarning',
Expand Down
12 changes: 0 additions & 12 deletions tests/test_fakeasync.py

This file was deleted.