summaryrefslogtreecommitdiff
path: root/urwid/main_loop.py
diff options
context:
space:
mode:
Diffstat (limited to 'urwid/main_loop.py')
-rwxr-xr-xurwid/main_loop.py274
1 files changed, 199 insertions, 75 deletions
diff --git a/urwid/main_loop.py b/urwid/main_loop.py
index 0a93b5e..77022bf 100755
--- a/urwid/main_loop.py
+++ b/urwid/main_loop.py
@@ -34,7 +34,7 @@ try:
except ImportError:
pass # windows
-from urwid.util import is_mouse_event
+from urwid.util import StoppingContext, is_mouse_event
from urwid.compat import PYTHON3
from urwid.command_map import command_map, REDRAW_SCREEN
from urwid.wimp import PopUpTarget
@@ -50,6 +50,9 @@ class ExitMainLoop(Exception):
"""
pass
+class CantUseExternalLoop(Exception):
+ pass
+
class MainLoop(object):
"""
This is the standard main loop implementation for a single interactive
@@ -116,7 +119,7 @@ class MainLoop(object):
self._unhandled_input = unhandled_input
self._input_filter = input_filter
- if not hasattr(screen, 'get_input_descriptors'
+ if not hasattr(screen, 'hook_event_loop'
) and event_loop is not None:
raise NotImplementedError("screen object passed "
"%r does not support external event loops" % (screen,))
@@ -124,7 +127,6 @@ class MainLoop(object):
event_loop = SelectEventLoop()
self.event_loop = event_loop
- self._input_timeout = None
self._watch_pipes = {}
def _set_widget(self, widget):
@@ -268,14 +270,12 @@ class MainLoop(object):
Start the main loop handling input events and updating the screen. The
loop will continue until an :exc:`ExitMainLoop` exception is raised.
- This method will use :attr:`screen`'s run_wrapper() method if
- :attr:`screen`'s start() method has not already been called.
+ If you would prefer to manage the event loop yourself, don't use this
+ method. Instead, call :meth:`start` before starting the event loop,
+ and :meth:`stop` once it's finished.
"""
try:
- if self.screen.started:
- self._run()
- else:
- self.screen.run_wrapper(self._run)
+ self._run()
except ExitMainLoop:
pass
@@ -293,101 +293,106 @@ class MainLoop(object):
>>> evl.watch_file_rval = 2
>>> ml = MainLoop(w, [], scr, event_loop=evl)
>>> ml.run() # doctest:+ELLIPSIS
+ screen.start()
screen.set_mouse_tracking()
+ screen.unhook_event_loop(...)
+ screen.hook_event_loop(...)
+ event_loop.enter_idle(<bound method MainLoop.entering_idle...>)
+ event_loop.run()
+ event_loop.remove_enter_idle(1)
+ screen.unhook_event_loop(...)
+ screen.stop()
+ >>> ml.draw_screen() # doctest:+ELLIPSIS
screen.get_cols_rows()
widget.render((20, 10), focus=True)
screen.draw_screen((20, 10), 'fake canvas')
- screen.get_input_descriptors()
- event_loop.watch_file(42, <bound method ...>)
- event_loop.enter_idle(<bound method ...>)
- event_loop.run()
- event_loop.remove_enter_idle(1)
- event_loop.remove_watch_file(2)
- >>> scr.started = False
- >>> ml.run() # doctest:+ELLIPSIS
- screen.run_wrapper(<bound method ...>)
"""
- def _run(self):
- if self.handle_mouse:
- self.screen.set_mouse_tracking()
+ def start(self):
+ """
+ Sets up the main loop, hooking into the event loop where necessary.
+ Starts the :attr:`screen` if it hasn't already been started.
- if not hasattr(self.screen, 'get_input_descriptors'):
- return self._run_screen_event_loop()
+ If you want to control starting and stopping the event loop yourself,
+ you should call this method before starting, and call `stop` once the
+ loop has finished. You may also use this method as a context manager,
+ which will stop the loop automatically at the end of the block:
- self.draw_screen()
+ with main_loop.start():
+ ...
- fd_handles = []
- def reset_input_descriptors(only_remove=False):
- for handle in fd_handles:
- self.event_loop.remove_watch_file(handle)
- if only_remove:
- del fd_handles[:]
- else:
- fd_handles[:] = [
- self.event_loop.watch_file(fd, self._update)
- for fd in self.screen.get_input_descriptors()]
- if not fd_handles and self._input_timeout is not None:
- self.event_loop.remove_alarm(self._input_timeout)
+ Note that some event loop implementations don't handle exceptions
+ specially if you manage the event loop yourself. In particular, the
+ Twisted and asyncio loops won't stop automatically when
+ :exc:`ExitMainLoop` (or anything else) is raised.
+ """
+ self.screen.start()
+
+ if self.handle_mouse:
+ self.screen.set_mouse_tracking()
+
+ if not hasattr(self.screen, 'hook_event_loop'):
+ raise CantUseExternalLoop(
+ "Screen {0!r} doesn't support external event loops")
try:
signals.connect_signal(self.screen, INPUT_DESCRIPTORS_CHANGED,
- reset_input_descriptors)
+ self._reset_input_descriptors)
except NameError:
pass
# watch our input descriptors
- reset_input_descriptors()
- idle_handle = self.event_loop.enter_idle(self.entering_idle)
+ self._reset_input_descriptors()
+ self.idle_handle = self.event_loop.enter_idle(self.entering_idle)
- # Go..
- self.event_loop.run()
+ return StoppingContext(self)
- # tidy up
- self.event_loop.remove_enter_idle(idle_handle)
- reset_input_descriptors(True)
+ def stop(self):
+ """
+ Cleans up any hooks added to the event loop. Only call this if you're
+ managing the event loop yourself, after the loop stops.
+ """
+ self.event_loop.remove_enter_idle(self.idle_handle)
+ del self.idle_handle
signals.disconnect_signal(self.screen, INPUT_DESCRIPTORS_CHANGED,
- reset_input_descriptors)
+ self._reset_input_descriptors)
+ self.screen.unhook_event_loop(self.event_loop)
+
+ self.screen.stop()
+
+ def _reset_input_descriptors(self):
+ self.screen.unhook_event_loop(self.event_loop)
+ self.screen.hook_event_loop(self.event_loop, self._update)
+
+ def _run(self):
+ try:
+ self.start()
+ except CantUseExternalLoop:
+ try:
+ return self._run_screen_event_loop()
+ finally:
+ self.screen.stop()
- def _update(self, timeout=False):
+ self.event_loop.run()
+ self.stop()
+
+ def _update(self, keys, raw):
"""
>>> w = _refl("widget")
>>> w.selectable_rval = True
>>> w.mouse_event_rval = True
>>> scr = _refl("screen")
>>> scr.get_cols_rows_rval = (15, 5)
- >>> scr.get_input_nonblocking_rval = 1, ['y'], [121]
>>> evl = _refl("event_loop")
>>> ml = MainLoop(w, [], scr, event_loop=evl)
>>> ml._input_timeout = "old timeout"
- >>> ml._update() # doctest:+ELLIPSIS
- event_loop.remove_alarm('old timeout')
- screen.get_input_nonblocking()
- event_loop.alarm(1, <function ...>)
+ >>> ml._update(['y'], [121]) # doctest:+ELLIPSIS
screen.get_cols_rows()
widget.selectable()
widget.keypress((15, 5), 'y')
- >>> scr.get_input_nonblocking_rval = None, [("mouse press", 1, 5, 4)
- ... ], []
- >>> ml._update()
- screen.get_input_nonblocking()
+ >>> ml._update([("mouse press", 1, 5, 4)], [])
widget.mouse_event((15, 5), 'mouse press', 1, 5, 4, focus=True)
- >>> scr.get_input_nonblocking_rval = None, [], []
- >>> ml._update()
- screen.get_input_nonblocking()
+ >>> ml._update([], [])
"""
- if self._input_timeout is not None and not timeout:
- # cancel the timeout, something else triggered the update
- self.event_loop.remove_alarm(self._input_timeout)
- self._input_timeout = None
-
- max_wait, keys, raw = self.screen.get_input_nonblocking()
-
- if max_wait is not None:
- # if get_input_nonblocking wants to be called back
- # make sure it happens with an alarm
- self._input_timeout = self.event_loop.alarm(max_wait,
- lambda: self._update(timeout=True))
-
keys = self.input_filter(keys, raw)
if keys:
@@ -583,7 +588,7 @@ class SelectEventLoop(object):
def alarm(self, seconds, callback):
"""
- Call callback() given time from from now. No parameters are
+ Call callback() a given time from now. No parameters are
passed to callback.
Returns a handle that may be passed to remove_alarm()
@@ -671,7 +676,7 @@ class SelectEventLoop(object):
while True:
try:
self._loop()
- except select.error, e:
+ except select.error as e:
if e.args[0] != 4:
# not just something we need to retry
raise
@@ -730,7 +735,7 @@ class GLibEventLoop(object):
def alarm(self, seconds, callback):
"""
- Call callback() given time from from now. No parameters are
+ Call callback() a given time from now. No parameters are
passed to callback.
Returns a handle that may be passed to remove_alarm()
@@ -1044,6 +1049,10 @@ class TwistedEventLoop(object):
``manage_reactor=False`` and take care of running/stopping the reactor
at the beginning/ending of your program yourself.
+ You can also forego using :class:`MainLoop`'s run() entirely, and
+ instead call start() and stop() before and after starting the
+ reactor.
+
.. _Twisted: http://twistedmatrix.com/trac/
"""
if reactor is None:
@@ -1061,7 +1070,7 @@ class TwistedEventLoop(object):
def alarm(self, seconds, callback):
"""
- Call callback() given time from from now. No parameters are
+ Call callback() a given time from now. No parameters are
passed to callback.
Returns a handle that may be passed to remove_alarm()
@@ -1200,6 +1209,121 @@ class TwistedEventLoop(object):
return wrapper
+class AsyncioEventLoop(object):
+ """
+ Event loop based on the standard library ``asyncio`` module.
+
+ ``asyncio`` is new in Python 3.4, but also exists as a backport on PyPI for
+ Python 3.3. The ``trollius`` package is available for older Pythons with
+ slightly different syntax, but also works with this loop.
+ """
+ _we_started_event_loop = False
+
+ _idle_emulation_delay = 1.0/256 # a short time (in seconds)
+
+ def __init__(self, **kwargs):
+ if 'loop' in kwargs:
+ self._loop = kwargs.pop('loop')
+ else:
+ import asyncio
+ self._loop = asyncio.get_event_loop()
+
+ def alarm(self, seconds, callback):
+ """
+ Call callback() a given time from now. No parameters are
+ passed to callback.
+
+ Returns a handle that may be passed to remove_alarm()
+
+ seconds -- time in seconds to wait before calling callback
+ callback -- function to call from event loop
+ """
+ return self._loop.call_later(seconds, callback)
+
+ def remove_alarm(self, handle):
+ """
+ Remove an alarm.
+
+ Returns True if the alarm exists, False otherwise
+ """
+ existed = not handle._cancelled
+ handle.cancel()
+ return existed
+
+ def watch_file(self, fd, callback):
+ """
+ Call callback() when fd has some data to read. No parameters
+ are passed to callback.
+
+ Returns a handle that may be passed to remove_watch_file()
+
+ fd -- file descriptor to watch for input
+ callback -- function to call when input is available
+ """
+ self._loop.add_reader(fd, callback)
+ return fd
+
+ def remove_watch_file(self, handle):
+ """
+ Remove an input file.
+
+ Returns True if the input file exists, False otherwise
+ """
+ return self._loop.remove_reader(handle)
+
+ def enter_idle(self, callback):
+ """
+ Add a callback for entering idle.
+
+ Returns a handle that may be passed to remove_idle()
+ """
+ # XXX there's no such thing as "idle" in most event loops; this fakes
+ # it the same way as Twisted, by scheduling the callback to be called
+ # repeatedly
+ mutable_handle = [None]
+ def faux_idle_callback():
+ callback()
+ mutable_handle[0] = self._loop.call_later(
+ self._idle_emulation_delay, faux_idle_callback)
+
+ mutable_handle[0] = self._loop.call_later(
+ self._idle_emulation_delay, faux_idle_callback)
+
+ return mutable_handle
+
+ def remove_enter_idle(self, handle):
+ """
+ Remove an idle callback.
+
+ Returns True if the handle was removed.
+ """
+ # `handle` is just a list containing the current actual handle
+ return self.remove_alarm(handle[0])
+
+ _exc_info = None
+
+ def _exception_handler(self, loop, context):
+ exc = context.get('exception')
+ if exc:
+ loop.stop()
+ if not isinstance(exc, ExitMainLoop):
+ # Store the exc_info so we can re-raise after the loop stops
+ import sys
+ self._exc_info = sys.exc_info()
+ else:
+ loop.default_exception_handler(context)
+
+ def run(self):
+ """
+ Start the event loop. Exit the loop when any callback raises
+ an exception. If ExitMainLoop is raised, exit cleanly.
+ """
+ self._loop.set_exception_handler(self._exception_handler)
+ self._loop.run_forever()
+ if self._exc_info:
+ raise self._exc_info[0], self._exc_info[1], self._exc_info[2]
+ self._exc_info = None
+
def _refl(name, rval=None, exit=False):
"""