]> git.ipfire.org Git - thirdparty/tornado.git/commitdiff
asyncio: Use dynamic magic for AnyThreadEventLoopPolicy
authorBen Darnell <ben@bendarnell.com>
Thu, 20 Feb 2025 19:15:54 +0000 (14:15 -0500)
committerBen Darnell <ben@bendarnell.com>
Thu, 20 Feb 2025 19:21:31 +0000 (14:21 -0500)
Accessing the base policy classes now triggers a deprecation warning
so we must use our own getattr hook to avoid it except when needed.

docs/asyncio.rst
tornado/platform/asyncio.py
tornado/test/asyncio_test.py
tornado/test/import_test.py

index fc737451c7398a373b67d58905d40a30b71f1ae0..1f90bf68c7363e9801894088c02af66e7bb3d684 100644 (file)
@@ -3,3 +3,13 @@
 
 .. automodule:: tornado.platform.asyncio
    :members:
+
+
+   ..
+      AnyThreadEventLoopPolicy is created dynamically in getattr, so
+      introspection won't find it automatically. This has the unfortunate
+      side effect of moving it to the top of the page but it's better than
+      having it missing entirely.
+
+   .. autoclass:: AnyThreadEventLoopPolicy
+      :members:
\ No newline at end of file
index 2e9f424842128c33414b468f268b51042c8cc931..4635fecb26fa015abed492a2ac0dd0aaa86cd0c2 100644 (file)
@@ -386,58 +386,76 @@ def to_asyncio_future(tornado_future: asyncio.Future) -> asyncio.Future:
     return convert_yielded(tornado_future)
 
 
-if sys.platform == "win32" and hasattr(asyncio, "WindowsSelectorEventLoopPolicy"):
-    # "Any thread" and "selector" should be orthogonal, but there's not a clean
-    # interface for composing policies so pick the right base.
-    _BasePolicy = asyncio.WindowsSelectorEventLoopPolicy  # type: ignore
-else:
-    _BasePolicy = asyncio.DefaultEventLoopPolicy
+_AnyThreadEventLoopPolicy = None
 
 
-class AnyThreadEventLoopPolicy(_BasePolicy):  # type: ignore
-    """Event loop policy that allows loop creation on any thread.
+def __getattr__(name: str) -> typing.Any:
+    # The event loop policy system is deprecated in Python 3.14; simply accessing
+    # the name asyncio.DefaultEventLoopPolicy will raise a warning. Lazily create
+    # the AnyThreadEventLoopPolicy class so that the warning is only raised if
+    # the policy is used.
+    if name != "AnyThreadEventLoopPolicy":
+        raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
 
-    The default `asyncio` event loop policy only automatically creates
-    event loops in the main threads. Other threads must create event
-    loops explicitly or `asyncio.get_event_loop` (and therefore
-    `.IOLoop.current`) will fail. Installing this policy allows event
-    loops to be created automatically on any thread, matching the
-    behavior of Tornado versions prior to 5.0 (or 5.0 on Python 2).
+    global _AnyThreadEventLoopPolicy
+    if _AnyThreadEventLoopPolicy is None:
+        if sys.platform == "win32" and hasattr(
+            asyncio, "WindowsSelectorEventLoopPolicy"
+        ):
+            # "Any thread" and "selector" should be orthogonal, but there's not a clean
+            # interface for composing policies so pick the right base.
+            _BasePolicy = asyncio.WindowsSelectorEventLoopPolicy  # type: ignore
+        else:
+            _BasePolicy = asyncio.DefaultEventLoopPolicy
 
-    Usage::
+        class AnyThreadEventLoopPolicy(_BasePolicy):  # type: ignore
+            """Event loop policy that allows loop creation on any thread.
 
-        asyncio.set_event_loop_policy(AnyThreadEventLoopPolicy())
+            The default `asyncio` event loop policy only automatically creates
+            event loops in the main threads. Other threads must create event
+            loops explicitly or `asyncio.get_event_loop` (and therefore
+            `.IOLoop.current`) will fail. Installing this policy allows event
+            loops to be created automatically on any thread, matching the
+            behavior of Tornado versions prior to 5.0 (or 5.0 on Python 2).
 
-    .. versionadded:: 5.0
+            Usage::
 
-    .. deprecated:: 6.2
+                asyncio.set_event_loop_policy(AnyThreadEventLoopPolicy())
 
-        ``AnyThreadEventLoopPolicy`` affects the implicit creation
-        of an event loop, which is deprecated in Python 3.10 and
-        will be removed in a future version of Python. At that time
-        ``AnyThreadEventLoopPolicy`` will no longer be useful.
-        If you are relying on it, use `asyncio.new_event_loop`
-        or `asyncio.run` explicitly in any non-main threads that
-        need event loops.
-    """
+            .. versionadded:: 5.0
 
-    def __init__(self) -> None:
-        super().__init__()
-        warnings.warn(
-            "AnyThreadEventLoopPolicy is deprecated, use asyncio.run "
-            "or asyncio.new_event_loop instead",
-            DeprecationWarning,
-            stacklevel=2,
-        )
+            .. deprecated:: 6.2
 
-    def get_event_loop(self) -> asyncio.AbstractEventLoop:
-        try:
-            return super().get_event_loop()
-        except RuntimeError:
-            # "There is no current event loop in thread %r"
-            loop = self.new_event_loop()
-            self.set_event_loop(loop)
-            return loop
+                ``AnyThreadEventLoopPolicy`` affects the implicit creation
+                of an event loop, which is deprecated in Python 3.10 and
+                will be removed in a future version of Python. At that time
+                ``AnyThreadEventLoopPolicy`` will no longer be useful.
+                If you are relying on it, use `asyncio.new_event_loop`
+                or `asyncio.run` explicitly in any non-main threads that
+                need event loops.
+            """
+
+            def __init__(self) -> None:
+                super().__init__()
+                warnings.warn(
+                    "AnyThreadEventLoopPolicy is deprecated, use asyncio.run "
+                    "or asyncio.new_event_loop instead",
+                    DeprecationWarning,
+                    stacklevel=2,
+                )
+
+            def get_event_loop(self) -> asyncio.AbstractEventLoop:
+                try:
+                    return super().get_event_loop()
+                except RuntimeError:
+                    # "There is no current event loop in thread %r"
+                    loop = self.new_event_loop()
+                    self.set_event_loop(loop)
+                    return loop
+
+        _AnyThreadEventLoopPolicy = AnyThreadEventLoopPolicy
+
+    return _AnyThreadEventLoopPolicy
 
 
 class SelectorThread:
index 3c865aae5fda1ee1cfa110d7e76488c20d70ae71..6c355c04fe08cb306ca9cb98ec57fb2eac3e337b 100644 (file)
@@ -17,15 +17,16 @@ import unittest
 import warnings
 
 from concurrent.futures import ThreadPoolExecutor
+import tornado.platform.asyncio
 from tornado import gen
 from tornado.ioloop import IOLoop
 from tornado.platform.asyncio import (
     AsyncIOLoop,
     to_asyncio_future,
-    AnyThreadEventLoopPolicy,
     AddThreadSelectorEventLoop,
 )
-from tornado.testing import AsyncTestCase, gen_test
+from tornado.testing import AsyncTestCase, gen_test, setup_with_context_manager
+from tornado.test.util import ignore_deprecation
 
 
 class AsyncIOLoopTest(AsyncTestCase):
@@ -200,6 +201,12 @@ class SelectorThreadLeakTest(unittest.TestCase):
 
 class AnyThreadEventLoopPolicyTest(unittest.TestCase):
     def setUp(self):
+        setup_with_context_manager(self, ignore_deprecation())
+        # Referencing the event loop policy attributes raises deprecation warnings,
+        # so instead of importing this at the top of the file we capture it here.
+        self.AnyThreadEventLoopPolicy = (
+            tornado.platform.asyncio.AnyThreadEventLoopPolicy
+        )
         self.orig_policy = asyncio.get_event_loop_policy()
         self.executor = ThreadPoolExecutor(1)
 
@@ -232,7 +239,7 @@ class AnyThreadEventLoopPolicyTest(unittest.TestCase):
                 RuntimeError, self.executor.submit(asyncio.get_event_loop).result
             )
             # Set the policy and we can get a loop.
-            asyncio.set_event_loop_policy(AnyThreadEventLoopPolicy())
+            asyncio.set_event_loop_policy(self.AnyThreadEventLoopPolicy())
             self.assertIsInstance(
                 self.executor.submit(asyncio.get_event_loop).result(),
                 asyncio.AbstractEventLoop,
@@ -251,6 +258,6 @@ class AnyThreadEventLoopPolicyTest(unittest.TestCase):
             # IOLoop doesn't (currently) close the underlying loop.
             self.executor.submit(lambda: asyncio.get_event_loop().close()).result()  # type: ignore
 
-            asyncio.set_event_loop_policy(AnyThreadEventLoopPolicy())
+            asyncio.set_event_loop_policy(self.AnyThreadEventLoopPolicy())
             self.assertIsInstance(self.executor.submit(IOLoop.current).result(), IOLoop)
             self.executor.submit(lambda: asyncio.get_event_loop().close()).result()  # type: ignore
index 1ff52206a0e11c3ae8889403961410c3281422e0..261d3d346980c5fa54cd0cafc82baa90499adfa7 100644 (file)
@@ -9,7 +9,10 @@ _import_everything = b"""
 # Explicitly disallow the default event loop so that an error will be raised
 # if something tries to touch it.
 import asyncio
-asyncio.set_event_loop(None)
+import warnings
+with warnings.catch_warnings():
+    warnings.simplefilter("ignore", DeprecationWarning)
+    asyncio.set_event_loop(None)
 
 import importlib
 import tornado