From 1a7964d4d509df88f61054513e3f7f074a627b71 Mon Sep 17 00:00:00 2001 From: "Eevee (Alex Munroe)" Date: Mon, 24 Mar 2014 20:45:41 -0700 Subject: Push input-watching down into the Screen. --- urwid/main_loop.py | 16 +++++----------- urwid/raw_display.py | 13 +++++++++++++ 2 files changed, 18 insertions(+), 11 deletions(-) diff --git a/urwid/main_loop.py b/urwid/main_loop.py index 0a93b5e..9b9cee0 100755 --- a/urwid/main_loop.py +++ b/urwid/main_loop.py @@ -319,16 +319,8 @@ class MainLoop(object): 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) + self.screen.unhook_event_loop(self.event_loop) + self.screen.hook_event_loop(self.event_loop, self._update) try: signals.connect_signal(self.screen, INPUT_DESCRIPTORS_CHANGED, @@ -344,9 +336,11 @@ class MainLoop(object): # tidy up self.event_loop.remove_enter_idle(idle_handle) - reset_input_descriptors(True) signals.disconnect_signal(self.screen, INPUT_DESCRIPTORS_CHANGED, reset_input_descriptors) + self.screen.unhook_event_loop(self.event_loop) + if self._input_timeout is not None: + self.event_loop.remove_alarm(self._input_timeout) def _update(self, timeout=False): """ diff --git a/urwid/raw_display.py b/urwid/raw_display.py index 185f1ab..27cf60a 100644 --- a/urwid/raw_display.py +++ b/urwid/raw_display.py @@ -364,6 +364,19 @@ class Screen(BaseScreen, RealTerminal): fd_list.append(self.gpm_mev.stdout.fileno()) return fd_list + _current_event_loop_handles = () + + def unhook_event_loop(self, event_loop): + for handle in self._current_event_loop_handles: + event_loop.remove_watch_file(handle) + + def hook_event_loop(self, event_loop, callback): + fds = self.get_input_descriptors() + handles = [] + for fd in fds: + event_loop.watch_file(fd, callback) + self._current_event_loop_handles = handles + def get_input_nonblocking(self): """ Return a (next_input_timeout, keys_pressed, raw_keycodes) -- cgit v1.2.1 From ba5e8ae2724d5069f2f95193ab89310bc7eb4b35 Mon Sep 17 00:00:00 2001 From: "Eevee (Alex Munroe)" Date: Mon, 24 Mar 2014 21:57:41 -0700 Subject: Have the Screen call back into MainLoop on new input. This should be much more readily extended by async loops like asyncio and Twisted. --- urwid/main_loop.py | 39 +++---------- urwid/raw_display.py | 155 +++++++++++++++++++++++++++++++-------------------- 2 files changed, 103 insertions(+), 91 deletions(-) diff --git a/urwid/main_loop.py b/urwid/main_loop.py index 9b9cee0..75972c3 100755 --- a/urwid/main_loop.py +++ b/urwid/main_loop.py @@ -124,7 +124,6 @@ class MainLoop(object): event_loop = SelectEventLoop() self.event_loop = event_loop - self._input_timeout = None self._watch_pipes = {} def _set_widget(self, widget): @@ -297,12 +296,12 @@ class MainLoop(object): 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, ) + screen.unhook_event_loop(...) + screen.hook_event_loop(...) event_loop.enter_idle() event_loop.run() event_loop.remove_enter_idle(1) - event_loop.remove_watch_file(2) + screen.unhook_event_loop(...) >>> scr.started = False >>> ml.run() # doctest:+ELLIPSIS screen.run_wrapper() @@ -339,49 +338,25 @@ class MainLoop(object): signals.disconnect_signal(self.screen, INPUT_DESCRIPTORS_CHANGED, reset_input_descriptors) self.screen.unhook_event_loop(self.event_loop) - if self._input_timeout is not None: - self.event_loop.remove_alarm(self._input_timeout) - def _update(self, timeout=False): + 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, ) + >>> 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: diff --git a/urwid/raw_display.py b/urwid/raw_display.py index 27cf60a..610709e 100644 --- a/urwid/raw_display.py +++ b/urwid/raw_display.py @@ -209,7 +209,6 @@ class Screen(BaseScreen, RealTerminal): self.signal_init() self._alternate_buffer = alternate_buffer - self._input_iter = self._run_input_iter() self._next_timeout = self.max_wait if not self._signal_keys_set: @@ -251,7 +250,6 @@ class Screen(BaseScreen, RealTerminal): + escape.SI + move_cursor + escape.SHOW_CURSOR) - self._input_iter = self._fake_input_iter() if self._old_signal_keys: self.tty_signal_keys(*(self._old_signal_keys + (fd,))) @@ -367,81 +365,120 @@ class Screen(BaseScreen, RealTerminal): _current_event_loop_handles = () def unhook_event_loop(self, event_loop): + """ + Remove any hooks added by hook_event_loop. + """ for handle in self._current_event_loop_handles: event_loop.remove_watch_file(handle) + if self._input_timeout: + event_loop.remove_alarm(self._input_timeout) + self._input_timeout = None + def hook_event_loop(self, event_loop, callback): + """ + Register the given callback with the event loop, to be called with new + input whenever it's available. The callback should be passed a list of + processed keys and a list of unprocessed keycodes. + + Subclasses may wish to use parse_input to wrap the callback. + """ + if hasattr(self, 'get_input_nonblocking'): + wrapper = self._make_legacy_input_wrapper(event_loop, callback) + else: + wrapper = lambda: self.parse_input(event_loop, callback) fds = self.get_input_descriptors() handles = [] for fd in fds: - event_loop.watch_file(fd, callback) + event_loop.watch_file(fd, wrapper) self._current_event_loop_handles = handles - def get_input_nonblocking(self): + _input_timeout = None + _partial_codes = None + + def _make_legacy_input_wrapper(self, event_loop, callback): """ - Return a (next_input_timeout, keys_pressed, raw_keycodes) - tuple. + Support old Screen classes that still have a get_input_nonblocking and + expect it to work. + """ + def wrapper(): + if self._input_timeout: + event_loop.remove_alarm(self._input_timeout) + self._input_timeout = None + timeout, keys, raw = self.get_input_nonblocking() + if timeout is not None: + self._input_timeout = event_loop.alarm(timeout, wrapper) - Use this method if you are implementing your own event loop. + callback(keys, raw) - When there is input waiting on one of the descriptors returned - by get_input_descriptors() this method should be called to - read and process the input. + return wrapper - This method expects to be called in next_input_timeout seconds - (a floating point number) if there is no input waiting. + def get_available_raw_input(self): """ - return self._input_iter.next() + Return any currently-available input. Does not block. - def _run_input_iter(self): - def empty_resize_pipe(): - # clean out the pipe used to signal external event loops - # that a resize has occurred - try: - while True: os.read(self._resize_pipe_rd, 1) - except OSError: - pass + This method is only used by parse_input; you can safely ignore it if + you implement your own parse_input. + """ + codes = self._get_gpm_codes() + self._get_keyboard_codes() - while True: - processed = [] - codes = self._get_gpm_codes() + \ - self._get_keyboard_codes() + if self._partial_codes: + codes = self._partial_codes + codes + self._partial_codes = None - original_codes = codes - try: - while codes: - run, codes = escape.process_keyqueue( - codes, True) - processed.extend(run) - except escape.MoreInputRequired: - k = len(original_codes) - len(codes) - yield (self.complete_wait, processed, - original_codes[:k]) - empty_resize_pipe() - original_codes = codes - processed = [] - - codes += self._get_keyboard_codes() + \ - self._get_gpm_codes() - while codes: - run, codes = escape.process_keyqueue( - codes, False) - processed.extend(run) - - if self._resized: - processed.append('window resize') - self._resized = False - - yield (self.max_wait, processed, original_codes) - empty_resize_pipe() - - def _fake_input_iter(self): - """ - This generator is a placeholder for when the screen is stopped - to always return that no input is available. + # clean out the pipe used to signal external event loops + # that a resize has occurred + try: + while True: os.read(self._resize_pipe_rd, 1) + except OSError: + pass + + return codes + + def parse_input(self, event_loop, callback, wait_for_more=True): """ - while True: - yield (self.max_wait, [], []) + Read any available input from get_available_raw_input, parses it into + keys, and calls the given callback. + + The current implementation tries to avoid any assumptions about what + the screen or event loop look like; it only deals with parsing keycodes + and setting a timeout when an incomplete one is detected. + """ + if self._input_timeout: + event_loop.remove_alarm(self._input_timeout) + self._input_timeout = None + + codes = self.get_available_raw_input() + original_codes = codes + processed = [] + try: + while codes: + run, codes = escape.process_keyqueue( + codes, wait_for_more) + processed.extend(run) + except escape.MoreInputRequired: + # Set a timer to wait for the rest of the input; if it goes off + # without any new input having come in, use the partial input + k = len(original_codes) - len(codes) + processed_codes = original_codes[:k] + self._partial_codes = codes + + def _parse_incomplete_input(): + self._input_timeout = None + self.parse_input( + event_loop, callback, wait_for_more=False) + self._input_timeout = event_loop.alarm( + self.complete_wait, self._parse_incomplete_input) + + else: + processed_codes = original_codes + self._partial_codes = None + + if self._resized: + processed.append('window resize') + self._resized = False + + callback(processed, processed_codes) def _get_keyboard_codes(self): codes = [] -- cgit v1.2.1 From 52d94d1c9634095e408fa499f57558d232412ee5 Mon Sep 17 00:00:00 2001 From: "Eevee (Alex Munroe)" Date: Sun, 11 May 2014 17:03:45 -0700 Subject: Fix up the Twisted example. --- examples/twisted_serve_ssh.py | 23 ++++++++++++++++------- urwid/main_loop.py | 4 ++-- 2 files changed, 18 insertions(+), 9 deletions(-) diff --git a/examples/twisted_serve_ssh.py b/examples/twisted_serve_ssh.py index 36396dd..aad63f9 100644 --- a/examples/twisted_serve_ssh.py +++ b/examples/twisted_serve_ssh.py @@ -34,6 +34,7 @@ Licence: LGPL import os import urwid +from urwid.raw_display import Screen from zope.interface import Interface, Attribute, implements from twisted.application.service import Application @@ -159,7 +160,7 @@ class UrwidMind(Adapter): -class TwistedScreen(urwid.BaseScreen): +class TwistedScreen(Screen): """A Urwid screen which knows about the Twisted terminal protocol that is driving it. @@ -180,7 +181,7 @@ class TwistedScreen(urwid.BaseScreen): # We will need these later self.terminalProtocol = terminalProtocol self.terminal = terminalProtocol.terminal - urwid.BaseScreen.__init__(self) + Screen.__init__(self) self.colors = 16 self._pal_escape = {} self.bright_is_bold = True @@ -235,13 +236,22 @@ class TwistedScreen(urwid.BaseScreen): # twisted handles polling, so we don't need the loop to do it, we just # push what we get to the loop from dataReceived. - def get_input_descriptors(self): - return [] + def hook_event_loop(self, event_loop, callback): + self._urwid_callback = callback + self._evl = event_loop + + def unhook_event_loop(self, event_loop): + pass # Do nothing here either. Not entirely sure when it gets called. def get_input(self, raw_keys=False): return + def get_available_raw_input(self): + data = self._data + self._data = [] + return data + # Twisted driven def push(self, data): """Receive data from Twisted and push it into the urwid main loop. @@ -254,9 +264,8 @@ class TwistedScreen(urwid.BaseScreen): 3. Pass the calculated keys as a list to the Urwid main loop. 4. Redraw the screen """ - keys = self.loop.input_filter(data, []) - keys, remainder = urwid.escape.process_keyqueue(map(ord, keys), True) - self.loop.process_input(keys) + self._data = list(map(ord, data)) + self.parse_input(self._evl, self._urwid_callback) self.loop.draw_screen() # Convenience diff --git a/urwid/main_loop.py b/urwid/main_loop.py index 75972c3..5d3be1f 100755 --- a/urwid/main_loop.py +++ b/urwid/main_loop.py @@ -116,7 +116,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,)) @@ -311,7 +311,7 @@ class MainLoop(object): if self.handle_mouse: self.screen.set_mouse_tracking() - if not hasattr(self.screen, 'get_input_descriptors'): + if not hasattr(self.screen, 'hook_event_loop'): return self._run_screen_event_loop() self.draw_screen() -- cgit v1.2.1 From 28079171a037e9bdd7a190fe8bd1446e02c8071f Mon Sep 17 00:00:00 2001 From: "Eevee (Alex Munroe)" Date: Sun, 11 May 2014 17:04:45 -0700 Subject: Fix `except ... as`. This would normally be done with 2to3. But I'm trying to run the test suite against 3, and urwid requires 2.6 anyway. --- urwid/main_loop.py | 2 +- urwid/raw_display.py | 6 +++--- urwid/util.py | 2 +- urwid/vterm.py | 4 ++-- urwid/web_display.py | 2 +- 5 files changed, 8 insertions(+), 8 deletions(-) diff --git a/urwid/main_loop.py b/urwid/main_loop.py index 5d3be1f..2c314d8 100755 --- a/urwid/main_loop.py +++ b/urwid/main_loop.py @@ -640,7 +640,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 diff --git a/urwid/raw_display.py b/urwid/raw_display.py index 610709e..cdccdef 100644 --- a/urwid/raw_display.py +++ b/urwid/raw_display.py @@ -494,7 +494,7 @@ class Screen(BaseScreen, RealTerminal): try: while self.gpm_mev is not None and self.gpm_event_pending: codes.extend(self._encode_gpm_event()) - except IOError, e: + except IOError as e: if e.args[0] != 11: raise return codes @@ -513,7 +513,7 @@ class Screen(BaseScreen, RealTerminal): ready,w,err = select.select( fd_list,[],fd_list, timeout) break - except select.error, e: + except select.error as e: if e.args[0] != 4: raise if self._resized: @@ -822,7 +822,7 @@ class Screen(BaseScreen, RealTerminal): l = l.decode('utf-8') self._term_output_file.write(l) self._term_output_file.flush() - except IOError, e: + except IOError as e: # ignore interrupted syscall if e.args[0] != 4: raise diff --git a/urwid/util.py b/urwid/util.py index ced247e..4e71f97 100644 --- a/urwid/util.py +++ b/urwid/util.py @@ -45,7 +45,7 @@ def detect_encoding(): except locale.Error: pass return locale.getlocale()[1] or "" - except ValueError, e: + except ValueError as e: # with invalid LANG value python will throw ValueError if e.args and e.args[0].startswith("unknown locale"): return "" diff --git a/urwid/vterm.py b/urwid/vterm.py index 212094c..cc4eb7f 100644 --- a/urwid/vterm.py +++ b/urwid/vterm.py @@ -1522,7 +1522,7 @@ class Terminal(Widget): try: select.select([self.master], [], [], timeout) break - except select.error, e: + except select.error as e: if e.args[0] != 4: raise self.feed() @@ -1532,7 +1532,7 @@ class Terminal(Widget): try: data = os.read(self.master, 4096) - except OSError, e: + except OSError as e: if e.errno == 5: # End Of File data = '' elif e.errno == errno.EWOULDBLOCK: # empty buffer diff --git a/urwid/web_display.py b/urwid/web_display.py index e18555e..e088476 100755 --- a/urwid/web_display.py +++ b/urwid/web_display.py @@ -880,7 +880,7 @@ class Screen: try: iready,oready,eready = select.select( [self.input_fd],[],[],0.5) - except select.error, e: + except select.error as e: # return on interruptions if e.args[0] == 4: if raw_keys: -- cgit v1.2.1 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 From ef05b48a1cd4d0071873022d1a31d2a076911eb0 Mon Sep 17 00:00:00 2001 From: "Eevee (Alex Munroe)" Date: Sun, 11 May 2014 18:58:04 -0700 Subject: Split up MainLoop._run, so the loop can be managed outside urwid. --- urwid/main_loop.py | 68 +++++++++++++++++++++++++++++++++++++++--------------- 1 file changed, 50 insertions(+), 18 deletions(-) diff --git a/urwid/main_loop.py b/urwid/main_loop.py index 84ed2d6..da57f0d 100755 --- a/urwid/main_loop.py +++ b/urwid/main_loop.py @@ -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 @@ -269,6 +272,10 @@ class MainLoop(object): 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: @@ -307,38 +314,59 @@ class MainLoop(object): screen.run_wrapper() """ - def _run(self): + def start(self): + """ + Sets up the main loop, hooking into the event loop where necessary. + + 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. + + 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` is raised. + """ if self.handle_mouse: self.screen.set_mouse_tracking() if not hasattr(self.screen, 'hook_event_loop'): - return self._run_screen_event_loop() - - self.draw_screen() - - fd_handles = [] - def reset_input_descriptors(only_remove=False): - self.screen.unhook_event_loop(self.event_loop) - self.screen.hook_event_loop(self.event_loop, self._update) + 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) - - # Go.. - self.event_loop.run() + self._reset_input_descriptors() + self.idle_handle = self.event_loop.enter_idle(self.entering_idle) - # tidy up - self.event_loop.remove_enter_idle(idle_handle) + 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) + 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: + return self._run_screen_event_loop() + + self.event_loop.run() + self.stop() + def _update(self, keys, raw): """ >>> w = _refl("widget") @@ -1013,6 +1041,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: -- cgit v1.2.1 From be714a183206490aafec0e244b166ba3de803142 Mon Sep 17 00:00:00 2001 From: "Eevee (Alex Munroe)" Date: Sun, 11 May 2014 19:54:35 -0700 Subject: Fix some Python 3 things that work fine in 2.6 anyway. --- examples/calc.py | 2 +- examples/dialog.py | 2 +- urwid/container.py | 20 ++++++++++---------- urwid/display_common.py | 2 +- urwid/escape.py | 8 ++++---- urwid/font.py | 2 +- urwid/monitored_list.py | 4 ++-- urwid/old_str_util.py | 7 ++++--- urwid/signals.py | 4 ++-- urwid/util.py | 12 +++++++----- urwid/web_display.py | 6 +++--- 11 files changed, 36 insertions(+), 33 deletions(-) diff --git a/examples/calc.py b/examples/calc.py index ec1f039..2ac324a 100755 --- a/examples/calc.py +++ b/examples/calc.py @@ -311,7 +311,7 @@ class CellColumn( urwid.WidgetWrap ): if sub != 0: # f is not an edit widget return key - if OPERATORS.has_key(key): + if key in OPERATORS: # move trailing text to new cell below edit = self.walker.get_cell(i).edit cursor_pos = edit.edit_pos diff --git a/examples/dialog.py b/examples/dialog.py index dbdeb28..7f3a4d5 100755 --- a/examples/dialog.py +++ b/examples/dialog.py @@ -321,7 +321,7 @@ status may be either on or off. def main(): - if len(sys.argv) < 2 or not MODES.has_key(sys.argv[1]): + if len(sys.argv) < 2 or sys.argv[1] not in MODES: show_usage() return diff --git a/urwid/container.py b/urwid/container.py index 24dddf9..8b61383 100755 --- a/urwid/container.py +++ b/urwid/container.py @@ -284,7 +284,7 @@ class GridFlow(WidgetWrap, WidgetContainerMixin, WidgetContainerListContentsMixi empty. """ if not self.contents: - raise IndexError, "No focus_position, GridFlow is empty" + raise IndexError("No focus_position, GridFlow is empty") return self.contents.focus def _set_focus_position(self, position): """ @@ -296,7 +296,7 @@ class GridFlow(WidgetWrap, WidgetContainerMixin, WidgetContainerListContentsMixi if position < 0 or position >= len(self.contents): raise IndexError except (TypeError, IndexError): - raise IndexError, "No GridFlow child widget at position %s" % (position,) + raise IndexError("No GridFlow child widget at position %s" % (position,)) self.contents.focus = position focus_position = property(_get_focus_position, _set_focus_position, doc=""" index of child widget in focus. Raises :exc:`IndexError` if read when @@ -607,7 +607,7 @@ class Overlay(Widget, WidgetContainerMixin, WidgetContainerListContentsMixin): position -- index of child widget to be made focus """ if position != 1: - raise IndexError, ("Overlay widget focus_position currently " + raise IndexError("Overlay widget focus_position currently " "must always be set to 1, not %s" % (position,)) focus_position = property(_get_focus_position, _set_focus_position, doc="index of child widget in focus, currently always 1") @@ -871,10 +871,10 @@ class Frame(Widget, WidgetContainerMixin): :type part: str """ if part not in ('header', 'footer', 'body'): - raise IndexError, 'Invalid position for Frame: %s' % (part,) + raise IndexError('Invalid position for Frame: %s' % (part,)) if (part == 'header' and self._header is None) or ( part == 'footer' and self._footer is None): - raise IndexError, 'This Frame has no %s' % (part,) + raise IndexError('This Frame has no %s' % (part,)) self.focus_part = part self._invalidate() @@ -1407,7 +1407,7 @@ class Pile(Widget, WidgetContainerMixin, WidgetContainerListContentsMixin): empty. """ if not self.contents: - raise IndexError, "No focus_position, Pile is empty" + raise IndexError("No focus_position, Pile is empty") return self.contents.focus def _set_focus_position(self, position): """ @@ -1419,7 +1419,7 @@ class Pile(Widget, WidgetContainerMixin, WidgetContainerListContentsMixin): if position < 0 or position >= len(self.contents): raise IndexError except (TypeError, IndexError): - raise IndexError, "No Pile child widget at position %s" % (position,) + raise IndexError("No Pile child widget at position %s" % (position,)) self.contents.focus = position focus_position = property(_get_focus_position, _set_focus_position, doc=""" index of child widget in focus. Raises :exc:`IndexError` if read when @@ -1488,7 +1488,7 @@ class Pile(Widget, WidgetContainerMixin, WidgetContainerListContentsMixin): l.append(0) # zero-weighted items treated as ('given', 0) if wtotal == 0: - raise PileError, "No weighted widgets found for Pile treated as a box widget" + raise PileError("No weighted widgets found for Pile treated as a box widget") if remaining < 0: remaining = 0 @@ -1957,7 +1957,7 @@ class Columns(Widget, WidgetContainerMixin, WidgetContainerListContentsMixin): empty. """ if not self.widget_list: - raise IndexError, "No focus_position, Columns is empty" + raise IndexError("No focus_position, Columns is empty") return self.contents.focus def _set_focus_position(self, position): """ @@ -1969,7 +1969,7 @@ class Columns(Widget, WidgetContainerMixin, WidgetContainerListContentsMixin): if position < 0 or position >= len(self.contents): raise IndexError except (TypeError, IndexError): - raise IndexError, "No Columns child widget at position %s" % (position,) + raise IndexError("No Columns child widget at position %s" % (position,)) self.contents.focus = position focus_position = property(_get_focus_position, _set_focus_position, doc=""" index of child widget in focus. Raises :exc:`IndexError` if read when diff --git a/urwid/display_common.py b/urwid/display_common.py index 789442f..b539f53 100755 --- a/urwid/display_common.py +++ b/urwid/display_common.py @@ -750,7 +750,7 @@ class BaseScreen(object): raise ScreenError("Invalid register_palette entry: %s" % repr(item)) name, like_name = item - if not self._palette.has_key(like_name): + if like_name not in self._palette: raise ScreenError("palette entry '%s' doesn't exist"%like_name) self._palette[name] = self._palette[like_name] diff --git a/urwid/escape.py b/urwid/escape.py index 077e38e..12501b8 100644 --- a/urwid/escape.py +++ b/urwid/escape.py @@ -102,7 +102,7 @@ input_sequences = [ ] + [ # modified cursor keys + home, end, 5 -- [#X and [1;#X forms (prefix+digit+letter, escape_modifier(digit) + key) - for prefix in "[","[1;" + for prefix in ("[", "[1;") for digit in "12345678" for letter,key in zip("ABCDEFGH", ('up','down','right','left','5','end','5','home')) @@ -138,7 +138,7 @@ class KeyqueueTrie(object): assert type(root) == dict, "trie conflict detected" assert len(s) > 0, "trie conflict detected" - if root.has_key(ord(s[0])): + if ord(s[0]) in root: return self.add(root[ord(s[0])], s[1:], result) if len(s)>1: d = {} @@ -163,7 +163,7 @@ class KeyqueueTrie(object): if more_available: raise MoreInputRequired() return None - if not root.has_key(keys[0]): + if keys[0] not in root: return None return self.get_recurse(root[keys[0]], keys[1:], more_available) @@ -318,7 +318,7 @@ def process_keyqueue(codes, more_available): if code >= 32 and code <= 126: key = chr(code) return [key], codes[1:] - if _keyconv.has_key(code): + if code in _keyconv: return [_keyconv[code]], codes[1:] if code >0 and code <27: return ["ctrl %s" % chr(ord('a')+code-1)], codes[1:] diff --git a/urwid/font.py b/urwid/font.py index 0a3261b..bf0c2b1 100755 --- a/urwid/font.py +++ b/urwid/font.py @@ -111,7 +111,7 @@ class Font(object): return "".join(l) def char_width(self, c): - if self.char.has_key(c): + if c in self.char: return self.char[c][0] return 0 diff --git a/urwid/monitored_list.py b/urwid/monitored_list.py index 86d749e..2e8bd00 100755 --- a/urwid/monitored_list.py +++ b/urwid/monitored_list.py @@ -158,9 +158,9 @@ class MonitoredFocusList(MonitoredList): self._focus = 0 return if index < 0 or index >= len(self): - raise IndexError, 'focus index is out of range: %s' % (index,) + raise IndexError('focus index is out of range: %s' % (index,)) if index != int(index): - raise IndexError, 'invalid focus index: %s' % (index,) + raise IndexError('invalid focus index: %s' % (index,)) index = int(index) if index != self._focus: self._focus_changed(index) diff --git a/urwid/old_str_util.py b/urwid/old_str_util.py index 04594ec..83190f5 100755 --- a/urwid/old_str_util.py +++ b/urwid/old_str_util.py @@ -19,6 +19,7 @@ # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA # # Urwid web site: http://excess.org/urwid/ +from __future__ import print_function import re @@ -357,10 +358,10 @@ def process_east_asian_width(): out.append( (num, l) ) last = l - print "widths = [" + print("widths = [") for o in out[1:]: # treat control characters same as ascii - print "\t%r," % (o,) - print "]" + print("\t%r," % (o,)) + print("]") if __name__ == "__main__": process_east_asian_width() diff --git a/urwid/signals.py b/urwid/signals.py index 80b6646..b716939 100644 --- a/urwid/signals.py +++ b/urwid/signals.py @@ -150,8 +150,8 @@ class Signals(object): sig_cls = obj.__class__ if not name in self._supported.get(sig_cls, []): - raise NameError, "No such signal %r for object %r" % \ - (name, obj) + raise NameError("No such signal %r for object %r" % + (name, obj)) # Just generate an arbitrary (but unique) key key = Key() diff --git a/urwid/util.py b/urwid/util.py index 4e71f97..f04e152 100644 --- a/urwid/util.py +++ b/urwid/util.py @@ -281,13 +281,14 @@ def rle_len( rle ): run += r return run -def rle_append_beginning_modify( rle, (a, r) ): +def rle_append_beginning_modify(rle, a_r): """ Append (a, r) to BEGINNING of rle. Merge with first run when possible MODIFIES rle parameter contents. Returns None. """ + a, r = a_r if not rle: rle[:] = [(a, r)] else: @@ -298,13 +299,14 @@ def rle_append_beginning_modify( rle, (a, r) ): rle[0:0] = [(al, r)] -def rle_append_modify( rle, (a, r) ): +def rle_append_modify(rle, a_r): """ Append (a,r) to the rle list rle. Merge with last run when possible. MODIFIES rle parameter contents. Returns None. """ + a, r = a_r if not rle or rle[-1][0] != a: rle.append( (a,r) ) return @@ -405,13 +407,13 @@ def _tagmarkup_recurse( tm, attr ): if type(tm) == tuple: # tuples mark a new attribute boundary if len(tm) != 2: - raise TagMarkupException, "Tuples must be in the form (attribute, tagmarkup): %r" % (tm,) + raise TagMarkupException("Tuples must be in the form (attribute, tagmarkup): %r" % (tm,)) attr, element = tm return _tagmarkup_recurse( element, attr ) if not isinstance(tm,(basestring, bytes)): - raise TagMarkupException, "Invalid markup element: %r" % tm + raise TagMarkupException("Invalid markup element: %r" % tm) # text return [tm], [(attr, len(tm))] @@ -431,7 +433,7 @@ class MetaSuper(type): def __init__(cls, name, bases, d): super(MetaSuper, cls).__init__(name, bases, d) if hasattr(cls, "_%s__super" % name): - raise AttributeError, "Class has same name as one of its super classes" + raise AttributeError("Class has same name as one of its super classes") setattr(cls, "_%s__super" % name, super(cls)) diff --git a/urwid/web_display.py b/urwid/web_display.py index e088476..eccbad8 100755 --- a/urwid/web_display.py +++ b/urwid/web_display.py @@ -593,7 +593,7 @@ class Screen: continue assert len(item) == 2, "Invalid register_palette usage" name, like_name = item - if not self.palette.has_key(like_name): + if like_name not in self.palette: raise Exception("palette entry '%s' doesn't exist"%like_name) self.palette[name] = self.palette[like_name] @@ -946,7 +946,7 @@ def is_web_request(): """ Return True if this is a CGI web request. """ - return os.environ.has_key('REQUEST_METHOD') + return 'REQUEST_METHOD' in os.environ def handle_short_request(): """ @@ -973,7 +973,7 @@ def handle_short_request(): # Don't know what to do with head requests etc. return False - if not os.environ.has_key('HTTP_X_URWID_ID'): + if 'HTTP_X_URWID_ID' not in os.environ: # If no urwid id, then the application should be started. return False -- cgit v1.2.1 From 3c7d8a32484c52dc0767dc8bde19d4fcb8349dd4 Mon Sep 17 00:00:00 2001 From: "Eevee (Alex Munroe)" Date: Mon, 12 May 2014 21:47:30 -0700 Subject: Fix Screen.parse_input to be easily reused by subclasses. --- urwid/raw_display.py | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/urwid/raw_display.py b/urwid/raw_display.py index cdccdef..000e4af 100644 --- a/urwid/raw_display.py +++ b/urwid/raw_display.py @@ -386,7 +386,8 @@ class Screen(BaseScreen, RealTerminal): if hasattr(self, 'get_input_nonblocking'): wrapper = self._make_legacy_input_wrapper(event_loop, callback) else: - wrapper = lambda: self.parse_input(event_loop, callback) + wrapper = lambda: self.parse_input( + event_loop, callback, self.get_available_raw_input()) fds = self.get_input_descriptors() handles = [] for fd in fds: @@ -417,8 +418,8 @@ class Screen(BaseScreen, RealTerminal): """ Return any currently-available input. Does not block. - This method is only used by parse_input; you can safely ignore it if - you implement your own parse_input. + This method is only used by the default `hook_event_loop` + implementation; you can safely ignore it if you implement your own. """ codes = self._get_gpm_codes() + self._get_keyboard_codes() @@ -435,7 +436,7 @@ class Screen(BaseScreen, RealTerminal): return codes - def parse_input(self, event_loop, callback, wait_for_more=True): + def parse_input(self, event_loop, callback, codes, wait_for_more=True): """ Read any available input from get_available_raw_input, parses it into keys, and calls the given callback. @@ -443,12 +444,15 @@ class Screen(BaseScreen, RealTerminal): The current implementation tries to avoid any assumptions about what the screen or event loop look like; it only deals with parsing keycodes and setting a timeout when an incomplete one is detected. + + `codes` should be a sequence of keycodes, i.e. bytes. A bytearray is + appropriate, but beware of using bytes, which only iterates as integers + on Python 3. """ if self._input_timeout: event_loop.remove_alarm(self._input_timeout) self._input_timeout = None - codes = self.get_available_raw_input() original_codes = codes processed = [] try: @@ -465,10 +469,11 @@ class Screen(BaseScreen, RealTerminal): def _parse_incomplete_input(): self._input_timeout = None + self._partial_codes = None self.parse_input( - event_loop, callback, wait_for_more=False) + event_loop, callback, codes, wait_for_more=False) self._input_timeout = event_loop.alarm( - self.complete_wait, self._parse_incomplete_input) + self.complete_wait, _parse_incomplete_input) else: processed_codes = original_codes -- cgit v1.2.1 From f1179521fa69e73f5b4e8cf2b31294546b4bd747 Mon Sep 17 00:00:00 2001 From: "Eevee (Alex Munroe)" Date: Tue, 3 Jun 2014 17:06:44 -0700 Subject: Put run_wrapper in the base class; make BaseScreen.start() a contextmanager. --- urwid/curses_display.py | 15 +-------------- urwid/display_common.py | 20 +++++++++++++++++++- urwid/html_fragment.py | 4 ---- urwid/lcd_display.py | 3 --- urwid/main_loop.py | 4 +++- urwid/raw_display.py | 14 -------------- urwid/util.py | 14 ++++++++++++++ urwid/web_display.py | 2 ++ 8 files changed, 39 insertions(+), 37 deletions(-) diff --git a/urwid/curses_display.py b/urwid/curses_display.py index 758d621..0a6155a 100755 --- a/urwid/curses_display.py +++ b/urwid/curses_display.py @@ -130,7 +130,7 @@ class Screen(BaseScreen, RealTerminal): if not self._signal_keys_set: self._old_signal_keys = self.tty_signal_keys() - super(Screen, self).start() + return super(Screen, self).start() def stop(self): @@ -152,19 +152,6 @@ class Screen(BaseScreen, RealTerminal): super(Screen, self).stop() - def run_wrapper(self,fn): - """Call fn in fullscreen mode. Return to normal on exit. - - This function should be called to wrap your main program loop. - Exception tracebacks will be displayed in normal mode. - """ - - try: - self.start() - return fn() - finally: - self.stop() - def _setup_colour_pairs(self): """ Initialize all 63 color pairs based on the term: diff --git a/urwid/display_common.py b/urwid/display_common.py index b539f53..3f0e975 100755 --- a/urwid/display_common.py +++ b/urwid/display_common.py @@ -26,7 +26,7 @@ try: except ImportError: pass # windows -from urwid.util import int_scale +from urwid.util import StoppingContext, int_scale from urwid import signals from urwid.compat import B, bytes3 @@ -719,11 +719,29 @@ class BaseScreen(object): started = property(lambda self: self._started) def start(self): + """Set up the screen. + + May be used as a context manager, in which case `stop` will + automatically be called at the end of the block: + + with screen.start(): + ... + """ self._started = True + return StoppingContext(self) def stop(self): self._started = False + def run_wrapper(self, fn, *args, **kwargs): + """Start the screen, call a function, then stop the screen. Extra + arguments are passed to `start`. + + Deprecated in favor of calling `start` as a context manager. + """ + with self.start(*args, **kwargs): + return fn() + def register_palette(self, palette): """Register a set of palette entries. diff --git a/urwid/html_fragment.py b/urwid/html_fragment.py index 159bc81..86c7781 100755 --- a/urwid/html_fragment.py +++ b/urwid/html_fragment.py @@ -82,10 +82,6 @@ class HtmlGenerator(BaseScreen): def reset_default_terminal_palette(self, *args): pass - def run_wrapper(self,fn): - """Call fn.""" - return fn() - def draw_screen(self, (cols, rows), r ): """Create an html fragment from the render object. Append it to HtmlGenerator.fragments list. diff --git a/urwid/lcd_display.py b/urwid/lcd_display.py index ca6336f..13190bd 100644 --- a/urwid/lcd_display.py +++ b/urwid/lcd_display.py @@ -45,9 +45,6 @@ class LCDScreen(BaseScreen): def reset_default_terminal_palette(self, *args): pass - def run_wrapper(self,fn): - return fn() - def draw_screen(self, (cols, rows), r ): pass diff --git a/urwid/main_loop.py b/urwid/main_loop.py index da57f0d..f5f03dc 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 @@ -343,6 +343,8 @@ class MainLoop(object): self._reset_input_descriptors() self.idle_handle = self.event_loop.enter_idle(self.entering_idle) + return StoppingContext(self) + def stop(self): """ Cleans up any hooks added to the event loop. Only call this if you're diff --git a/urwid/raw_display.py b/urwid/raw_display.py index 000e4af..e4cab86 100644 --- a/urwid/raw_display.py +++ b/urwid/raw_display.py @@ -257,20 +257,6 @@ class Screen(BaseScreen, RealTerminal): super(Screen, self).stop() - def run_wrapper(self, fn, alternate_buffer=True): - """ - Call start to initialize screen, then call fn. - When fn exits call stop to restore the screen to normal. - - alternate_buffer -- use alternate screen buffer and restore - normal screen buffer on exit - """ - try: - self.start(alternate_buffer) - return fn() - finally: - self.stop() - def get_input(self, raw_keys=False): """Return pending input as a list. diff --git a/urwid/util.py b/urwid/util.py index f04e152..61e5eb6 100644 --- a/urwid/util.py +++ b/urwid/util.py @@ -458,3 +458,17 @@ def int_scale(val, val_range, out_range): # if num % dem == 0 then we are exactly half-way and have rounded up. return num // dem + +class StoppingContext(object): + """Context manager that calls ``stop`` on a given object on exit. Used to + make the ``start`` method on `MainLoop` and `BaseScreen` optionally act as + context managers. + """ + def __init__(self, wrapped): + self._wrapped = wrapped + + def __enter__(self): + return self + + def __exit__(self, *exc_info): + self._wrapped.stop() diff --git a/urwid/web_display.py b/urwid/web_display.py index eccbad8..2258718 100755 --- a/urwid/web_display.py +++ b/urwid/web_display.py @@ -679,6 +679,8 @@ class Screen: signal.alarm( ALARM_DELAY ) self._started = True + return util.StoppingContext(self) + def stop(self): """ Restore settings and clean up. -- cgit v1.2.1 From 5386dd6e283464ef492a6ce70fed0074f48719ae Mon Sep 17 00:00:00 2001 From: "Eevee (Alex Munroe)" Date: Tue, 3 Jun 2014 18:03:36 -0700 Subject: Make BaseScreen.start() and stop() idempotent. --- urwid/curses_display.py | 13 ++++--------- urwid/display_common.py | 20 +++++++++++++++++--- urwid/html_fragment.py | 6 ------ urwid/lcd_display.py | 6 ------ urwid/main_loop.py | 16 ++++++++-------- urwid/raw_display.py | 14 ++++++-------- urwid/web_display.py | 7 +++++-- 7 files changed, 40 insertions(+), 42 deletions(-) diff --git a/urwid/curses_display.py b/urwid/curses_display.py index 0a6155a..441042e 100755 --- a/urwid/curses_display.py +++ b/urwid/curses_display.py @@ -102,12 +102,10 @@ class Screen(BaseScreen, RealTerminal): self._mouse_tracking_enabled = enable - def start(self): + def _start(self): """ Initialize the screen and input mode. """ - assert self._started == False - self.s = curses.initscr() self.has_color = curses.has_colors() if self.has_color: @@ -130,15 +128,12 @@ class Screen(BaseScreen, RealTerminal): if not self._signal_keys_set: self._old_signal_keys = self.tty_signal_keys() - return super(Screen, self).start() - + super(Screen, self)._start() - def stop(self): + def _stop(self): """ Restore the screen. """ - if self._started == False: - return curses.echo() self._curs_set(1) try: @@ -149,7 +144,7 @@ class Screen(BaseScreen, RealTerminal): if self._old_signal_keys: self.tty_signal_keys(*self._old_signal_keys) - super(Screen, self).stop() + super(Screen, self)._stop() def _setup_colour_pairs(self): diff --git a/urwid/display_common.py b/urwid/display_common.py index 3f0e975..bf292b3 100755 --- a/urwid/display_common.py +++ b/urwid/display_common.py @@ -718,21 +718,35 @@ class BaseScreen(object): started = property(lambda self: self._started) - def start(self): - """Set up the screen. + def start(self, *args, **kwargs): + """Set up the screen. If the screen has already been started, does + nothing. - May be used as a context manager, in which case `stop` will + May be used as a context manager, in which case :meth:`stop` will automatically be called at the end of the block: with screen.start(): ... + + You shouldn't override this method in a subclass; instead, override + :meth:`_start`. """ + if not self._started: + self._start(*args, **kwargs) self._started = True return StoppingContext(self) + def _start(self): + pass + def stop(self): + if self._started: + self._stop() self._started = False + def _stop(self): + pass + def run_wrapper(self, fn, *args, **kwargs): """Start the screen, call a function, then stop the screen. Extra arguments are passed to `start`. diff --git a/urwid/html_fragment.py b/urwid/html_fragment.py index 86c7781..380d1d3 100755 --- a/urwid/html_fragment.py +++ b/urwid/html_fragment.py @@ -70,12 +70,6 @@ class HtmlGenerator(BaseScreen): """Not yet implemented""" pass - def start(self): - pass - - def stop(self): - pass - def set_input_timeouts(self, *args): pass diff --git a/urwid/lcd_display.py b/urwid/lcd_display.py index 13190bd..4f62173 100644 --- a/urwid/lcd_display.py +++ b/urwid/lcd_display.py @@ -33,12 +33,6 @@ class LCDScreen(BaseScreen): def set_mouse_tracking(self, enable=True): pass - def start(self): - pass - - def stop(self): - pass - def set_input_timeouts(self, *args): pass diff --git a/urwid/main_loop.py b/urwid/main_loop.py index f5f03dc..967596d 100755 --- a/urwid/main_loop.py +++ b/urwid/main_loop.py @@ -270,18 +270,13 @@ 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: + with self.screen.start(): self._run() - else: - self.screen.run_wrapper(self._run) except ExitMainLoop: pass @@ -317,15 +312,20 @@ class MainLoop(object): 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 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. + 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: + + with main_loop.start(): + ... 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` is raised. + :exc:`ExitMainLoop` (or anything else) is raised. """ if self.handle_mouse: self.screen.set_mouse_tracking() diff --git a/urwid/raw_display.py b/urwid/raw_display.py index e4cab86..6dbeb3f 100644 --- a/urwid/raw_display.py +++ b/urwid/raw_display.py @@ -189,13 +189,12 @@ class Screen(BaseScreen, RealTerminal): os.waitpid(self.gpm_mev.pid, 0) self.gpm_mev = None - def start(self, alternate_buffer=True): + def _start(self, alternate_buffer=True): """ Initialize the screen and input mode. alternate_buffer -- use alternate screen buffer """ - assert not self._started if alternate_buffer: self._term_output_file.write(escape.SWITCH_TO_ALTERNATE_BUFFER) self._rows_used = None @@ -214,19 +213,17 @@ class Screen(BaseScreen, RealTerminal): if not self._signal_keys_set: self._old_signal_keys = self.tty_signal_keys(fileno=fd) - super(Screen, self).start() - signals.emit_signal(self, INPUT_DESCRIPTORS_CHANGED) # restore mouse tracking to previous state self._mouse_tracking(self._mouse_tracking_enabled) - def stop(self): + return super(Screen, self)._start() + + def _stop(self): """ Restore the screen. """ self.clear() - if not self._started: - return signals.emit_signal(self, INPUT_DESCRIPTORS_CHANGED) @@ -254,7 +251,8 @@ class Screen(BaseScreen, RealTerminal): if self._old_signal_keys: self.tty_signal_keys(*(self._old_signal_keys + (fd,))) - super(Screen, self).stop() + super(Screen, self)._stop() + def get_input(self, raw_keys=False): diff --git a/urwid/web_display.py b/urwid/web_display.py index 2258718..44a505c 100755 --- a/urwid/web_display.py +++ b/urwid/web_display.py @@ -632,7 +632,8 @@ class Screen: """ global _prefs - assert not self._started + if self._started: + return util.StoppingContext(self) client_init = sys.stdin.read(50) assert client_init.startswith("window resize "),client_init @@ -685,7 +686,9 @@ class Screen: """ Restore settings and clean up. """ - assert self._started + if not self._started: + return + # XXX which exceptions does this actually raise? EnvironmentError? try: self._close_connection() -- cgit v1.2.1 From 0761c961963b7deb3256bfdafb1b7664f9f80be3 Mon Sep 17 00:00:00 2001 From: "Eevee (Alex Munroe)" Date: Tue, 3 Jun 2014 19:47:00 -0700 Subject: MainLoop.start/stop now start/stop the screen, too. --- urwid/main_loop.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/urwid/main_loop.py b/urwid/main_loop.py index 967596d..613f5c4 100755 --- a/urwid/main_loop.py +++ b/urwid/main_loop.py @@ -275,8 +275,7 @@ class MainLoop(object): and :meth:`stop` once it's finished. """ try: - with self.screen.start(): - self._run() + self._run() except ExitMainLoop: pass @@ -327,6 +326,8 @@ class MainLoop(object): 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() @@ -356,6 +357,8 @@ class MainLoop(object): 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) -- cgit v1.2.1 From d0275264e4f421cbf1378b634061c121442265fe Mon Sep 17 00:00:00 2001 From: "Eevee (Alex Munroe)" Date: Tue, 3 Jun 2014 19:49:56 -0700 Subject: Add Screen.write and Screen.flush. --- urwid/raw_display.py | 44 +++++++++++++++++++++++++++++++------------- 1 file changed, 31 insertions(+), 13 deletions(-) diff --git a/urwid/raw_display.py b/urwid/raw_display.py index 6dbeb3f..6733c5e 100644 --- a/urwid/raw_display.py +++ b/urwid/raw_display.py @@ -48,7 +48,7 @@ from subprocess import Popen, PIPE class Screen(BaseScreen, RealTerminal): - def __init__(self): + def __init__(self, input=sys.stdin, output=sys.stdout): """Initialize a screen that directly prints escape codes to an output terminal. """ @@ -78,8 +78,11 @@ class Screen(BaseScreen, RealTerminal): self.bright_is_bold = not term.startswith("xterm") self.back_color_erase = not term.startswith("screen") self._next_timeout = None - self._term_output_file = sys.stdout - self._term_input_file = sys.stdin + + # Our connections to the world + self._term_output_file = output + self._term_input_file = input + # pipe for signalling external event loops about resize events self._resize_pipe_rd, self._resize_pipe_wr = os.pipe() fcntl.fcntl(self._resize_pipe_rd, fcntl.F_SETFL, os.O_NONBLOCK) @@ -164,10 +167,10 @@ class Screen(BaseScreen, RealTerminal): def _mouse_tracking(self, enable): if enable: - self._term_output_file.write(escape.MOUSE_TRACKING_ON) + self.write(escape.MOUSE_TRACKING_ON) self._start_gpm_tracking() else: - self._term_output_file.write(escape.MOUSE_TRACKING_OFF) + self.write(escape.MOUSE_TRACKING_OFF) self._stop_gpm_tracking() def _start_gpm_tracking(self): @@ -196,7 +199,7 @@ class Screen(BaseScreen, RealTerminal): alternate_buffer -- use alternate screen buffer """ if alternate_buffer: - self._term_output_file.write(escape.SWITCH_TO_ALTERNATE_BUFFER) + self.write(escape.SWITCH_TO_ALTERNATE_BUFFER) self._rows_used = None else: self._rows_used = 0 @@ -242,7 +245,7 @@ class Screen(BaseScreen, RealTerminal): elif self.maxrow is not None: move_cursor = escape.set_cursor_position( 0, self.maxrow) - self._term_output_file.write( + self.write( self._attrspec_to_escape(AttrSpec('','')) + escape.SI + move_cursor @@ -254,6 +257,21 @@ class Screen(BaseScreen, RealTerminal): super(Screen, self)._stop() + def write(self, data): + """Write some data to the terminal. + + You may wish to override this if you're using something other than + regular files for input and output. + """ + self._term_output_file.write(data) + + def flush(self): + """Flush the output buffer. + + You may wish to override this if you're using something other than + regular files for input and output. + """ + self._term_output_file.flush() def get_input(self, raw_keys=False): """Return pending input as a list. @@ -631,8 +649,8 @@ class Screen(BaseScreen, RealTerminal): while True: try: - self._term_output_file.write(escape.DESIGNATE_G1_SPECIAL) - self._term_output_file.flush() + self.write(escape.DESIGNATE_G1_SPECIAL) + self.flush() break except IOError: pass @@ -809,8 +827,8 @@ class Screen(BaseScreen, RealTerminal): for l in o: if isinstance(l, bytes) and PYTHON3: l = l.decode('utf-8') - self._term_output_file.write(l) - self._term_output_file.flush() + self.write(l) + self.flush() except IOError as e: # ignore interrupted syscall if e.args[0] != 4: @@ -984,8 +1002,8 @@ class Screen(BaseScreen, RealTerminal): modify = ["%d;rgb:%02x/%02x/%02x" % (index, red, green, blue) for index, red, green, blue in entries] - self._term_output_file.write("\x1b]4;"+";".join(modify)+"\x1b\\") - self._term_output_file.flush() + self.write("\x1b]4;"+";".join(modify)+"\x1b\\") + self.flush() # shortcut for creating an AttrSpec with this screen object's -- cgit v1.2.1 From 83b64fee60fd77bc80f3dda307c74b53b35f6581 Mon Sep 17 00:00:00 2001 From: "Eevee (Alex Munroe)" Date: Tue, 3 Jun 2014 19:50:08 -0700 Subject: Add an example that uses asyncio. --- examples/asyncio_socket_server.py | 186 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 186 insertions(+) create mode 100644 examples/asyncio_socket_server.py diff --git a/examples/asyncio_socket_server.py b/examples/asyncio_socket_server.py new file mode 100644 index 0000000..87592d3 --- /dev/null +++ b/examples/asyncio_socket_server.py @@ -0,0 +1,186 @@ +"""Demo of using urwid with Python 3.4's asyncio. + +This code works on older Python 3.x if you install `asyncio` from PyPI, and +even Python 2 if you install `trollius`! +""" +from __future__ import print_function + +import asyncio +from datetime import datetime +import sys +import weakref + +import urwid +from urwid.raw_display import Screen + + +loop = asyncio.get_event_loop() + + +# ----------------------------------------------------------------------------- +# General-purpose setup code + +def build_widgets(): + input1 = urwid.Edit('What is your name? ') + input2 = urwid.Edit('What is your quest? ') + input3 = urwid.Edit('What is the capital of Assyria? ') + inputs = [input1, input2, input3] + + def update_clock(widget_ref): + widget = widget_ref() + if not widget: + # widget is dead; the main loop must've been destroyed + return + + widget.set_text(datetime.now().isoformat()) + + # Schedule us to update the clock again in one second + loop.call_later(1, update_clock, widget_ref) + + clock = urwid.Text('') + update_clock(weakref.ref(clock)) + + return urwid.Filler(urwid.Pile([clock] + inputs), 'top') + + +def unhandled(key): + if key == 'ctrl c': + raise urwid.ExitMainLoop + + +# ----------------------------------------------------------------------------- +# Demo 1 + +def demo1(): + """Plain old urwid app. Just happens to be run atop asyncio as the event + loop. + + Note that the clock is updated using the asyncio loop directly, not via any + of urwid's facilities. + """ + main_widget = build_widgets() + + urwid_loop = urwid.MainLoop( + main_widget, + event_loop=urwid.AsyncioEventLoop(loop=loop), + unhandled_input=unhandled, + ) + urwid_loop.run() + + +# ----------------------------------------------------------------------------- +# Demo 2 + +class AsyncScreen(Screen): + """An urwid screen that speaks to an asyncio stream, rather than polling + file descriptors. + """ + def __init__(self, reader, writer): + self.reader = reader + self.writer = writer + + Screen.__init__(self, None, None) + + _pending_task = None + + def write(self, data): + self.writer.write(data) + + def flush(self): + pass + + def hook_event_loop(self, event_loop, callback): + # Wait on the reader's read coro, and when there's data to read, call + # the callback and then wait again + def pump_reader(fut=None): + if fut is None: + # First call, do nothing + pass + elif fut.cancelled(): + # This is in response to an earlier .read() call, so don't + # schedule another one! + return + elif fut.exception(): + pass + else: + try: + self.parse_input( + event_loop, callback, bytearray(fut.result())) + except urwid.ExitMainLoop: + # This will immediately close the transport and thus the + # connection, which in turn calls connection_lost, which + # stops the screen and the loop + self.writer.abort() + + # asyncio.async() schedules a coroutine without using `yield from`, + # which would make this code not work on Python 2 + self._pending_task = asyncio.async( + self.reader.read(1024), loop=event_loop._loop) + self._pending_task.add_done_callback(pump_reader) + + pump_reader() + + def unhook_event_loop(self, event_loop): + if self._pending_task: + self._pending_task.cancel() + del self._pending_task + + +class UrwidProtocol(asyncio.Protocol): + def connection_made(self, transport): + print("Got a client!") + self.transport = transport + + # StreamReader is super convenient here; it has a regular method on our + # end (feed_data), and a coroutine on the other end that will + # faux-block until there's data to be read. We could also just call a + # method directly on the screen, but this keeps the screen somewhat + # separate from the protocol. + self.reader = asyncio.StreamReader(loop=loop) + screen = AsyncScreen(self.reader, transport) + + main_widget = build_widgets() + self.urwid_loop = urwid.MainLoop( + main_widget, + event_loop=urwid.AsyncioEventLoop(loop=loop), + screen=screen, + unhandled_input=unhandled, + ) + + self.urwid_loop.start() + + def data_received(self, data): + self.reader.feed_data(data) + + def connection_lost(self, exc): + print("Lost a client...") + self.reader.feed_eof() + self.urwid_loop.stop() + + +def demo2(): + """Urwid app served over the network to multiple clients at once, using an + asyncio Protocol. + """ + coro = loop.create_server(UrwidProtocol, port=12345) + loop.run_until_complete(coro) + print("OK, good to go! Try this in another terminal (or two):") + print() + print(" socat TCP:127.0.0.1:12345 STDIN,raw") + print() + loop.run_forever() + + +if __name__ == '__main__': + if len(sys.argv) == 2: + which = sys.argv[1] + else: + which = None + + if which == '1': + demo1() + elif which == '2': + demo2() + else: + print("Please run me with an argument of either 1 or 2.") + sys.exit(1) -- cgit v1.2.1 From bccb52c5071d2efc4f9445d6e3085d6cb5ef1efd Mon Sep 17 00:00:00 2001 From: "Eevee (Alex Munroe)" Date: Tue, 3 Jun 2014 20:12:28 -0700 Subject: Fix a doctest. --- urwid/main_loop.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/urwid/main_loop.py b/urwid/main_loop.py index 613f5c4..e45208f 100755 --- a/urwid/main_loop.py +++ b/urwid/main_loop.py @@ -293,19 +293,19 @@ 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.get_cols_rows() - widget.render((20, 10), focus=True) - screen.draw_screen((20, 10), 'fake canvas') screen.unhook_event_loop(...) screen.hook_event_loop(...) - event_loop.enter_idle() + event_loop.enter_idle() event_loop.run() event_loop.remove_enter_idle(1) screen.unhook_event_loop(...) - >>> scr.started = False - >>> ml.run() # doctest:+ELLIPSIS - screen.run_wrapper() + screen.stop() + >>> ml.draw_screen() # doctest:+ELLIPSIS + screen.get_cols_rows() + widget.render((20, 10), focus=True) + screen.draw_screen((20, 10), 'fake canvas') """ def start(self): -- cgit v1.2.1 From 9957eab12c2db5f258a7de42a9c32a651d8c9e85 Mon Sep 17 00:00:00 2001 From: Jonas Wielicki Date: Wed, 9 Jul 2014 18:27:58 +0200 Subject: Adapt docstrings to changed arguments --- urwid/util.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/urwid/util.py b/urwid/util.py index 61e5eb6..3569f8c 100644 --- a/urwid/util.py +++ b/urwid/util.py @@ -283,7 +283,7 @@ def rle_len( rle ): def rle_append_beginning_modify(rle, a_r): """ - Append (a, r) to BEGINNING of rle. + Append (a, r) (unpacked from *a_r*) to BEGINNING of rle. Merge with first run when possible MODIFIES rle parameter contents. Returns None. @@ -301,7 +301,7 @@ def rle_append_beginning_modify(rle, a_r): def rle_append_modify(rle, a_r): """ - Append (a,r) to the rle list rle. + Append (a, r) (unpacked from *a_r*) to the rle list rle. Merge with last run when possible. MODIFIES rle parameter contents. Returns None. -- cgit v1.2.1 From 6e964f59c7c676c896f8892c304080e59900051e Mon Sep 17 00:00:00 2001 From: Jonas Wielicki Date: Wed, 9 Jul 2014 19:11:40 +0200 Subject: Stop screen even without external event loop --- urwid/main_loop.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/urwid/main_loop.py b/urwid/main_loop.py index e45208f..77022bf 100755 --- a/urwid/main_loop.py +++ b/urwid/main_loop.py @@ -367,7 +367,10 @@ class MainLoop(object): try: self.start() except CantUseExternalLoop: - return self._run_screen_event_loop() + try: + return self._run_screen_event_loop() + finally: + self.screen.stop() self.event_loop.run() self.stop() -- cgit v1.2.1 From 50c98dd5ea4d3feade969489461911f5d00ebd09 Mon Sep 17 00:00:00 2001 From: "Eevee (Alex Munroe)" Date: Sun, 27 Jul 2014 18:00:05 -0700 Subject: Fix calling get_input() on the raw screen. This isn't something urwid ever does by itself, but pudb apparently does it, and it was completely broken as written. --- urwid/raw_display.py | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/urwid/raw_display.py b/urwid/raw_display.py index 6733c5e..a3d14a0 100644 --- a/urwid/raw_display.py +++ b/urwid/raw_display.py @@ -320,14 +320,13 @@ class Screen(BaseScreen, RealTerminal): assert self._started self._wait_for_input_ready(self._next_timeout) - self._next_timeout, keys, raw = self._input_iter.next() + keys, raw = self.parse_input(None, None, self.get_available_raw_input()) # Avoid pegging CPU at 100% when slowly resizing if keys==['window resize'] and self.prev_input_resize: while True: self._wait_for_input_ready(self.resize_wait) - self._next_timeout, keys, raw2 = \ - self._input_iter.next() + keys, raw2 = self.parse_input(None, None, self.get_available_raw_input()) raw += raw2 #if not keys: # keys, raw2 = self._get_input( @@ -451,7 +450,9 @@ class Screen(BaseScreen, RealTerminal): appropriate, but beware of using bytes, which only iterates as integers on Python 3. """ - if self._input_timeout: + # Note: event_loop may be None for 100% synchronous support, only used + # by get_input. Not documented because you shouldn't be doing it. + if self._input_timeout and event_loop: event_loop.remove_alarm(self._input_timeout) self._input_timeout = None @@ -474,8 +475,9 @@ class Screen(BaseScreen, RealTerminal): self._partial_codes = None self.parse_input( event_loop, callback, codes, wait_for_more=False) - self._input_timeout = event_loop.alarm( - self.complete_wait, _parse_incomplete_input) + if event_loop: + self._input_timeout = event_loop.alarm( + self.complete_wait, _parse_incomplete_input) else: processed_codes = original_codes @@ -485,7 +487,11 @@ class Screen(BaseScreen, RealTerminal): processed.append('window resize') self._resized = False - callback(processed, processed_codes) + if callback: + callback(processed, processed_codes) + else: + # For get_input + return processed, processed_codes def _get_keyboard_codes(self): codes = [] -- cgit v1.2.1