Repository: fastapi/fastapi
Pull request: ✨ Add support for Trio via AnyIO
Fixes #310 

see also https://github.com/encode/starlette/pull/1157

Problem statement:
Trio -  Python library for async concurrency and I/O
**Description**

Hello!
I'm using Hypercorn with uvloop.
I'd like to use Hypercorn with trio, but the program doesn't start when i switch uvloop by trio.

Do you have plans to implement trio?

Thank's!

PS: Nice work.

Hints from issue discussion:
I guess most FastAPI users use it with uvloop which trio is not compatible with: https://github.com/python-trio/trio/issues/138
Thanks for the help here @haizaar ! Nice investigation.

I think @haizaar is right, by checking that issue.
This is a great place to mention that you can use hypercorn, which [seems to support trio](https://medium.com/@pgjones/hypercorn-is-now-a-trio-asgi-server-2e198898c08f) (I haven't tested it personally though).
Maybe, again maybe, if you configure uvicorn to use asyncio instead of uvloop as its loop, trio will work with it and consequently, with FastAPI

Of course that's in case one does not need absolutely cutting edge performance.
So, FastAPI uses Starlette's `run_in_threadpool` internally, and that uses `asyncio`.

Prior to FastAPI supporting Trio, we would need Starlette itself to support Trio. But that's probably quite a lot of work there. Also, if Starlette supported Trio that would be automatically inherited by FastAPI.

You can track it here: https://github.com/encode/starlette/issues/811

<!-- issue-manager: answered -->
Assuming the original issue was solved, it will be automatically closed now. But feel free to add more comments or create new issues.
there's some progress on this here: https://github.com/encode/starlette/pull/1157

Commit message: Merge branch 'master' into anyio

Patch to review:
diff --git a/docs/en/docs/advanced/async-tests.md b/docs/en/docs/advanced/async-tests.md
index 921bdb708c871..7b8e3cd3eb9af 100644
--- a/docs/en/docs/advanced/async-tests.md
+++ b/docs/en/docs/advanced/async-tests.md
@@ -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
 
@@ -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!}
@@ -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, eg an `'@app.on_event("startup")` callback.
diff --git a/docs_src/async_tests/test_main.py b/docs_src/async_tests/test_main.py
index c141d86ca7eef..9f1527d5f6028 100644
--- a/docs_src/async_tests/test_main.py
+++ b/docs_src/async_tests/test_main.py
@@ -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("/")
diff --git a/docs_src/custom_response/tutorial008.py b/docs_src/custom_response/tutorial008.py
index d332736741c57..8edaa85249948 100644
--- a/docs_src/custom_response/tutorial008.py
+++ b/docs_src/custom_response/tutorial008.py
@@ -7,5 +7,8 @@
 
 @app.get("/")
 def main():
-    file_like = open(some_file_path, mode="rb")
-    return StreamingResponse(file_like, media_type="video/mp4")
+    def iterfile():
+        with open(some_file_path, mode="rb") as file_like:
+            yield from file_like
+
+    return StreamingResponse(iterfile(), media_type="video/mp4")
diff --git a/fastapi/dependencies/utils.py b/fastapi/dependencies/utils.py
index 923669b94ba85..c86dd303bad40 100644
--- a/fastapi/dependencies/utils.py
+++ b/fastapi/dependencies/utils.py
@@ -1,10 +1,10 @@
-import asyncio
 import inspect
 from contextlib import contextmanager
 from copy import deepcopy
 from typing import (
     Any,
     Callable,
+    Coroutine,
     Dict,
     List,
     Mapping,
@@ -12,10 +12,12 @@
     Sequence,
     Tuple,
     Type,
+    TypeVar,
     Union,
     cast,
 )
 
+import anyio
 from fastapi import params
 from fastapi.concurrency import (
     AsyncExitStack,
@@ -85,6 +87,25 @@
 )
 
 
+_T = TypeVar("_T")
+
+
+async def _run_and_assign(
+    results: List[Optional[_T]], i: int, async_fn: Callable[[], Coroutine[Any, Any, _T]]
+) -> None:
+    results[i] = await async_fn()
+
+
+async def _wait_all(*async_fns: Callable[[], Coroutine[Any, Any, _T]]) -> List[_T]:
+    results: List[Optional[_T]] = [None] * len(async_fns)
+
+    async with anyio.create_task_group() as tg:
+        for i, async_fn in enumerate(async_fns):
+            tg.start_soon(_run_and_assign, results, i, async_fn)
+
+    return results  # type: ignore[return-value]
+
+
 def check_file_field(field: ModelField) -> None:
     field_info = field.field_info
     if isinstance(field_info, params.Form):
@@ -695,8 +716,7 @@ 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)
+                contents = await _wait_all(*(sub_value.read for sub_value in value))
                 value = sequence_shape_to_type[field.shape](contents)
 
             v_, errors_ = field.validate(value, values, loc=loc)
diff --git a/pyproject.toml b/pyproject.toml
index 25440bde4b08f..714a828101e44 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -32,7 +32,7 @@ classifiers = [
     "Topic :: Internet :: WWW/HTTP",
 ]
 requires = [
-    "starlette ==0.14.2",
+    "starlette ==0.15.0",
     "pydantic >=1.6.2,!=1.7,!=1.7.1,!=1.7.2,!=1.7.3,!=1.8,!=1.8.1,<2.0.0"
 ]
 description-file = "README.md"
@@ -43,9 +43,8 @@ Documentation = "https://fastapi.tiangolo.com/"
 
 [tool.flit.metadata.requires-extra]
 test = [
-    "pytest ==5.4.3",
-    "pytest-cov ==2.10.0",
-    "pytest-asyncio >=0.14.0,<0.15.0",
+    "pytest ==6.2.4",
+    "pytest-cov ==2.12.1",
     "mypy ==0.812",
     "flake8 >=3.8.3,<4.0.0",
     "black ==20.8b1",
@@ -62,7 +61,8 @@ test = [
     "async_generator >=1.10,<2.0.0",
     "python-multipart >=0.0.5,<0.0.6",
     "aiofiles >=0.5.0,<0.6.0",
-    "flask >=1.1.2,<2.0.0"
+    "flask >=1.1.2,<2.0.0",
+    "anyio[trio] >=3.1.0,<4",
 ]
 doc = [
     "mkdocs >=1.1.2,<2.0.0",
@@ -99,3 +99,18 @@ all = [
 [tool.isort]
 profile = "black"
 known_third_party = ["fastapi", "pydantic", "starlette"]
+
+[tool.pytest.ini_options]
+addopts = [
+  "--strict-config",
+  "--strict-markers",
+]
+xfail_strict = true
+junit_family = "xunit2"
+filterwarnings = [
+    "error",
+    'ignore:"@coroutine" decorator is deprecated since Python 3\.8, use "async def" instead:DeprecationWarning',
+    'ignore:The explicit passing of coroutine objects to asyncio\.wait\(\) is deprecated since Python 3\.8:DeprecationWarning',
+    'ignore:int_from_bytes is deprecated, use int\.from_bytes instead:cryptography.utils.CryptographyDeprecationWarning',
+    'ignore:Exception ignored in. <socket\.socket fd=-1:pytest.PytestUnraisableExceptionWarning',
+]
diff --git a/tests/test_tutorial/test_async_tests/test_main.py b/tests/test_tutorial/test_async_tests/test_main.py
index 8104c9056301b..1f5d7186cc016 100644
--- a/tests/test_tutorial/test_async_tests/test_main.py
+++ b/tests/test_tutorial/test_async_tests/test_main.py
@@ -3,6 +3,6 @@
 from docs_src.async_tests.test_main import test_root
 
 
-@pytest.mark.asyncio
+@pytest.mark.anyio
 async def test_async_testing():
     await test_root()
diff --git a/tests/test_tutorial/test_request_files/test_tutorial001.py b/tests/test_tutorial/test_request_files/test_tutorial001.py
index 4eba18496d093..c1537f445da17 100644
--- a/tests/test_tutorial/test_request_files/test_tutorial001.py
+++ b/tests/test_tutorial/test_request_files/test_tutorial001.py
@@ -1,5 +1,3 @@
-import os
-
 from fastapi.testclient import TestClient
 
 from docs_src.request_files.tutorial001 import app
@@ -152,35 +150,35 @@ def test_post_body_json():
     assert response.json() == file_required
 
 
-def test_post_file(tmpdir):
-    path = os.path.join(tmpdir, "test.txt")
-    with open(path, "wb") as file:
-        file.write(b"<file content>")
+def test_post_file(tmp_path):
+    path = tmp_path / "test.txt"
+    path.write_bytes(b"<file content>")
 
     client = TestClient(app)
-    response = client.post("/files/", files={"file": open(path, "rb")})
+    with path.open("rb") as file:
+        response = client.post("/files/", files={"file": file})
     assert response.status_code == 200, response.text
     assert response.json() == {"file_size": 14}
 
 
-def test_post_large_file(tmpdir):
+def test_post_large_file(tmp_path):
     default_pydantic_max_size = 2 ** 16
-    path = os.path.join(tmpdir, "test.txt")
-    with open(path, "wb") as file:
-        file.write(b"x" * (default_pydantic_max_size + 1))
+    path = tmp_path / "test.txt"
+    path.write_bytes(b"x" * (default_pydantic_max_size + 1))
 
     client = TestClient(app)
-    response = client.post("/files/", files={"file": open(path, "rb")})
+    with path.open("rb") as file:
+        response = client.post("/files/", files={"file": file})
     assert response.status_code == 200, response.text
     assert response.json() == {"file_size": default_pydantic_max_size + 1}
 
 
-def test_post_upload_file(tmpdir):
-    path = os.path.join(tmpdir, "test.txt")
-    with open(path, "wb") as file:
-        file.write(b"<file content>")
+def test_post_upload_file(tmp_path):
+    path = tmp_path / "test.txt"
+    path.write_bytes(b"<file content>")
 
     client = TestClient(app)
-    response = client.post("/uploadfile/", files={"file": open(path, "rb")})
+    with path.open("rb") as file:
+        response = client.post("/uploadfile/", files={"file": file})
     assert response.status_code == 200, response.text
     assert response.json() == {"filename": "test.txt"}
diff --git a/tests/test_tutorial/test_request_files/test_tutorial002.py b/tests/test_tutorial/test_request_files/test_tutorial002.py
index 58fe4269f2df7..4e33ef464dd7d 100644
--- a/tests/test_tutorial/test_request_files/test_tutorial002.py
+++ b/tests/test_tutorial/test_request_files/test_tutorial002.py
@@ -1,5 +1,3 @@
-import os
-
 from fastapi.testclient import TestClient
 
 from docs_src.request_files.tutorial002 import app
@@ -172,42 +170,40 @@ def test_post_body_json():
     assert response.json() == file_required
 
 
-def test_post_files(tmpdir):
-    path = os.path.join(tmpdir, "test.txt")
-    with open(path, "wb") as file:
-        file.write(b"<file content>")
-    path2 = os.path.join(tmpdir, "test2.txt")
-    with open(path2, "wb") as file:
-        file.write(b"<file content2>")
+def test_post_files(tmp_path):
+    path = tmp_path / "test.txt"
+    path.write_bytes(b"<file content>")
+    path2 = tmp_path / "test2.txt"
+    path2.write_bytes(b"<file content2>")
 
     client = TestClient(app)
-    response = client.post(
-        "/files/",
-        files=(
-            ("files", ("test.txt", open(path, "rb"))),
-            ("files", ("test2.txt", open(path2, "rb"))),
-        ),
-    )
+    with path.open("rb") as file, path2.open("rb") as file2:
+        response = client.post(
+            "/files/",
+            files=(
+                ("files", ("test.txt", file)),
+                ("files", ("test2.txt", file2)),
+            ),
+        )
     assert response.status_code == 200, response.text
     assert response.json() == {"file_sizes": [14, 15]}
 
 
-def test_post_upload_file(tmpdir):
-    path = os.path.join(tmpdir, "test.txt")
-    with open(path, "wb") as file:
-        file.write(b"<file content>")
-    path2 = os.path.join(tmpdir, "test2.txt")
-    with open(path2, "wb") as file:
-        file.write(b"<file content2>")
+def test_post_upload_file(tmp_path):
+    path = tmp_path / "test.txt"
+    path.write_bytes(b"<file content>")
+    path2 = tmp_path / "test2.txt"
+    path2.write_bytes(b"<file content2>")
 
     client = TestClient(app)
-    response = client.post(
-        "/uploadfiles/",
-        files=(
-            ("files", ("test.txt", open(path, "rb"))),
-            ("files", ("test2.txt", open(path2, "rb"))),
-        ),
-    )
+    with path.open("rb") as file, path2.open("rb") as file2:
+        response = client.post(
+            "/uploadfiles/",
+            files=(
+                ("files", ("test.txt", file)),
+                ("files", ("test2.txt", file2)),
+            ),
+        )
     assert response.status_code == 200, response.text
     assert response.json() == {"filenames": ["test.txt", "test2.txt"]}
 
diff --git a/tests/test_tutorial/test_request_forms_and_files/test_tutorial001.py b/tests/test_tutorial/test_request_forms_and_files/test_tutorial001.py
index 6a98ee324d7b2..10cce5e61284a 100644
--- a/tests/test_tutorial/test_request_forms_and_files/test_tutorial001.py
+++ b/tests/test_tutorial/test_request_forms_and_files/test_tutorial001.py
@@ -1,6 +1,3 @@
-import os
-from pathlib import Path
-
 from fastapi.testclient import TestClient
 
 from docs_src.request_forms_and_files.tutorial001 import app
@@ -163,32 +160,30 @@ def test_post_body_json():
     assert response.json() == file_and_token_required
 
 
-def test_post_file_no_token(tmpdir):
-    path = os.path.join(tmpdir, "test.txt")
-    with open(path, "wb") as file:
-        file.write(b"<file content>")
+def test_post_file_no_token(tmp_path):
+    path = tmp_path / "test.txt"
+    path.write_bytes(b"<file content>")
 
     client = TestClient(app)
-    response = client.post("/files/", files={"file": open(path, "rb")})
+    with path.open("rb") as file:
+        response = client.post("/files/", files={"file": file})
     assert response.status_code == 422, response.text
     assert response.json() == token_required
 
 
-def test_post_files_and_token(tmpdir):
-    patha = Path(tmpdir) / "test.txt"
-    pathb = Path(tmpdir) / "testb.txt"
+def test_post_files_and_token(tmp_path):
+    patha = tmp_path / "test.txt"
+    pathb = tmp_path / "testb.txt"
     patha.write_text("<file content>")
     pathb.write_text("<file b content>")
 
     client = TestClient(app)
-    response = client.post(
-        "/files/",
-        data={"token": "foo"},
-        files={
-            "file": patha.open("rb"),
-            "fileb": ("testb.txt", pathb.open("rb"), "text/plain"),
-        },
-    )
+    with patha.open("rb") as filea, pathb.open("rb") as fileb:
+        response = client.post(
+            "/files/",
+            data={"token": "foo"},
+            files={"file": filea, "fileb": ("testb.txt", fileb, "text/plain")},
+        )
     assert response.status_code == 200, response.text
     assert response.json() == {
         "file_size": 14,
diff --git a/tests/test_tutorial/test_websockets/test_tutorial002.py b/tests/test_tutorial/test_websockets/test_tutorial002.py
index 7c56eb26057e9..f3aa285abfa4d 100644
--- a/tests/test_tutorial/test_websockets/test_tutorial002.py
+++ b/tests/test_tutorial/test_websockets/test_tutorial002.py
@@ -72,9 +72,11 @@ def test_websocket_with_header_and_query():
 
 def test_websocket_no_credentials():
     with pytest.raises(WebSocketDisconnect):
-        client.websocket_connect("/items/foo/ws")
+        with client.websocket_connect("/items/foo/ws"):
+            pass
 
 
 def test_websocket_invalid_data():
     with pytest.raises(WebSocketDisconnect):
-        client.websocket_connect("/items/foo/ws?q=bar&token=some-token")
+        with client.websocket_connect("/items/foo/ws?q=bar&token=some-token"):
+            pass


Code review comments:
diff_hunk: @@ -62,7 +61,8 @@ test = [
     "async_generator >=1.10,<2.0.0",
     "python-multipart >=0.0.5,<0.0.6",
     "aiofiles >=0.5.0,<0.6.0",
-    "flask >=1.1.2,<2.0.0"
+    "flask >=1.1.2,<2.0.0",
+    "anyio[trio] >=3.1.0,<4",
path: pyproject.toml
text: @author:
```suggestion
    "anyio[trio] >=3.2.1,<4",
```