Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 841d9ee

Browse files
committed
Issue #25304: Add asyncio.run_coroutine_threadsafe(). By Vincent Michel.
1 parent 3795d12 commit 841d9ee

6 files changed

Lines changed: 147 additions & 19 deletions

File tree

Lib/asyncio/futures.py

Lines changed: 58 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -390,22 +390,64 @@ def __iter__(self):
390390
__await__ = __iter__ # make compatible with 'await' expression
391391

392392

393-
def wrap_future(fut, *, loop=None):
394-
"""Wrap concurrent.futures.Future object."""
395-
if isinstance(fut, Future):
396-
return fut
397-
assert isinstance(fut, concurrent.futures.Future), \
398-
'concurrent.futures.Future is expected, got {!r}'.format(fut)
399-
if loop is None:
400-
loop = events.get_event_loop()
401-
new_future = Future(loop=loop)
393+
def _set_concurrent_future_state(concurrent, source):
394+
"""Copy state from a future to a concurrent.futures.Future."""
395+
assert source.done()
396+
if source.cancelled():
397+
concurrent.cancel()
398+
if not concurrent.set_running_or_notify_cancel():
399+
return
400+
exception = source.exception()
401+
if exception is not None:
402+
concurrent.set_exception(exception)
403+
else:
404+
result = source.result()
405+
concurrent.set_result(result)
406+
407+
408+
def _chain_future(source, destination):
409+
"""Chain two futures so that when one completes, so does the other.
410+
411+
The result (or exception) of source will be copied to destination.
412+
If destination is cancelled, source gets cancelled too.
413+
Compatible with both asyncio.Future and concurrent.futures.Future.
414+
"""
415+
if not isinstance(source, (Future, concurrent.futures.Future)):
416+
raise TypeError('A future is required for source argument')
417+
if not isinstance(destination, (Future, concurrent.futures.Future)):
418+
raise TypeError('A future is required for destination argument')
419+
source_loop = source._loop if isinstance(source, Future) else None
420+
dest_loop = destination._loop if isinstance(destination, Future) else None
421+
422+
def _set_state(future, other):
423+
if isinstance(future, Future):
424+
future._copy_state(other)
425+
else:
426+
_set_concurrent_future_state(future, other)
402427

403-
def _check_cancel_other(f):
404-
if f.cancelled():
405-
fut.cancel()
428+
def _call_check_cancel(destination):
429+
if destination.cancelled():
430+
if source_loop is None or source_loop is dest_loop:
431+
source.cancel()
432+
else:
433+
source_loop.call_soon_threadsafe(source.cancel)
406434

407-
new_future.add_done_callback(_check_cancel_other)
408-
fut.add_done_callback(
409-
lambda future: loop.call_soon_threadsafe(
410-
new_future._copy_state, future))
435+
def _call_set_state(source):
436+
if dest_loop is None or dest_loop is source_loop:
437+
_set_state(destination, source)
438+
else:
439+
dest_loop.call_soon_threadsafe(_set_state, destination, source)
440+
441+
destination.add_done_callback(_call_check_cancel)
442+
source.add_done_callback(_call_set_state)
443+
444+
445+
def wrap_future(future, *, loop=None):
446+
"""Wrap concurrent.futures.Future object."""
447+
if isinstance(future, Future):
448+
return future
449+
assert isinstance(future, concurrent.futures.Future), \
450+
'concurrent.futures.Future is expected, got {!r}'.format(future)
451+
new_future = Future(loop=loop)
452+
_chain_future(future, new_future)
411453
return new_future

Lib/asyncio/tasks.py

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
__all__ = ['Task',
44
'FIRST_COMPLETED', 'FIRST_EXCEPTION', 'ALL_COMPLETED',
55
'wait', 'wait_for', 'as_completed', 'sleep', 'async',
6-
'gather', 'shield', 'ensure_future',
6+
'gather', 'shield', 'ensure_future', 'run_coroutine_threadsafe',
77
]
88

99
import concurrent.futures
@@ -692,3 +692,19 @@ def _done_callback(inner):
692692

693693
inner.add_done_callback(_done_callback)
694694
return outer
695+
696+
697+
def run_coroutine_threadsafe(coro, loop):
698+
"""Submit a coroutine object to a given event loop.
699+
700+
Return a concurrent.futures.Future to access the result.
701+
"""
702+
if not coroutines.iscoroutine(coro):
703+
raise TypeError('A coroutine object is required')
704+
future = concurrent.futures.Future()
705+
706+
def callback():
707+
futures._chain_future(ensure_future(coro, loop=loop), future)
708+
709+
loop.call_soon_threadsafe(callback)
710+
return future

Lib/test/test_asyncio/test_futures.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -174,8 +174,6 @@ def func_repr(func):
174174
'<Future cancelled>')
175175

176176
def test_copy_state(self):
177-
# Test the internal _copy_state method since it's being directly
178-
# invoked in other modules.
179177
f = asyncio.Future(loop=self.loop)
180178
f.set_result(10)
181179

Lib/test/test_asyncio/test_tasks.py

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2100,5 +2100,72 @@ def outer():
21002100
self.assertIsInstance(f.exception(), RuntimeError)
21012101

21022102

2103+
class RunCoroutineThreadsafeTests(test_utils.TestCase):
2104+
"""Test case for futures.submit_to_loop."""
2105+
2106+
def setUp(self):
2107+
self.loop = self.new_test_loop(self.time_gen)
2108+
2109+
def time_gen(self):
2110+
"""Handle the timer."""
2111+
yield 0 # second
2112+
yield 1 # second
2113+
2114+
@asyncio.coroutine
2115+
def add(self, a, b, fail=False, cancel=False):
2116+
"""Wait 1 second and return a + b."""
2117+
yield from asyncio.sleep(1, loop=self.loop)
2118+
if fail:
2119+
raise RuntimeError("Fail!")
2120+
if cancel:
2121+
asyncio.tasks.Task.current_task(self.loop).cancel()
2122+
yield
2123+
return a + b
2124+
2125+
def target(self, fail=False, cancel=False, timeout=None):
2126+
"""Run add coroutine in the event loop."""
2127+
coro = self.add(1, 2, fail=fail, cancel=cancel)
2128+
future = asyncio.run_coroutine_threadsafe(coro, self.loop)
2129+
try:
2130+
return future.result(timeout)
2131+
finally:
2132+
future.done() or future.cancel()
2133+
2134+
def test_run_coroutine_threadsafe(self):
2135+
"""Test coroutine submission from a thread to an event loop."""
2136+
future = self.loop.run_in_executor(None, self.target)
2137+
result = self.loop.run_until_complete(future)
2138+
self.assertEqual(result, 3)
2139+
2140+
def test_run_coroutine_threadsafe_with_exception(self):
2141+
"""Test coroutine submission from a thread to an event loop
2142+
when an exception is raised."""
2143+
future = self.loop.run_in_executor(None, self.target, True)
2144+
with self.assertRaises(RuntimeError) as exc_context:
2145+
self.loop.run_until_complete(future)
2146+
self.assertIn("Fail!", exc_context.exception.args)
2147+
2148+
def test_run_coroutine_threadsafe_with_timeout(self):
2149+
"""Test coroutine submission from a thread to an event loop
2150+
when a timeout is raised."""
2151+
callback = lambda: self.target(timeout=0)
2152+
future = self.loop.run_in_executor(None, callback)
2153+
with self.assertRaises(asyncio.TimeoutError):
2154+
self.loop.run_until_complete(future)
2155+
# Clear the time generator and tasks
2156+
test_utils.run_briefly(self.loop)
2157+
# Check that there's no pending task (add has been cancelled)
2158+
for task in asyncio.Task.all_tasks(self.loop):
2159+
self.assertTrue(task.done())
2160+
2161+
def test_run_coroutine_threadsafe_task_cancelled(self):
2162+
"""Test coroutine submission from a tread to an event loop
2163+
when the task is cancelled."""
2164+
callback = lambda: self.target(cancel=True)
2165+
future = self.loop.run_in_executor(None, callback)
2166+
with self.assertRaises(asyncio.CancelledError):
2167+
self.loop.run_until_complete(future)
2168+
2169+
21032170
if __name__ == '__main__':
21042171
unittest.main()

Misc/ACKS

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -929,6 +929,7 @@ Steven Miale
929929
Trent Mick
930930
Jason Michalski
931931
Franck Michea
932+
Vincent Michel
932933
Tom Middleton
933934
Thomas Miedema
934935
Stan Mihai

Misc/NEWS

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,10 @@ Core and Builtins
9090
Library
9191
-------
9292

93+
- Issue #25304: Add asyncio.run_coroutine_threadsafe(). This lets you
94+
submit a coroutine to a loop from another thread, returning a
95+
concurrent.futures.Future. By Vincent Michel.
96+
9397
- Issue #25232: Fix CGIRequestHandler to split the query from the URL at the
9498
first question mark (?) rather than the last. Patch from Xiang Zhang.
9599

0 commit comments

Comments
 (0)