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-100160: Restore and deprecate implicit creation of an event loop #100410

Merged
10 changes: 4 additions & 6 deletions Doc/library/asyncio-eventloop.rst
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ an event loop:
running event loop.

If there is no running event loop set, the function will return
the result of calling ``get_event_loop_policy().get_event_loop()``.
the result of the ``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 @@ -59,11 +59,9 @@ an event loop:
instead of using these lower level functions to manually create and close an
event loop.

.. note::
In Python versions 3.10.0--3.10.8 and 3.11.0 this function
(and other functions which used it implicitly) emitted a
:exc:`DeprecationWarning` if there was no running event loop, even if
the current loop was set.
.. deprecated:: 3.12
Deprecation warning is emitted if there is no current event loop.
In some future Python release it will become an error.

.. function:: set_event_loop(loop)

Expand Down
7 changes: 4 additions & 3 deletions Doc/library/asyncio-policy.rst
Original file line number Diff line number Diff line change
Expand Up @@ -116,9 +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 a :exc:`RuntimeError` if there is no
current event loop set.
.. deprecated:: 3.12
:meth:`get_event_loop` now emits a :exc:`DeprecationWarning` if there
is no current event loop set and it decides to create one.
In some future Python release it will become an error.


.. class:: WindowsSelectorEventLoopPolicy
Expand Down
17 changes: 5 additions & 12 deletions Doc/whatsnew/3.12.rst
Original file line number Diff line number Diff line change
Expand Up @@ -392,6 +392,11 @@ Deprecated
:exc:`ImportWarning`).
(Contributed by Brett Cannon in :gh:`65961`.)

* The :meth:`~asyncio.DefaultEventLoopPolicy.get_event_loop` of the default
event loop policy now emits a :exc:`DeprecationWarning` if there
is no current event loop set and it decides to create one.
(Contributed by Serhiy Storchaka and Łukasz Langa in :gh:`100160`.)


Pending Removal in Python 3.13
------------------------------
Expand Down Expand Up @@ -692,18 +697,6 @@ 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 a :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 is set in the policy.
(Contributed by Serhiy Storchaka in :gh:`93453`.)


Build Changes
=============
Expand Down
24 changes: 23 additions & 1 deletion 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.
current context and the current policy does not specify to create one.

It should never return None."""
raise NotImplementedError
Expand Down Expand Up @@ -672,6 +672,28 @@ 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()):
stacklevel = 2
try:
f = sys._getframe(1)
except AttributeError:
pass
else:
# Move up the call stack so that the warning is attached
# to the line outside asyncio itself.
while f:
serhiy-storchaka marked this conversation as resolved.
Show resolved Hide resolved
module = f.f_globals.get('__name__')
if not (module == 'asyncio' or module.startswith('asyncio.')):
break
f = f.f_back
stacklevel += 1
import warnings
serhiy-storchaka marked this conversation as resolved.
Show resolved Hide resolved
warnings.warn('There is no current event loop',
DeprecationWarning, stacklevel=stacklevel)
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
35 changes: 31 additions & 4 deletions Lib/test/test_asyncio/test_events.py
Original file line number Diff line number Diff line change
Expand Up @@ -2550,8 +2550,33 @@ def test_event_loop_policy(self):
def test_get_event_loop(self):
policy = asyncio.DefaultEventLoopPolicy()
self.assertIsNone(policy._local._loop)
with self.assertRaisesRegex(RuntimeError, 'no current event loop'):
policy.get_event_loop()
with self.assertWarns(DeprecationWarning) as cm:
loop = policy.get_event_loop()
self.assertEqual(cm.filename, __file__)
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:

with self.assertWarns(DeprecationWarning) as cm:
loop = policy.get_event_loop()
self.addCleanup(loop.close)
self.assertEqual(cm.filename, __file__)

# 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()

def test_get_event_loop_after_set_none(self):
policy = asyncio.DefaultEventLoopPolicy()
Expand Down Expand Up @@ -2737,8 +2762,10 @@ def test_get_event_loop_returns_running_loop2(self):
loop = asyncio.new_event_loop()
self.addCleanup(loop.close)

with self.assertRaisesRegex(RuntimeError, 'no current'):
asyncio.get_event_loop()
with self.assertWarns(DeprecationWarning) as cm:
loop2 = asyncio.get_event_loop()
self.addCleanup(loop2.close)
self.assertEqual(cm.filename, __file__)
asyncio.set_event_loop(None)
with self.assertRaisesRegex(RuntimeError, 'no current'):
asyncio.get_event_loop()
Expand Down
8 changes: 6 additions & 2 deletions Lib/test/test_asyncio/test_unix_events.py
Original file line number Diff line number Diff line change
Expand Up @@ -1884,7 +1884,9 @@ async def test_fork_not_share_event_loop(self):
if pid == 0:
# child
try:
loop = asyncio.get_event_loop_policy().get_event_loop()
with self.assertWarns(DeprecationWarning):
loop = asyncio.get_event_loop_policy().get_event_loop()
os.write(w, b'LOOP:' + str(id(loop)).encode())
except RuntimeError:
os.write(w, b'NO LOOP')
except:
Expand All @@ -1893,7 +1895,9 @@ async def test_fork_not_share_event_loop(self):
os._exit(0)
else:
# parent
self.assertEqual(os.read(r, 100), b'NO LOOP')
result = os.read(r, 100)
self.assertEqual(result[:5], b'LOOP:', result)
self.assertNotEqual(int(result[5:]), id(loop))
wait_process(pid, exitcode=0)

@hashlib_helper.requires_hashdigest('md5')
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
Emit a deprecation warning in
:meth:`asyncio.DefaultEventLoopPolicy.get_event_loop` if there is no current
event loop set and it decides to create one.