From dd227290a3831bf2b8716610332e28648bc63697 Mon Sep 17 00:00:00 2001 From: "Eevee (Alex Munroe)" Date: Sun, 11 May 2014 18:39:39 -0700 Subject: Add AsyncioEventLoop. Fixes #52. --- urwid/__init__.py | 2 +- urwid/main_loop.py | 121 +++++++++++++++++++++++++++++++++++++++- urwid/tests/test_event_loops.py | 18 +++++- 3 files changed, 136 insertions(+), 5 deletions(-) diff --git a/urwid/__init__.py b/urwid/__init__.py index a6cacf8..bc5170e 100644 --- a/urwid/__init__.py +++ b/urwid/__init__.py @@ -53,7 +53,7 @@ from urwid.command_map import (CommandMap, command_map, CURSOR_PAGE_UP, CURSOR_PAGE_DOWN, CURSOR_MAX_LEFT, CURSOR_MAX_RIGHT, ACTIVATE) from urwid.main_loop import (ExitMainLoop, MainLoop, SelectEventLoop, - GLibEventLoop, TornadoEventLoop) + GLibEventLoop, TornadoEventLoop, AsyncioEventLoop) try: from urwid.main_loop import TwistedEventLoop except ImportError: diff --git a/urwid/main_loop.py b/urwid/main_loop.py index 2c314d8..84ed2d6 100755 --- a/urwid/main_loop.py +++ b/urwid/main_loop.py @@ -552,7 +552,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() @@ -699,7 +699,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() @@ -1030,7 +1030,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() @@ -1169,6 +1169,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): """ diff --git a/urwid/tests/test_event_loops.py b/urwid/tests/test_event_loops.py index 0793602..c85bbed 100644 --- a/urwid/tests/test_event_loops.py +++ b/urwid/tests/test_event_loops.py @@ -34,6 +34,8 @@ class EventLoopTestMixin(object): self.assertTrue(evl.remove_watch_file(handle)) self.assertFalse(evl.remove_watch_file(handle)) + _expected_idle_handle = 1 + def test_run(self): evl = self.evl out = [] @@ -50,7 +52,9 @@ class EventLoopTestMixin(object): 1/0 handle = evl.alarm(0.01, exit_clean) handle = evl.alarm(0.005, say_hello) - self.assertEqual(evl.enter_idle(say_waiting), 1) + idle_handle = evl.enter_idle(say_waiting) + if self._expected_idle_handle is not None: + self.assertEqual(idle_handle, 1) evl.run() self.assertTrue("hello" in out, out) self.assertTrue("clean exit"in out, out) @@ -129,3 +133,15 @@ else: self.assertTrue("ta" in out, out) self.assertTrue("hello" in out, out) self.assertTrue("clean exit" in out, out) + + +try: + import asyncio +except ImportError: + pass +else: + class AsyncioEventLoopTest(unittest.TestCase, EventLoopTestMixin): + def setUp(self): + self.evl = urwid.AsyncioEventLoop() + + _expected_idle_handle = None -- cgit v1.2.1