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)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy