Search code examples
pythonstandard-library

Abort evaluation of python's interactive console


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.


Solution

  • 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()