1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
|
#!/usr/bin/python
#
# Urwid main loop code using Python-3.5 features (Trio, Curio, etc)
# Copyright (C) 2018 Toshio Kuratomi
# Copyright (C) 2019 Tamas Nepusz
#
# This library is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
# License as published by the Free Software Foundation; either
# version 2.1 of the License, or (at your option) any later version.
#
# This library is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public
# License along with this library; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
#
# Urwid web site: http://excess.org/urwid/
from .main_loop import EventLoop, ExitMainLoop
class TrioEventLoop(EventLoop):
"""
Event loop based on the ``trio`` module.
``trio`` is an async library for Python 3.5 and later.
"""
def __init__(self):
"""Constructor."""
import trio
self._idle_handle = 0
self._idle_callbacks = {}
self._pending_tasks = []
self._trio = trio
self._nursery = None
self._sleep = trio.sleep
try:
self._wait_readable = trio.lowlevel.wait_readable
except AttributeError:
# Trio 0.14 or older
self._wait_readable = trio.hazmat.wait_readable
def alarm(self, seconds, callback):
"""Calls `callback()` a given time from now. No parameters are passed
to the callback.
Parameters:
seconds: time in seconds to wait before calling the callback
callback: function to call from the event loop
Returns:
a handle that may be passed to `remove_alarm()`
"""
return self._start_task(self._alarm_task, seconds, callback)
def enter_idle(self, callback):
"""Calls `callback()` when the event loop enters the idle state.
There is no such thing as being idle in a Trio event loop so we
simulate it by repeatedly calling `callback()` with a short delay.
"""
self._idle_handle += 1
self._idle_callbacks[self._idle_handle] = callback
return self._idle_handle
def remove_alarm(self, handle):
"""Removes an alarm.
Parameters:
handle: the handle of the alarm to remove
"""
return self._cancel_scope(handle)
def remove_enter_idle(self, handle):
"""Removes an idle callback.
Parameters:
handle: the handle of the idle callback to remove
"""
try:
del self._idle_callbacks[handle]
except KeyError:
return False
return True
def remove_watch_file(self, handle):
"""Removes a file descriptor being watched for input.
Parameters:
handle: the handle of the file descriptor callback to remove
Returns:
True if the file descriptor was watched, False otherwise
"""
return self._cancel_scope(handle)
def _cancel_scope(self, scope):
"""Cancels the given Trio cancellation scope.
Returns:
True if the scope was cancelled, False if it was cancelled already
before invoking this function
"""
existed = not scope.cancel_called
scope.cancel()
return existed
def run(self):
"""Starts the event loop. Exits the loop when any callback raises an
exception. If ExitMainLoop is raised, exits cleanly.
"""
idle_callbacks = self._idle_callbacks
# This class is duplicated in run_async(). It would be nice to move
# this somewhere outside, but we cannot do it yet becase we need to
# derive it from self._trio.abc.Instrument
class TrioIdleCallbackInstrument(self._trio.abc.Instrument):
def before_io_wait(self, timeout):
if timeout > 0:
for idle_callback in idle_callbacks.values():
idle_callback()
emulate_idle_callbacks = TrioIdleCallbackInstrument()
with self._trio.MultiError.catch(self._handle_main_loop_exception):
self._trio.run(self._main_task, instruments=[emulate_idle_callbacks])
async def run_async(self):
"""Starts the main loop and blocks asynchronously until the main loop
exits. This allows one to embed an urwid app in a Trio app even if the
Trio event loop is already running. Example::
with trio.open_nursery() as nursery:
event_loop = urwid.TrioEventLoop()
# [...launch other async tasks in the nursery...]
loop = urwid.MainLoop(widget, event_loop=event_loop)
with loop.start():
await event_loop.run_async()
nursery.cancel_scope.cancel()
"""
idle_callbacks = self._idle_callbacks
# This class is duplicated in run_async(). It would be nice to move
# this somewhere outside, but we cannot do it yet becase we need to
# derive it from self._trio.abc.Instrument
class TrioIdleCallbackInstrument(self._trio.abc.Instrument):
def before_io_wait(self, timeout):
if timeout > 0:
for idle_callback in idle_callbacks.values():
idle_callback()
emulate_idle_callbacks = TrioIdleCallbackInstrument()
try:
add_instrument = self._trio.lowlevel.add_instrument
remove_instrument = self._trio.lowlevel.remove_instrument
except AttributeError:
# Trio 0.14 or older
add_instrument = self._trio.hazmat.add_instrument
remove_instrument = self._trio.hazmat.remove_instrument
with self._trio.MultiError.catch(self._handle_main_loop_exception):
add_instrument(emulate_idle_callbacks)
try:
await self._main_task()
finally:
remove_instrument(emulate_idle_callbacks)
def watch_file(self, fd, callback):
"""Calls `callback()` when the given file descriptor has some data
to read. No parameters are passed to the callback.
Parameters:
fd: file descriptor to watch for input
callback: function to call when some input is available
Returns:
a handle that may be passed to `remove_watch_file()`
"""
return self._start_task(self._watch_task, fd, callback)
async def _alarm_task(self, scope, seconds, callback):
"""Asynchronous task that sleeps for a given number of seconds and then
calls the given callback.
Parameters:
scope: the cancellation scope that can be used to cancel the task
seconds: the number of seconds to wait
callback: the callback to call
"""
with scope:
await self._sleep(seconds)
callback()
def _handle_main_loop_exception(self, exc):
"""Handles exceptions raised from the main loop, catching ExitMainLoop
instead of letting it propagate through.
Note that since Trio may collect multiple exceptions from tasks into a
Trio MultiError, we cannot simply use a try..catch clause, we need a
helper function like this.
"""
self._idle_callbacks.clear()
if isinstance(exc, ExitMainLoop):
return None
else:
return exc
async def _main_task(self):
"""Main Trio task that opens a nursery and then sleeps until the user
exits the app by raising ExitMainLoop.
"""
try:
async with self._trio.open_nursery() as self._nursery:
self._schedule_pending_tasks()
await self._trio.sleep_forever()
finally:
self._nursery = None
def _schedule_pending_tasks(self):
"""Schedules all pending asynchronous tasks that were created before
the nursery to be executed on the nursery soon.
"""
for task, scope, args in self._pending_tasks:
self._nursery.start_soon(task, scope, *args)
del self._pending_tasks[:]
def _start_task(self, task, *args):
"""Starts an asynchronous task in the Trio nursery managed by the
main loop. If the nursery has not started yet, store a reference to
the task and the arguments so we can start the task when the nursery
is open.
Parameters:
task: a Trio task to run
Returns:
a cancellation scope for the Trio task
"""
scope = self._trio.CancelScope()
if self._nursery:
self._nursery.start_soon(task, scope, *args)
else:
self._pending_tasks.append((task, scope, args))
return scope
async def _watch_task(self, scope, fd, callback):
"""Asynchronous task that watches the given file descriptor and calls
the given callback whenever the file descriptor becomes readable.
Parameters:
scope: the cancellation scope that can be used to cancel the task
fd: the file descriptor to watch
callback: the callback to call
"""
with scope:
# We check for the scope being cancelled before calling
# wait_readable because if callback cancels the scope, fd might be
# closed and calling wait_readable with a closed fd does not work.
while not scope.cancel_called:
await self._wait_readable(fd)
callback()
|