Esempio n. 1
0
    def __init__(self, parent, info):
        BaseShell.__init__(self, parent)

        # Get standard info if not given.
        if info is None and iep.config.shellConfigs2:
            info = iep.config.shellConfigs2[0]
        if not info:
            info = KernelInfo(None)

        # Store info so we can reuse it on a restart
        self._info = info

        # For the editor to keep track of attempted imports
        self._importAttempts = []

        # To keep track of the response for introspection
        self._currentCTO = None
        self._currentACO = None

        # Write buffer to store messages in for writing
        self._write_buffer = None

        # Create timer to keep polling any results
        # todo: Maybe use yoton events to process messages as they arrive.
        # I tried this briefly, but it seemd to be less efficient because
        # messages are not so much bach-processed anymore. We should decide
        # on either method.
        self._timer = QtCore.QTimer(self)
        self._timer.setInterval(POLL_TIMER_INTERVAL)  # ms
        self._timer.setSingleShot(False)
        self._timer.timeout.connect(self.poll)
        self._timer.start()

        # Add context menu
        self._menu = ShellContextMenu(shell=self, parent=self)
        self.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
        self.customContextMenuRequested.connect(lambda p: self._menu.popup(
            self.mapToGlobal(p + QtCore.QPoint(0, 3))))

        # Start!
        self.resetVariables()
        self.connectToKernel(info)
Esempio n. 2
0
 def __init__(self, parent, info):
     BaseShell.__init__(self, parent)
     
     # Get standard info if not given.
     if info is None and iep.config.shellConfigs2:
         info = iep.config.shellConfigs2[0]
     if not info:
         info = KernelInfo(None)
     
     # Store info so we can reuse it on a restart
     self._info = info
     
     # For the editor to keep track of attempted imports
     self._importAttempts = []
     
     # To keep track of the response for introspection
     self._currentCTO = None
     self._currentACO = None
     
     # Write buffer to store messages in for writing
     self._write_buffer = None
     
     # Create timer to keep polling any results
     # todo: Maybe use yoton events to process messages as they arrive.
     # I tried this briefly, but it seemd to be less efficient because 
     # messages are not so much bach-processed anymore. We should decide
     # on either method.
     self._timer = QtCore.QTimer(self)
     self._timer.setInterval(POLL_TIMER_INTERVAL)  # ms
     self._timer.setSingleShot(False)
     self._timer.timeout.connect(self.poll)
     self._timer.start()
     
     # Add context menu
     self._menu = ShellContextMenu(shell=self, parent=self)
     self.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
     self.customContextMenuRequested.connect(lambda p: self._menu.popup(self.mapToGlobal(p+QtCore.QPoint(0,3)))) 
     
     # Start!
     self.resetVariables()
     self.connectToKernel(info)
Esempio n. 3
0
class PythonShell(BaseShell):
    """ The PythonShell class implements the python part of the shell
    by connecting to a remote process that runs a Python interpreter.
    """

    # Emits when the status string has changed or when receiving a new prompt
    stateChanged = QtCore.Signal(BaseShell)

    # Emits when the debug status is changed
    debugStateChanged = QtCore.Signal(BaseShell)

    def __init__(self, parent, info):
        BaseShell.__init__(self, parent)

        # Get standard info if not given.
        if info is None and iep.config.shellConfigs2:
            info = iep.config.shellConfigs2[0]
        if not info:
            info = KernelInfo(None)

        # Store info so we can reuse it on a restart
        self._info = info

        # For the editor to keep track of attempted imports
        self._importAttempts = []

        # To keep track of the response for introspection
        self._currentCTO = None
        self._currentACO = None

        # Write buffer to store messages in for writing
        self._write_buffer = None

        # Create timer to keep polling any results
        # todo: Maybe use yoton events to process messages as they arrive.
        # I tried this briefly, but it seemd to be less efficient because
        # messages are not so much bach-processed anymore. We should decide
        # on either method.
        self._timer = QtCore.QTimer(self)
        self._timer.setInterval(POLL_TIMER_INTERVAL)  # ms
        self._timer.setSingleShot(False)
        self._timer.timeout.connect(self.poll)
        self._timer.start()

        # Add context menu
        self._menu = ShellContextMenu(shell=self, parent=self)
        self.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
        self.customContextMenuRequested.connect(lambda p: self._menu.popup(
            self.mapToGlobal(p + QtCore.QPoint(0, 3))))

        # Start!
        self.resetVariables()
        self.connectToKernel(info)

    def resetVariables(self):
        """ Resets some variables. """

        # Reset read state
        self.setReadOnly(False)

        # Variables to store state, python version, builtins and keywords
        self._state = ''
        self._debugState = {}
        self._version = ""
        self._builtins = []
        self._keywords = []
        self._startup_info = {}
        self._start_time = 0

        # (re)set import attempts
        self._importAttempts[:] = []

        # Update
        self.stateChanged.emit(self)

    def connectToKernel(self, info):
        """ connectToKernel()
        
        Create kernel and connect to it.
        
        """

        # Create yoton context
        self._context = ct = yoton.Context()

        # Create stream channels
        self._strm_out = yoton.SubChannel(ct, 'strm-out')
        self._strm_err = yoton.SubChannel(ct, 'strm-err')
        self._strm_raw = yoton.SubChannel(ct, 'strm-raw')
        self._strm_echo = yoton.SubChannel(ct, 'strm-echo')
        self._strm_prompt = yoton.SubChannel(ct, 'strm-prompt')
        self._strm_broker = yoton.SubChannel(ct, 'strm-broker')
        self._strm_action = yoton.SubChannel(ct, 'strm-action', yoton.OBJECT)

        # Set channels to sync mode. This means that if the IEP cannot process
        # the messages fast enough, the sending side is blocked for a short
        # while. We don't want our users to miss any messages.
        for c in [self._strm_out, self._strm_err]:
            c.set_sync_mode(True)

        # Create control channels
        self._ctrl_command = yoton.PubChannel(ct, 'ctrl-command')
        self._ctrl_code = yoton.PubChannel(ct, 'ctrl-code', yoton.OBJECT)
        self._ctrl_broker = yoton.PubChannel(ct, 'ctrl-broker')

        # Create status channels
        self._stat_interpreter = yoton.StateChannel(ct, 'stat-interpreter')
        self._stat_debug = yoton.StateChannel(ct, 'stat-debug', yoton.OBJECT)
        self._stat_startup = yoton.StateChannel(ct, 'stat-startup',
                                                yoton.OBJECT)
        self._stat_startup.received.bind(self._onReceivedStartupInfo)

        # Create introspection request channel
        self._request = yoton.ReqChannel(ct, 'reqp-introspect')

        # Connect! The broker will only start the kernel AFTER
        # we connect, so we do not miss out on anything.
        slot = iep.localKernelManager.createKernel(finishKernelInfo(info))
        self._brokerConnection = ct.connect('localhost:%i' % slot)
        self._brokerConnection.closed.bind(self._onConnectionClose)

        # todo: see polling vs events
#         # Detect incoming messages
#         for c in [self._strm_out, self._strm_err, self._strm_raw,
#                 self._strm_echo, self._strm_prompt, self._strm_broker,
#                 self._strm_action,
#                 self._stat_interpreter, self._stat_debug]:
#             c.received.bind(self.poll)

    def _onReceivedStartupInfo(self, channel):
        startup_info = channel.recv()

        # Store the whole dict
        self._startup_info = startup_info

        # Store when we received this
        self._start_time = time.time()

        # Set version
        version = startup_info.get('version', None)
        if isinstance(version, tuple):
            version = [str(v) for v in version]
            self._version = '.'.join(version[:2])

        # Set keywords
        L = startup_info.get('keywords', None)
        if isinstance(L, list):
            self._keywords = L

        # Set builtins
        L = startup_info.get('builtins', None)
        if isinstance(L, list):
            self._builtins = L

        # Notify
        self.stateChanged.emit(self)

    ## Introspection processing methods

    def processCallTip(self, cto):
        """ Processes a calltip request using a CallTipObject instance. 
        """

        # Try using buffer first (not if we're not the requester)
        if self is cto.textCtrl:
            if cto.tryUsingBuffer():
                return

        # Clear buffer to prevent doing a second request
        # and store cto to see whether the response is still wanted.
        cto.setBuffer('')
        self._currentCTO = cto

        # Post request
        future = self._request.signature(cto.name)
        future.add_done_callback(self._processCallTip_response)
        future.cto = cto

    def _processCallTip_response(self, future):
        """ Process response of shell to show signature. 
        """

        # Process future
        if future.cancelled():
            #print('Introspect cancelled')  # No kernel
            return
        elif future.exception():
            print('Introspect-exception: ', future.exception())
            return
        else:
            response = future.result()
            cto = future.cto

        # First see if this is still the right editor (can also be a shell)
        editor1 = iep.editors.getCurrentEditor()
        editor2 = iep.shells.getCurrentShell()
        if cto.textCtrl not in [editor1, editor2]:
            # The editor or shell starting the autocomp is no longer active
            aco.textCtrl.autocompleteCancel()
            return

        # Invalid response
        if response is None:
            cto.textCtrl.autocompleteCancel()
            return

        # If still required, show tip, otherwise only store result
        if cto is self._currentCTO:
            cto.finish(response)
        else:
            cto.setBuffer(response)

    def processAutoComp(self, aco):
        """ Processes an autocomp request using an AutoCompObject instance. 
        """

        # Try using buffer first (not if we're not the requester)
        if self is aco.textCtrl:
            if aco.tryUsingBuffer():
                return

        # Include builtins and keywords?
        if not aco.name:
            aco.addNames(self._builtins)
            if iep.config.settings.autoComplete_keywords:
                aco.addNames(self._keywords)

        # Set buffer to prevent doing a second request
        # and store aco to see whether the response is still wanted.
        aco.setBuffer()
        self._currentACO = aco

        # Post request
        future = self._request.dir(aco.name)
        future.add_done_callback(self._processAutoComp_response)
        future.aco = aco

    def _processAutoComp_response(self, future):
        """ Process the response of the shell for the auto completion. 
        """

        # Process future
        if future.cancelled():
            #print('Introspect cancelled') # No living kernel
            return
        elif future.exception():
            print('Introspect-exception: ', future.exception())
            return
        else:
            response = future.result()
            aco = future.aco

        # First see if this is still the right editor (can also be a shell)
        editor1 = iep.editors.getCurrentEditor()
        editor2 = iep.shells.getCurrentShell()
        if aco.textCtrl not in [editor1, editor2]:
            # The editor or shell starting the autocomp is no longer active
            aco.textCtrl.autocompleteCancel()
            return

        # Add result to the list
        foundNames = []
        if response is not None:
            foundNames = response
        aco.addNames(foundNames)

        # Process list
        if aco.name and not foundNames:
            # No names found for the requested name. This means
            # it does not exist, let's try to import it
            importNames, importLines = iep.parser.getFictiveImports(editor1)
            baseName = aco.nameInImportNames(importNames)
            if baseName:
                line = importLines[baseName].strip()
                if line not in self._importAttempts:
                    # Do import
                    self.processLine(line + ' # auto-import')
                    self._importAttempts.append(line)
                    # Wait a barely noticable time to increase the chances
                    # That the import is complete when we repost the request.
                    time.sleep(0.2)
                    # To be sure, decrease the experiration date on the buffer
                    aco.setBuffer(timeout=1)
                    # Repost request
                    future = self._request.signature(aco.name)
                    future.add_done_callback(self._processAutoComp_response)
                    future.aco = aco
        else:
            # If still required, show list, otherwise only store result
            if self._currentACO is aco:
                aco.finish()
            else:
                aco.setBuffer()

    ## Methods for executing code

    def executeCommand(self, text):
        """ executeCommand(text)
        Execute one-line command in the remote Python session. 
        """
        self._ctrl_command.send(text)

    def executeCode(self, text, fname, lineno=0, cellName=None):
        """ executeCode(text, fname, lineno, cellName=None)
        Execute (run) a large piece of code in the remote shell.
        text: the source code to execute
        filename: the file from which the source comes
        lineno: the first lineno of the text in the file, where 0 would be
        the first line of the file...
        
        The text (source code) is first pre-processed:
        - convert all line-endings to \n
        - remove all empty lines at the end
        - remove commented lines at the end
        - convert tabs to spaces
        - dedent so minimal indentation is zero        
        """

        # Convert tabs to spaces
        text = text.replace("\t", " " * 4)

        # Make sure there is always *some* text
        if not text:
            text = ' '

        # Examine the text line by line...
        # - check for empty/commented lined at the end
        # - calculate minimal indentation
        lines = text.splitlines()
        lastLineOfCode = 0
        minIndent = 99
        for linenr in range(len(lines)):
            # Get line
            line = lines[linenr]
            # Check if empty (can be commented, but nothing more)
            tmp = line.split("#", 1)[0]  # get part before first #
            if tmp.count(" ") == len(tmp):
                continue  # empty line, proceed
            else:
                lastLineOfCode = linenr
            # Calculate indentation
            tmp = line.lstrip(" ")
            indent = len(line) - len(tmp)
            if indent < minIndent:
                minIndent = indent

        # Copy all proper lines to a new list,
        # remove minimal indentation, but only if we then would only remove
        # spaces (in the case of commented lines)
        lines2 = []
        for linenr in range(lastLineOfCode + 1):
            line = lines[linenr]
            # Remove indentation,
            if line[:minIndent].count(" ") == minIndent:
                line = line[minIndent:]
            else:
                line = line.lstrip(" ")
            lines2.append(line)

        # Send message
        text = "\n".join(lines2)
        msg = {
            'source': text,
            'fname': fname,
            'lineno': lineno,
            'cellName': cellName
        }
        self._ctrl_code.send(msg)

    ## The polling methods and terminating methods

    def poll(self, channel=None):
        """ poll()
        To keep the shell up-to-date.
        Call this periodically. 
        """

        if self._write_buffer:
            # There is still data in the buffer
            sub, M = self._write_buffer
        else:
            # Check what subchannel has the latest message pending
            sub = yoton.select_sub_channel(self._strm_out, self._strm_err,
                                           self._strm_echo, self._strm_raw,
                                           self._strm_broker,
                                           self._strm_prompt)
            # Read messages from it
            if sub:
                M = sub.recv_selected()
                #M = [sub.recv()] # Slow version (for testing)
                # Optimization: handle backspaces on stack of messages
                if sub is self._strm_out:
                    M = self._handleBackspacesOnList(M)
            # New prompt?
            if sub is self._strm_prompt:
                self.stateChanged.emit(self)

        # Write all pending messages that are later than any other message
        if sub:
            # Select messages to process
            N = 256
            M, buffer = M[:N], M[N:]
            # Buffer the rest
            if buffer:
                self._write_buffer = sub, buffer
            else:
                self._write_buffer = None
            # Get how to deal with prompt
            prompt = 0
            if sub is self._strm_echo:
                prompt = 1
            elif sub is self._strm_prompt:
                prompt = 2
            # Get color
            color = None
            if sub is self._strm_broker:
                color = '#000'
            elif sub is self._strm_raw:
                color = '#888888'  # Halfway
            elif sub is self._strm_err:
                color = '#F00'
            # Write
            self.write(''.join(M), prompt, color)

        # Do any actions?
        action = self._strm_action.recv(False)
        if action:
            if action.startswith('open '):
                fname = action.split(' ', 1)[1]
                iep.editors.loadFile(fname)
            else:
                print('Unkown action: %s' % action)

        # Update status
        state = self._stat_interpreter.recv()
        if state != self._state:
            self._state = state
            self.stateChanged.emit(self)

        # Update debug status
        state = self._stat_debug.recv()
        if state != self._debugState:
            self._debugState = state
            self.debugStateChanged.emit(self)

    def interrupt(self):
        """ interrupt()
        Send a Keyboard interrupt signal to the main thread of the 
        remote process. 
        """
        self._ctrl_broker.send('INT')

    def restart(self, scriptFile=None):
        """ restart(scriptFile=None)
        Terminate the shell, after which it is restarted. 
        Args can be a filename, to execute as a script as soon as the
        shell is back up.
        """

        # Get info
        info = finishKernelInfo(self._info, scriptFile)

        # Create message and send
        msg = 'RESTART\n' + ssdf.saves(info)
        self._ctrl_broker.send(msg)

        # Reset
        self.resetVariables()

    def terminate(self):
        """ terminate()
        Terminates the python process. It will first try gently, but 
        if that does not work, the process shall be killed.
        To be notified of the termination, connect to the "terminated"
        signal of the shell.
        """
        self._ctrl_broker.send('TERM')

    def closeShell(self):  # do not call it close(); that is a reserved method.
        """ closeShell()
        
        Very simple. This closes the shell. If possible, we will first
        tell the broker to terminate the kernel.
        
        The broker will be cleaned up if there are no clients connected
        and if there is no active kernel. In a multi-user environment,
        we should thus be able to close the shell without killing the
        kernel. But in a closed 1-to-1 environment we really want to 
        prevent loose brokers and kernels dangling around.
        
        In both cases however, it is the responsibility of the broker to
        terminate the kernel, and the shell will simply assume that this
        will work :) 
        
        """

        # If we can, try to tell the broker to terminate the kernel
        if self._context and self._context.connection_count:
            self.terminate()
            self._context.flush()  # Important, make sure the message is send!
            self._context.close()

        # Adios
        iep.shells.removeShell(self)

    def _onConnectionClose(self, c, why):
        """ To be called after disconnecting.
        In general, the broker will not close the connection, so it can
        be considered an error-state if this function is called.
        """

        # Stop context
        if self._context:
            self._context.close()

        # New (empty prompt)
        self._cursor1.movePosition(self._cursor1.End, A_MOVE)
        self._cursor2.movePosition(self._cursor2.End, A_MOVE)

        self.write('\n\n')
        self.write('Lost connection with broker:\n')
        self.write(why)
        self.write('\n\n')

        # Set style to indicate dead-ness
        self.setReadOnly(True)

        # Goto end such that the closing message is visible
        cursor = self.textCursor()
        cursor.movePosition(cursor.End, A_MOVE)
        self.setTextCursor(cursor)
        self.ensureCursorVisible()
Esempio n. 4
0
class PythonShell(BaseShell):
    """ The PythonShell class implements the python part of the shell
    by connecting to a remote process that runs a Python interpreter.
    """
    
    # Emits when the status string has changed or when receiving a new prompt
    stateChanged = QtCore.Signal(BaseShell)
    
    # Emits when the debug status is changed
    debugStateChanged = QtCore.Signal(BaseShell)
    
    
    def __init__(self, parent, info):
        BaseShell.__init__(self, parent)
        
        # Get standard info if not given.
        if info is None and iep.config.shellConfigs2:
            info = iep.config.shellConfigs2[0]
        if not info:
            info = KernelInfo(None)
        
        # Store info so we can reuse it on a restart
        self._info = info
        
        # For the editor to keep track of attempted imports
        self._importAttempts = []
        
        # To keep track of the response for introspection
        self._currentCTO = None
        self._currentACO = None
        
        # Write buffer to store messages in for writing
        self._write_buffer = None
        
        # Create timer to keep polling any results
        # todo: Maybe use yoton events to process messages as they arrive.
        # I tried this briefly, but it seemd to be less efficient because 
        # messages are not so much bach-processed anymore. We should decide
        # on either method.
        self._timer = QtCore.QTimer(self)
        self._timer.setInterval(POLL_TIMER_INTERVAL)  # ms
        self._timer.setSingleShot(False)
        self._timer.timeout.connect(self.poll)
        self._timer.start()
        
        # Add context menu
        self._menu = ShellContextMenu(shell=self, parent=self)
        self.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
        self.customContextMenuRequested.connect(lambda p: self._menu.popup(self.mapToGlobal(p+QtCore.QPoint(0,3)))) 
        
        # Start!
        self.resetVariables()
        self.connectToKernel(info)
    
    
    def resetVariables(self):
        """ Resets some variables. """
        
        # Reset read state
        self.setReadOnly(False)
        
        # Variables to store state, python version, builtins and keywords 
        self._state = ''
        self._debugState = {}
        self._version = ""
        self._builtins = []
        self._keywords = []
        self._startup_info = {}
        self._start_time = 0
        
        # (re)set import attempts
        self._importAttempts[:] = []
        
        # Update
        self.stateChanged.emit(self)
    
    
    def connectToKernel(self, info):
        """ connectToKernel()
        
        Create kernel and connect to it.
        
        """
        
        # Create yoton context
        self._context = ct = yoton.Context()
        
        # Create stream channels        
        self._strm_out = yoton.SubChannel(ct, 'strm-out')
        self._strm_err = yoton.SubChannel(ct, 'strm-err')
        self._strm_raw = yoton.SubChannel(ct, 'strm-raw')
        self._strm_echo = yoton.SubChannel(ct, 'strm-echo')
        self._strm_prompt = yoton.SubChannel(ct, 'strm-prompt')
        self._strm_broker = yoton.SubChannel(ct, 'strm-broker')
        self._strm_action = yoton.SubChannel(ct, 'strm-action', yoton.OBJECT)
        
        # Set channels to sync mode. This means that if the IEP cannot process
        # the messages fast enough, the sending side is blocked for a short
        # while. We don't want our users to miss any messages.
        for c in [self._strm_out, self._strm_err]:
            c.set_sync_mode(True)
        
        # Create control channels
        self._ctrl_command = yoton.PubChannel(ct, 'ctrl-command')
        self._ctrl_code = yoton.PubChannel(ct, 'ctrl-code', yoton.OBJECT)
        self._ctrl_broker = yoton.PubChannel(ct, 'ctrl-broker')
        
        # Create status channels
        self._stat_interpreter = yoton.StateChannel(ct, 'stat-interpreter')
        self._stat_debug = yoton.StateChannel(ct, 'stat-debug', yoton.OBJECT)
        self._stat_startup = yoton.StateChannel(ct, 'stat-startup', yoton.OBJECT)
        self._stat_startup.received.bind(self._onReceivedStartupInfo)
        
        # Create introspection request channel
        self._request = yoton.ReqChannel(ct, 'reqp-introspect')
        
        # Connect! The broker will only start the kernel AFTER
        # we connect, so we do not miss out on anything.
        slot = iep.localKernelManager.createKernel(finishKernelInfo(info))
        self._brokerConnection = ct.connect('localhost:%i'%slot)
        self._brokerConnection.closed.bind(self._onConnectionClose)
        
        # todo: see polling vs events
#         # Detect incoming messages 
#         for c in [self._strm_out, self._strm_err, self._strm_raw, 
#                 self._strm_echo, self._strm_prompt, self._strm_broker,
#                 self._strm_action,
#                 self._stat_interpreter, self._stat_debug]:
#             c.received.bind(self.poll)
        
    
    def _onReceivedStartupInfo(self, channel):
        startup_info = channel.recv()
        
        # Store the whole dict
        self._startup_info = startup_info
        
        # Store when we received this
        self._start_time = time.time()
        
        # Set version
        version = startup_info.get('version', None)
        if isinstance(version, tuple):
            version = [str(v) for v in version]
            self._version = '.'.join(version[:2])
        
        # Set keywords
        L = startup_info.get('keywords', None)
        if isinstance(L, list):
            self._keywords = L
        
        # Set builtins
        L = startup_info.get('builtins', None)
        if isinstance(L, list):
            self._builtins = L
        
        # Notify
        self.stateChanged.emit(self)
    
    
    ## Introspection processing methods
    
    
    def processCallTip(self, cto):
        """ Processes a calltip request using a CallTipObject instance. 
        """
        
        # Try using buffer first (not if we're not the requester)
        if self is cto.textCtrl:
            if cto.tryUsingBuffer():
                return
        
        # Clear buffer to prevent doing a second request
        # and store cto to see whether the response is still wanted.
        cto.setBuffer('')
        self._currentCTO = cto
        
        # Post request
        future = self._request.signature(cto.name)
        future.add_done_callback(self._processCallTip_response)
        future.cto = cto
    
    
    def _processCallTip_response(self, future):
        """ Process response of shell to show signature. 
        """
        
        # Process future
        if future.cancelled():
            #print('Introspect cancelled')  # No kernel
            return
        elif future.exception():
            print('Introspect-exception: ', future.exception())
            return
        else:
            response = future.result()
            cto = future.cto
        
        # First see if this is still the right editor (can also be a shell)
        editor1 = iep.editors.getCurrentEditor()
        editor2 = iep.shells.getCurrentShell()
        if cto.textCtrl not in [editor1, editor2]:
            # The editor or shell starting the autocomp is no longer active
            aco.textCtrl.autocompleteCancel()
            return
        
        # Invalid response
        if response is None:
            cto.textCtrl.autocompleteCancel()
            return
        
        # If still required, show tip, otherwise only store result
        if cto is self._currentCTO:
            cto.finish(response)
        else:
            cto.setBuffer(response)
    
    
    def processAutoComp(self, aco):
        """ Processes an autocomp request using an AutoCompObject instance. 
        """
        
        # Try using buffer first (not if we're not the requester)
        if self is aco.textCtrl:
            if aco.tryUsingBuffer():
                return
        
        # Include builtins and keywords?
        if not aco.name:
            aco.addNames(self._builtins)
            if iep.config.settings.autoComplete_keywords:
                aco.addNames(self._keywords)
        
        # Set buffer to prevent doing a second request
        # and store aco to see whether the response is still wanted.
        aco.setBuffer()
        self._currentACO = aco
        
        # Post request
        future = self._request.dir(aco.name)
        future.add_done_callback(self._processAutoComp_response)
        future.aco = aco
    
    
    def _processAutoComp_response(self, future):
        """ Process the response of the shell for the auto completion. 
        """ 
        
        # Process future
        if future.cancelled():
            #print('Introspect cancelled') # No living kernel
            return
        elif future.exception():
            print('Introspect-exception: ', future.exception())
            return
        else:
            response = future.result()
            aco = future.aco
        
        # First see if this is still the right editor (can also be a shell)
        editor1 = iep.editors.getCurrentEditor()
        editor2 = iep.shells.getCurrentShell()
        if aco.textCtrl not in [editor1, editor2]:
            # The editor or shell starting the autocomp is no longer active
            aco.textCtrl.autocompleteCancel()
            return
        
        # Add result to the list
        foundNames = []
        if response is not None:
            foundNames = response
        aco.addNames(foundNames)
        
        # Process list
        if aco.name and not foundNames:
            # No names found for the requested name. This means
            # it does not exist, let's try to import it
            importNames, importLines = iep.parser.getFictiveImports(editor1)
            baseName = aco.nameInImportNames(importNames)
            if baseName:
                line = importLines[baseName].strip()
                if line not in self._importAttempts:
                    # Do import
                    self.processLine(line + ' # auto-import')
                    self._importAttempts.append(line)
                    # Wait a barely noticable time to increase the chances
                    # That the import is complete when we repost the request.
                    time.sleep(0.2)
                    # To be sure, decrease the experiration date on the buffer
                    aco.setBuffer(timeout=1)
                    # Repost request
                    future = self._request.signature(aco.name)
                    future.add_done_callback(self._processAutoComp_response)
                    future.aco = aco
        else:
            # If still required, show list, otherwise only store result
            if self._currentACO is aco:
                aco.finish()
            else:
                aco.setBuffer()
    
    
    ## Methods for executing code
    
    
    def executeCommand(self, text):
        """ executeCommand(text)
        Execute one-line command in the remote Python session. 
        """
        self._ctrl_command.send(text)
    
    
    def executeCode(self, text, fname, lineno=0, cellName=None):
        """ executeCode(text, fname, lineno, cellName=None)
        Execute (run) a large piece of code in the remote shell.
        text: the source code to execute
        filename: the file from which the source comes
        lineno: the first lineno of the text in the file, where 0 would be
        the first line of the file...
        
        The text (source code) is first pre-processed:
        - convert all line-endings to \n
        - remove all empty lines at the end
        - remove commented lines at the end
        - convert tabs to spaces
        - dedent so minimal indentation is zero        
        """ 
        
        # Convert tabs to spaces
        text = text.replace("\t"," "*4)
        
        # Make sure there is always *some* text
        if not text:
            text = ' '
        
        # Examine the text line by line...
        # - check for empty/commented lined at the end
        # - calculate minimal indentation
        lines = text.splitlines()        
        lastLineOfCode = 0
        minIndent = 99
        for linenr in range(len(lines)):
            # Get line
            line = lines[linenr]
            # Check if empty (can be commented, but nothing more)
            tmp = line.split("#",1)[0]  # get part before first #
            if tmp.count(" ") == len(tmp):
                continue  # empty line, proceed
            else:
                lastLineOfCode = linenr
            # Calculate indentation
            tmp = line.lstrip(" ")
            indent = len(line) - len(tmp)
            if indent < minIndent:
                minIndent = indent 
        
        # Copy all proper lines to a new list, 
        # remove minimal indentation, but only if we then would only remove 
        # spaces (in the case of commented lines)
        lines2 = []
        for linenr in range(lastLineOfCode+1):
            line = lines[linenr]
            # Remove indentation, 
            if line[:minIndent].count(" ") == minIndent:
                line = line[minIndent:]
            else:
                line = line.lstrip(" ")
            lines2.append( line )
        
        
        # Send message
        text = "\n".join(lines2)
        msg = {'source':text, 'fname':fname, 'lineno':lineno, 'cellName': cellName}
        self._ctrl_code.send(msg)
    
    
    ## The polling methods and terminating methods
    
    def poll(self, channel=None):
        """ poll()
        To keep the shell up-to-date.
        Call this periodically. 
        """
        
        if self._write_buffer:
            # There is still data in the buffer
            sub, M = self._write_buffer
        else:
            # Check what subchannel has the latest message pending
            sub = yoton.select_sub_channel(self._strm_out, self._strm_err, 
                                self._strm_echo, self._strm_raw,
                                self._strm_broker, self._strm_prompt )
            # Read messages from it
            if sub:
                M = sub.recv_selected()
                #M = [sub.recv()] # Slow version (for testing)
                # Optimization: handle backspaces on stack of messages
                if sub is self._strm_out:
                    M = self._handleBackspacesOnList(M)
            # New prompt?
            if sub is self._strm_prompt:
                self.stateChanged.emit(self)
        
        # Write all pending messages that are later than any other message
        if sub:
            # Select messages to process
            N = 256
            M, buffer = M[:N], M[N:]
            # Buffer the rest
            if buffer:
                self._write_buffer = sub, buffer
            else:
                self._write_buffer = None
            # Get how to deal with prompt
            prompt = 0
            if sub is self._strm_echo:
                prompt = 1 
            elif sub is  self._strm_prompt:
                prompt = 2
            # Get color
            color = None
            if sub is self._strm_broker:
                color = '#000'
            elif sub is self._strm_raw:
                color = '#888888' # Halfway
            elif sub is self._strm_err:
                color = '#F00'
            # Write
            self.write(''.join(M), prompt, color)
        
        
        # Do any actions?
        action = self._strm_action.recv(False)
        if action:
            if action.startswith('open '):
                fname = action.split(' ',1)[1]
                iep.editors.loadFile(fname)
            else:
                print('Unkown action: %s' % action)
        
        # Update status
        state = self._stat_interpreter.recv()
        if state != self._state:
            self._state = state
            self.stateChanged.emit(self)
        
        # Update debug status
        state = self._stat_debug.recv()        
        if state != self._debugState:
            self._debugState = state
            self.debugStateChanged.emit(self)
    
    
    def interrupt(self):
        """ interrupt()
        Send a Keyboard interrupt signal to the main thread of the 
        remote process. 
        """
        self._ctrl_broker.send('INT')
    
    
    def restart(self, scriptFile=None):
        """ restart(scriptFile=None)
        Terminate the shell, after which it is restarted. 
        Args can be a filename, to execute as a script as soon as the
        shell is back up.
        """
        
        # Get info
        info = finishKernelInfo(self._info, scriptFile)
        
        # Create message and send
        msg = 'RESTART\n' + ssdf.saves(info)
        self._ctrl_broker.send(msg)
        
        # Reset
        self.resetVariables()
    
    
    def terminate(self):
        """ terminate()
        Terminates the python process. It will first try gently, but 
        if that does not work, the process shall be killed.
        To be notified of the termination, connect to the "terminated"
        signal of the shell.
        """
        self._ctrl_broker.send('TERM')
    
    
    def closeShell(self): # do not call it close(); that is a reserved method.
        """ closeShell()
        
        Very simple. This closes the shell. If possible, we will first
        tell the broker to terminate the kernel.
        
        The broker will be cleaned up if there are no clients connected
        and if there is no active kernel. In a multi-user environment,
        we should thus be able to close the shell without killing the
        kernel. But in a closed 1-to-1 environment we really want to 
        prevent loose brokers and kernels dangling around.
        
        In both cases however, it is the responsibility of the broker to
        terminate the kernel, and the shell will simply assume that this
        will work :) 
        
        """
        
        # If we can, try to tell the broker to terminate the kernel
        if self._context and self._context.connection_count:
            self.terminate()
            self._context.flush() # Important, make sure the message is send!
            self._context.close()
        
        # Adios
        iep.shells.removeShell(self)
    
    
    def _onConnectionClose(self, c, why):
        """ To be called after disconnecting.
        In general, the broker will not close the connection, so it can
        be considered an error-state if this function is called.
        """
        
        # Stop context
        if self._context:
            self._context.close()
        
        # New (empty prompt)
        self._cursor1.movePosition(self._cursor1.End, A_MOVE)
        self._cursor2.movePosition(self._cursor2.End, A_MOVE)
        
        self.write('\n\n');
        self.write('Lost connection with broker:\n')
        self.write(why)
        self.write('\n\n')
        
        # Set style to indicate dead-ness
        self.setReadOnly(True)
        
        # Goto end such that the closing message is visible
        cursor = self.textCursor()
        cursor.movePosition(cursor.End, A_MOVE)
        self.setTextCursor(cursor)
        self.ensureCursorVisible()