I am in the process of writing my own python code editor and terminal for fun and to implement it in existing programs to add scribtability.
Now I have found the problem that I don't know how to stop evaluation of the code once it is running. How could that be done?
Here is my implementation:
import code
import contextlib
import sys
from io import StringIO
import copy
@contextlib.contextmanager
def capture():
oldout,olderr = sys.stdout, sys.stderr
try:
out=[StringIO(), StringIO()]
sys.stdout,sys.stderr = out
yield out
finally:
sys.stdout,sys.stderr = oldout, olderr
out[0] = out[0].getvalue()
out[1] = out[1].getvalue()
class PythonTerminal(code.InteractiveConsole):
def __init__(self, shared_vars):
self.shared_vars_start = copy.deepcopy(shared_vars)
self.shared_vars = shared_vars
super().__init__(shared_vars)
self.out_history = []
def run_code(self,code_string):
with capture() as out:
self.runcode(code_string)
self.out_history.append(out)
return out
def restart_interpreter(self):
self.__init__(self.shared_vars_start)
def stop(self):
raise NotImplementedError
if __name__ == '__main__':
a = range(10)
PyTerm = PythonTerminal({'Betrag': a})
test_code = """
for i in range(10000):
for j in range(1000):
temp = i*j
print('Finished'+str(i))
"""
print('Starting')
t = threading.Thread(target=PyTerm.run_code,args=(test_code,))
t.start()
PyTerm.stop()
t.join()
print(PyTerm.out_history[-1]) # This line should be executed immediately and contain an InterruptError
The goal is that the evaluation stops but the interpreter is still alive, so like ctrl+c.
I don't think you can easily kill a thread in Python. You can kill a multiprocessing.Process
, though. So you could you use a separate process to execute the code in your console and communicate with it via a multiprocessing.Queue
. To do this I implemented a TerminalManager class that can execute PythonTerminal.run_code
in a separate process and kill it. See the modified code below. A major draw back to this is that the locals of the InteractiveConsole do not persist between calls. I've added in a hack (that is probably terrible) that stores the locals to a shelve file. Quickest thing that came to mind.
import code
import contextlib
import sys
from io import StringIO
import copy
import threading
import multiprocessing
import json
import shelve
class QueueIO:
"""Uses a multiprocessing.Queue object o capture stdout and stderr"""
def __init__(self, q=None):
self.q = multiprocessing.Queue() if q is None else q
def write(self, value):
self.q.put(value)
def writelines(self, values):
self.q.put("\n".join(str(v) for v in values))
def read(self):
return self.q.get()
def readlines(self):
result = ""
while not self.q.empty():
result += self.q.get() + "\n"
@contextlib.contextmanager
def capture2(q: multiprocessing.Queue):
oldout,olderr = sys.stdout, sys.stderr
try:
qio = QueueIO(q)
out=[qio, qio]
sys.stdout,sys.stderr = out
yield out
finally:
sys.stdout,sys.stderr = oldout, olderr
class PythonTerminal(code.InteractiveConsole):
def __init__(self, shared_vars):
self.shared_vars_start = copy.deepcopy(shared_vars)
self.shared_vars = shared_vars
super().__init__(shared_vars)
self.out_history = []
def run_code(self,code_string, q):
# retrieve locals
d = shelve.open(r'd:\temp\shelve.pydb')
for k, v in d.items():
self.locals[k] = v
# execute code
with capture2(q) as out:
self.runcode(code_string)
# store locals
for k, v in self.locals.items():
try:
if k != '__builtins__':
d[k] = v
except TypeError:
pass
d.close()
def restart_interpreter(self):
self.__init__(self.shared_vars_start)
class TerminalManager():
def __init__(self, terminal):
self.terminal = terminal
self.process = None
self.q = multiprocessing.Queue()
def run_code(self, test_code):
self.process = multiprocessing.Process(
target=self.terminal.run_code,args=(test_code, self.q))
self.process.start()
def stop(self):
self.process.terminate()
self.q.put(repr(Exception('User interupted execution.')))
def wait(self):
if self.process.is_alive:
self.process.join()
while not self.q.empty():
print(self.q.get())
if __name__ == '__main__':
import time
a = range(10)
PyTerm = PythonTerminal({'Betrag': a})
test_code = """
import time
a = 'hello'
for i in range(10):
time.sleep(0.2)
print(i)
print('Finished')
"""
mgr = TerminalManager(PyTerm)
print('Starting')
mgr.run_code(test_code)
time.sleep(1)
mgr.stop()
mgr.wait()
test_code = """
import time
_l = locals()
print('a = {}'.format(a))
for i in range(10):
time.sleep(0.1)
print(i)
print('Finished')
"""
print('Starting again')
mgr.run_code(test_code)
mgr.wait()