Skip to content

Commit be211d8

Browse files
WIP - First draft of moving back to threads.
1 parent 471fa82 commit be211d8

File tree

6 files changed

+77
-57
lines changed

6 files changed

+77
-57
lines changed

bpython/curtsiesfrontend/coderunner.py

Lines changed: 39 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, is_main_thread
@@ -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

8987
def __init__(self, interp=None, request_refresh=lambda: None):
@@ -96,64 +94,68 @@ def __init__(self, interp=None, request_refresh=lambda: None):
9694
"""
9795
self.interp = interp or code.InteractiveInterpreter()
9896
self.source = None
99-
self.main_context = greenlet.getcurrent()
100-
self.code_context = None
97+
self.code_thread = None
98+
self.requests_from_code_thread = queue.Queue(maxsize=0)
99+
self.responses_for_code_thread = queue.Queue()
101100
self.request_refresh = request_refresh
102101
# waiting for response from main thread
103102
self.code_is_waiting = False
104103
# sigint happened while in main thread
105-
self.sigint_happened_in_main_context = False
104+
self.sigint_happened_in_main_context = False # TODO rename context to thread
106105
self.orig_sigint_handler = None
107106

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

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

122120
def _unload_code(self):
123121
"""Called when done running code"""
124122
self.source = None
125-
self.code_context = None
123+
self.code_thread = None
126124
self.code_is_waiting = False
127125

128126
def run_code(self, for_code=None):
129127
"""Returns Truthy values if code finishes, False otherwise
130128
131-
if for_code is provided, send that value to the code greenlet
129+
if for_code is provided, send that value to the code thread
132130
if source code is complete, returns "done"
133131
if source code is incomplete, returns "unfinished"
134132
"""
135-
if self.code_context is None:
133+
if self.code_thread is None:
136134
assert self.source is not None
137-
self.code_context = greenlet.greenlet(self._blocking_run_code)
135+
self.code_thread = threading.Thread(
136+
target=self._blocking_run_code,
137+
name='codethread')
138+
self.code_thread.daemon = True
138139
if is_main_thread():
139140
self.orig_sigint_handler = signal.getsignal(signal.SIGINT)
140141
signal.signal(signal.SIGINT, self.sigint_handler)
141-
request = self.code_context.switch()
142+
self.code_thread.start()
142143
else:
143144
assert self.code_is_waiting
144145
self.code_is_waiting = False
145146
if is_main_thread():
146147
signal.signal(signal.SIGINT, self.sigint_handler)
147148
if self.sigint_happened_in_main_context:
148149
self.sigint_happened_in_main_context = False
149-
request = self.code_context.switch(SigintHappened)
150+
self.responses_for_code_thread.put(SigintHappened)
150151
else:
151-
request = self.code_context.switch(for_code)
152+
self.responses_for_code_thread.put(for_code)
152153

154+
request = self.requests_from_code_thread.get()
153155
logger.debug("request received from code was %r", request)
154156
if not isinstance(request, RequestFromCodeRunner):
155157
raise ValueError(
156-
"Not a valid value from code greenlet: %r" % request
158+
"Not a valid value from code thread: %r" % request
157159
)
158160
if isinstance(request, (Wait, Refresh)):
159161
self.code_is_waiting = True
@@ -173,7 +175,7 @@ def run_code(self, for_code=None):
173175
def sigint_handler(self, *args):
174176
"""SIGINT handler to use while code is running or request being
175177
fulfilled"""
176-
if greenlet.getcurrent() is self.code_context:
178+
if threading.current_thread() is self.code_thread:
177179
logger.debug("sigint while running user code!")
178180
raise KeyboardInterrupt()
179181
else:
@@ -187,18 +189,23 @@ def _blocking_run_code(self):
187189
try:
188190
unfinished = self.interp.runsource(self.source)
189191
except SystemExit as e:
190-
return SystemExitRequest(*e.args)
191-
return Unfinished() if unfinished else Done()
192+
self.requests_from_code_thread.push(SystemExitRequest(*e.args))
193+
return
194+
self.requests_from_code_thread.put(Unfinished()
195+
if unfinished
196+
else Done())
192197

193198
def request_from_main_context(self, force_refresh=False):
194199
"""Return the argument passed in to .run_code(for_code)
195200
196201
Nothing means calls to run_code must be... ???
197202
"""
198203
if force_refresh:
199-
value = self.main_context.switch(Refresh())
204+
self.requests_from_code_thread.put(Refresh())
205+
value = self.responses_for_code_thread.get()
200206
else:
201-
value = self.main_context.switch(Wait())
207+
self.requests_from_code_thread.put(Wait())
208+
value = self.responses_for_code_thread.get()
202209
if value is SigintHappened:
203210
raise KeyboardInterrupt()
204211
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

@@ -46,8 +46,8 @@ def __init__(
4646
self.permanent_stack = []
4747
if permanent_text:
4848
self.permanent_stack.append(permanent_text)
49-
self.main_context = greenlet.getcurrent()
50-
self.request_context = None
49+
self.response_queue = queue.Queue()
50+
self.request_or_notify_queue = queue.Queue()
5151
self.request_refresh = request_refresh
5252
self.schedule_refresh = schedule_refresh
5353

@@ -105,12 +105,12 @@ def process_event(self, e):
105105
elif self.in_prompt and e in ("\n", "\r", "<Ctrl-j>", "Ctrl-m>"):
106106
line = self._current_line
107107
self.escape()
108-
self.request_context.switch(line)
108+
self.response_queue.put(line)
109109
elif self.in_confirm:
110110
if e in ("y", "Y"):
111-
self.request_context.switch(True)
111+
self.request_queue.put(True)
112112
else:
113-
self.request_context.switch(False)
113+
self.request_queue.put(False)
114114
self.escape()
115115
else: # add normal character
116116
self.add_normal_character(e)
@@ -129,6 +129,7 @@ def add_normal_character(self, e):
129129

130130
def escape(self):
131131
"""unfocus from statusbar, clear prompt state, wait for notify call"""
132+
self.wait_for_request_or_notify()
132133
self.in_prompt = False
133134
self.in_confirm = False
134135
self.prompt = ""
@@ -151,28 +152,34 @@ def current_line(self):
151152
def should_show_message(self):
152153
return bool(self.current_line)
153154

154-
# interaction interface - should be called from other greenlets
155+
def wait_for_request_or_notify(self):
156+
try:
157+
r = self.request_or_notify_queue.get(True, 1)
158+
except queue.Empty:
159+
raise Exception('Main thread blocked because task thread not calling back')
160+
return r
161+
162+
# interaction interface - should be called from other threads
155163
def notify(self, msg, n=3, wait_for_keypress=False):
156-
self.request_context = greenlet.getcurrent()
157164
self.message_time = n
158165
self.message(msg, schedule_refresh=wait_for_keypress)
159166
self.waiting_for_refresh = True
160167
self.request_refresh()
161-
self.main_context.switch(msg)
168+
self.request_or_notify_queue.push(msg)
162169

163-
# below Really ought to be called from greenlets other than main because
164-
# they block
170+
###################################
171+
# These methods should be called from threads other than main because
172+
# they block.
165173
def confirm(self, q):
166174
"""Expected to return True or False, given question prompt q"""
167-
self.request_context = greenlet.getcurrent()
168175
self.prompt = q
169176
self.in_confirm = True
170-
return self.main_context.switch(q)
177+
self.request_or_notify_queue.put(q)
178+
return self.response_queue.get()
171179

172180
def file_prompt(self, s):
173181
"""Expected to return a file name, given """
174-
self.request_context = greenlet.getcurrent()
175182
self.prompt = s
176183
self.in_prompt = True
177-
result = self.main_context.switch(s)
178-
return result
184+
self.request_or_notify_queue.put(s)
185+
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
@@ -778,15 +778,15 @@ def process_key_event(self, e):
778778
elif e in key_dispatch[self.config.redo_key]: # ctrl-g for redo
779779
self.redo()
780780
elif e in key_dispatch[self.config.save_key]: # ctrl-s for save
781-
greenlet.greenlet(self.write2file).switch()
781+
self.switch(self.write2file)
782782
elif e in key_dispatch[self.config.pastebin_key]: # F8 for pastebin
783-
greenlet.greenlet(self.pastebin).switch()
783+
self.switch(self.pastebin)
784784
elif e in key_dispatch[self.config.copy_clipboard_key]:
785-
greenlet.greenlet(self.copy2clipboard).switch()
785+
self.switch(self.copy2clipboard)
786786
elif e in key_dispatch[self.config.external_editor_key]:
787787
self.send_session_to_external_editor()
788788
elif e in key_dispatch[self.config.edit_config_key]:
789-
greenlet.greenlet(self.edit_config).switch()
789+
self.switch(self.edit_config)
790790
# TODO add PAD keys hack as in bpython.cli
791791
elif e in key_dispatch[self.config.edit_current_block_key]:
792792
self.send_current_block_to_external_editor()
@@ -797,6 +797,14 @@ def process_key_event(self, e):
797797
else:
798798
self.add_normal_character(e)
799799

800+
def switch(self, task):
801+
"""Runs task in another thread"""
802+
t = threading.Thread(target=task)
803+
t.daemon = True
804+
logging.debug('starting task thread')
805+
t.start()
806+
self.interact.wait_for_request_or_notify()
807+
800808
def get_last_word(self):
801809

802810
previous_word = _last_word(self.rl_history.entry)
@@ -1814,7 +1822,7 @@ def prompt_for_undo():
18141822
if n > 0:
18151823
self.request_undo(n=n)
18161824

1817-
greenlet.greenlet(prompt_for_undo).switch()
1825+
self.switch(prompt_for_undo)
18181826

18191827
def redo(self):
18201828
if (self.redo_stack):

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
@@ -226,7 +226,6 @@ def initialize_options(self):
226226
"pygments",
227227
"requests",
228228
"curtsies >=0.1.18",
229-
"greenlet",
230229
"six >=1.5",
231230
]
232231

0 commit comments

Comments
 (0)