-
-
Notifications
You must be signed in to change notification settings - Fork 245
WIP - Moving back to threads #681
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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,64 +94,68 @@ 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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. See above. |
||
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""" | ||
assert self.source is None, ( | ||
"you shouldn't load code when some is " "already running" | ||
) | ||
self.source = source | ||
self.code_context = None | ||
self.code_thread = None | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. See above. This is just noise. |
||
|
||
def _unload_code(self): | ||
"""Called when done running code""" | ||
self.source = None | ||
self.code_context = None | ||
self.code_thread = None | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. As above. |
||
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: | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. As above. |
||
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 | ||
if is_main_thread(): | ||
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,18 +189,23 @@ 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) | ||
|
||
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 | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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() | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. As above. |
||
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-j>", "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() |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,6 +1,5 @@ | ||
Pygments | ||
curtsies >=0.1.18 | ||
greenlet | ||
requests | ||
setuptools | ||
six >=1.5 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The intention of 3057732 and 2b8c30b was to use generic names so we do not have to rename them every time we change the implementation.