Skip to content

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

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
71 changes: 39 additions & 32 deletions bpython/curtsiesfrontend/coderunner.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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):
Expand Down Expand Up @@ -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.
Expand All @@ -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):
Expand All @@ -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
Copy link
Contributor

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.

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
Copy link
Contributor

Choose a reason for hiding this comment

The 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
Copy link
Contributor

Choose a reason for hiding this comment

The 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
Copy link
Contributor

Choose a reason for hiding this comment

The 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:
Copy link
Contributor

Choose a reason for hiding this comment

The 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
Expand All @@ -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:
Expand All @@ -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
Expand Down
39 changes: 23 additions & 16 deletions bpython/curtsiesfrontend/interaction.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

from __future__ import unicode_literals

import greenlet
from six.moves import queue
import time
import curtsies.events as events

Expand Down Expand Up @@ -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()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As above.

self.request_refresh = request_refresh
self.schedule_refresh = schedule_refresh

Expand Down Expand Up @@ -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)
Expand All @@ -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 = ""
Expand All @@ -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()
20 changes: 14 additions & 6 deletions bpython/curtsiesfrontend/repl.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

import contextlib
import errno
import greenlet
import threading
import logging
import os
import re
Expand Down Expand Up @@ -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()
Expand All @@ -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)
Expand Down Expand Up @@ -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):
Expand Down
2 changes: 1 addition & 1 deletion doc/sphinx/source/contributing.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
1 change: 0 additions & 1 deletion requirements.txt
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
1 change: 0 additions & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -226,7 +226,6 @@ def initialize_options(self):
"pygments",
"requests",
"curtsies >=0.1.18",
"greenlet",
"six >=1.5",
]

Expand Down