8000 gh-110771: Decompose run_forever() into parts (#110773) · python/cpython@a7e2a10 · GitHub
[go: up one dir, main page]

Skip to content

Commit a7e2a10

Browse files
authored
gh-110771: Decompose run_forever() into parts (#110773)
Effectively introduce an unstable, private (really: protected) API for subclasses that want to override `run_forever()`.
1 parent 0ed2329 commit a7e2a10

File tree

4 files changed

+95
-31
lines changed

4 files changed

+95
-31
lines changed

Lib/asyncio/base_events.py

Lines changed: 38 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -400,6 +400,8 @@ def __init__(self):
400400
self._clock_resolution = time.get_clock_info('monotonic').resolution
401401
self._exception_handler = None
402402
self.set_debug(coroutines._is_debug_mode())
403+
# The preserved state of async generator hooks.
404+
self._old_agen_hooks = None
403405
# In debug mode, if the execution of a callback or a step of a task
404406
# exceed this duration in seconds, the slow callback/task is logged.
405407
self.slow_callback_duration = 0.1
@@ -601,29 +603,52 @@ def _check_running(self):
601603
raise RuntimeError(
602604
'Cannot run the event loop while another loop is running')
603605

604-
def run_forever(self):
605-
"""Run until stop() is called."""
606+
def _run_forever_setup(self):
607+
"""Prepare the run loop to process events.
608+
609+
This method exists so that custom custom event loop subclasses (e.g., event loops
610+
that integrate a GUI event loop with Python's event loop) have access to all the
611+
loop setup logic.
612+
"""
606613
self._check_closed()
607614
self._check_running()
608615
self._set_coroutine_origin_tracking(self._debug)
609616

610-
old_agen_hooks = sys.get_asyncgen_hooks()
611-
try:
612-
self._thread_id = threading.get_ident()
613-
sys.set_asyncgen_hooks(firstiter=self._asyncgen_firstiter_hook,
614-
finalizer=self._asyncgen_finalizer_hook)
617+
self._old_agen_hooks = sys.get_asyncgen_hooks()
618+
self._thread_id = threading.get_ident()
619+
sys.set_asyncgen_hooks(
620+
firstiter=self._asyncgen_firstiter_hook,
621+
finalizer=self._asyncgen_finalizer_hook
622+
)
623+
624+
events._set_running_loop(self)
625+
626+
def _run_forever_cleanup(self):
627+
"""Clean up after an event loop finishes the looping over events.
615628
616-
events._set_running_loop(self)
629+
This method exists so that custom custom event loop subclasses (e.g., event loops
630+
that integrate a GUI event loop with Python's event loop) have access to all the
631+
loop cleanup logic.
632+
"""
633+
self._stopping = False
634+
self._thread_id = None
635+
events._set_running_loop(None)
636+
self._set_coroutine_origin_tracking(False)
637+
# Restore any pre-existing async generator hooks.
638+
if self._old_agen_hooks is not None:
639+
sys.set_asyncgen_hooks(*self._old_agen_hooks)
640+
self._old_agen_hooks = None
641+
642+
def run_forever(self):
643+
"""Run until stop() is called."""
644+
try:
645+
self._run_forever_setup()
617646
while True:
618647
self._run_once()
619648
if self._stopping:
620649
break
621650
finally:
622-
self._stopping = False
623-
self._thread_id = None
624-
events._set_running_loop(None)
625-
self._set_coroutine_origin_tracking(False)
626-
sys.set_asyncgen_hooks(*old_agen_hooks)
651+
self._run_forever_cleanup()
627652

628653
def run_until_complete(self, future):
629654
"""Run until the Future is done.

Lib/asyncio/windows_events.py

Lines changed: 19 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -314,24 +314,25 @@ def __init__(self, proactor=None):
314314
proactor = IocpProactor()
315315
super().__init__(proactor)
316316

317-
def run_forever(self):
318-
try:
319-
assert self._self_reading_future is None
320-
self.call_soon(self._loop_self_reading)
321-
super().run_forever()
322-
finally:
323-
if self._self_reading_future is not None:
324-
ov = self._self_reading_future._ov
325-
self._self_reading_future.cancel()
326-
# self_reading_future was just cancelled so if it hasn't been
327-
# finished yet, it never will be (it's possible that it has
328-
# already finished and its callback is waiting in the queue,
329-
# where it could still happen if the event loop is restarted).
330-
# Unregister it otherwise IocpProactor.close will wait for it
331-
# forever
332-
if ov is not None:
333-
self._proactor._unregister(ov)
334-
self._self_reading_future = None
317+
def _run_forever_setup(self):
318+
assert self._self_reading_future is None
319+
self.call_soon(self._loop_self_reading)
320+
super()._run_forever_setup()
321+
322+
def _run_forever_cleanup(self):
323+
super()._run_forever_cleanup()
324+
if self._self_reading_future is not None:
325+
ov = self._self_reading_future._ov
326+
self._self_reading_future.cancel()
327+
# self_reading_future was just cancelled so if it hasn't been
328+
# finished yet, it never will be (it's possible that it has
329+
# already finished and its callback is waiting in the queue,
330+
# where it could still happen if the event loop is restarted).
331+
# Unregister it otherwise IocpProactor.close will wait for it
332+
# forever
333+
if ov is not None:
334+
self._proactor._unregister(ov)
335+
self._self_reading_future = None
335336

336337
async def create_pipe_connection(self, protocol_factory, address):
337338
f = self._proactor.connect_pipe(address)

Lib/test/test_asyncio/test_base_events.py

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -922,6 +922,43 @@ def test_run_forever_pre_stopped(self):
922922
self.loop.run_forever()
923923
self.loop._selector.select.assert_called_once_with(0)
924924

925+
def test_custom_run_forever_integration(self):
926+
# Test that the run_forever_setup() and run_forever_cleanup() primitives
927+
# can be used to implement a custom run_forever loop.
928+
self.loop._process_events = mock.Mock()
929+
930+
count = 0
931+
932+
def callback():
933+
nonlocal count
934+
count += 1
935+
936+
self.loop.call_soon(callback)
937+
938+
# Set up the custom event loop
939+
self.loop._run_forever_setup()
940+
941+
# Confirm the loop has been started
942+
self.assertEqual(asyncio.get_running_loop(), self.loop)
943+
self.assertTrue(self.loop.is_running())
944+
945+
# Our custom "event loop" just iterates 10 times before exiting.
946+
for i in range(10):
947+
self.loop._run_once()
948+
949+
# Clean up the event loop
950+
self.loop._run_forever_cleanup()
951+
952+
# Confirm the loop has been cleaned up
953+
with self.assertRaises(RuntimeError):
954+
asyncio.get_running_loop()
955+
self.assertFalse(self.loop.is_running())
956+
957+
# Confirm the loop actually did run, processing events 10 times,
958+
# and invoking the callback once.
959+
self.assertEqual(self.loop._process_events.call_count, 10)
960+
self.assertEqual(count, 1)
961+
925962
async def leave_unfinalized_asyncgen(self):
926963
# Create an async generator, iterate it partially, and leave it
927964
# to be garbage collected.
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Expose the setup and cleanup portions of ``asyncio.run_forever()`` as the standalone methods ``asyncio.run_forever_setup()`` and ``asyncio.run_forever_cleanup()``. This allows for tighter integration with GUI event loops.

0 commit comments

Comments
 (0)
0