From: Ben Darnell Date: Thu, 20 Feb 2025 19:15:54 +0000 (-0500) Subject: asyncio: Use dynamic magic for AnyThreadEventLoopPolicy X-Git-Tag: v6.5.0b1~26^2~3 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=40bbd6e8c55e33eca8c57f53983da2a1bb3df613;p=thirdparty%2Ftornado.git asyncio: Use dynamic magic for AnyThreadEventLoopPolicy Accessing the base policy classes now triggers a deprecation warning so we must use our own getattr hook to avoid it except when needed. --- diff --git a/docs/asyncio.rst b/docs/asyncio.rst index fc737451..1f90bf68 100644 --- a/docs/asyncio.rst +++ b/docs/asyncio.rst @@ -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 diff --git a/tornado/platform/asyncio.py b/tornado/platform/asyncio.py index 2e9f4248..4635fecb 100644 --- a/tornado/platform/asyncio.py +++ b/tornado/platform/asyncio.py @@ -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: diff --git a/tornado/test/asyncio_test.py b/tornado/test/asyncio_test.py index 3c865aae..6c355c04 100644 --- a/tornado/test/asyncio_test.py +++ b/tornado/test/asyncio_test.py @@ -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 diff --git a/tornado/test/import_test.py b/tornado/test/import_test.py index 1ff52206..261d3d34 100644 --- a/tornado/test/import_test.py +++ b/tornado/test/import_test.py @@ -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