Skip to content

Commit 623dbb1

Browse files
committed
Merge remote-tracking branch 'origin/master' into asyncgen
2 parents b2023ec + 5b4e768 commit 623dbb1

7 files changed

Lines changed: 136 additions & 61 deletions

File tree

docs/source/usage.rst

Lines changed: 84 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -34,36 +34,72 @@ After::
3434
3535
trio_asyncio.run(async_main, *args)
3636

37-
Equivalently, wrap your main loop in a :func:`trio_asyncio.open_loop` call ::
37+
Equivalently, wrap your main loop (or any other code that needs to talk to
38+
asyncio) in a :func:`trio_asyncio.open_loop` call ::
3839

40+
import trio
3941
import trio_asyncio
4042

41-
async def async_main(*args):
43+
async def async_main_wrapper(*args):
4244
async with trio_asyncio.open_loop() as loop:
43-
pass # async main code goes here
45+
assert loop == asyncio.get_event_loop()
46+
await async_main(*args)
47+
48+
trio.run(async_main_wrapper, *args)
49+
50+
Within the ``async with`` block, an asyncio mainloop is active.
51+
52+
As this code demonstrates, you don't need to pass the ``loop`` argument
53+
around, as :func:`asyncio.get_event_loop` will retrieve it when you're in
54+
the loop's context.
55+
56+
.. note::
57+
58+
Don't do both. The following code **will not work**::
59+
60+
import trio
61+
import trio_asyncio
4462

45-
Within the ``async with`` block, the asyncio mainloop is active. You don't
46-
need to pass the ``loop`` argument around, as
47-
:func:`asyncio.get_event_loop` will do the right thing.
63+
async def async_main_wrapper(*args):
64+
async with trio_asyncio.open_loop() as loop:
65+
await async_main(*args)
66+
67+
trio_asyncio.run(async_main_wrapper, *args)
4868

4969
.. autofunction:: trio_asyncio.open_loop
5070

5171
.. autofunction:: trio_asyncio.run
5272

73+
.. note::
74+
75+
The ``async with open_loop()`` way of running ``trio_asyncio`` is
76+
intended to transparently allow a library to use ``asyncio`` code,
77+
supported by a "local" asyncio loop, without affecting the rest of your
78+
Trio program.
79+
80+
However, currently this doesn't work because Trio does not yet support
81+
``contextvars``. Progress on this limitation is tracked in `this issue
82+
on github <https://github.com/python-trio/trio-asyncio/issues/9>`_.
83+
5384
Stopping
5485
--------
5586

5687
The asyncio mainloop will be stopped automatically when the code within
57-
``async with open_loop()`` exits. Trio-asyncio will process all outstanding
58-
callbacks and terminate. As in asyncio, callbacks which are added during
59-
this step will be ignored.
88+
``async with open_loop()`` / ``trio_asyncion.run()`` exits. Trio-asyncio
89+
will process all outstanding callbacks and terminate. As in asyncio,
90+
callbacks which are added during this step will be ignored.
6091

6192
You cannot restart the loop, nor would you want to.
6293

63-
Asyncio main loop
64-
+++++++++++++++++
94+
Asyncio main loop.
95+
++++++++++++++++++
96+
97+
Short answer: don't.
98+
99+
.. _native-loop:
65100

66-
Well …
101+
Native Mode
102+
-----------
67103

68104
What you really want to do is to use a Trio main loop, and run your asyncio
69105
code in its context. In other words, you should transform this code::
@@ -75,35 +111,38 @@ code in its context. In other words, you should transform this code::
75111
to this::
76112

77113
async def trio_main():
78-
async with trio_asyncio.open_loop() as loop:
79-
await loop.run_asyncio(async_main)
114+
await loop.run_asyncio(async_main)
80115

81116
def main():
82-
trio.run(trio_main)
83-
84-
You don't need to pass around the ``loop`` argument since trio remembers it
85-
in its task structure: ``asyncio.get_event_loop()`` always works while
86-
your program is executing an ``async with open_loop():`` block.
117+
trio_asyncio.run(trio_main)
87118

88-
There is no Trio equivalent to ``loop.run_forever()``. The loop terminates
89-
when you leave the ``async with`` block; it cannot be halted or restarted.
119+
Beside this, no changes to your code are required.
90120

91-
This mode is called an "async loop" or "asynchronous loop" because it is
92-
started from an async (Trio) context.
93-
94-
Compatibility mode
121+
Compatibility Mode
95122
------------------
96123

97124
You still can do things "the asyncio way": the to-be-replaced code from the
98-
previous section still works. However, behind the scenes
99-
a separate thread executes the Trio main loop. It runs in lock-step with
100-
the thread that calls ``loop.run_forever()`` or
101-
``loop.run_until_complete(coro)``. Signals etc. get
102-
delegated if possible (except for [SIGCHLD]_). Thus, there should be no
103-
concurrency issues.
125+
:ref:`previous section <native-loop>`
126+
still works – or at least it attempts to work::
104127

105-
Caveat: you may still experience problems, particularly if your code (or
106-
a library you're calling) does not expect to run in a different thread.
128+
import asyncio
129+
import trio_asyncio
130+
asyncio.set_event_loop_policy(trio_asyncio.TrioPolicy())
131+
132+
def main():
133+
loop = asyncio.get_event_loop()
134+
loop.run_until_complete(async_main())
135+
136+
.. warning::
137+
138+
tl;dr: Don't use Compatibility Mode in production code.
139+
140+
However, this is only possible because this mode starts a separate thread
141+
which executes the asyncio main
142+
loop. It runs in lock-step with the code that calls ``loop.run_forever()``
143+
or ``loop.run_until_complete(coro)``. Signals etc. get
144+
delegated if possible (except for [SIGCHLD]_). Thus, while there should be no
145+
concurrency issues, you may still experience hard-to-debug problems.
107146

108147
.. [SIGCHLD] Python requires you to register SIGCHLD handlers in the main
109148
thread, but doesn't run them at all when waiting for another thread.
@@ -116,20 +155,23 @@ a library you're calling) does not expect to run in a different thread.
116155
with another call to ``loop.run_forever()`` or ``loop.run_until_complete(coro)``,
117156
just as with a regular asyncio loop.
118157

119-
This mode is called a "sync loop" or "synchronous loop" because it is
120-
started and used from a traditional synchronous Python context.
158+
If you use a compatibility-mode loop in a separate thread, you *must* stop and close it
159+
before terminating that thread. Otherwise your thread will leak resources.
121160

122-
If you use a sync loop in a separate thread, you *must* stop and close it
123-
before terminating the thread. Otherwise your thread will leak resources.
161+
In a multi-threaded program, globally setting the event loop policy may not
162+
be a good idea. If you want to run trio-asyncio in a separate thread, you
163+
might get away with using ``TrioPolicy().new_event_loop()`` to create a new
164+
event loop – but a far better idea is to use native mode.
165+
166+
.. note::
124167

125-
.. warning::
126168
Compatibility mode has been added to verify that various test suites,
127-
most notably the one from asyncio itself, continue to work. In a
169+
most notably the tests from asyncio itself, continue to work. In a
128170
real-world program with a long-running asyncio mainloop, you *really*
129-
want to use a :ref:`Trio mainloop <trio-loop>` instead.
171+
want to use a :ref:`native-mode main loop <native-loop>` instead.
130172

131-
The authors reserve the right to not fix compatibility mode bugs if that
132-
would negatively impact trio-asyncio's core functions.
173+
The authors reserve the right to not fix compatibility mode bugs, or
174+
even to remove compatibility mode entirely.
133175

134176
.. autoclass:: trio_asyncio.sync.SyncTrioEventLoop
135177

tests/aiotest/test_timer.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ class TestTimer(aiotest.TestCase):
88
@pytest.mark.trio
99
async def test_display_date(self, loop):
1010
result = []
11-
delay = 0.1
11+
delay = 0.2
1212
count = 3
1313
h = trio.Event()
1414

tests/conftest.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,3 +46,6 @@ def sync_loop():
4646
def pytest_pyfunc_call(pyfuncitem):
4747
if inspect.iscoroutinefunction(pyfuncitem.obj):
4848
pyfuncitem.obj = pytest.mark.trio(pyfuncitem.obj)
49+
50+
51+
asyncio.set_event_loop_policy(trio_asyncio.TrioPolicy())

trio_asyncio/adapter.py

Lines changed: 25 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -7,36 +7,52 @@
77
# import logging
88
# logger = logging.getLogger(__name__)
99

10-
from functools import wraps
10+
from functools import wraps, partial
1111

1212
__all__ = ['trio2aio', 'aio2trio']
1313

1414

1515
def trio2aio(proc):
1616
if isasyncgenfunction(proc):
17+
1718
@wraps(proc)
18-
def call(*args):
19-
return trio_asyncio.wrap_generator(proc, *args)
19+
def call(*args, **kwargs):
20+
proc_ = proc
21+
if kwargs:
22+
proc_ = partial(proc_, **kwargs)
23+
return trio_asyncio.wrap_generator(proc_, *args)
2024

2125
else:
26+
2227
@wraps(proc)
23-
async def call(*args):
24-
return await trio_asyncio.run_asyncio(proc, *args)
28+
async def call(*args, **kwargs):
29+
proc_ = proc
30+
if kwargs:
31+
proc_ = partial(proc_, **kwargs)
32+
return await trio_asyncio.run_asyncio(proc_, *args)
2533

2634
return call
2735

2836

2937
def aio2trio(proc):
38+
"""Decorate a Trio function so that it's callable by asyncio (only)."""
39+
3040
@wraps(proc)
31-
async def call(*args):
32-
return await trio_asyncio.run_trio(proc, *args)
41+
async def call(*args, **kwargs):
42+
proc_ = proc
43+
if kwargs:
44+
proc_ = partial(proc_, **kwargs)
45+
return await trio_asyncio.run_trio(proc_, *args)
3346

3447
return call
3548

3649

3750
def aio2trio_task(proc):
3851
@wraps(proc)
39-
async def call(*args):
40-
trio_asyncio.run_trio_task(proc, *args)
52+
async def call(*args, **kwargs):
53+
proc_ = proc
54+
if kwargs:
55+
proc_ = partial(proc_, **kwargs)
56+
trio_asyncio.run_trio_task(proc_, *args)
4157

4258
return call

trio_asyncio/async_.py

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -106,11 +106,9 @@ def _main_loop_exit(self):
106106
self._thread = None
107107

108108
async with trio.open_nursery() as nursery:
109-
old_loop = asyncio.get_event_loop()
110109
loop = TrioEventLoop(queue_len=queue_len)
111110
try:
112111
loop._closed = False
113-
asyncio.set_event_loop(loop)
114112
await loop._main_loop_init(nursery)
115113
await nursery.start(loop._main_loop)
116114
await yield_(loop)
@@ -120,5 +118,4 @@ def _main_loop_exit(self):
120118
finally:
121119
await loop._main_loop_exit()
122120
loop.close()
123-
asyncio.set_event_loop(old_loop)
124121
nursery.cancel_scope.cancel()

trio_asyncio/base.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -646,6 +646,7 @@ async def _main_loop_init(self, nursery):
646646
self._nursery = nursery
647647
self._task = trio.hazmat.current_task()
648648
self._token = trio.hazmat.current_trio_token()
649+
asyncio.events._set_running_loop(self)
649650

650651
async def _main_loop(self, task_status=trio.TASK_STATUS_IGNORED):
651652
"""Run the loop by processing its event queue.
@@ -725,6 +726,7 @@ async def _main_loop_exit(self):
725726
# clean core fields
726727
self._nursery = None
727728
self._task = None
729+
asyncio.events._set_running_loop(None)
728730

729731
def is_running(self):
730732
if self._stopped is None:

trio_asyncio/loop.py

Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,7 @@ def set_event_loop(self, loop):
9898
# in a sub-task, which is exactly what we intend to be possible
9999
if self._trio_local._loop is not None and loop is not None and \
100100
self._trio_local._task == task:
101-
raise RuntimeError('You cannot replace an event loop.')
101+
raise RuntimeError('You cannot replace an event loop.', self._trio_local._loop, loop)
102102
self._trio_local._loop = loop
103103
self._trio_local._task = task
104104

@@ -178,17 +178,35 @@ def wrap_generator(proc, *args):
178178

179179

180180
async def run_asyncio(proc, *args):
181+
"""Run an asyncio function or method from Trio.
182+
183+
:return: whatever the procedure returns.
184+
:raises: whatever the procedure raises.
185+
186+
This is a Trio coroutine.
187+
"""
188+
181189
loop = asyncio.get_event_loop()
182190
if not isinstance(loop, TrioEventLoop):
183191
raise RuntimeError("Need to run in a trio_asyncio.open_loop() context")
184192
return await loop.run_asyncio(proc, *args)
185193

186194

187-
async def run_coroutine(fut, scope=None):
195+
async def run_coroutine(fut):
196+
"""Wait for an asyncio future/coroutine.
197+
198+
Cancelling the current Trio scope will cancel the future/coroutine.
199+
200+
Cancelling the future/coroutine will cause an
201+
``asyncio.CancelledError``.
202+
203+
This is a Trio coroutine.
204+
"""
205+
188206
loop = asyncio.get_event_loop()
189207
if not isinstance(loop, TrioEventLoop):
190208
raise RuntimeError("Need to run in a trio_asyncio.open_loop() context")
191-
return await loop.run_coroutine(fut, scope=scope)
209+
return await loop.run_coroutine(fut)
192210

193211

194212
def run_trio(proc, *args):
@@ -230,6 +248,3 @@ async def _run_task(proc, args):
230248
return await proc(*args)
231249

232250
trio.run(_run_task, proc, args)
233-
234-
235-
asyncio.set_event_loop_policy(TrioPolicy())

0 commit comments

Comments
 (0)