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.

Files changed (75) hide show
  1. urwid/__init__.py +333 -0
  2. urwid/canvas.py +1413 -0
  3. urwid/command_map.py +137 -0
  4. urwid/container.py +59 -0
  5. urwid/decoration.py +65 -0
  6. urwid/display/__init__.py +97 -0
  7. urwid/display/_posix_raw_display.py +413 -0
  8. urwid/display/_raw_display_base.py +914 -0
  9. urwid/display/_web.css +12 -0
  10. urwid/display/_web.js +462 -0
  11. urwid/display/_win32.py +171 -0
  12. urwid/display/_win32_raw_display.py +269 -0
  13. urwid/display/common.py +1219 -0
  14. urwid/display/curses.py +690 -0
  15. urwid/display/escape.py +624 -0
  16. urwid/display/html_fragment.py +251 -0
  17. urwid/display/lcd.py +518 -0
  18. urwid/display/raw.py +37 -0
  19. urwid/display/web.py +636 -0
  20. urwid/event_loop/__init__.py +55 -0
  21. urwid/event_loop/abstract_loop.py +175 -0
  22. urwid/event_loop/asyncio_loop.py +231 -0
  23. urwid/event_loop/glib_loop.py +294 -0
  24. urwid/event_loop/main_loop.py +721 -0
  25. urwid/event_loop/select_loop.py +230 -0
  26. urwid/event_loop/tornado_loop.py +206 -0
  27. urwid/event_loop/trio_loop.py +302 -0
  28. urwid/event_loop/twisted_loop.py +269 -0
  29. urwid/event_loop/zmq_loop.py +275 -0
  30. urwid/font.py +695 -0
  31. urwid/graphics.py +96 -0
  32. urwid/highlight.css +19 -0
  33. urwid/listbox.py +1899 -0
  34. urwid/monitored_list.py +522 -0
  35. urwid/numedit.py +376 -0
  36. urwid/signals.py +330 -0
  37. urwid/split_repr.py +130 -0
  38. urwid/str_util.py +358 -0
  39. urwid/text_layout.py +632 -0
  40. urwid/treetools.py +515 -0
  41. urwid/util.py +557 -0
  42. urwid/version.py +16 -0
  43. urwid/vterm.py +1806 -0
  44. urwid/widget/__init__.py +181 -0
  45. urwid/widget/attr_map.py +161 -0
  46. urwid/widget/attr_wrap.py +140 -0
  47. urwid/widget/bar_graph.py +649 -0
  48. urwid/widget/big_text.py +77 -0
  49. urwid/widget/box_adapter.py +126 -0
  50. urwid/widget/columns.py +1145 -0
  51. urwid/widget/constants.py +574 -0
  52. urwid/widget/container.py +227 -0
  53. urwid/widget/divider.py +110 -0
  54. urwid/widget/edit.py +718 -0
  55. urwid/widget/filler.py +403 -0
  56. urwid/widget/frame.py +539 -0
  57. urwid/widget/grid_flow.py +539 -0
  58. urwid/widget/line_box.py +194 -0
  59. urwid/widget/overlay.py +829 -0
  60. urwid/widget/padding.py +597 -0
  61. urwid/widget/pile.py +971 -0
  62. urwid/widget/popup.py +170 -0
  63. urwid/widget/progress_bar.py +141 -0
  64. urwid/widget/scrollable.py +597 -0
  65. urwid/widget/solid_fill.py +44 -0
  66. urwid/widget/text.py +354 -0
  67. urwid/widget/widget.py +852 -0
  68. urwid/widget/widget_decoration.py +166 -0
  69. urwid/widget/wimp.py +792 -0
  70. urwid/wimp.py +23 -0
  71. urwid-2.6.0.post0.dist-info/COPYING +504 -0
  72. urwid-2.6.0.post0.dist-info/METADATA +332 -0
  73. urwid-2.6.0.post0.dist-info/RECORD +75 -0
  74. urwid-2.6.0.post0.dist-info/WHEEL +5 -0
  75. 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