Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

gh-93453: No longer create an event loop in get_event_loop() #98440

Merged
merged 6 commits into from
Dec 6, 2022
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 11 additions & 8 deletions Doc/library/asyncio-eventloop.rst
Original file line number Diff line number Diff line change
Expand Up @@ -42,10 +42,12 @@ an event loop:

Get the current event loop.

If there is no current event loop set in the current OS thread,
the OS thread is main, and :func:`set_event_loop` has not yet
been called, asyncio will create a new event loop and set it as the
current one.
When called from a coroutine or a callback (e.g. scheduled with
call_soon or similar API), this function will always return the
running event loop.

If there is no running event loop set, the function will return
the result of ``get_event_loop_policy().get_event_loop()`` call.

Because this function has rather complex behavior (especially
when custom event loop policies are in use), using the
Expand All @@ -55,10 +57,11 @@ an event loop:
Consider also using the :func:`asyncio.run` function instead of using
lower level functions to manually create and close an event loop.

.. deprecated:: 3.10
Deprecation warning is emitted if there is no running event loop.
In future Python releases, this function will be an alias of
:func:`get_running_loop`.
.. note::
In Python versions 3.10.0--3.10.8 and 3.11.0 this function
(and other functions which used it implicitly) emmitted a
:exc:`DeprecationWarning` if there was no running event loop, even if
the current loop was set.

.. function:: set_event_loop(loop)

Expand Down
2 changes: 1 addition & 1 deletion Doc/library/asyncio-llapi-index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ Obtaining the Event Loop
- The **preferred** function to get the running event loop.

* - :func:`asyncio.get_event_loop`
- Get an event loop instance (current or via the policy).
- Get an event loop instance (running or current via the current policy).

* - :func:`asyncio.set_event_loop`
- Set the event loop as current via the current policy.
Expand Down
4 changes: 4 additions & 0 deletions Doc/library/asyncio-policy.rst
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,10 @@ asyncio ships with the following built-in policies:

On Windows, :class:`ProactorEventLoop` is now used by default.

.. versionchanged:: 3.12
:meth:`get_event_loop` now raises an :exc:`RuntimeError` if there is no
current event loop set.


.. class:: WindowsSelectorEventLoopPolicy

Expand Down
12 changes: 12 additions & 0 deletions Doc/whatsnew/3.12.rst
Original file line number Diff line number Diff line change
Expand Up @@ -682,6 +682,18 @@ Changes in the Python API
around process-global resources, which are best managed from the main interpreter.
(Contributed by Dong-hee Na in :gh:`99127`.)

* :func:`asyncio.get_event_loop` and many other :mod:`asyncio` functions like
:func:`~asyncio.ensure_future`, :func:`~asyncio.shield` or
:func:`~asyncio.gather`, and also the
:meth:`~asyncio.BaseDefaultEventLoopPolicy.get_event_loop` method of
:class:`~asyncio.BaseDefaultEventLoopPolicy` now raise an :exc:`RuntimeError`
if called when there is no running event loop and the current event loop was
not set.
Previously they implicitly created and set a new current event loop.
:exc:`DeprecationWarning` is no longer emitted if there is no running
event loop but the current event loop was set.
(Contributed by Serhiy Storchaka in :gh:`93453`.)


Build Changes
=============
Expand Down
18 changes: 2 additions & 16 deletions Lib/asyncio/events.py
Original file line number Diff line number Diff line change
Expand Up @@ -619,7 +619,7 @@ def get_event_loop(self):

Returns an event loop object implementing the BaseEventLoop interface,
or raises an exception in case no event loop has been set for the
current context and the current policy does not specify to create one.
current context.

It should never return None."""
raise NotImplementedError
Expand Down Expand Up @@ -672,11 +672,6 @@ def get_event_loop(self):

Returns an instance of EventLoop or raises an exception.
"""
if (self._local._loop is None and
not self._local._set_called and
threading.current_thread() is threading.main_thread()):
self.set_event_loop(self.new_event_loop())

if self._local._loop is None:
raise RuntimeError('There is no current event loop in thread %r.'
% threading.current_thread().name)
Expand Down Expand Up @@ -786,16 +781,9 @@ def get_event_loop():
the result of `get_event_loop_policy().get_event_loop()` call.
"""
# NOTE: this function is implemented in C (see _asynciomodule.c)
return _py__get_event_loop()


def _get_event_loop(stacklevel=3):
current_loop = _get_running_loop()
if current_loop is not None:
return current_loop
import warnings
warnings.warn('There is no current event loop',
DeprecationWarning, stacklevel=stacklevel)
return get_event_loop_policy().get_event_loop()


Expand Down Expand Up @@ -825,15 +813,14 @@ def set_child_watcher(watcher):
_py__set_running_loop = _set_running_loop
_py_get_running_loop = get_running_loop
_py_get_event_loop = get_event_loop
_py__get_event_loop = _get_event_loop


try:
# get_event_loop() is one of the most frequently called
# functions in asyncio. Pure Python implementation is
# about 4 times slower than C-accelerated.
from _asyncio import (_get_running_loop, _set_running_loop,
get_running_loop, get_event_loop, _get_event_loop)
get_running_loop, get_event_loop)
except ImportError:
pass
else:
Expand All @@ -842,7 +829,6 @@ def set_child_watcher(watcher):
_c__set_running_loop = _set_running_loop
_c_get_running_loop = get_running_loop
_c_get_event_loop = get_event_loop
_c__get_event_loop = _get_event_loop


if hasattr(os, 'fork'):
Expand Down
4 changes: 2 additions & 2 deletions Lib/asyncio/futures.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ def __init__(self, *, loop=None):
the default event loop.
"""
if loop is None:
self._loop = events._get_event_loop()
self._loop = events.get_event_loop()
else:
self._loop = loop
self._callbacks = []
Expand Down Expand Up @@ -413,7 +413,7 @@ def wrap_future(future, *, loop=None):
assert isinstance(future, concurrent.futures.Future), \
f'concurrent.futures.Future is expected, got {future!r}'
if loop is None:
loop = events._get_event_loop()
loop = events.get_event_loop()
new_future = loop.create_future()
_chain_future(future, new_future)
return new_future
Expand Down
4 changes: 2 additions & 2 deletions Lib/asyncio/streams.py
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,7 @@ class FlowControlMixin(protocols.Protocol):

def __init__(self, loop=None):
if loop is None:
self._loop = events._get_event_loop(stacklevel=4)
self._loop = events.get_event_loop()
else:
self._loop = loop
self._paused = False
Expand Down Expand Up @@ -404,7 +404,7 @@ def __init__(self, limit=_DEFAULT_LIMIT, loop=None):

self._limit = limit
if loop is None:
self._loop = events._get_event_loop()
self._loop = events.get_event_loop()
else:
self._loop = loop
self._buffer = bytearray()
Expand Down
6 changes: 3 additions & 3 deletions Lib/asyncio/tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -582,7 +582,7 @@ def as_completed(fs, *, timeout=None):
from .queues import Queue # Import here to avoid circular import problem.
done = Queue()

loop = events._get_event_loop()
loop = events.get_event_loop()
todo = {ensure_future(f, loop=loop) for f in set(fs)}
timeout_handle = None

Expand Down Expand Up @@ -668,7 +668,7 @@ def _ensure_future(coro_or_future, *, loop=None):
'is required')

if loop is None:
loop = events._get_event_loop(stacklevel=4)
loop = events.get_event_loop()
try:
return loop.create_task(coro_or_future)
except RuntimeError:
Expand Down Expand Up @@ -749,7 +749,7 @@ def gather(*coros_or_futures, return_exceptions=False):
gather won't cancel any other awaitables.
"""
if not coros_or_futures:
loop = events._get_event_loop()
loop = events.get_event_loop()
outer = loop.create_future()
outer.set_result([])
return outer
Expand Down
2 changes: 1 addition & 1 deletion Lib/test/test_asyncio/test_base_events.py
Original file line number Diff line number Diff line change
Expand Up @@ -746,7 +746,7 @@ async def coro():
def test_env_var_debug(self):
code = '\n'.join((
'import asyncio',
'loop = asyncio.get_event_loop()',
'loop = asyncio.new_event_loop()',
'print(loop.get_debug())'))

# Test with -E to not fail if the unit test was run with
Expand Down
75 changes: 19 additions & 56 deletions Lib/test/test_asyncio/test_events.py
Original file line number Diff line number Diff line change
Expand Up @@ -2550,29 +2550,8 @@ def test_event_loop_policy(self):
def test_get_event_loop(self):
policy = asyncio.DefaultEventLoopPolicy()
self.assertIsNone(policy._local._loop)

loop = policy.get_event_loop()
self.assertIsInstance(loop, asyncio.AbstractEventLoop)

self.assertIs(policy._local._loop, loop)
self.assertIs(loop, policy.get_event_loop())
loop.close()

def test_get_event_loop_calls_set_event_loop(self):
policy = asyncio.DefaultEventLoopPolicy()

with mock.patch.object(
policy, "set_event_loop",
wraps=policy.set_event_loop) as m_set_event_loop:

loop = policy.get_event_loop()

# policy._local._loop must be set through .set_event_loop()
# (the unix DefaultEventLoopPolicy needs this call to attach
# the child watcher correctly)
m_set_event_loop.assert_called_with(loop)

loop.close()
with self.assertRaisesRegex(RuntimeError, 'no current event loop'):
policy.get_event_loop()

def test_get_event_loop_after_set_none(self):
policy = asyncio.DefaultEventLoopPolicy()
Expand All @@ -2599,7 +2578,8 @@ def test_new_event_loop(self):

def test_set_event_loop(self):
policy = asyncio.DefaultEventLoopPolicy()
old_loop = policy.get_event_loop()
old_loop = policy.new_event_loop()
policy.set_event_loop(old_loop)

self.assertRaises(TypeError, policy.set_event_loop, object())

Expand Down Expand Up @@ -2716,15 +2696,11 @@ def get_event_loop(self):
asyncio.set_event_loop_policy(Policy())
loop = asyncio.new_event_loop()

with self.assertWarns(DeprecationWarning) as cm:
with self.assertRaises(TestError):
asyncio.get_event_loop()
self.assertEqual(cm.filename, __file__)
with self.assertRaises(TestError):
asyncio.get_event_loop()
asyncio.set_event_loop(None)
with self.assertWarns(DeprecationWarning) as cm:
with self.assertRaises(TestError):
asyncio.get_event_loop()
self.assertEqual(cm.filename, __file__)
with self.assertRaises(TestError):
asyncio.get_event_loop()

with self.assertRaisesRegex(RuntimeError, 'no running'):
asyncio.get_running_loop()
Expand All @@ -2738,16 +2714,11 @@ async def func():
loop.run_until_complete(func())

asyncio.set_event_loop(loop)
with self.assertWarns(DeprecationWarning) as cm:
with self.assertRaises(TestError):
asyncio.get_event_loop()
self.assertEqual(cm.filename, __file__)

with self.assertRaises(TestError):
asyncio.get_event_loop()
asyncio.set_event_loop(None)
with self.assertWarns(DeprecationWarning) as cm:
with self.assertRaises(TestError):
asyncio.get_event_loop()
self.assertEqual(cm.filename, __file__)
with self.assertRaises(TestError):
asyncio.get_event_loop()

finally:
asyncio.set_event_loop_policy(old_policy)
Expand All @@ -2766,15 +2737,11 @@ def test_get_event_loop_returns_running_loop2(self):
loop = asyncio.new_event_loop()
self.addCleanup(loop.close)

with self.assertWarns(DeprecationWarning) as cm:
loop2 = asyncio.get_event_loop()
self.addCleanup(loop2.close)
self.assertEqual(cm.filename, __file__)
with self.assertRaisesRegex(RuntimeError, 'no current'):
asyncio.get_event_loop()
asyncio.set_event_loop(None)
with self.assertWarns(DeprecationWarning) as cm:
with self.assertRaisesRegex(RuntimeError, 'no current'):
asyncio.get_event_loop()
self.assertEqual(cm.filename, __file__)
with self.assertRaisesRegex(RuntimeError, 'no current'):
asyncio.get_event_loop()

with self.assertRaisesRegex(RuntimeError, 'no running'):
asyncio.get_running_loop()
Expand All @@ -2788,15 +2755,11 @@ async def func():
loop.run_until_complete(func())

asyncio.set_event_loop(loop)
with self.assertWarns(DeprecationWarning) as cm:
self.assertIs(asyncio.get_event_loop(), loop)
self.assertEqual(cm.filename, __file__)
self.assertIs(asyncio.get_event_loop(), loop)

asyncio.set_event_loop(None)
with self.assertWarns(DeprecationWarning) as cm:
with self.assertRaisesRegex(RuntimeError, 'no current'):
asyncio.get_event_loop()
self.assertEqual(cm.filename, __file__)
with self.assertRaisesRegex(RuntimeError, 'no current'):
asyncio.get_event_loop()

finally:
asyncio.set_event_loop_policy(old_policy)
Expand Down
24 changes: 8 additions & 16 deletions Lib/test/test_asyncio/test_futures.py
Original file line number Diff line number Diff line change
Expand Up @@ -146,10 +146,8 @@ def test_initial_state(self):
self.assertTrue(f.cancelled())

def test_constructor_without_loop(self):
with self.assertWarns(DeprecationWarning) as cm:
with self.assertRaisesRegex(RuntimeError, 'There is no current event loop'):
self._new_future()
self.assertEqual(cm.filename, __file__)
with self.assertRaisesRegex(RuntimeError, 'no current event loop'):
self._new_future()

def test_constructor_use_running_loop(self):
async def test():
Expand All @@ -159,12 +157,10 @@ async def test():
self.assertIs(f.get_loop(), self.loop)

def test_constructor_use_global_loop(self):
# Deprecated in 3.10
# Deprecated in 3.10, undeprecated in 3.12
asyncio.set_event_loop(self.loop)
self.addCleanup(asyncio.set_event_loop, None)
with self.assertWarns(DeprecationWarning) as cm:
f = self._new_future()
self.assertEqual(cm.filename, __file__)
f = self._new_future()
self.assertIs(f._loop, self.loop)
self.assertIs(f.get_loop(), self.loop)

Expand Down Expand Up @@ -500,10 +496,8 @@ def run(arg):
return (arg, threading.get_ident())
ex = concurrent.futures.ThreadPoolExecutor(1)
f1 = ex.submit(run, 'oi')
with self.assertWarns(DeprecationWarning) as cm:
with self.assertRaises(RuntimeError):
asyncio.wrap_future(f1)
self.assertEqual(cm.filename, __file__)
with self.assertRaisesRegex(RuntimeError, 'no current event loop'):
asyncio.wrap_future(f1)
ex.shutdown(wait=True)

def test_wrap_future_use_running_loop(self):
Expand All @@ -518,16 +512,14 @@ async def test():
ex.shutdown(wait=True)

def test_wrap_future_use_global_loop(self):
# Deprecated in 3.10
# Deprecated in 3.10, undeprecated in 3.12
asyncio.set_event_loop(self.loop)
self.addCleanup(asyncio.set_event_loop, None)
def run(arg):
return (arg, threading.get_ident())
ex = concurrent.futures.ThreadPoolExecutor(1)
f1 = ex.submit(run, 'oi')
with self.assertWarns(DeprecationWarning) as cm:
f2 = asyncio.wrap_future(f1)
self.assertEqual(cm.filename, __file__)
f2 = asyncio.wrap_future(f1)
self.assertIs(self.loop, f2._loop)
ex.shutdown(wait=True)

Expand Down
Loading