Skip to content

Commit cd8febb

Browse files
WIP - First draft of moving back to threads.
1 parent 91bdd42 commit cd8febb

File tree

6 files changed

+76
-57
lines changed

6 files changed

+76
-57
lines changed

bpython/curtsiesfrontend/coderunner.py

Lines changed: 38 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -3,18 +3,15 @@
33
"""For running Python code that could interrupt itself at any time in order to,
44
for example, ask for a read on stdin, or a write on stdout
55
6-
The CodeRunner spawns a greenlet to run code in, and that code can suspend its
7-
own execution to ask the main greenlet to refresh the display or get
6+
The CodeRunner spawns a thread to run code in, and that code can block
7+
on a queue to ask the main (UI) thread to refresh the display or get
88
information.
9-
10-
Greenlets are basically threads that can explicitly switch control to each
11-
other. You can replace the word "greenlet" with "thread" in these docs if that
12-
makes more sense to you.
139
"""
1410

1511
import code
1612
import signal
17-
import greenlet
13+
from six.moves import queue
14+
import threading
1815
import logging
1916

2017
from bpython._py3compat import py3
@@ -24,12 +21,12 @@
2421

2522

2623
class SigintHappened(object):
27-
"""If this class is returned, a SIGINT happened while the main greenlet"""
24+
"""If this class is returned, a SIGINT happened while the main thread"""
2825

2926

3027
class SystemExitFromCodeRunner(SystemExit):
3128
"""If this class is returned, a SystemExit happened while in the code
32-
greenlet"""
29+
thread"""
3330

3431

3532
class RequestFromCodeRunner(object):
@@ -64,7 +61,8 @@ class CodeRunner(object):
6461
6562
Running code requests a refresh by calling
6663
request_from_main_context(force_refresh=True), which
67-
suspends execution of the code and switches back to the main greenlet
64+
suspends execution of the code by blocking on a queue
65+
that the main thread was blocked on.
6866
6967
After load_code() is called with the source code to be run,
7068
the run_code() method should be called to start running the code.
@@ -80,10 +78,10 @@ class CodeRunner(object):
8078
has been gathered, run_code() should be called again, passing in any
8179
requested user input. This continues until run_code returns Done.
8280
83-
The code greenlet is responsible for telling the main greenlet
81+
The code thread is responsible for telling the main thread
8482
what it wants returned in the next run_code call - CodeRunner
8583
just passes whatever is passed in to run_code(for_code) to the
86-
code greenlet
84+
code thread.
8785
"""
8886
def __init__(self, interp=None, request_refresh=lambda: None):
8987
"""
@@ -95,60 +93,63 @@ def __init__(self, interp=None, request_refresh=lambda: None):
9593
"""
9694
self.interp = interp or code.InteractiveInterpreter()
9795
self.source = None
98-
self.main_context = greenlet.getcurrent()
99-
self.code_context = None
96+
self.code_thread = None
97+
self.requests_from_code_thread = queue.Queue(maxsize=0)
98+
self.responses_for_code_thread = queue.Queue()
10099
self.request_refresh = request_refresh
101100
# waiting for response from main thread
102101
self.code_is_waiting = False
103102
# sigint happened while in main thread
104-
self.sigint_happened_in_main_context = False
103+
self.sigint_happened_in_main_context = False # TODO rename context to thread
105104
self.orig_sigint_handler = None
106105

107106
@property
108107
def running(self):
109-
"""Returns greenlet if code has been loaded greenlet has been
110-
started"""
111-
return self.source and self.code_context
108+
"""Returns the running thread if code has been loaded and started."""
109+
return self.source and self.code_thread
112110

113111
def load_code(self, source):
114112
"""Prep code to be run"""
115113
assert self.source is None, "you shouldn't load code when some is " \
116114
"already running"
117115
self.source = source
118-
self.code_context = None
116+
self.code_thread = None
119117

120118
def _unload_code(self):
121119
"""Called when done running code"""
122120
self.source = None
123-
self.code_context = None
121+
self.code_thread = None
124122
self.code_is_waiting = False
125123

126124
def run_code(self, for_code=None):
127125
"""Returns Truthy values if code finishes, False otherwise
128126
129-
if for_code is provided, send that value to the code greenlet
127+
if for_code is provided, send that value to the code thread
130128
if source code is complete, returns "done"
131129
if source code is incomplete, returns "unfinished"
132130
"""
133-
if self.code_context is None:
131+
if self.code_thread is None:
134132
assert self.source is not None
135-
self.code_context = greenlet.greenlet(self._blocking_run_code)
133+
self.code_thread = threading.Thread(target=self._blocking_run_code,
134+
name='codethread')
135+
self.code_thread.daemon = True
136136
self.orig_sigint_handler = signal.getsignal(signal.SIGINT)
137137
signal.signal(signal.SIGINT, self.sigint_handler)
138-
request = self.code_context.switch()
138+
self.code_thread.start()
139139
else:
140140
assert self.code_is_waiting
141141
self.code_is_waiting = False
142142
signal.signal(signal.SIGINT, self.sigint_handler)
143143
if self.sigint_happened_in_main_context:
144144
self.sigint_happened_in_main_context = False
145-
request = self.code_context.switch(SigintHappened)
145+
self.responses_for_code_thread.put(SigintHappened)
146146
else:
147-
request = self.code_context.switch(for_code)
147+
self.responses_for_code_thread.put(for_code)
148148

149+
request = self.requests_from_code_thread.get()
149150
logger.debug('request received from code was %r', request)
150151
if not isinstance(request, RequestFromCodeRunner):
151-
raise ValueError("Not a valid value from code greenlet: %r" %
152+
raise ValueError("Not a valid value from code thread: %r" %
152153
request)
153154
if isinstance(request, (Wait, Refresh)):
154155
self.code_is_waiting = True
@@ -167,7 +168,7 @@ def run_code(self, for_code=None):
167168
def sigint_handler(self, *args):
168169
"""SIGINT handler to use while code is running or request being
169170
fulfilled"""
170-
if greenlet.getcurrent() is self.code_context:
171+
if threading.current_thread() is self.code_thread:
171172
logger.debug('sigint while running user code!')
172173
raise KeyboardInterrupt()
173174
else:
@@ -179,18 +180,23 @@ def _blocking_run_code(self):
179180
try:
180181
unfinished = self.interp.runsource(self.source)
181182
except SystemExit as e:
182-
return SystemExitRequest(e.args)
183-
return Unfinished() if unfinished else Done()
183+
self.requests_from_code_thread.push(SystemExitRequest(e.args))
184+
return
185+
self.requests_from_code_thread.put(Unfinished()
186+
if unfinished
187+
else Done())
184188

185189
def request_from_main_context(self, force_refresh=False):
186190
"""Return the argument passed in to .run_code(for_code)
187191
188192
Nothing means calls to run_code must be... ???
189193
"""
190194
if force_refresh:
191-
value = self.main_context.switch(Refresh())
195+
self.requests_from_code_thread.put(Refresh())
196+
value = self.responses_for_code_thread.get()
192197
else:
193-
value = self.main_context.switch(Wait())
198+
self.requests_from_code_thread.put(Wait())
199+
value = self.responses_for_code_thread.get()
194200
if value is SigintHappened:
195201
raise KeyboardInterrupt()
196202
return value

bpython/curtsiesfrontend/interaction.py

Lines changed: 23 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
from __future__ import unicode_literals
44

5-
import greenlet
5+
from six.moves import queue
66
import time
77
import curtsies.events as events
88

@@ -42,8 +42,8 @@ def __init__(self,
4242
self.permanent_stack = []
4343
if permanent_text:
4444
self.permanent_stack.append(permanent_text)
45-
self.main_context = greenlet.getcurrent()
46-
self.request_context = None
45+
self.response_queue = queue.Queue()
46+
self.request_or_notify_queue = queue.Queue()
4747
self.request_refresh = request_refresh
4848
self.schedule_refresh = schedule_refresh
4949

@@ -96,12 +96,12 @@ def process_event(self, e):
9696
elif self.in_prompt and e in ("\n", "\r", "<Ctrl-j>", "Ctrl-m>"):
9797
line = self._current_line
9898
self.escape()
99-
self.request_context.switch(line)
99+
self.response_queue.put(line)
100100
elif self.in_confirm:
101101
if e in ('y', 'Y'):
102-
self.request_context.switch(True)
102+
self.request_queue.put(True)
103103
else:
104-
self.request_context.switch(False)
104+
self.request_queue.put(False)
105105
self.escape()
106106
else: # add normal character
107107
self.add_normal_character(e)
@@ -118,6 +118,7 @@ def add_normal_character(self, e):
118118

119119
def escape(self):
120120
"""unfocus from statusbar, clear prompt state, wait for notify call"""
121+
self.wait_for_request_or_notify()
121122
self.in_prompt = False
122123
self.in_confirm = False
123124
self.prompt = ''
@@ -140,28 +141,34 @@ def current_line(self):
140141
def should_show_message(self):
141142
return bool(self.current_line)
142143

143-
# interaction interface - should be called from other greenlets
144+
def wait_for_request_or_notify(self):
145+
try:
146+
r = self.request_or_notify_queue.get(True, 1)
147+
except queue.Empty:
148+
raise Exception('Main thread blocked because task thread not calling back')
149+
return r
150+
151+
# interaction interface - should be called from other threads
144152
def notify(self, msg, n=3, wait_for_keypress=False):
145-
self.request_context = greenlet.getcurrent()
146153
self.message_time = n
147154
self.message(msg, schedule_refresh=wait_for_keypress)
148155
self.waiting_for_refresh = True
149156
self.request_refresh()
150-
self.main_context.switch(msg)
157+
self.request_or_notify_queue.push(msg)
151158

152-
# below Really ought to be called from greenlets other than main because
153-
# they block
159+
###################################
160+
# These methods should be called from threads other than main because
161+
# they block.
154162
def confirm(self, q):
155163
"""Expected to return True or False, given question prompt q"""
156-
self.request_context = greenlet.getcurrent()
157164
self.prompt = q
158165
self.in_confirm = True
159-
return self.main_context.switch(q)
166+
self.request_or_notify_queue.put(q)
167+
return self.response_queue.get()
160168

161169
def file_prompt(self, s):
162170
"""Expected to return a file name, given """
163-
self.request_context = greenlet.getcurrent()
164171
self.prompt = s
165172
self.in_prompt = True
166-
result = self.main_context.switch(s)
167-
return result
173+
self.request_or_notify_queue.put(s)
174+
return self.response_queue.get()

bpython/curtsiesfrontend/repl.py

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33

44
import contextlib
55
import errno
6-
import greenlet
6+
import threading
77
import logging
88
import os
99
import re
@@ -718,15 +718,15 @@ def process_key_event(self, e):
718718
elif e in key_dispatch[self.config.undo_key]: # ctrl-r for undo
719719
self.prompt_undo()
720720
elif e in key_dispatch[self.config.save_key]: # ctrl-s for save
721-
greenlet.greenlet(self.write2file).switch()
721+
self.switch(self.write2file)
722722
elif e in key_dispatch[self.config.pastebin_key]: # F8 for pastebin
723-
greenlet.greenlet(self.pastebin).switch()
723+
self.switch(self.pastebin)
724724
elif e in key_dispatch[self.config.copy_clipboard_key]:
725-
greenlet.greenlet(self.copy2clipboard).switch()
725+
self.switch(self.copy2clipboard)
726726
elif e in key_dispatch[self.config.external_editor_key]:
727727
self.send_session_to_external_editor()
728728
elif e in key_dispatch[self.config.edit_config_key]:
729-
greenlet.greenlet(self.edit_config).switch()
729+
self.switch(self.edit_config)
730730
# TODO add PAD keys hack as in bpython.cli
731731
elif e in key_dispatch[self.config.edit_current_block_key]:
732732
self.send_current_block_to_external_editor()
@@ -737,6 +737,14 @@ def process_key_event(self, e):
737737
else:
738738
self.add_normal_character(e)
739739

740+
def switch(self, task):
741+
"""Runs task in another thread"""
742+
t = threading.Thread(target=task)
743+
t.daemon = True
744+
logging.debug('starting task thread')
745+
t.start()
746+
self.interact.wait_for_request_or_notify()
747+
740748
def get_last_word(self):
741749

742750
previous_word = _last_word(self.rl_history.entry)
@@ -1613,7 +1621,7 @@ def prompt_for_undo():
16131621
if n > 0:
16141622
self.request_undo(n=n)
16151623

1616-
greenlet.greenlet(prompt_for_undo).switch()
1624+
self.switch(prompt_for_undo)
16171625

16181626
def reevaluate(self, insert_into_history=False):
16191627
"""bpython.Repl.undo calls this"""

doc/sphinx/source/contributing.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ Next install your development copy of bpython and its dependencies:
6060

6161
.. code-block:: bash
6262
63-
$ sudp apt-get install python-greenlet python-pygments python-requests
63+
$ sudp apt-get install python-pygments python-requests
6464
$ sudo apt-get install python-watchdog python-urwid
6565
$ sudo apt-get install python-sphinx python-mock python-nose
6666

requirements.txt

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
Pygments
22
curtsies >=0.1.18
3-
greenlet
43
requests
54
setuptools
65
six >=1.5

setup.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -209,7 +209,6 @@ def initialize_options(self):
209209
'pygments',
210210
'requests',
211211
'curtsies >=0.1.18',
212-
'greenlet',
213212
'six >=1.5'
214213
]
215214

0 commit comments

Comments
 (0)