From be211d8278bc38e3677970f60f6005d4f9b735fe Mon Sep 17 00:00:00 2001 From: Thomas Ballinger Date: Sat, 22 Apr 2017 10:38:56 -0700 Subject: [PATCH] WIP - First draft of moving back to threads. --- bpython/curtsiesfrontend/coderunner.py | 71 ++++++++++++++----------- bpython/curtsiesfrontend/interaction.py | 39 ++++++++------ bpython/curtsiesfrontend/repl.py | 20 ++++--- doc/sphinx/source/contributing.rst | 2 +- requirements.txt | 1 - setup.py | 1 - 6 files changed, 77 insertions(+), 57 deletions(-) diff --git a/bpython/curtsiesfrontend/coderunner.py b/bpython/curtsiesfrontend/coderunner.py index 6087edb08..9a1e512f0 100644 --- a/bpython/curtsiesfrontend/coderunner.py +++ b/bpython/curtsiesfrontend/coderunner.py @@ -3,18 +3,15 @@ """For running Python code that could interrupt itself at any time in order to, for example, ask for a read on stdin, or a write on stdout -The CodeRunner spawns a greenlet to run code in, and that code can suspend its -own execution to ask the main greenlet to refresh the display or get +The CodeRunner spawns a thread to run code in, and that code can block +on a queue to ask the main (UI) thread to refresh the display or get information. - -Greenlets are basically threads that can explicitly switch control to each -other. You can replace the word "greenlet" with "thread" in these docs if that -makes more sense to you. """ import code import signal -import greenlet +from six.moves import queue +import threading import logging from bpython._py3compat import py3, is_main_thread @@ -24,12 +21,12 @@ class SigintHappened(object): - """If this class is returned, a SIGINT happened while the main greenlet""" + """If this class is returned, a SIGINT happened while the main thread""" class SystemExitFromCodeRunner(SystemExit): """If this class is returned, a SystemExit happened while in the code - greenlet""" + thread""" class RequestFromCodeRunner(object): @@ -64,7 +61,8 @@ class CodeRunner(object): Running code requests a refresh by calling request_from_main_context(force_refresh=True), which - suspends execution of the code and switches back to the main greenlet + suspends execution of the code by blocking on a queue + that the main thread was blocked on. After load_code() is called with the source code to be run, the run_code() method should be called to start running the code. @@ -80,10 +78,10 @@ class CodeRunner(object): has been gathered, run_code() should be called again, passing in any requested user input. This continues until run_code returns Done. - The code greenlet is responsible for telling the main greenlet + The code thread is responsible for telling the main thread what it wants returned in the next run_code call - CodeRunner just passes whatever is passed in to run_code(for_code) to the - code greenlet + code thread. """ def __init__(self, interp=None, request_refresh=lambda: None): @@ -96,20 +94,20 @@ def __init__(self, interp=None, request_refresh=lambda: None): """ self.interp = interp or code.InteractiveInterpreter() self.source = None - self.main_context = greenlet.getcurrent() - self.code_context = None + self.code_thread = None + self.requests_from_code_thread = queue.Queue(maxsize=0) + self.responses_for_code_thread = queue.Queue() self.request_refresh = request_refresh # waiting for response from main thread self.code_is_waiting = False # sigint happened while in main thread - self.sigint_happened_in_main_context = False + self.sigint_happened_in_main_context = False # TODO rename context to thread self.orig_sigint_handler = None @property def running(self): - """Returns greenlet if code has been loaded greenlet has been - started""" - return self.source and self.code_context + """Returns the running thread if code has been loaded and started.""" + return self.source and self.code_thread def load_code(self, source): """Prep code to be run""" @@ -117,28 +115,31 @@ def load_code(self, source): "you shouldn't load code when some is " "already running" ) self.source = source - self.code_context = None + self.code_thread = None def _unload_code(self): """Called when done running code""" self.source = None - self.code_context = None + self.code_thread = None self.code_is_waiting = False def run_code(self, for_code=None): """Returns Truthy values if code finishes, False otherwise - if for_code is provided, send that value to the code greenlet + if for_code is provided, send that value to the code thread if source code is complete, returns "done" if source code is incomplete, returns "unfinished" """ - if self.code_context is None: + if self.code_thread is None: assert self.source is not None - self.code_context = greenlet.greenlet(self._blocking_run_code) + self.code_thread = threading.Thread( + target=self._blocking_run_code, + name='codethread') + self.code_thread.daemon = True if is_main_thread(): self.orig_sigint_handler = signal.getsignal(signal.SIGINT) signal.signal(signal.SIGINT, self.sigint_handler) - request = self.code_context.switch() + self.code_thread.start() else: assert self.code_is_waiting self.code_is_waiting = False @@ -146,14 +147,15 @@ def run_code(self, for_code=None): signal.signal(signal.SIGINT, self.sigint_handler) if self.sigint_happened_in_main_context: self.sigint_happened_in_main_context = False - request = self.code_context.switch(SigintHappened) + self.responses_for_code_thread.put(SigintHappened) else: - request = self.code_context.switch(for_code) + self.responses_for_code_thread.put(for_code) + request = self.requests_from_code_thread.get() logger.debug("request received from code was %r", request) if not isinstance(request, RequestFromCodeRunner): raise ValueError( - "Not a valid value from code greenlet: %r" % request + "Not a valid value from code thread: %r" % request ) if isinstance(request, (Wait, Refresh)): self.code_is_waiting = True @@ -173,7 +175,7 @@ def run_code(self, for_code=None): def sigint_handler(self, *args): """SIGINT handler to use while code is running or request being fulfilled""" - if greenlet.getcurrent() is self.code_context: + if threading.current_thread() is self.code_thread: logger.debug("sigint while running user code!") raise KeyboardInterrupt() else: @@ -187,8 +189,11 @@ def _blocking_run_code(self): try: unfinished = self.interp.runsource(self.source) except SystemExit as e: - return SystemExitRequest(*e.args) - return Unfinished() if unfinished else Done() + self.requests_from_code_thread.push(SystemExitRequest(*e.args)) + return + self.requests_from_code_thread.put(Unfinished() + if unfinished + else Done()) def request_from_main_context(self, force_refresh=False): """Return the argument passed in to .run_code(for_code) @@ -196,9 +201,11 @@ def request_from_main_context(self, force_refresh=False): Nothing means calls to run_code must be... ??? """ if force_refresh: - value = self.main_context.switch(Refresh()) + self.requests_from_code_thread.put(Refresh()) + value = self.responses_for_code_thread.get() else: - value = self.main_context.switch(Wait()) + self.requests_from_code_thread.put(Wait()) + value = self.responses_for_code_thread.get() if value is SigintHappened: raise KeyboardInterrupt() return value diff --git a/bpython/curtsiesfrontend/interaction.py b/bpython/curtsiesfrontend/interaction.py index 65b554ead..9d140d359 100644 --- a/bpython/curtsiesfrontend/interaction.py +++ b/bpython/curtsiesfrontend/interaction.py @@ -2,7 +2,7 @@ from __future__ import unicode_literals -import greenlet +from six.moves import queue import time import curtsies.events as events @@ -46,8 +46,8 @@ def __init__( self.permanent_stack = [] if permanent_text: self.permanent_stack.append(permanent_text) - self.main_context = greenlet.getcurrent() - self.request_context = None + self.response_queue = queue.Queue() + self.request_or_notify_queue = queue.Queue() self.request_refresh = request_refresh self.schedule_refresh = schedule_refresh @@ -105,12 +105,12 @@ def process_event(self, e): elif self.in_prompt and e in ("\n", "\r", "", "Ctrl-m>"): line = self._current_line self.escape() - self.request_context.switch(line) + self.response_queue.put(line) elif self.in_confirm: if e in ("y", "Y"): - self.request_context.switch(True) + self.request_queue.put(True) else: - self.request_context.switch(False) + self.request_queue.put(False) self.escape() else: # add normal character self.add_normal_character(e) @@ -129,6 +129,7 @@ def add_normal_character(self, e): def escape(self): """unfocus from statusbar, clear prompt state, wait for notify call""" + self.wait_for_request_or_notify() self.in_prompt = False self.in_confirm = False self.prompt = "" @@ -151,28 +152,34 @@ def current_line(self): def should_show_message(self): return bool(self.current_line) - # interaction interface - should be called from other greenlets + def wait_for_request_or_notify(self): + try: + r = self.request_or_notify_queue.get(True, 1) + except queue.Empty: + raise Exception('Main thread blocked because task thread not calling back') + return r + + # interaction interface - should be called from other threads def notify(self, msg, n=3, wait_for_keypress=False): - self.request_context = greenlet.getcurrent() self.message_time = n self.message(msg, schedule_refresh=wait_for_keypress) self.waiting_for_refresh = True self.request_refresh() - self.main_context.switch(msg) + self.request_or_notify_queue.push(msg) - # below Really ought to be called from greenlets other than main because - # they block + ################################### + # These methods should be called from threads other than main because + # they block. def confirm(self, q): """Expected to return True or False, given question prompt q""" - self.request_context = greenlet.getcurrent() self.prompt = q self.in_confirm = True - return self.main_context.switch(q) + self.request_or_notify_queue.put(q) + return self.response_queue.get() def file_prompt(self, s): """Expected to return a file name, given """ - self.request_context = greenlet.getcurrent() self.prompt = s self.in_prompt = True - result = self.main_context.switch(s) - return result + self.request_or_notify_queue.put(s) + return self.response_queue.get() diff --git a/bpython/curtsiesfrontend/repl.py b/bpython/curtsiesfrontend/repl.py index 9bccca5fe..cdb77ce16 100644 --- a/bpython/curtsiesfrontend/repl.py +++ b/bpython/curtsiesfrontend/repl.py @@ -3,7 +3,7 @@ import contextlib import errno -import greenlet +import threading import logging import os import re @@ -778,15 +778,15 @@ def process_key_event(self, e): elif e in key_dispatch[self.config.redo_key]: # ctrl-g for redo self.redo() elif e in key_dispatch[self.config.save_key]: # ctrl-s for save - greenlet.greenlet(self.write2file).switch() + self.switch(self.write2file) elif e in key_dispatch[self.config.pastebin_key]: # F8 for pastebin - greenlet.greenlet(self.pastebin).switch() + self.switch(self.pastebin) elif e in key_dispatch[self.config.copy_clipboard_key]: - greenlet.greenlet(self.copy2clipboard).switch() + self.switch(self.copy2clipboard) elif e in key_dispatch[self.config.external_editor_key]: self.send_session_to_external_editor() elif e in key_dispatch[self.config.edit_config_key]: - greenlet.greenlet(self.edit_config).switch() + self.switch(self.edit_config) # TODO add PAD keys hack as in bpython.cli elif e in key_dispatch[self.config.edit_current_block_key]: self.send_current_block_to_external_editor() @@ -797,6 +797,14 @@ def process_key_event(self, e): else: self.add_normal_character(e) + def switch(self, task): + """Runs task in another thread""" + t = threading.Thread(target=task) + t.daemon = True + logging.debug('starting task thread') + t.start() + self.interact.wait_for_request_or_notify() + def get_last_word(self): previous_word = _last_word(self.rl_history.entry) @@ -1814,7 +1822,7 @@ def prompt_for_undo(): if n > 0: self.request_undo(n=n) - greenlet.greenlet(prompt_for_undo).switch() + self.switch(prompt_for_undo) def redo(self): if (self.redo_stack): diff --git a/doc/sphinx/source/contributing.rst b/doc/sphinx/source/contributing.rst index 430aabc6e..733513c19 100644 --- a/doc/sphinx/source/contributing.rst +++ b/doc/sphinx/source/contributing.rst @@ -60,7 +60,7 @@ Next install your development copy of bpython and its dependencies: .. code-block:: bash - $ sudp apt-get install python-greenlet python-pygments python-requests + $ sudp apt-get install python-pygments python-requests $ sudo apt-get install python-watchdog python-urwid $ sudo apt-get install python-sphinx python-mock python-nose diff --git a/requirements.txt b/requirements.txt index 78619ff24..39c514091 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,5 @@ Pygments curtsies >=0.1.18 -greenlet requests setuptools six >=1.5 diff --git a/setup.py b/setup.py index 24ec50055..848490502 100755 --- a/setup.py +++ b/setup.py @@ -226,7 +226,6 @@ def initialize_options(self): "pygments", "requests", "curtsies >=0.1.18", - "greenlet", "six >=1.5", ]