Skip to content

Commit 8742987

Browse files
checkpoint: mid-refactor for interrupting running code to stdout.write
raw_input is currently broken, but otherwise things are working. Plan is to use the same pattern of coderunner + stdout for err and in --HG-- branch : scroll-frontend
1 parent 00e7f3b commit 8742987

File tree

3 files changed

+113
-25
lines changed

3 files changed

+113
-25
lines changed

bpython/scroll.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ def main(locals_=None, config=None, exec_args=None, options=None):
2222

2323
with TerminalController() as tc:
2424
with Terminal(tc, keep_last_line=True, hide_cursor=False) as term:
25-
with Repl(config=config, locals_=locals_) as repl:
25+
with Repl(config=config, locals_=locals_, stuff_a_refresh_request=tc.stuff_a_refresh_request) as repl:
2626
rows, columns = tc.get_screen_size()
2727
repl.width = columns
2828
repl.height = rows
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
import code
2+
import Queue
3+
import signal
4+
import sys
5+
import threading
6+
import logging
7+
8+
class CodeRunner(object):
9+
"""Runs user code in an interpreter, taking care of stdout/in/err"""
10+
def __init__(self, interp=None, stuff_a_refresh_request=lambda:None):
11+
self.interp = interp or code.InteractiveInterpreter()
12+
self.source = None
13+
self.code_thread = None
14+
self.requests_from_code_thread = Queue.Queue(maxsize=1)
15+
self.responses_for_code_thread = Queue.Queue(maxsize=1)
16+
self.stuff_a_refresh_request = stuff_a_refresh_request
17+
self.code_is_waiting = False
18+
19+
def load_code(self, source):
20+
self.source = source
21+
self.code_thread = None
22+
23+
def run_code(self, input=None):
24+
"""Returns Truthy values if code finishes, False otherwise
25+
26+
if source code is complete, returns "done"
27+
if source code is incomplete, returns "unfinished"
28+
"""
29+
if self.code_thread is None:
30+
assert self.source
31+
self.code_thread = threading.Thread(target=self._blocking_run_code, name='codethread')
32+
self.code_thread.daemon = True
33+
self.code_thread.start()
34+
else:
35+
assert self.code_is_waiting
36+
self.code_is_waiting = False
37+
self.responses_for_code_thread.put(input)
38+
39+
request = self.requests_from_code_thread.get()
40+
if request[0] == 'done':
41+
return 'unfinished' if request[1] else 'done'
42+
else:
43+
method, args, kwargs = request
44+
self.code_is_waiting = True
45+
method(*args, **kwargs)
46+
self.stuff_a_refresh_request()
47+
return False
48+
49+
def _blocking_run_code(self):
50+
unfinished = self.interp.runsource(self.source)
51+
self.requests_from_code_thread.put(('done', unfinished))
52+
def _blocking_wait_for(self, method, args, kwargs):
53+
self.requests_from_code_thread.put((method, args, kwargs))
54+
return self.responses_for_code_thread.get()
55+
56+
class FakeOutput(object):
57+
def __init__(self, coderunner, please):
58+
self.coderunner = coderunner
59+
self.please = please
60+
def write(self, *args, **kwargs):
61+
return self.coderunner._blocking_wait_for(self.please, args, kwargs)
62+
63+
if __name__ == '__main__':
64+
orig_stdout = sys.stdout
65+
orig_stderr = sys.stderr
66+
c = CodeRunner(stuff_a_refresh_request=lambda: orig_stdout.flush() or orig_stderr.flush())
67+
stdout = FakeOutput(c, orig_stdout.write)
68+
stderr = FakeOutput(c, orig_stderr.write)
69+
sys.stdout = stdout
70+
sys.stderr = stderr
71+
c.load_code('1 + 1')
72+
c.run_code()
73+
c.run_code()
74+
c.run_code()

bpython/scrollfrontend/repl.py

Lines changed: 38 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
import bpython.scrollfrontend.replpainter as paint
3535
import fmtstr.events as events
3636
from bpython.scrollfrontend.friendly import NotImplementedError
37+
from bpython.scrollfrontend.coderunner import CodeRunner, FakeOutput
3738

3839
#TODO implement paste mode and figure out what the deal with config.paste_time is
3940
#TODO figure out how config.auto_display_list=False behaves and implement it
@@ -51,8 +52,6 @@
5152
class FakeStdout(object):
5253
def __init__(self):
5354
self.data = StringIO()
54-
def seek(self, pos, mode=0): return self.data.seek(pos, mode)
55-
def tell(self): return self.data.tell()
5655
def read(self, n=None):
5756
data = self.data.read()
5857
if isinstance(data, bytes):
@@ -76,7 +75,7 @@ def __init__(self, on_receive_tuple):
7675

7776
def wait_for_request_or_finish(self):
7877
logging.debug('waiting for message from running code (stack depth %r)', len(traceback.format_stack()))
79-
msg = self.request_from_executing_code.get(timeout=3)
78+
msg = self.request_from_executing_code.get()
8079
logging.debug('FakeStdin received message %r', msg)
8180
self.process_request(msg)
8281

@@ -112,7 +111,7 @@ def process_event(self, e):
112111

113112
def readline(self):
114113
self.request_from_executing_code.put('can haz stdin line plz')
115-
return self.response_queue.get(timeout=15)
114+
return self.response_queue.get()
116115

117116

118117
class Repl(BpythonRepl):
@@ -134,7 +133,7 @@ class Repl(BpythonRepl):
134133
"""
135134

136135
## initialization, cleanup
137-
def __init__(self, locals_=None, config=None):
136+
def __init__(self, locals_=None, config=None, stuff_a_refresh_request=None):
138137
logging.debug("starting init")
139138
interp = code.InteractiveInterpreter(locals=locals_)
140139

@@ -171,6 +170,8 @@ def __init__(self, locals_=None, config=None):
171170
self.cursor_offset_in_line = 0 # from the left, 0 means first char
172171
self.done = True
173172

173+
self.coderunner = CodeRunner(self.interp, stuff_a_refresh_request)
174+
self.stdout = FakeOutput(self.coderunner, self.send_to_stdout)
174175
self.stdin = FakeStdin(self.on_finish_running_code)
175176

176177
self.paste_mode = False
@@ -186,7 +187,7 @@ def __enter__(self):
186187
self.orig_stdout = sys.stdout
187188
self.orig_stderr = sys.stderr
188189
self.orig_stdin = sys.stdin
189-
sys.stdout = FakeStdout()
190+
sys.stdout = self.stdout
190191
sys.stderr = StringIO()
191192
sys.stdin = self.stdin
192193
return self
@@ -218,9 +219,12 @@ def clean_up_current_line_for_exit(self):
218219
def process_event(self, e):
219220
"""Returns True if shutting down, otherwise mutates state of Repl object"""
220221

222+
logging.debug("processing event %r", e)
223+
if isinstance(e, events.RefreshRequestEvent):
224+
self.run_runsource_part_2_when_finished()
225+
return
221226
self.last_events.append(e)
222227
self.last_events.pop(0)
223-
#logging.debug("processing event %r", e)
224228
if isinstance(e, events.WindowChangeEvent):
225229
logging.debug('window change to %d %d', e.width, e.height)
226230
self.width, self.height = e.width, e.height
@@ -311,9 +315,12 @@ def on_enter(self, insert_into_history=True):
311315
self.history.append(self._current_line)
312316
self.push(self._current_line, insert_into_history=insert_into_history)
313317

318+
def send_to_stdout(self, output):
319+
self.display_lines.extend(sum([paint.display_linize(line, self.width) for line in output.split('\n')], []))
320+
314321
def on_finish_running_code(self, output, error, done, indent):
315-
if output:
316-
self.display_lines.extend(sum([paint.display_linize(line, self.width) for line in output.split('\n')], []))
322+
#if output:
323+
# self.display_lines.extend(sum([paint.display_linize(line, self.width) for line in output.split('\n')], []))
317324
if error:
318325
self.display_lines.extend([func_for_letter(self.config.color_scheme['error'])(line)
319326
for line in sum([paint.display_linize(line, self.width)
@@ -405,12 +412,7 @@ def push(self, line, insert_into_history=True):
405412
"""
406413
if insert_into_history:
407414
self.insert_into_history(line)
408-
t = threading.Thread(target=self.runsource, args=(line,))
409-
t.daemon = True
410-
t.start()
411-
logging.debug('push() is now waiting')
412-
self.stdin.wait_for_request_or_finish()
413-
logging.debug('push() done waiting')
415+
self.runsource(line)
414416

415417
def runsource(self, line):
416418
"""Push a line of code on to the buffer, run the buffer, clean up
@@ -429,20 +431,36 @@ def runsource(self, line):
429431
indent = max(0, indent - self.config.tab_length)
430432
elif line and ':' not in line and line.strip().startswith(('return', 'pass', 'raise', 'yield')):
431433
indent = max(0, indent - self.config.tab_length)
432-
out_spot = sys.stdout.tell()
433434
err_spot = sys.stderr.tell()
434435
logging.debug('running %r in interpreter', self.buffer)
435-
unfinished = self.interp.runsource('\n'.join(self.buffer))
436+
self.coderunner.load_code('\n'.join(self.buffer))
437+
self.saved_err_spot = err_spot
438+
self.saved_indent = indent
439+
self.saved_line = line
436440

437441
#current line not added to display buffer if quitting
438442
if self.config.syntax:
439443
self.display_buffer.append(bpythonparse(format(self.tokenize(line), self.formatter)))
440444
else:
441445
self.display_buffer.append(fmtstr(line))
442446

443-
sys.stdout.seek(out_spot)
447+
if code.compile_command('\n'.join(self.buffer)):
448+
logging.debug('finished - buffer cleared')
449+
self.display_lines.extend(self.display_buffer_lines)
450+
self.display_buffer = []
451+
self.buffer = []
452+
453+
self.run_runsource_part_2_when_finished()
454+
455+
def run_runsource_part_2_when_finished(self):
456+
r = self.coderunner.run_code()
457+
if r:
458+
unfinished = r == 'unfinished'
459+
self.runsource_part_2(self.saved_line, self.saved_err_spot, unfinished, self.saved_indent)
460+
self.stdin.wait_for_request_or_finish()
461+
462+
def runsource_part_2(self, line, err_spot, unfinished, indent):
444463
sys.stderr.seek(err_spot)
445-
out = sys.stdout.read()
446464
err = sys.stderr.read()
447465

448466
# easier debugging: save only errors that aren't from this interpreter
@@ -455,14 +473,10 @@ def runsource(self, line):
455473
logging.debug('unfinished - line added to buffer')
456474
self.stdin.request_from_executing_code.put((None, None, False, indent))
457475
else:
458-
logging.debug('finished - buffer cleared')
459-
self.display_lines.extend(self.display_buffer_lines)
460-
self.display_buffer = []
461-
self.buffer = []
462476
if err:
463477
indent = 0
464478
logging.debug('sending output info')
465-
self.stdin.request_from_executing_code.put((out[:-1], err[:-1], True, indent))
479+
self.stdin.request_from_executing_code.put((None, err[:-1], True, indent))
466480
logging.debug('sent output info')
467481

468482
def unhighlight_paren(self):

0 commit comments

Comments
 (0)