summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorTamas Nepusz <ntamas@gmail.com>2019-10-24 14:19:10 +0200
committerTamas Nepusz <ntamas@gmail.com>2019-10-24 14:19:10 +0200
commita02f7e6c68b04d22780d14611510d1387682908a (patch)
tree8f76cd4a826c7e5aa6b7d6bf8bf55dc8178e1dc7
parent96a1be0ddf441244def94a36736084f9b9c780a4 (diff)
downloadurwid-a02f7e6c68b04d22780d14611510d1387682908a.tar.gz
added TrioEventLoop
-rwxr-xr-xbin/deps.py6
-rw-r--r--tox.ini5
-rw-r--r--urwid/__init__.py2
-rw-r--r--urwid/_async_kw_event_loop.py244
-rwxr-xr-xurwid/main_loop.py7
-rw-r--r--urwid/tests/test_event_loops.py18
6 files changed, 281 insertions, 1 deletions
diff --git a/bin/deps.py b/bin/deps.py
index 01489d6..4da0496 100755
--- a/bin/deps.py
+++ b/bin/deps.py
@@ -17,6 +17,12 @@ except ImportError:
pass
try:
+ import trio
+ deps.append("trio")
+except ImportError:
+ pass
+
+try:
import twisted
deps.append("twisted")
except ImportError:
diff --git a/tox.ini b/tox.ini
index 5175647..48d37b9 100644
--- a/tox.ini
+++ b/tox.ini
@@ -19,6 +19,11 @@ deps =
py37: twisted
pypy: twisted
# NOTE: py34 is tested without Twisted: they had abandoned Py < 3.5.
+ py35: trio
+ py36: trio
+ py37: trio
+ pypy: trio
+ # NOTE: py34 is tested without Trio; Trio is not supported on Py < 3.5.
commands =
coverage run ./setup.py test
diff --git a/urwid/__init__.py b/urwid/__init__.py
index 3af6dcd..192f0e9 100644
--- a/urwid/__init__.py
+++ b/urwid/__init__.py
@@ -55,7 +55,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, AsyncioEventLoop)
+ GLibEventLoop, TornadoEventLoop, AsyncioEventLoop, TrioEventLoop)
try:
from urwid.main_loop import TwistedEventLoop
except ImportError:
diff --git a/urwid/_async_kw_event_loop.py b/urwid/_async_kw_event_loop.py
new file mode 100644
index 0000000..4038921
--- /dev/null
+++ b/urwid/_async_kw_event_loop.py
@@ -0,0 +1,244 @@
+#!/usr/bin/python
+#
+# Urwid main loop code using Python-3.5 features (Trio, Curio, etc)
+# Copyright (C) 2018 Toshio Kuratomi
+# Copyright (C) 2019 Tamas Nepusz
+#
+# This library is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 2.1 of the License, or (at your option) any later version.
+#
+# This library is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this library; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+#
+# Urwid web site: http://excess.org/urwid/
+
+from .main_loop import EventLoop, ExitMainLoop
+
+
+class TrioEventLoop(EventLoop):
+ """
+ Event loop based on the ``trio`` module.
+
+ ``trio`` is an async library for Python 3.5 and later.
+ """
+
+ _idle_emulation_delay = 1.0/256 # a short time (in seconds)
+
+ def __init__(self, nursery=None):
+ """Constructor.
+
+ Parameters:
+ nursery: the Trio nursery in which the asynchronous tasks will
+ execute. `None` will make the event loop use `trio.run()` when
+ the loop is started and force it to create a nursery on its
+ own.
+ """
+ import trio
+
+ self._idle_handle = 0
+ self._idle_callbacks = {}
+ self._pending_tasks = []
+
+ self._trio = trio
+ self._nursery = None
+
+ self._sleep = trio.sleep
+ self._wait_readable = trio.hazmat.wait_readable
+
+ def alarm(self, seconds, callback):
+ """Calls `callback()` a given time from now. No parameters are passed
+ to the callback.
+
+ Parameters:
+ seconds: time in seconds to wait before calling the callback
+ callback: function to call from the event loop
+
+ Returns:
+ a handle that may be passed to `remove_alarm()`
+ """
+ return self._start_task(self._alarm_task, seconds, callback)
+
+ def enter_idle(self, callback):
+ """Calls `callback()` when the event loop enters the idle state.
+
+ There is no such thing as being idle in a Trio event loop so we
+ simulate it by repeatedly calling `callback()` with a short delay.
+ """
+ self._idle_handle += 1
+ self._idle_callbacks[self._idle_handle] = callback
+ return self._idle_handle
+
+ def remove_alarm(self, handle):
+ """Removes an alarm.
+
+ Parameters:
+ handle: the handle of the alarm to remove
+ """
+ return self._cancel_scope(handle)
+
+ def remove_enter_idle(self, handle):
+ """Removes an idle callback.
+
+ Parameters:
+ handle: the handle of the idle callback to remove
+ """
+ try:
+ del self._idle_callbacks[handle]
+ except KeyError:
+ return False
+ return True
+
+ def remove_watch_file(self, handle):
+ """Removes a file descriptor being watched for input.
+
+ Parameters:
+ handle: the handle of the file descriptor callback to remove
+
+ Returns:
+ True if the file descriptor was watched, False otherwise
+ """
+ return self._cancel_scope(handle)
+
+ def _cancel_scope(self, scope):
+ """Cancels the given Trio cancellation scope.
+
+ Returns:
+ True if the scope was cancelled, False if it was cancelled already
+ before invoking this function
+ """
+ scope, cancelled = scope
+ existed = not cancelled[0]
+ cancelled[0] = True
+ scope.cancel()
+ return existed
+
+ def _create_cancel_scope(self):
+ """Creates a Trio cancellation scope and a corresponding mutable flag
+ that indicates whether the scope was cancelled already _from_ urwid.
+
+ This is needed because `CancelScope.cancelled_caught` stays `False` until
+ someone actually _handles_ the cancellation, and
+ `CancelScope.cancel_called` can only be called from an async context
+ (which we cannot guarantee).
+ """
+ return self._trio.CancelScope(), [False]
+
+ def run(self):
+ """Starts the event loop. Exits the loop when any callback raises an
+ exception. If ExitMainLoop is raised, exits cleanly.
+ """
+ def _exception_handler(exc):
+ if isinstance(exc, ExitMainLoop):
+ return None
+ else:
+ return exc
+
+ with self._trio.MultiError.catch(_exception_handler):
+ if not self._nursery:
+ self._trio.run(self._main_task)
+ else:
+ self._nursery.start_soon(self._main_task)
+
+ def watch_file(self, fd, callback):
+ """Calls `callback()` when the given file descriptor has some data
+ to read. No parameters are passed to the callback.
+
+ Parameters:
+ fd: file descriptor to watch for input
+ callback: function to call when some input is available
+
+ Returns:
+ a handle that may be passed to `remove_watch_file()`
+ """
+ return self._start_task(self._watch_task, fd, callback)
+
+ async def _alarm_task(self, scope, seconds, callback):
+ """Asynchronous task that sleeps for a given number of seconds and then
+ calls the given callback.
+
+ Parameters:
+ scope: the cancellation scope that can be used to cancel the task
+ seconds: the number of seconds to wait
+ callback: the callback to call
+ """
+ with scope:
+ await self._sleep(seconds)
+ callback()
+
+ async def _idle_task(self):
+ """Asynchronous task that sleeps for a short amount of time and then
+ calls all the registered idle callbacks.
+
+ Used to simulate idle callbacks in the Trio event loop that has no
+ concept of being idle.
+ """
+ while True:
+ await self._sleep(self._idle_emulation_delay)
+ for idle_callback in self._idle_callbacks.values():
+ idle_callback()
+
+ async def _main_task(self):
+ """Main Trio task that opens a nursery and then sleeps until the user
+ exits the app by raising ExitMainLoop.
+ """
+
+ if self._nursery:
+ self._schedule_pending_tasks()
+ await self._idle_task()
+ else:
+ try:
+ async with self._trio.open_nursery() as self._nursery:
+ self._schedule_pending_tasks()
+ await self._idle_task()
+ finally:
+ self._nursery = None
+
+ def _schedule_pending_tasks(self):
+ """Schedules all pending asynchronous tasks that were created before
+ the nursery to be executed on the nursery soon.
+ """
+ for task, scope, args in self._pending_tasks:
+ self._nursery.start_soon(task, scope, *args)
+ del self._pending_tasks[:]
+
+ def _start_task(self, task, *args):
+ """Starts an asynchronous task in the Trio nursery managed by the
+ main loop. If the nursery has not started yet, store a reference to
+ the task and the arguments so we can start the task when the nursery
+ is open.
+
+ Parameters:
+ task: a Trio task to run
+
+ Returns:
+ a cancellation scope for the Trio task
+ """
+ handle = self._create_cancel_scope()
+ scope, _ = handle
+ if self._nursery:
+ self._nursery.start_soon(task, scope, *args)
+ else:
+ self._pending_tasks.append((task, scope, args))
+ return handle
+
+ async def _watch_task(self, scope, fd, callback):
+ """Asynchronous task that watches the given file descriptor and calls
+ the given callback whenever the file descriptor becomes readable.
+
+ Parameters:
+ scope: the cancellation scope that can be used to cancel the task
+ fd: the file descriptor to watch
+ callback: the callback to call
+ """
+ with scope:
+ while True:
+ await self._wait_readable(fd)
+ callback()
diff --git a/urwid/main_loop.py b/urwid/main_loop.py
index d1318e8..5149505 100755
--- a/urwid/main_loop.py
+++ b/urwid/main_loop.py
@@ -28,6 +28,7 @@ import heapq
import select
import os
import signal
+import sys
from functools import wraps
from itertools import count
from weakref import WeakKeyDictionary
@@ -1493,6 +1494,12 @@ class AsyncioEventLoop(EventLoop):
reraise(*exc_info)
+# Import Trio's event loop only if we are on Python 3.5 or above (async def is
+# not supported in earlier versions).
+if sys.version_info >= (3, 5):
+ from ._async_kw_event_loop import TrioEventLoop
+
+
def _refl(name, rval=None, exit=False):
"""
This function is used to test the main loop classes.
diff --git a/urwid/tests/test_event_loops.py b/urwid/tests/test_event_loops.py
index bde0d1b..0d8cc8b 100644
--- a/urwid/tests/test_event_loops.py
+++ b/urwid/tests/test_event_loops.py
@@ -179,3 +179,21 @@ else:
asyncio.ensure_future(error_coro())
self.assertRaises(ZeroDivisionError, evl.run)
+
+
+try:
+ import trio
+except ImportError:
+ pass
+else:
+ class TrioEventLoopTest(unittest.TestCase, EventLoopTestMixin):
+ def setUp(self):
+ self.evl = urwid.TrioEventLoop()
+
+ _expected_idle_handle = None
+
+ def test_error(self):
+ evl = self.evl
+ evl.alarm(0.5, lambda: 1 / 0) # Simulate error in event loop
+ self.assertRaises(ZeroDivisionError, evl.run)
+