Repository: falconry/falcon

Issue:
CPython 3.10 support
CPython 3.10 has been released.

Although it may already work out of the box, we need to add official first class support anyway:
- [x] Add a CPython 3.10 CI gate: (https://github.com/falconry/falcon/pull/1922).
- [x] Build CPython 3.10 wheels.
- [x] Advertise support using ["trove classifiers"](https://pypi.org/classifiers/).
- [x] Check if anything needs an update in `CONTRIBUTING.md`.

In addition, check for any  new warnings emitted when running tests, e.g., whether we are relying on any deprecated functionality that will be removed in future Python versions:
- [x] Multiple `DeprecationWarning`: non-integer arguments to randrange() have been deprecated since Python 3.10 and will be removed in a subsequent version https://github.com/falconry/falcon/pull/1972
- [x] `falcon/util/sync.py`:224: `DeprecationWarning`: There is no current event loop
 loop = asyncio.get_event_loop()
 [`asyncio.get_event_loop()`](https://docs.python.org/3/library/asyncio-eventloop.html#asyncio.get_event_loop): _Deprecated since version 3.10:_ Deprecation warning is emitted if there is no running event loop. In future Python releases, this function will be an alias of [`get_running_loop()`](https://docs.python.org/3/library/asyncio-eventloop.html#asyncio.get_running_loop).
- [x] `tests/asgi/test_ws.py`:344: `DeprecationWarning`: The explicit passing of coroutine objects to asyncio.wait() is deprecated since Python 3.8, and scheduled for removal in Python 3.11.
- [x] Anything else?


Discussion:

Resolving patch:
diff --git a/falcon/util/sync.py b/falcon/util/sync.py
--- a/falcon/util/sync.py
+++ b/falcon/util/sync.py
@@ -210,9 +210,6 @@ def async_to_sync(coroutine, *args, **kwargs):
         This method is very inefficient and is intended primarily for testing
         and prototyping.
 
-    Additional arguments not mentioned below are bound to the given
-    coroutine function via :any:`functools.partial`.
-
     Args:
         coroutine: A coroutine function to invoke.
         *args: Additional args are passed through to the coroutine function.
@@ -221,8 +218,16 @@ def async_to_sync(coroutine, *args, **kwargs):
         **kwargs: Additional args are passed through to the coroutine function.
     """
 
-    loop = asyncio.get_event_loop()
-    return loop.run_until_complete(partial(coroutine, *args, **kwargs)())
+    # TODO(vytas): The canonical way of doing this for simple use cases is
+    #   asyncio.run(), but that would be a breaking change wrt the above
+    #   documented behaviour; breaking enough to break some of our own tests.
+
+    # NOTE(vytas): Work around get_event_loop deprecation in 3.10 by going via
+    #   get_event_loop_policy(). This should be equivalent for async_to_sync's
+    #   use case as it is currently impossible to invoke run_until_complete()
+    #   from a running loop anyway.
+    loop = asyncio.get_event_loop_policy().get_event_loop()
+    return loop.run_until_complete(coroutine(*args, **kwargs))
 
 
 def runs_sync(coroutine):


Test patch:
diff --git a/tests/asgi/test_ws.py b/tests/asgi/test_ws.py
--- a/tests/asgi/test_ws.py
+++ b/tests/asgi/test_ws.py
@@ -341,7 +341,9 @@ async def on_websocket(self, req, ws):
                             await asyncio.sleep(
                                 0
                             )  # Ensure recv_task() has a chance to get ahead
-                            await asyncio.wait([recv_task, ws.close(4099)])
+                            await asyncio.wait(
+                                [recv_task, falcon.create_task(ws.close(4099))]
+                            )
 
                         self.data_received.set()