Skip to content

Commit bc5b923

Browse files
authored
Merge pull request #3461 from bdarnell/deprecations-314
Python 3.14 deprecates the asyncio event loop policy system, so make (most of) the necessary changes. The deprecation of set_event_loop is extremely disruptive to AsyncTestCase, so I've asked if it can remain undeprecated in python/cpython#130322. The testing.py changes are temporary until this is resolved. Fixes #3458
2 parents e2afb86 + 5eea953 commit bc5b923

File tree

8 files changed

+110
-58
lines changed

8 files changed

+110
-58
lines changed

.github/workflows/test.yml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,9 @@ jobs:
5555
tox_env: py312-full
5656
- python: '3.13'
5757
tox_env: py313-full
58-
- python: '3.14.0-alpha.1 - 3.14'
58+
- python: '3.14.0-alpha.4'
59+
# TODO: Alpha 5 has a bug that affects us, so stick with alpha 4 until 6 is released.
60+
# https://github.com/python/cpython/issues/130380
5961
tox_env: py314-full
6062
- python: 'pypy-3.10'
6163
# Pypy is a lot slower due to jit warmup costs, so don't run the

docs/asyncio.rst

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,13 @@
33

44
.. automodule:: tornado.platform.asyncio
55
:members:
6+
7+
8+
..
9+
AnyThreadEventLoopPolicy is created dynamically in getattr, so
10+
introspection won't find it automatically. This has the unfortunate
11+
side effect of moving it to the top of the page but it's better than
12+
having it missing entirely.
13+
14+
.. autoclass:: AnyThreadEventLoopPolicy
15+
:members:

tornado/platform/asyncio.py

Lines changed: 60 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -386,58 +386,76 @@ def to_asyncio_future(tornado_future: asyncio.Future) -> asyncio.Future:
386386
return convert_yielded(tornado_future)
387387

388388

389-
if sys.platform == "win32" and hasattr(asyncio, "WindowsSelectorEventLoopPolicy"):
390-
# "Any thread" and "selector" should be orthogonal, but there's not a clean
391-
# interface for composing policies so pick the right base.
392-
_BasePolicy = asyncio.WindowsSelectorEventLoopPolicy # type: ignore
393-
else:
394-
_BasePolicy = asyncio.DefaultEventLoopPolicy
389+
_AnyThreadEventLoopPolicy = None
395390

396391

397-
class AnyThreadEventLoopPolicy(_BasePolicy): # type: ignore
398-
"""Event loop policy that allows loop creation on any thread.
392+
def __getattr__(name: str) -> typing.Any:
393+
# The event loop policy system is deprecated in Python 3.14; simply accessing
394+
# the name asyncio.DefaultEventLoopPolicy will raise a warning. Lazily create
395+
# the AnyThreadEventLoopPolicy class so that the warning is only raised if
396+
# the policy is used.
397+
if name != "AnyThreadEventLoopPolicy":
398+
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
399399

400-
The default `asyncio` event loop policy only automatically creates
401-
event loops in the main threads. Other threads must create event
402-
loops explicitly or `asyncio.get_event_loop` (and therefore
403-
`.IOLoop.current`) will fail. Installing this policy allows event
404-
loops to be created automatically on any thread, matching the
405-
behavior of Tornado versions prior to 5.0 (or 5.0 on Python 2).
400+
global _AnyThreadEventLoopPolicy
401+
if _AnyThreadEventLoopPolicy is None:
402+
if sys.platform == "win32" and hasattr(
403+
asyncio, "WindowsSelectorEventLoopPolicy"
404+
):
405+
# "Any thread" and "selector" should be orthogonal, but there's not a clean
406+
# interface for composing policies so pick the right base.
407+
_BasePolicy = asyncio.WindowsSelectorEventLoopPolicy # type: ignore
408+
else:
409+
_BasePolicy = asyncio.DefaultEventLoopPolicy
406410

407-
Usage::
411+
class AnyThreadEventLoopPolicy(_BasePolicy): # type: ignore
412+
"""Event loop policy that allows loop creation on any thread.
408413
409-
asyncio.set_event_loop_policy(AnyThreadEventLoopPolicy())
414+
The default `asyncio` event loop policy only automatically creates
415+
event loops in the main threads. Other threads must create event
416+
loops explicitly or `asyncio.get_event_loop` (and therefore
417+
`.IOLoop.current`) will fail. Installing this policy allows event
418+
loops to be created automatically on any thread, matching the
419+
behavior of Tornado versions prior to 5.0 (or 5.0 on Python 2).
410420
411-
.. versionadded:: 5.0
421+
Usage::
412422
413-
.. deprecated:: 6.2
423+
asyncio.set_event_loop_policy(AnyThreadEventLoopPolicy())
414424
415-
``AnyThreadEventLoopPolicy`` affects the implicit creation
416-
of an event loop, which is deprecated in Python 3.10 and
417-
will be removed in a future version of Python. At that time
418-
``AnyThreadEventLoopPolicy`` will no longer be useful.
419-
If you are relying on it, use `asyncio.new_event_loop`
420-
or `asyncio.run` explicitly in any non-main threads that
421-
need event loops.
422-
"""
425+
.. versionadded:: 5.0
423426
424-
def __init__(self) -> None:
425-
super().__init__()
426-
warnings.warn(
427-
"AnyThreadEventLoopPolicy is deprecated, use asyncio.run "
428-
"or asyncio.new_event_loop instead",
429-
DeprecationWarning,
430-
stacklevel=2,
431-
)
427+
.. deprecated:: 6.2
432428
433-
def get_event_loop(self) -> asyncio.AbstractEventLoop:
434-
try:
435-
return super().get_event_loop()
436-
except RuntimeError:
437-
# "There is no current event loop in thread %r"
438-
loop = self.new_event_loop()
439-
self.set_event_loop(loop)
440-
return loop
429+
``AnyThreadEventLoopPolicy`` affects the implicit creation
430+
of an event loop, which is deprecated in Python 3.10 and
431+
will be removed in a future version of Python. At that time
432+
``AnyThreadEventLoopPolicy`` will no longer be useful.
433+
If you are relying on it, use `asyncio.new_event_loop`
434+
or `asyncio.run` explicitly in any non-main threads that
435+
need event loops.
436+
"""
437+
438+
def __init__(self) -> None:
439+
super().__init__()
440+
warnings.warn(
441+
"AnyThreadEventLoopPolicy is deprecated, use asyncio.run "
442+
"or asyncio.new_event_loop instead",
443+
DeprecationWarning,
444+
stacklevel=2,
445+
)
446+
447+
def get_event_loop(self) -> asyncio.AbstractEventLoop:
448+
try:
449+
return super().get_event_loop()
450+
except RuntimeError:
451+
# "There is no current event loop in thread %r"
452+
loop = self.new_event_loop()
453+
self.set_event_loop(loop)
454+
return loop
455+
456+
_AnyThreadEventLoopPolicy = AnyThreadEventLoopPolicy
457+
458+
return _AnyThreadEventLoopPolicy
441459

442460

443461
class SelectorThread:

tornado/test/asyncio_test.py

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -17,15 +17,16 @@
1717
import warnings
1818

1919
from concurrent.futures import ThreadPoolExecutor
20+
import tornado.platform.asyncio
2021
from tornado import gen
2122
from tornado.ioloop import IOLoop
2223
from tornado.platform.asyncio import (
2324
AsyncIOLoop,
2425
to_asyncio_future,
25-
AnyThreadEventLoopPolicy,
2626
AddThreadSelectorEventLoop,
2727
)
28-
from tornado.testing import AsyncTestCase, gen_test
28+
from tornado.testing import AsyncTestCase, gen_test, setup_with_context_manager
29+
from tornado.test.util import ignore_deprecation
2930

3031

3132
class AsyncIOLoopTest(AsyncTestCase):
@@ -111,10 +112,6 @@ class LeakTest(unittest.TestCase):
111112
def setUp(self):
112113
# Trigger a cleanup of the mapping so we start with a clean slate.
113114
AsyncIOLoop(make_current=False).close()
114-
# If we don't clean up after ourselves other tests may fail on
115-
# py34.
116-
self.orig_policy = asyncio.get_event_loop_policy()
117-
asyncio.set_event_loop_policy(asyncio.DefaultEventLoopPolicy())
118115

119116
def tearDown(self):
120117
try:
@@ -124,7 +121,6 @@ def tearDown(self):
124121
pass
125122
else:
126123
loop.close()
127-
asyncio.set_event_loop_policy(self.orig_policy)
128124

129125
def test_ioloop_close_leak(self):
130126
orig_count = len(IOLoop._ioloop_for_asyncio)
@@ -205,6 +201,12 @@ def test_tornado(self):
205201

206202
class AnyThreadEventLoopPolicyTest(unittest.TestCase):
207203
def setUp(self):
204+
setup_with_context_manager(self, ignore_deprecation())
205+
# Referencing the event loop policy attributes raises deprecation warnings,
206+
# so instead of importing this at the top of the file we capture it here.
207+
self.AnyThreadEventLoopPolicy = (
208+
tornado.platform.asyncio.AnyThreadEventLoopPolicy
209+
)
208210
self.orig_policy = asyncio.get_event_loop_policy()
209211
self.executor = ThreadPoolExecutor(1)
210212

@@ -237,7 +239,7 @@ def test_asyncio_accessor(self):
237239
RuntimeError, self.executor.submit(asyncio.get_event_loop).result
238240
)
239241
# Set the policy and we can get a loop.
240-
asyncio.set_event_loop_policy(AnyThreadEventLoopPolicy())
242+
asyncio.set_event_loop_policy(self.AnyThreadEventLoopPolicy())
241243
self.assertIsInstance(
242244
self.executor.submit(asyncio.get_event_loop).result(),
243245
asyncio.AbstractEventLoop,
@@ -256,6 +258,6 @@ def test_tornado_accessor(self):
256258
# IOLoop doesn't (currently) close the underlying loop.
257259
self.executor.submit(lambda: asyncio.get_event_loop().close()).result() # type: ignore
258260

259-
asyncio.set_event_loop_policy(AnyThreadEventLoopPolicy())
261+
asyncio.set_event_loop_policy(self.AnyThreadEventLoopPolicy())
260262
self.assertIsInstance(self.executor.submit(IOLoop.current).result(), IOLoop)
261263
self.executor.submit(lambda: asyncio.get_event_loop().close()).result() # type: ignore

tornado/test/import_test.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,10 @@
99
# Explicitly disallow the default event loop so that an error will be raised
1010
# if something tries to touch it.
1111
import asyncio
12-
asyncio.set_event_loop(None)
12+
import warnings
13+
with warnings.catch_warnings():
14+
warnings.simplefilter("ignore", DeprecationWarning)
15+
asyncio.set_event_loop(None)
1316
1417
import importlib
1518
import tornado

tornado/test/ioloop_test.py

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -132,7 +132,7 @@ def test_add_callback_from_signal_other_thread(self):
132132
# Very crude test, just to make sure that we cover this case.
133133
# This also happens to be the first test where we run an IOLoop in
134134
# a non-main thread.
135-
other_ioloop = IOLoop()
135+
other_ioloop = IOLoop(make_current=False)
136136
thread = threading.Thread(target=other_ioloop.start)
137137
thread.start()
138138
with ignore_deprecation():
@@ -152,7 +152,7 @@ def target():
152152
closing.set()
153153
other_ioloop.close(all_fds=True)
154154

155-
other_ioloop = IOLoop()
155+
other_ioloop = IOLoop(make_current=False)
156156
thread = threading.Thread(target=target)
157157
thread.start()
158158
closing.wait()
@@ -276,8 +276,12 @@ def close(self):
276276

277277
sockobj, port = bind_unused_port()
278278
socket_wrapper = SocketWrapper(sockobj)
279-
io_loop = IOLoop()
280-
io_loop.add_handler(socket_wrapper, lambda fd, events: None, IOLoop.READ)
279+
io_loop = IOLoop(make_current=False)
280+
io_loop.run_sync(
281+
lambda: io_loop.add_handler(
282+
socket_wrapper, lambda fd, events: None, IOLoop.READ
283+
)
284+
)
281285
io_loop.close(all_fds=True)
282286
self.assertTrue(socket_wrapper.closed)
283287

tornado/test/simple_httpclient_test.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -210,7 +210,7 @@ def test_singleton(self):
210210
SimpleAsyncHTTPClient(), SimpleAsyncHTTPClient(force_instance=True)
211211
)
212212
# different IOLoops use different objects
213-
with closing(IOLoop()) as io_loop2:
213+
with closing(IOLoop(make_current=False)) as io_loop2:
214214

215215
async def make_client():
216216
await gen.sleep(0)

tornado/testing.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,19 @@ def setUp(self) -> None:
155155
category=DeprecationWarning,
156156
module=r"tornado\..*",
157157
)
158+
if (3, 14) <= py_ver:
159+
# TODO: This is a temporary hack pending resolution of
160+
# https://github.com/python/cpython/issues/130322
161+
# If set_event_loop is undeprecated, we can remove it; if not
162+
# we need substantial changes to this class to use asyncio.Runner
163+
# like IsolatedAsyncioTestCase does.
164+
setup_with_context_manager(self, warnings.catch_warnings())
165+
warnings.filterwarnings(
166+
"ignore",
167+
message="'asyncio.set_event_loop' is deprecated",
168+
category=DeprecationWarning,
169+
module="tornado.testing",
170+
)
158171
super().setUp()
159172
if type(self).get_new_ioloop is not AsyncTestCase.get_new_ioloop:
160173
warnings.warn("get_new_ioloop is deprecated", DeprecationWarning)

0 commit comments

Comments
 (0)