diff options
author | Ian Ward <ian@excess.org> | 2013-12-26 15:00:33 -0500 |
---|---|---|
committer | Ian Ward <ian@excess.org> | 2013-12-26 15:00:33 -0500 |
commit | e198e246817e0b898b8e680e4ff1d37e7b2e3560 (patch) | |
tree | 3cc5fd7a859358d6b3f60ee72a778152c6ed0762 /urwid | |
parent | 7814d4c93fd43e34bdfc2c54189e7efb9fe802f1 (diff) | |
parent | 75a39b9ce2730d6617b328e69d4c4710e769e102 (diff) | |
download | urwid-e198e246817e0b898b8e680e4ff1d37e7b2e3560.tar.gz |
Merge TornadoEventLoop from aglyzov
Conflicts:
urwid/main_loop.py
Diffstat (limited to 'urwid')
-rw-r--r-- | urwid/__init__.py | 5 | ||||
-rwxr-xr-x | urwid/main_loop.py | 201 |
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: |