summaryrefslogtreecommitdiff
path: root/urwid
diff options
context:
space:
mode:
authorIan Ward <ian@excess.org>2013-12-26 15:00:33 -0500
committerIan Ward <ian@excess.org>2013-12-26 15:00:33 -0500
commite198e246817e0b898b8e680e4ff1d37e7b2e3560 (patch)
tree3cc5fd7a859358d6b3f60ee72a778152c6ed0762 /urwid
parent7814d4c93fd43e34bdfc2c54189e7efb9fe802f1 (diff)
parent75a39b9ce2730d6617b328e69d4c4710e769e102 (diff)
downloadurwid-e198e246817e0b898b8e680e4ff1d37e7b2e3560.tar.gz
Merge TornadoEventLoop from aglyzov
Conflicts: urwid/main_loop.py
Diffstat (limited to 'urwid')
-rw-r--r--urwid/__init__.py5
-rwxr-xr-xurwid/main_loop.py201
2 files changed, 204 insertions, 2 deletions
diff --git a/urwid/__init__.py b/urwid/__init__.py
index 899a4da..a6cacf8 100644
--- a/urwid/__init__.py
+++ b/urwid/__init__.py
@@ -52,9 +52,10 @@ from urwid.command_map import (CommandMap, command_map,
REDRAW_SCREEN, CURSOR_UP, CURSOR_DOWN, CURSOR_LEFT, CURSOR_RIGHT,
CURSOR_PAGE_UP, CURSOR_PAGE_DOWN, CURSOR_MAX_LEFT, CURSOR_MAX_RIGHT,
ACTIVATE)
-from urwid.main_loop import ExitMainLoop, MainLoop, SelectEventLoop
+from urwid.main_loop import (ExitMainLoop, MainLoop, SelectEventLoop,
+ GLibEventLoop, TornadoEventLoop)
try:
- from urwid.main_loop import GLibEventLoop, TwistedEventLoop
+ from urwid.main_loop import TwistedEventLoop
except ImportError:
pass
from urwid.text_layout import (TextLayout, StandardTextLayout, default_layout,
diff --git a/urwid/main_loop.py b/urwid/main_loop.py
index 43e9c0e..f209c51 100755
--- a/urwid/main_loop.py
+++ b/urwid/main_loop.py
@@ -27,6 +27,8 @@ import heapq
import select
import fcntl
import os
+from functools import wraps
+from weakref import WeakKeyDictionary
from urwid.util import is_mouse_event
from urwid.compat import PYTHON3
@@ -858,6 +860,205 @@ class GLibEventLoop(object):
return wrapper
+class TornadoEventLoop(object):
+ """ This is an Urwid-specific event loop to plug into its MainLoop.
+ It acts as an adaptor for Tornado's IOLoop which does all
+ heavy lifting except idle-callbacks.
+
+ Notice, since Tornado has no concept of idle callbacks we
+ monkey patch ioloop._impl.poll() function to be able to detect
+ potential idle periods.
+ """
+ _ioloop_registry = WeakKeyDictionary() # {<ioloop> : {<handle> : <idle_func>}}
+ _max_idle_handle = 0
+
+ class PollProxy(object):
+ """ A simple proxy for a Python's poll object that wraps the .poll() method
+ in order to detect idle periods and call Urwid callbacks
+ """
+ def __init__(self, poll_obj, idle_map):
+ self.__poll_obj = poll_obj
+ self.__idle_map = idle_map
+
+ def __getattr__(self, name):
+ return getattr(self.__poll_obj, name)
+
+ def poll(self, timeout):
+ if timeout >= 0.01: # only trigger idle event if the delay is big enough
+ for callback in list(self.__idle_map.values()):
+ callback()
+ return self.__poll_obj.poll(timeout)
+
+ @classmethod
+ def _patch_poll_impl(cls, ioloop):
+ """ Wraps original poll object in the IOLoop's poll object
+ """
+ if ioloop in cls._ioloop_registry:
+ return # we already patched this instance
+
+ cls._ioloop_registry[ioloop] = idle_map = {}
+ ioloop._impl = cls.PollProxy(ioloop._impl, idle_map)
+
+ def __init__(self, ioloop=None):
+ if not ioloop:
+ from tornado.ioloop import IOLoop
+ ioloop = IOLoop.instance()
+ self._ioloop = ioloop
+ self._patch_poll_impl(ioloop)
+
+ self._watch_handles = {} # {<watch_handle> : <file_descriptor>}
+ self._max_watch_handle = 0
+ self._exception = None
+
+ def alarm(self, secs, callback):
+ ioloop = self._ioloop
+ wrapped = self.handle_exit(callback)
+ return ioloop.add_timeout(ioloop.time() + secs, wrapped)
+
+ def remove_alarm(self, handle):
+ self._ioloop.remove_timeout(handle)
+ return True
+
+ def watch_file(self, fd, callback):
+ from tornado.ioloop import IOLoop
+ handler = lambda fd,events: self.handle_exit(callback)()
+ self._ioloop.add_handler(fd, handler, IOLoop.READ)
+ self._max_watch_handle += 1
+ handle = self._max_watch_handle
+ self._watch_handles[handle] = fd
+ return handle
+
+ def remove_watch_file(self, handle):
+ fd = self._watch_handles.pop(handle, None)
+ if fd is None:
+ return False
+ else:
+ self._ioloop.remove_handler(fd)
+ return True
+
+ def enter_idle(self, callback):
+ self._max_idle_handle += 1
+ handle = self._max_idle_handle
+ idle_map = self._ioloop_registry[self._ioloop]
+ idle_map[handle] = callback
+ return handle
+
+ def remove_enter_idle(self, handle):
+ idle_map = self._ioloop_registry[self._ioloop]
+ cb = idle_map.pop(handle, None)
+ return cb is not None
+
+ def handle_exit(self, func):
+ @wraps(func)
+ def wrapper(*args, **kw):
+ try:
+ return func(*args, **kw)
+ except ExitMainLoop:
+ self._ioloop.stop()
+ except Exception as exc:
+ self._exception = exc
+ self._ioloop.stop()
+ return False
+ return wrapper
+
+ def run(self):
+ self._ioloop.start()
+ if self._exception:
+ exc, self._exception = self._exception, None
+ raise exc
+
+ def _test_event_loop(self):
+ """
+ >>> import os
+ >>> from tornado.ioloop import IOLoop
+ >>> rd, wr = os.pipe()
+ >>> evl = TornadoEventLoop(IOLoop())
+ >>> def step1():
+ ... print("writing")
+ ... os.write(wr, b"hi")
+ >>> def step2():
+ ... print(os.read(rd, 2).decode())
+ ... raise ExitMainLoop
+ >>> handle = evl.alarm(0, step1)
+ >>> handle = evl.watch_file(rd, step2)
+ >>> evl.run()
+ writing
+ hi
+ """
+
+ def _test_remove_alarm(self):
+ """
+ >>> from tornado.ioloop import IOLoop
+ >>> evl = TornadoEventLoop(IOLoop())
+ >>> handle = evl.alarm(50, lambda: None)
+ >>> evl.remove_alarm(handle)
+ True
+ >>> evl.remove_alarm(handle) # always True
+ True
+ """
+
+ def _test_remove_watch_file(self):
+ """
+ >>> from tornado.ioloop import IOLoop
+ >>> evl = TornadoEventLoop(IOLoop())
+ >>> handle = evl.watch_file(1, lambda: None)
+ >>> evl.remove_watch_file(handle)
+ True
+ >>> evl.remove_watch_file(handle)
+ False
+ """
+
+ def _test_run(self):
+ """
+ >>> import os
+ >>> from tornado.ioloop import IOLoop
+ >>> rd, wr = os.pipe()
+ >>> os.write(wr, b"data") # something to read from rd
+ 4
+
+ >>> evl = TornadoEventLoop(IOLoop())
+ >>> def say_hello():
+ ... print("hello")
+ >>> def say_waiting():
+ ... print("waiting")
+ >>> def exit_clean():
+ ... print("clean exit")
+ ... raise ExitMainLoop
+ >>> def exit_error():
+ ... 1//0
+ >>> handle = evl.alarm(0.1, exit_clean)
+ >>> handle = evl.alarm(0.05, say_hello)
+ >>> evl.enter_idle(say_waiting)
+ 1
+
+ >>> evl.run()
+ waiting
+ hello
+ waiting
+ clean exit
+
+ >>> handle = evl.watch_file(rd, exit_clean)
+ >>> evl.run()
+ waiting
+ clean exit
+
+ >>> evl.remove_watch_file(handle)
+ True
+
+ >>> handle = evl.alarm(0, exit_error)
+ >>> evl.run()
+ Traceback (most recent call last):
+ ...
+ ZeroDivisionError: integer division or modulo by zero
+ >>> handle = evl.watch_file(rd, exit_error)
+ >>> evl.run()
+ Traceback (most recent call last):
+ ...
+ ZeroDivisionError: integer division or modulo by zero
+ """
+
+
+
try:
from twisted.internet.abstract import FileDescriptor
except ImportError: