urwid 2.6.0.post0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of urwid might be problematic. Click here for more details.
- urwid/__init__.py +333 -0
- urwid/canvas.py +1413 -0
- urwid/command_map.py +137 -0
- urwid/container.py +59 -0
- urwid/decoration.py +65 -0
- urwid/display/__init__.py +97 -0
- urwid/display/_posix_raw_display.py +413 -0
- urwid/display/_raw_display_base.py +914 -0
- urwid/display/_web.css +12 -0
- urwid/display/_web.js +462 -0
- urwid/display/_win32.py +171 -0
- urwid/display/_win32_raw_display.py +269 -0
- urwid/display/common.py +1219 -0
- urwid/display/curses.py +690 -0
- urwid/display/escape.py +624 -0
- urwid/display/html_fragment.py +251 -0
- urwid/display/lcd.py +518 -0
- urwid/display/raw.py +37 -0
- urwid/display/web.py +636 -0
- urwid/event_loop/__init__.py +55 -0
- urwid/event_loop/abstract_loop.py +175 -0
- urwid/event_loop/asyncio_loop.py +231 -0
- urwid/event_loop/glib_loop.py +294 -0
- urwid/event_loop/main_loop.py +721 -0
- urwid/event_loop/select_loop.py +230 -0
- urwid/event_loop/tornado_loop.py +206 -0
- urwid/event_loop/trio_loop.py +302 -0
- urwid/event_loop/twisted_loop.py +269 -0
- urwid/event_loop/zmq_loop.py +275 -0
- urwid/font.py +695 -0
- urwid/graphics.py +96 -0
- urwid/highlight.css +19 -0
- urwid/listbox.py +1899 -0
- urwid/monitored_list.py +522 -0
- urwid/numedit.py +376 -0
- urwid/signals.py +330 -0
- urwid/split_repr.py +130 -0
- urwid/str_util.py +358 -0
- urwid/text_layout.py +632 -0
- urwid/treetools.py +515 -0
- urwid/util.py +557 -0
- urwid/version.py +16 -0
- urwid/vterm.py +1806 -0
- urwid/widget/__init__.py +181 -0
- urwid/widget/attr_map.py +161 -0
- urwid/widget/attr_wrap.py +140 -0
- urwid/widget/bar_graph.py +649 -0
- urwid/widget/big_text.py +77 -0
- urwid/widget/box_adapter.py +126 -0
- urwid/widget/columns.py +1145 -0
- urwid/widget/constants.py +574 -0
- urwid/widget/container.py +227 -0
- urwid/widget/divider.py +110 -0
- urwid/widget/edit.py +718 -0
- urwid/widget/filler.py +403 -0
- urwid/widget/frame.py +539 -0
- urwid/widget/grid_flow.py +539 -0
- urwid/widget/line_box.py +194 -0
- urwid/widget/overlay.py +829 -0
- urwid/widget/padding.py +597 -0
- urwid/widget/pile.py +971 -0
- urwid/widget/popup.py +170 -0
- urwid/widget/progress_bar.py +141 -0
- urwid/widget/scrollable.py +597 -0
- urwid/widget/solid_fill.py +44 -0
- urwid/widget/text.py +354 -0
- urwid/widget/widget.py +852 -0
- urwid/widget/widget_decoration.py +166 -0
- urwid/widget/wimp.py +792 -0
- urwid/wimp.py +23 -0
- urwid-2.6.0.post0.dist-info/COPYING +504 -0
- urwid-2.6.0.post0.dist-info/METADATA +332 -0
- urwid-2.6.0.post0.dist-info/RECORD +75 -0
- urwid-2.6.0.post0.dist-info/WHEEL +5 -0
- urwid-2.6.0.post0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
# Urwid main loop code using Python-3.5 features (Trio, Curio, etc)
|
|
2
|
+
# Copyright (C) 2018 Toshio Kuratomi
|
|
3
|
+
# Copyright (C) 2019 Tamas Nepusz
|
|
4
|
+
#
|
|
5
|
+
# This library is free software; you can redistribute it and/or
|
|
6
|
+
# modify it under the terms of the GNU Lesser General Public
|
|
7
|
+
# License as published by the Free Software Foundation; either
|
|
8
|
+
# version 2.1 of the License, or (at your option) any later version.
|
|
9
|
+
#
|
|
10
|
+
# This library is distributed in the hope that it will be useful,
|
|
11
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
12
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
|
13
|
+
# Lesser General Public License for more details.
|
|
14
|
+
#
|
|
15
|
+
# You should have received a copy of the GNU Lesser General Public
|
|
16
|
+
# License along with this library; if not, write to the Free Software
|
|
17
|
+
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
|
18
|
+
#
|
|
19
|
+
# Urwid web site: https://urwid.org/
|
|
20
|
+
|
|
21
|
+
"""Trio Runner based urwid EventLoop implementation.
|
|
22
|
+
|
|
23
|
+
Trio library is required.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
from __future__ import annotations
|
|
27
|
+
|
|
28
|
+
import logging
|
|
29
|
+
import typing
|
|
30
|
+
|
|
31
|
+
import exceptiongroup
|
|
32
|
+
import trio
|
|
33
|
+
|
|
34
|
+
from .abstract_loop import EventLoop, ExitMainLoop
|
|
35
|
+
|
|
36
|
+
if typing.TYPE_CHECKING:
|
|
37
|
+
import io
|
|
38
|
+
from collections.abc import Awaitable, Callable, Hashable, Mapping
|
|
39
|
+
|
|
40
|
+
from typing_extensions import Concatenate, ParamSpec
|
|
41
|
+
|
|
42
|
+
_Spec = ParamSpec("_Spec")
|
|
43
|
+
|
|
44
|
+
__all__ = ("TrioEventLoop",)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class _TrioIdleCallbackInstrument(trio.abc.Instrument):
|
|
48
|
+
"""IDLE callbacks emulation helper."""
|
|
49
|
+
|
|
50
|
+
__slots__ = ("idle_callbacks",)
|
|
51
|
+
|
|
52
|
+
def __init__(self, idle_callbacks: Mapping[Hashable, Callable[[], typing.Any]]):
|
|
53
|
+
self.idle_callbacks = idle_callbacks
|
|
54
|
+
|
|
55
|
+
def before_io_wait(self, timeout: float) -> None:
|
|
56
|
+
if timeout > 0:
|
|
57
|
+
for idle_callback in self.idle_callbacks.values():
|
|
58
|
+
idle_callback()
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class TrioEventLoop(EventLoop):
|
|
62
|
+
"""
|
|
63
|
+
Event loop based on the ``trio`` module.
|
|
64
|
+
|
|
65
|
+
``trio`` is an async library for Python 3.5 and later.
|
|
66
|
+
"""
|
|
67
|
+
|
|
68
|
+
def __init__(self) -> None:
|
|
69
|
+
"""Constructor."""
|
|
70
|
+
super().__init__()
|
|
71
|
+
self.logger = logging.getLogger(__name__).getChild(self.__class__.__name__)
|
|
72
|
+
|
|
73
|
+
self._idle_handle = 0
|
|
74
|
+
self._idle_callbacks: dict[int, Callable[[], typing.Any]] = {}
|
|
75
|
+
self._pending_tasks: list[tuple[Callable[_Spec, Awaitable], trio.CancelScope, _Spec.args]] = []
|
|
76
|
+
|
|
77
|
+
self._nursery: trio.Nursery | None = None
|
|
78
|
+
|
|
79
|
+
self._sleep = trio.sleep
|
|
80
|
+
self._wait_readable = trio.lowlevel.wait_readable
|
|
81
|
+
|
|
82
|
+
def alarm(
|
|
83
|
+
self,
|
|
84
|
+
seconds: float,
|
|
85
|
+
callback: Callable[[], typing.Any],
|
|
86
|
+
) -> trio.CancelScope:
|
|
87
|
+
"""Calls `callback()` a given time from now.
|
|
88
|
+
|
|
89
|
+
:param seconds: time in seconds to wait before calling the callback
|
|
90
|
+
:type seconds: float
|
|
91
|
+
:param callback: function to call from the event loop
|
|
92
|
+
:type callback: Callable[[], typing.Any]
|
|
93
|
+
:return: a handle that may be passed to `remove_alarm()`
|
|
94
|
+
:rtype: trio.CancelScope
|
|
95
|
+
|
|
96
|
+
No parameters are passed to the callback.
|
|
97
|
+
"""
|
|
98
|
+
return self._start_task(self._alarm_task, seconds, callback)
|
|
99
|
+
|
|
100
|
+
def enter_idle(self, callback: Callable[[], typing.Any]) -> int:
|
|
101
|
+
"""Calls `callback()` when the event loop enters the idle state.
|
|
102
|
+
|
|
103
|
+
There is no such thing as being idle in a Trio event loop so we
|
|
104
|
+
simulate it by repeatedly calling `callback()` with a short delay.
|
|
105
|
+
"""
|
|
106
|
+
self._idle_handle += 1
|
|
107
|
+
self._idle_callbacks[self._idle_handle] = callback
|
|
108
|
+
return self._idle_handle
|
|
109
|
+
|
|
110
|
+
def remove_alarm(self, handle: trio.CancelScope) -> bool:
|
|
111
|
+
"""Removes an alarm.
|
|
112
|
+
|
|
113
|
+
Parameters:
|
|
114
|
+
handle: the handle of the alarm to remove
|
|
115
|
+
"""
|
|
116
|
+
return self._cancel_scope(handle)
|
|
117
|
+
|
|
118
|
+
def remove_enter_idle(self, handle: int) -> bool:
|
|
119
|
+
"""Removes an idle callback.
|
|
120
|
+
|
|
121
|
+
Parameters:
|
|
122
|
+
handle: the handle of the idle callback to remove
|
|
123
|
+
"""
|
|
124
|
+
try:
|
|
125
|
+
del self._idle_callbacks[handle]
|
|
126
|
+
except KeyError:
|
|
127
|
+
return False
|
|
128
|
+
return True
|
|
129
|
+
|
|
130
|
+
def remove_watch_file(self, handle: trio.CancelScope) -> bool:
|
|
131
|
+
"""Removes a file descriptor being watched for input.
|
|
132
|
+
|
|
133
|
+
Parameters:
|
|
134
|
+
handle: the handle of the file descriptor callback to remove
|
|
135
|
+
|
|
136
|
+
Returns:
|
|
137
|
+
True if the file descriptor was watched, False otherwise
|
|
138
|
+
"""
|
|
139
|
+
return self._cancel_scope(handle)
|
|
140
|
+
|
|
141
|
+
def _cancel_scope(self, scope: trio.CancelScope) -> bool:
|
|
142
|
+
"""Cancels the given Trio cancellation scope.
|
|
143
|
+
|
|
144
|
+
Returns:
|
|
145
|
+
True if the scope was cancelled, False if it was cancelled already
|
|
146
|
+
before invoking this function
|
|
147
|
+
"""
|
|
148
|
+
existed = not scope.cancel_called
|
|
149
|
+
scope.cancel()
|
|
150
|
+
return existed
|
|
151
|
+
|
|
152
|
+
def run(self) -> None:
|
|
153
|
+
"""Starts the event loop. Exits the loop when any callback raises an
|
|
154
|
+
exception. If ExitMainLoop is raised, exits cleanly.
|
|
155
|
+
"""
|
|
156
|
+
|
|
157
|
+
emulate_idle_callbacks = _TrioIdleCallbackInstrument(self._idle_callbacks)
|
|
158
|
+
|
|
159
|
+
with exceptiongroup.catch({BaseException: self._handle_main_loop_exception}):
|
|
160
|
+
trio.run(self._main_task, instruments=[emulate_idle_callbacks])
|
|
161
|
+
|
|
162
|
+
async def run_async(self) -> None:
|
|
163
|
+
"""Starts the main loop and blocks asynchronously until the main loop
|
|
164
|
+
exits. This allows one to embed an urwid app in a Trio app even if the
|
|
165
|
+
Trio event loop is already running. Example::
|
|
166
|
+
|
|
167
|
+
with trio.open_nursery() as nursery:
|
|
168
|
+
event_loop = urwid.TrioEventLoop()
|
|
169
|
+
|
|
170
|
+
# [...launch other async tasks in the nursery...]
|
|
171
|
+
|
|
172
|
+
loop = urwid.MainLoop(widget, event_loop=event_loop)
|
|
173
|
+
with loop.start():
|
|
174
|
+
await event_loop.run_async()
|
|
175
|
+
|
|
176
|
+
nursery.cancel_scope.cancel()
|
|
177
|
+
"""
|
|
178
|
+
|
|
179
|
+
emulate_idle_callbacks = _TrioIdleCallbackInstrument(self._idle_callbacks)
|
|
180
|
+
|
|
181
|
+
with exceptiongroup.catch({BaseException: self._handle_main_loop_exception}):
|
|
182
|
+
trio.lowlevel.add_instrument(emulate_idle_callbacks)
|
|
183
|
+
try:
|
|
184
|
+
await self._main_task()
|
|
185
|
+
finally:
|
|
186
|
+
trio.lowlevel.remove_instrument(emulate_idle_callbacks)
|
|
187
|
+
|
|
188
|
+
def watch_file(
|
|
189
|
+
self,
|
|
190
|
+
fd: int | io.IOBase,
|
|
191
|
+
callback: Callable[[], typing.Any],
|
|
192
|
+
) -> trio.CancelScope:
|
|
193
|
+
"""Calls `callback()` when the given file descriptor has some data
|
|
194
|
+
to read. No parameters are passed to the callback.
|
|
195
|
+
|
|
196
|
+
Parameters:
|
|
197
|
+
fd: file descriptor to watch for input
|
|
198
|
+
callback: function to call when some input is available
|
|
199
|
+
|
|
200
|
+
Returns:
|
|
201
|
+
a handle that may be passed to `remove_watch_file()`
|
|
202
|
+
"""
|
|
203
|
+
return self._start_task(self._watch_task, fd, callback)
|
|
204
|
+
|
|
205
|
+
async def _alarm_task(
|
|
206
|
+
self,
|
|
207
|
+
scope: trio.CancelScope,
|
|
208
|
+
seconds: float,
|
|
209
|
+
callback: Callable[[], typing.Any],
|
|
210
|
+
) -> None:
|
|
211
|
+
"""Asynchronous task that sleeps for a given number of seconds and then
|
|
212
|
+
calls the given callback.
|
|
213
|
+
|
|
214
|
+
Parameters:
|
|
215
|
+
scope: the cancellation scope that can be used to cancel the task
|
|
216
|
+
seconds: the number of seconds to wait
|
|
217
|
+
callback: the callback to call
|
|
218
|
+
"""
|
|
219
|
+
with scope:
|
|
220
|
+
await self._sleep(seconds)
|
|
221
|
+
callback()
|
|
222
|
+
|
|
223
|
+
def _handle_main_loop_exception(self, exc: BaseException) -> None:
|
|
224
|
+
"""Handles exceptions raised from the main loop, catching ExitMainLoop
|
|
225
|
+
instead of letting it propagate through.
|
|
226
|
+
|
|
227
|
+
Note that since Trio may collect multiple exceptions from tasks into a
|
|
228
|
+
Trio MultiError, we cannot simply use a try..catch clause, we need a
|
|
229
|
+
helper function like this.
|
|
230
|
+
"""
|
|
231
|
+
self._idle_callbacks.clear()
|
|
232
|
+
if isinstance(exc, exceptiongroup.BaseExceptionGroup) and len(exc.exceptions) == 1:
|
|
233
|
+
exc = exc.exceptions[0]
|
|
234
|
+
|
|
235
|
+
if isinstance(exc, ExitMainLoop):
|
|
236
|
+
return
|
|
237
|
+
|
|
238
|
+
raise exc.with_traceback(exc.__traceback__) from None
|
|
239
|
+
|
|
240
|
+
async def _main_task(self) -> None:
|
|
241
|
+
"""Main Trio task that opens a nursery and then sleeps until the user
|
|
242
|
+
exits the app by raising ExitMainLoop.
|
|
243
|
+
"""
|
|
244
|
+
try:
|
|
245
|
+
async with trio.open_nursery() as self._nursery:
|
|
246
|
+
self._schedule_pending_tasks()
|
|
247
|
+
await trio.sleep_forever()
|
|
248
|
+
finally:
|
|
249
|
+
self._nursery = None
|
|
250
|
+
|
|
251
|
+
def _schedule_pending_tasks(self) -> None:
|
|
252
|
+
"""Schedules all pending asynchronous tasks that were created before
|
|
253
|
+
the nursery to be executed on the nursery soon.
|
|
254
|
+
"""
|
|
255
|
+
for task, scope, args in self._pending_tasks:
|
|
256
|
+
self._nursery.start_soon(task, scope, *args)
|
|
257
|
+
del self._pending_tasks[:]
|
|
258
|
+
|
|
259
|
+
def _start_task(
|
|
260
|
+
self,
|
|
261
|
+
task: Callable[Concatenate[trio.CancelScope, _Spec], Awaitable],
|
|
262
|
+
*args: _Spec.args,
|
|
263
|
+
) -> trio.CancelScope:
|
|
264
|
+
"""Starts an asynchronous task in the Trio nursery managed by the
|
|
265
|
+
main loop. If the nursery has not started yet, store a reference to
|
|
266
|
+
the task and the arguments so we can start the task when the nursery
|
|
267
|
+
is open.
|
|
268
|
+
|
|
269
|
+
Parameters:
|
|
270
|
+
task: a Trio task to run
|
|
271
|
+
|
|
272
|
+
Returns:
|
|
273
|
+
a cancellation scope for the Trio task
|
|
274
|
+
"""
|
|
275
|
+
scope = trio.CancelScope()
|
|
276
|
+
if self._nursery:
|
|
277
|
+
self._nursery.start_soon(task, scope, *args)
|
|
278
|
+
else:
|
|
279
|
+
self._pending_tasks.append((task, scope, args))
|
|
280
|
+
return scope
|
|
281
|
+
|
|
282
|
+
async def _watch_task(
|
|
283
|
+
self,
|
|
284
|
+
scope: trio.CancelScope,
|
|
285
|
+
fd: int | io.IOBase,
|
|
286
|
+
callback: Callable[[], typing.Any],
|
|
287
|
+
) -> None:
|
|
288
|
+
"""Asynchronous task that watches the given file descriptor and calls
|
|
289
|
+
the given callback whenever the file descriptor becomes readable.
|
|
290
|
+
|
|
291
|
+
Parameters:
|
|
292
|
+
scope: the cancellation scope that can be used to cancel the task
|
|
293
|
+
fd: the file descriptor to watch
|
|
294
|
+
callback: the callback to call
|
|
295
|
+
"""
|
|
296
|
+
with scope:
|
|
297
|
+
# We check for the scope being cancelled before calling
|
|
298
|
+
# wait_readable because if callback cancels the scope, fd might be
|
|
299
|
+
# closed and calling wait_readable with a closed fd does not work.
|
|
300
|
+
while not scope.cancel_called:
|
|
301
|
+
await self._wait_readable(fd)
|
|
302
|
+
callback()
|
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
# Urwid main loop code
|
|
2
|
+
# Copyright (C) 2004-2012 Ian Ward
|
|
3
|
+
# Copyright (C) 2008 Walter Mundt
|
|
4
|
+
# Copyright (C) 2009 Andrew Psaltis
|
|
5
|
+
#
|
|
6
|
+
# This library is free software; you can redistribute it and/or
|
|
7
|
+
# modify it under the terms of the GNU Lesser General Public
|
|
8
|
+
# License as published by the Free Software Foundation; either
|
|
9
|
+
# version 2.1 of the License, or (at your option) any later version.
|
|
10
|
+
#
|
|
11
|
+
# This library is distributed in the hope that it will be useful,
|
|
12
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
13
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
|
14
|
+
# Lesser General Public License for more details.
|
|
15
|
+
#
|
|
16
|
+
# You should have received a copy of the GNU Lesser General Public
|
|
17
|
+
# License along with this library; if not, write to the Free Software
|
|
18
|
+
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
|
19
|
+
#
|
|
20
|
+
# Urwid web site: https://urwid.org/
|
|
21
|
+
|
|
22
|
+
"""Twisted Reactor based urwid EventLoop implementation.
|
|
23
|
+
|
|
24
|
+
Twisted library is required.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
from __future__ import annotations
|
|
28
|
+
|
|
29
|
+
import functools
|
|
30
|
+
import logging
|
|
31
|
+
import sys
|
|
32
|
+
import typing
|
|
33
|
+
|
|
34
|
+
from twisted.internet.abstract import FileDescriptor
|
|
35
|
+
from twisted.internet.error import AlreadyCalled, AlreadyCancelled
|
|
36
|
+
|
|
37
|
+
from .abstract_loop import EventLoop, ExitMainLoop
|
|
38
|
+
|
|
39
|
+
if typing.TYPE_CHECKING:
|
|
40
|
+
from collections.abc import Callable
|
|
41
|
+
from concurrent.futures import Executor, Future
|
|
42
|
+
|
|
43
|
+
from twisted.internet.base import DelayedCall, ReactorBase
|
|
44
|
+
from typing_extensions import ParamSpec
|
|
45
|
+
|
|
46
|
+
_Spec = ParamSpec("_Spec")
|
|
47
|
+
_T = typing.TypeVar("_T")
|
|
48
|
+
|
|
49
|
+
__all__ = ("TwistedEventLoop",)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class _TwistedInputDescriptor(FileDescriptor):
|
|
53
|
+
def __init__(self, reactor: ReactorBase, fd: int, cb: Callable[[], typing.Any]) -> None:
|
|
54
|
+
self._fileno = fd
|
|
55
|
+
self.cb = cb
|
|
56
|
+
super().__init__(reactor) # ReactorBase implement full API as required in interfaces
|
|
57
|
+
|
|
58
|
+
def fileno(self) -> int:
|
|
59
|
+
return self._fileno
|
|
60
|
+
|
|
61
|
+
def doRead(self):
|
|
62
|
+
return self.cb()
|
|
63
|
+
|
|
64
|
+
def getHost(self):
|
|
65
|
+
raise NotImplementedError("No network operation expected")
|
|
66
|
+
|
|
67
|
+
def getPeer(self):
|
|
68
|
+
raise NotImplementedError("No network operation expected")
|
|
69
|
+
|
|
70
|
+
def writeSomeData(self, data: bytes) -> None:
|
|
71
|
+
raise NotImplementedError("Reduced functionality: read-only")
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
class TwistedEventLoop(EventLoop):
|
|
75
|
+
"""
|
|
76
|
+
Event loop based on Twisted_
|
|
77
|
+
"""
|
|
78
|
+
|
|
79
|
+
_idle_emulation_delay = 1.0 / 256 # a short time (in seconds)
|
|
80
|
+
|
|
81
|
+
def __init__(self, reactor: ReactorBase | None = None, manage_reactor: bool = True) -> None:
|
|
82
|
+
"""
|
|
83
|
+
:param reactor: reactor to use
|
|
84
|
+
:type reactor: :class:`twisted.internet.reactor`.
|
|
85
|
+
:param: manage_reactor: `True` if you want this event loop to run
|
|
86
|
+
and stop the reactor.
|
|
87
|
+
:type manage_reactor: boolean
|
|
88
|
+
|
|
89
|
+
.. WARNING::
|
|
90
|
+
Twisted's reactor doesn't like to be stopped and run again. If you
|
|
91
|
+
need to stop and run your :class:`MainLoop`, consider setting
|
|
92
|
+
``manage_reactor=False`` and take care of running/stopping the reactor
|
|
93
|
+
at the beginning/ending of your program yourself.
|
|
94
|
+
|
|
95
|
+
You can also forego using :class:`MainLoop`'s run() entirely, and
|
|
96
|
+
instead call start() and stop() before and after starting the
|
|
97
|
+
reactor.
|
|
98
|
+
|
|
99
|
+
.. _Twisted: https://twisted.org/
|
|
100
|
+
"""
|
|
101
|
+
super().__init__()
|
|
102
|
+
self.logger = logging.getLogger(__name__).getChild(self.__class__.__name__)
|
|
103
|
+
if reactor is None:
|
|
104
|
+
import twisted.internet.reactor
|
|
105
|
+
|
|
106
|
+
reactor = twisted.internet.reactor
|
|
107
|
+
self.reactor: ReactorBase = reactor
|
|
108
|
+
self._watch_files: dict[int, _TwistedInputDescriptor] = {}
|
|
109
|
+
self._idle_handle: int = 0
|
|
110
|
+
self._twisted_idle_enabled = False
|
|
111
|
+
self._idle_callbacks: dict[int, Callable[[], typing.Any]] = {}
|
|
112
|
+
self._exc: BaseException | None = None
|
|
113
|
+
self.manage_reactor = manage_reactor
|
|
114
|
+
self._enable_twisted_idle()
|
|
115
|
+
|
|
116
|
+
def run_in_executor(
|
|
117
|
+
self,
|
|
118
|
+
executor: Executor,
|
|
119
|
+
func: Callable[..., _T],
|
|
120
|
+
*args: object,
|
|
121
|
+
) -> Future[_T]:
|
|
122
|
+
raise NotImplementedError(
|
|
123
|
+
"Twisted implement it's own ThreadPool executor. Please use native API for call:\n"
|
|
124
|
+
"'threads.deferToThread(Callable[..., Any], *args, **kwargs)'\n"
|
|
125
|
+
"And use 'addCallback' api for callbacks:\n"
|
|
126
|
+
"'threads.deferToThread(Callable[..., T], *args, **kwargs).addCallback(Callable[[T], None])'"
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
def alarm(self, seconds: float, callback: Callable[[], typing.Any]) -> DelayedCall:
|
|
130
|
+
"""
|
|
131
|
+
Call callback() a given time from now. No parameters are
|
|
132
|
+
passed to callback.
|
|
133
|
+
|
|
134
|
+
Returns a handle that may be passed to remove_alarm()
|
|
135
|
+
|
|
136
|
+
seconds -- floating point time to wait before calling callback
|
|
137
|
+
callback -- function to call from event loop
|
|
138
|
+
"""
|
|
139
|
+
handle = self.reactor.callLater(seconds, self.handle_exit(callback))
|
|
140
|
+
return handle
|
|
141
|
+
|
|
142
|
+
def remove_alarm(self, handle: DelayedCall) -> bool:
|
|
143
|
+
"""
|
|
144
|
+
Remove an alarm.
|
|
145
|
+
|
|
146
|
+
Returns True if the alarm exists, False otherwise
|
|
147
|
+
"""
|
|
148
|
+
try:
|
|
149
|
+
handle.cancel()
|
|
150
|
+
|
|
151
|
+
except (AlreadyCancelled, AlreadyCalled):
|
|
152
|
+
return False
|
|
153
|
+
|
|
154
|
+
return True
|
|
155
|
+
|
|
156
|
+
def watch_file(self, fd: int, callback: Callable[[], typing.Any]) -> int:
|
|
157
|
+
"""
|
|
158
|
+
Call callback() when fd has some data to read. No parameters
|
|
159
|
+
are passed to callback.
|
|
160
|
+
|
|
161
|
+
Returns a handle that may be passed to remove_watch_file()
|
|
162
|
+
|
|
163
|
+
fd -- file descriptor to watch for input
|
|
164
|
+
callback -- function to call when input is available
|
|
165
|
+
"""
|
|
166
|
+
ind = _TwistedInputDescriptor(self.reactor, fd, self.handle_exit(callback))
|
|
167
|
+
self._watch_files[fd] = ind
|
|
168
|
+
self.reactor.addReader(ind)
|
|
169
|
+
return fd
|
|
170
|
+
|
|
171
|
+
def remove_watch_file(self, handle: int) -> bool:
|
|
172
|
+
"""
|
|
173
|
+
Remove an input file.
|
|
174
|
+
|
|
175
|
+
Returns True if the input file exists, False otherwise
|
|
176
|
+
"""
|
|
177
|
+
if handle in self._watch_files:
|
|
178
|
+
self.reactor.removeReader(self._watch_files[handle])
|
|
179
|
+
del self._watch_files[handle]
|
|
180
|
+
return True
|
|
181
|
+
return False
|
|
182
|
+
|
|
183
|
+
def enter_idle(self, callback: Callable[[], typing.Any]) -> int:
|
|
184
|
+
"""
|
|
185
|
+
Add a callback for entering idle.
|
|
186
|
+
|
|
187
|
+
Returns a handle that may be passed to remove_enter_idle()
|
|
188
|
+
"""
|
|
189
|
+
self._idle_handle += 1
|
|
190
|
+
self._idle_callbacks[self._idle_handle] = callback
|
|
191
|
+
return self._idle_handle
|
|
192
|
+
|
|
193
|
+
def _enable_twisted_idle(self) -> None:
|
|
194
|
+
"""
|
|
195
|
+
Twisted's reactors don't have an idle or enter-idle callback
|
|
196
|
+
so the best we can do for now is to set a timer event in a very
|
|
197
|
+
short time to approximate an enter-idle callback.
|
|
198
|
+
|
|
199
|
+
.. WARNING::
|
|
200
|
+
This will perform worse than the other event loops until we can find a
|
|
201
|
+
fix or workaround
|
|
202
|
+
"""
|
|
203
|
+
if self._twisted_idle_enabled:
|
|
204
|
+
return
|
|
205
|
+
self.reactor.callLater(
|
|
206
|
+
self._idle_emulation_delay,
|
|
207
|
+
self.handle_exit(self._twisted_idle_callback, enable_idle=False),
|
|
208
|
+
)
|
|
209
|
+
self._twisted_idle_enabled = True
|
|
210
|
+
|
|
211
|
+
def _twisted_idle_callback(self) -> None:
|
|
212
|
+
for callback in self._idle_callbacks.values():
|
|
213
|
+
callback()
|
|
214
|
+
self._twisted_idle_enabled = False
|
|
215
|
+
|
|
216
|
+
def remove_enter_idle(self, handle: int) -> bool:
|
|
217
|
+
"""
|
|
218
|
+
Remove an idle callback.
|
|
219
|
+
|
|
220
|
+
Returns True if the handle was removed.
|
|
221
|
+
"""
|
|
222
|
+
try:
|
|
223
|
+
del self._idle_callbacks[handle]
|
|
224
|
+
except KeyError:
|
|
225
|
+
return False
|
|
226
|
+
return True
|
|
227
|
+
|
|
228
|
+
def run(self) -> None:
|
|
229
|
+
"""
|
|
230
|
+
Start the event loop. Exit the loop when any callback raises
|
|
231
|
+
an exception. If ExitMainLoop is raised, exit cleanly.
|
|
232
|
+
"""
|
|
233
|
+
if not self.manage_reactor:
|
|
234
|
+
return
|
|
235
|
+
self.reactor.run()
|
|
236
|
+
if self._exc:
|
|
237
|
+
# An exception caused us to exit, raise it now
|
|
238
|
+
exc = self._exc
|
|
239
|
+
self._exc = None
|
|
240
|
+
raise exc.with_traceback(exc.__traceback__)
|
|
241
|
+
|
|
242
|
+
def handle_exit(self, f: Callable[_Spec, _T], enable_idle: bool = True) -> Callable[_Spec, _T | None]:
|
|
243
|
+
"""
|
|
244
|
+
Decorator that cleanly exits the :class:`TwistedEventLoop` if
|
|
245
|
+
:class:`ExitMainLoop` is thrown inside of the wrapped function. Store the
|
|
246
|
+
exception info if some other exception occurs, it will be reraised after
|
|
247
|
+
the loop quits.
|
|
248
|
+
|
|
249
|
+
*f* -- function to be wrapped
|
|
250
|
+
"""
|
|
251
|
+
|
|
252
|
+
@functools.wraps(f)
|
|
253
|
+
def wrapper(*args: _Spec.args, **kwargs: _Spec.kwargs) -> _T | None:
|
|
254
|
+
rval = None
|
|
255
|
+
try:
|
|
256
|
+
rval = f(*args, **kwargs)
|
|
257
|
+
except ExitMainLoop:
|
|
258
|
+
if self.manage_reactor:
|
|
259
|
+
self.reactor.stop()
|
|
260
|
+
except BaseException as exc:
|
|
261
|
+
print(sys.exc_info())
|
|
262
|
+
self._exc = exc
|
|
263
|
+
if self.manage_reactor:
|
|
264
|
+
self.reactor.crash()
|
|
265
|
+
if enable_idle:
|
|
266
|
+
self._enable_twisted_idle()
|
|
267
|
+
return rval
|
|
268
|
+
|
|
269
|
+
return wrapper
|