Repository: tiangolo/fastapi

Issue:
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.



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.

Resolving patch:
diff --git a/fastapi/concurrency.py b/fastapi/concurrency.py
--- a/fastapi/concurrency.py
+++ b/fastapi/concurrency.py
@@ -1,4 +1,5 @@
-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
@@ -6,41 +7,21 @@
     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:
diff --git a/fastapi/dependencies/utils.py b/fastapi/dependencies/utils.py
--- a/fastapi/dependencies/utils.py
+++ b/fastapi/dependencies/utils.py
@@ -1,4 +1,3 @@
-import asyncio
 import dataclasses
 import inspect
 from contextlib import contextmanager
@@ -6,6 +5,7 @@
 from typing import (
     Any,
     Callable,
+    Coroutine,
     Dict,
     List,
     Mapping,
@@ -17,10 +17,10 @@
     cast,
 )
 
+import anyio
 from fastapi import params
 from fastapi.concurrency import (
     AsyncExitStack,
-    _fake_asynccontextmanager,
     asynccontextmanager,
     contextmanager_in_threadpool,
 )
@@ -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,
@@ -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):
@@ -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)
 
@@ -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
             )
@@ -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)
 


Test patch:
diff --git a/docs_src/async_tests/test_main.py b/docs_src/async_tests/test_main.py
--- 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/tests/test_fakeasync.py b/tests/test_fakeasync.py
deleted file mode 100644
--- a/tests/test_fakeasync.py
+++ /dev/null
@@ -1,12 +0,0 @@
-import pytest
-from fastapi.concurrency import _fake_asynccontextmanager
-
-
-@_fake_asynccontextmanager
-def never_run():
-    pass  # pragma: no cover
-
-
-def test_fake_async():
-    with pytest.raises(RuntimeError):
-        never_run()
diff --git a/tests/test_tutorial/test_async_tests/test_main.py b/tests/test_tutorial/test_async_tests/test_main.py
--- 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_websockets/test_tutorial002.py b/tests/test_tutorial/test_websockets/test_tutorial002.py
--- a/tests/test_tutorial/test_websockets/test_tutorial002.py
+++ b/tests/test_tutorial/test_websockets/test_tutorial002.py
@@ -72,9 +72,15 @@ 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"):
+            pytest.fail(
+                "did not raise WebSocketDisconnect on __enter__"
+            )  # pragma: no cover
 
 
 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"):
+            pytest.fail(
+                "did not raise WebSocketDisconnect on __enter__"
+            )  # pragma: no cover