Skip to content

Commit 290fafa

Browse files
WIP - First draft of moving back to threads.
1 parent 78ead4c commit 290fafa

File tree

4 files changed

+76
-52
lines changed

4 files changed

+76
-52
lines changed

bpython/curtsiesfrontend/coderunner.py

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

139
import code
14-
import greenlet
10+
from six.moves import queue
11+
import threading
1512
import logging
1613
import signal
1714

@@ -21,12 +18,12 @@
2118

2219

2320
class SigintHappened:
24-
"""If this class is returned, a SIGINT happened while the main greenlet"""
21+
"""If this class is returned, a SIGINT happened while the main thread"""
2522

2623

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

3128

3229
class RequestFromCodeRunner:
@@ -61,7 +58,8 @@ class CodeRunner:
6158
6259
Running code requests a refresh by calling
6360
request_from_main_context(force_refresh=True), which
64-
suspends execution of the code and switches back to the main greenlet
61+
suspends execution of the code by blocking on a queue
62+
that the main thread was blocked on.
6563
6664
After load_code() is called with the source code to be run,
6765
the run_code() method should be called to start running the code.
@@ -77,10 +75,10 @@ class CodeRunner:
7775
has been gathered, run_code() should be called again, passing in any
7876
requested user input. This continues until run_code returns Done.
7977
80-
The code greenlet is responsible for telling the main greenlet
78+
The code thread is responsible for telling the main thread
8179
what it wants returned in the next run_code call - CodeRunner
8280
just passes whatever is passed in to run_code(for_code) to the
83-
code greenlet
81+
code thread.
8482
"""
8583

8684
def __init__(self, interp=None, request_refresh=lambda: None):
@@ -93,64 +91,68 @@ def __init__(self, interp=None, request_refresh=lambda: None):
9391
"""
9492
self.interp = interp or code.InteractiveInterpreter()
9593
self.source = None
96-
self.main_context = greenlet.getcurrent()
97-
self.code_context = None
94+
self.code_thread = None
95+
self.requests_from_code_thread = queue.Queue(maxsize=0)
96+
self.responses_for_code_thread = queue.Queue()
9897
self.request_refresh = request_refresh
9998
# waiting for response from main thread
10099
self.code_is_waiting = False
101100
# sigint happened while in main thread
102-
self.sigint_happened_in_main_context = False
101+
self.sigint_happened_in_main_context = False # TODO rename context to thread
103102
self.orig_sigint_handler = None
104103

105104
@property
106105
def running(self):
107-
"""Returns greenlet if code has been loaded greenlet has been
108-
started"""
109-
return self.source and self.code_context
106+
"""Returns the running thread if code has been loaded and started."""
107+
return self.source and self.code_thread
110108

111109
def load_code(self, source):
112110
"""Prep code to be run"""
113111
assert self.source is None, (
114112
"you shouldn't load code when some is " "already running"
115113
)
116114
self.source = source
117-
self.code_context = None
115+
self.code_thread = None
118116

119117
def _unload_code(self):
120118
"""Called when done running code"""
121119
self.source = None
122-
self.code_context = None
120+
self.code_thread = None
123121
self.code_is_waiting = False
124122

125123
def run_code(self, for_code=None):
126124
"""Returns Truthy values if code finishes, False otherwise
127125
128-
if for_code is provided, send that value to the code greenlet
126+
if for_code is provided, send that value to the code thread
129127
if source code is complete, returns "done"
130128
if source code is incomplete, returns "unfinished"
131129
"""
132-
if self.code_context is None:
130+
if self.code_thread is None:
133131
assert self.source is not None
134-
self.code_context = greenlet.greenlet(self._blocking_run_code)
132+
self.code_thread = threading.Thread(
133+
target=self._blocking_run_code,
134+
name='codethread')
135+
self.code_thread.daemon = True
135136
if is_main_thread():
136137
self.orig_sigint_handler = signal.getsignal(signal.SIGINT)
137138
signal.signal(signal.SIGINT, self.sigint_handler)
138-
request = self.code_context.switch()
139+
self.code_thread.start()
139140
else:
140141
assert self.code_is_waiting
141142
self.code_is_waiting = False
142143
if is_main_thread():
143144
signal.signal(signal.SIGINT, self.sigint_handler)
144145
if self.sigint_happened_in_main_context:
145146
self.sigint_happened_in_main_context = False
146-
request = self.code_context.switch(SigintHappened)
147+
self.responses_for_code_thread.put(SigintHappened)
147148
else:
148-
request = self.code_context.switch(for_code)
149+
self.responses_for_code_thread.put(for_code)
149150

151+
request = self.requests_from_code_thread.get()
150152
logger.debug("request received from code was %r", request)
151153
if not isinstance(request, RequestFromCodeRunner):
152154
raise ValueError(
153-
"Not a valid value from code greenlet: %r" % request
155+
"Not a valid value from code thread: %r" % request
154156
)
155157
if isinstance(request, (Wait, Refresh)):
156158
self.code_is_waiting = True
@@ -170,7 +172,7 @@ def run_code(self, for_code=None):
170172
def sigint_handler(self, *args):
171173
"""SIGINT handler to use while code is running or request being
172174
fulfilled"""
173-
if greenlet.getcurrent() is self.code_context:
175+
if threading.current_thread() is self.code_thread:
174176
logger.debug("sigint while running user code!")
175177
raise KeyboardInterrupt()
176178
else:
@@ -184,18 +186,23 @@ def _blocking_run_code(self):
184186
try:
185187
unfinished = self.interp.runsource(self.source)
186188
except SystemExit as e:
187-
return SystemExitRequest(*e.args)
188-
return Unfinished() if unfinished else Done()
189+
self.requests_from_code_thread.push(SystemExitRequest(*e.args))
190+
return
191+
self.requests_from_code_thread.put(Unfinished()
192+
if unfinished
193+
else Done())
189194

190195
def request_from_main_context(self, force_refresh=False):
191196
"""Return the argument passed in to .run_code(for_code)
192197
193198
Nothing means calls to run_code must be... ???
194199
"""
195200
if force_refresh:
196-
value = self.main_context.switch(Refresh())
201+
self.requests_from_code_thread.put(Refresh())
202+
value = self.responses_for_code_thread.get()
197203
else:
198-
value = self.main_context.switch(Wait())
204+
self.requests_from_code_thread.put(Wait())
205+
value = self.responses_for_code_thread.get()
199206
if value is SigintHappened:
200207
raise KeyboardInterrupt()
201208
return value

bpython/curtsiesfrontend/interaction.py

Lines changed: 22 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
import greenlet
21
import time
2+
from queue import Queue
33
from curtsies import events
44

55
from ..translations import _
@@ -43,8 +43,8 @@ def __init__(
4343
self.permanent_stack = []
4444
if permanent_text:
4545
self.permanent_stack.append(permanent_text)
46-
self.main_context = greenlet.getcurrent()
47-
self.request_context = None
46+
self.response_queue = Queue()
47+
self.request_or_notify_queue = Queue()
4848
self.request_refresh = request_refresh
4949
self.schedule_refresh = schedule_refresh
5050

@@ -102,12 +102,12 @@ def process_event(self, e):
102102
elif self.in_prompt and e in ("\n", "\r", "<Ctrl-j>", "Ctrl-m>"):
103103
line = self._current_line
104104
self.escape()
105-
self.request_context.switch(line)
105+
self.response_queue.put(line)
106106
elif self.in_confirm:
107107
if e.lower() == _("y"):
108-
self.request_context.switch(True)
108+
self.request_queue.put(True)
109109
else:
110-
self.request_context.switch(False)
110+
self.request_queue.put(False)
111111
self.escape()
112112
else: # add normal character
113113
self.add_normal_character(e)
@@ -126,6 +126,7 @@ def add_normal_character(self, e):
126126

127127
def escape(self):
128128
"""unfocus from statusbar, clear prompt state, wait for notify call"""
129+
self.wait_for_request_or_notify()
129130
self.in_prompt = False
130131
self.in_confirm = False
131132
self.prompt = ""
@@ -148,27 +149,34 @@ def current_line(self):
148149
def should_show_message(self):
149150
return bool(self.current_line)
150151

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

160-
# below really ought to be called from greenlets other than main because
167+
###################################
168+
# below really ought to be called from threads other than main because
161169
# they block
162170
def confirm(self, q):
163171
"""Expected to return True or False, given question prompt q"""
164-
self.request_context = greenlet.getcurrent()
165172
self.prompt = q
166173
self.in_confirm = True
167-
return self.main_context.switch(q)
174+
self.request_or_notify_queue.put(q)
175+
return self.response_queue.get()
168176

169177
def file_prompt(self, s):
170178
"""Expected to return a file name, given"""
171-
self.request_context = greenlet.getcurrent()
172179
self.prompt = s
173180
self.in_prompt = True
174-
return self.main_context.switch(s)
181+
self.request_or_notify_queue.put(s)
182+
return self.response_queue.get()

bpython/curtsiesfrontend/repl.py

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import subprocess
99
import sys
1010
import tempfile
11+
import threading
1112
import time
1213
import unicodedata
1314
from enum import Enum
@@ -773,15 +774,15 @@ def process_key_event(self, e):
773774
elif e in key_dispatch[self.config.redo_key]: # ctrl-g for redo
774775
self.redo()
775776
elif e in key_dispatch[self.config.save_key]: # ctrl-s for save
776-
greenlet.greenlet(self.write2file).switch()
777+
self.switch(self.write2file)
777778
elif e in key_dispatch[self.config.pastebin_key]: # F8 for pastebin
778-
greenlet.greenlet(self.pastebin).switch()
779+
self.switch(self.pastebin)
779780
elif e in key_dispatch[self.config.copy_clipboard_key]:
780-
greenlet.greenlet(self.copy2clipboard).switch()
781+
self.switch(self.copy2clipboard)
781782
elif e in key_dispatch[self.config.external_editor_key]:
782783
self.send_session_to_external_editor()
783784
elif e in key_dispatch[self.config.edit_config_key]:
784-
greenlet.greenlet(self.edit_config).switch()
785+
self.switch(self.edit_config)
785786
# TODO add PAD keys hack as in bpython.cli
786787
elif e in key_dispatch[self.config.edit_current_block_key]:
787788
self.send_current_block_to_external_editor()
@@ -792,6 +793,14 @@ def process_key_event(self, e):
792793
else:
793794
self.add_normal_character(e)
794795

796+
def switch(self, task):
797+
"""Runs task in another thread"""
798+
t = threading.Thread(target=task)
799+
t.daemon = True
800+
logging.debug('starting task thread')
801+
t.start()
802+
self.interact.wait_for_request_or_notify()
803+
795804
def get_last_word(self):
796805

797806
previous_word = _last_word(self.rl_history.entry)
@@ -1849,7 +1858,7 @@ def prompt_for_undo():
18491858
if n > 0:
18501859
self.request_undo(n=n)
18511860

1852-
greenlet.greenlet(prompt_for_undo).switch()
1861+
self.switch(prompt_for_undo)
18531862

18541863
def redo(self):
18551864
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 install python3-greenlet python3-pygments python3-requests
63+
$ sudp apt install python3-pygments python3-requests
6464
$ sudo apt install python3-watchdog python3-urwid
6565
$ sudo apt install python3-sphinx python3-pytest
6666

0 commit comments

Comments
 (0)