RemotePyInterpreter

RemotePyInterpreter

RemotePyInterpreter is a full featured out of process Python interpreter in a NSTextView.

The source of this application demonstrates - Advanced usage of NSTextView - Text completion (Only in OS X 10.3) - Asynchronous TCP networking with NSFileHandle - One crazy repr/eval-based IPC protocol

Bob Ippolito bob@redivi.com

Sources

AsyncPythonInterpreter.py

__all__ = ['AsyncPythonInterpreter']

try:
    import fcntl
except:
    fcntl = None
import os
import sys
import socket
from netrepr import NetRepr, RemoteObjectPool, RemoteObjectReference
import objc
from Foundation import *

IMPORT_MODULES = ['netrepr', 'remote_console', 'remote_pipe', 'remote_bootstrap']
source = []
for fn in IMPORT_MODULES:
    for line in open(fn+'.py', 'rU'):
        source.append(line)
    source.append('\n\n')
SOURCE = repr(''.join(source)) + '\n'

def bind_and_listen(hostport):
    if isinstance(hostport, str):
        host, port = hostport.split(':')
        hostport = (host, int(port))
    serversock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    # set close-on-exec
    if hasattr(fcntl, 'FD_CLOEXEC'):
        old = fcntl.fcntl(serversock.fileno(), fcntl.F_GETFD)
        fcntl.fcntl(serversock.fileno(), fcntl.F_SETFD, old | fcntl.FD_CLOEXEC)
    # allow the address to be re-used in a reasonable amount of time
    if os.name == 'posix' and sys.platform != 'cygwin':
        serversock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)

    serversock.bind(hostport)
    serversock.listen(5)
    return serversock

class AsyncPythonInterpreter(NSObject):

    commandReactor = objc.IBOutlet('commandReactor')

    def init(self):
        self = super(AsyncPythonInterpreter, self).init()
        self.host = None
        self.port = None
        self.interpreterPath = None
        self.scriptPath = None
        self.commandReactor = None
        self.serverSocket = None
        self.serverFileHandle = None
        self.buffer = ''
        self.serverFileHandle = None
        self.remoteFileHandle = None
        self.childTask = None
        return self

    def initWithHost_port_interpreterPath_scriptPath_commandReactor_(self, host, port, interpreterPath, scriptPath, commandReactor):
        self = self.init()
        self.host = host
        self.port = port
        self.interpreterPath = interpreterPath
        self.scriptPath = scriptPath
        self.commandReactor = commandReactor
        self.serverSocket = None
        return self

    def awakeFromNib(self):
        defaults = NSUserDefaults.standardUserDefaults()
        def default(k, v, typeCheck=None):
            rval = defaults.objectForKey_(k)
            if typeCheck is not None and rval is not None:
                try:
                    rval = typeCheck(rval)
                except TypeError:
                    NSLog(u'%s failed type check %s with value %s', k, typeCheck.__name__, rval)
                    rval = None
            if rval is None:
                defaults.setObject_forKey_(v, k)
                rval = v
            return rval
        self.host = default(u'AsyncPythonInterpreterInterpreterHost', u'127.0.0.1', str)
        self.port = default(u'AsyncPythonInterpreterInterpreterPort', 0, int)
        self.interpreterPath = default(u'AsyncPythonInterpreterInterpreterPath', u'/usr/bin/python', unicode)
        self.scriptPath = type(self).bundleForClass().pathForResource_ofType_(u'tcpinterpreter', u'py')

    def connect(self):
        #NSLog(u'connect')
        self.serverSocket = bind_and_listen((self.host, self.port))
        self.serverFileHandle = NSFileHandle.alloc().initWithFileDescriptor_(self.serverSocket.fileno())
        nc = NSNotificationCenter.defaultCenter()
        nc.addObserver_selector_name_object_(
            self,
            'remoteSocketAccepted:',
            NSFileHandleConnectionAcceptedNotification,
            self.serverFileHandle)
        self.serverFileHandle.acceptConnectionInBackgroundAndNotify()
        self.remoteFileHandle = None
        for k in os.environ.keys():
            if k.startswith('PYTHON'):
                del os.environ[k]
        self.childTask = NSTask.launchedTaskWithLaunchPath_arguments_(self.interpreterPath, [self.scriptPath, repr(self.serverSocket.getsockname())])
        nc.addObserver_selector_name_object_(
            self,
            'childTaskTerminated:',
            NSTaskDidTerminateNotification,
            self.childTask)
        return self

    def remoteSocketAccepted_(self, notification):
        #NSLog(u'remoteSocketAccepted_')
        self.serverFileHandle.closeFile()
        self.serverFileHandle = None
        ui = notification.userInfo()
        self.remoteFileHandle = ui.objectForKey_(NSFileHandleNotificationFileHandleItem)
        nc = NSNotificationCenter.defaultCenter()
        nc.addObserver_selector_name_object_(
            self,
            'remoteFileHandleReadCompleted:',
            NSFileHandleReadCompletionNotification,
            self.remoteFileHandle)
        self.writeBytes_(SOURCE)
        self.remoteFileHandle.readInBackgroundAndNotify()
        self.commandReactor.connectionEstablished_(self)
        NSNotificationCenter.defaultCenter().postNotificationName_object_(u'AsyncPythonInterpreterOpened', self)

    def remoteFileHandleReadCompleted_(self, notification):
        #NSLog(u'remoteFileHandleReadCompleted_')
        ui = notification.userInfo()
        newData = ui.objectForKey_(NSFileHandleNotificationDataItem)
        if newData is None:
            self.close()
            NSLog(u'Error: %@', ui.objectForKey_(NSFileHandleError))
            return
        bytes = newData.bytes()[:]
        if len(bytes) == 0:
            self.close()
            return
        self.remoteFileHandle.readInBackgroundAndNotify()
        start = len(self.buffer)
        buff = self.buffer + newData.bytes()[:]
        #NSLog(u'current buffer: %s', buff)
        lines = []
        while True:
            linebreak = buff.find('\n', start) + 1
            if linebreak == 0:
                break
            lines.append(buff[:linebreak])
            buff = buff[linebreak:]
            start = 0
        #NSLog(u'lines: %s', lines)
        self.buffer = buff
        for line in lines:
            self.commandReactor.lineReceived_fromConnection_(line, self)

    def writeBytes_(self, bytes):
        #NSLog(u'Writing bytes: %s' bytes)
        try:
            self.remoteFileHandle.writeData_(NSData.dataWithBytes_length_(bytes, len(bytes)))
        except objc.error:
            self.close()
        #NSLog(u'bytes written.')

    def childTaskTerminated_(self, notification):
        #NSLog(u'childTaskTerminated_')
        self.close()

    def closeServerFileHandle(self):
        #NSLog(u'closeServerFileHandle')
        if self.serverFileHandle is not None:
            try:
                self.serverFileHandle.closeFile()
            except objc.error:
                pass
            self.serverFileHandle = None

    def closeRemoteFileHandle(self):
        #NSLog(u'closeRemoteFileHandle')
        if self.remoteFileHandle is not None:
            try:
                self.remoteFileHandle.closeFile()
            except objc.error:
                pass
            self.remoteFileHandle = None

    def terminateChildTask(self):
        #NSLog(u'terminateChildTask')
        if self.childTask is not None:
            try:
                self.childTask.terminate()
            except objc.error:
                pass
            self.childTask = None

    def close(self):
        #NSLog(u'close')
        NSNotificationCenter.defaultCenter().removeObserver_(self)
        self.finalClose()
        NSNotificationCenter.defaultCenter().postNotificationName_object_(u'AsyncPythonInterpreterClosed', self)

    def finalClose(self):
        if self.commandReactor is not None:
            self.commandReactor.connectionClosed_(self)
            self.commandReactor = None
        self.closeServerFileHandle()
        self.closeRemoteFileHandle()
        self.terminateChildTask()

def test_console():
    from PyObjCTools import AppHelper
    from ConsoleReactor import ConsoleReactor
    host = '127.0.0.1'
    port = 0
    interpreterPath = sys.executable
    scriptPath = unicode(os.path.abspath('tcpinterpreter.py'))
    commandReactor = ConsoleReactor.alloc().init()
    interp = AsyncPythonInterpreter.alloc().initWithHost_port_interpreterPath_scriptPath_commandReactor_(host, port, interpreterPath, scriptPath, commandReactor)
    interp.connect()
    class ThisEventLoopStopper(NSObject):
        def interpFinished_(self, notification):
            AppHelper.stopEventLoop()
    stopper = ThisEventLoopStopper.alloc().init()
    NSNotificationCenter.defaultCenter().addObserver_selector_name_object_(stopper, 'interpFinished:', u'AsyncPythonInterpreterClosed', interp)
    AppHelper.runConsoleEventLoop(installInterrupt=True)

def main():
    test_console()

if __name__ == '__main__':
    main()

ConsoleReactor.py

from __future__ import print_function

__all__ = ['ConsoleReactor']

import sys
from netrepr import NetRepr, RemoteObjectPool, RemoteObjectReference
from Foundation import *

class ConsoleReactor(NSObject):

    def init(self):
        self = super(ConsoleReactor, self).init()
        self.pool = None
        self.netReprCenter = None
        self.connection = None
        self.commands = {}
        return self

    def connectionEstablished_(self, connection):
        #NSLog(u'connectionEstablished_')
        self.connection = connection
        self.pool = RemoteObjectPool(self.writeCode_)
        self.netReprCenter = NetRepr(self.pool)

    def connectionClosed_(self, connection):
        #NSLog(u'connectionClosed_')
        self.connection = None
        self.pool = None
        self.netReprCenter = None

    def writeCode_(self, code):
        #NSLog(u'writeCode_')
        self.connection.writeBytes_(repr(code) + '\n')

    def netEval_(self, s):
        #NSLog(u'netEval_')
        return eval(s, self.pool.namespace, self.pool.namespace)

    def lineReceived_fromConnection_(self, lineReceived, connection):
        #NSLog(u'lineReceived_fromConnection_')
        code = lineReceived.rstrip()
        if not code:
            return
        self.pool.push()
        command = map(self.netEval_, eval(code))
        try:
            self.handleCommand_(command)
        finally:
            self.pool.pop()

    def handleCommand_(self, command):
        #NSLog(u'handleCommand_')
        basic = command[0]
        sel = 'handle%sCommand:' % (basic.capitalize())
        cmd = command[1:]
        if not self.respondsToSelector_(sel):
            NSLog(u'%r does not respond to %s', self, command)
        else:
            # XXX - this crashes PyObjC??
            # self.performSelector_withObject_(sel, cmd)
            getattr(self, sel.replace(':', '_'))(cmd)

    def handleRespondCommand_(self, command):
        self.doCallback_sequence_args_(
            self.commands.pop(command[0]),
            command[0],
            map(self.netEval_, command[1:]),
        )

    def sendResult_sequence_(self, rval, seq):
        nr = self.netReprCenter
        code = '__result__[%r] = %s' % (seq, nr.netrepr(rval))
        self.writeCode_(code)

    def sendException_sequence_(self, e, seq):
        nr = self.netReprCenter
        code = 'raise ' + nr.netrepr_exception(e)
        print("forwarding:", code)
        self.writeCode_(code)

    def doCallback_sequence_args_(self, callback, seq, args):
        nr = self.netReprCenter
        try:
            rval = callback(*args)
        except Exception as e:
            self.sendException_sequence_(e, seq)
        else:
            self.sendResult_sequence_(rval, seq)

    def deferCallback_sequence_value_(self, callback, seq, value):
        self.commands[seq] = callback
        self.writeCode_('pipe.respond(%r, netrepr(%s))' % (seq, value))

    def handleExpectCommand_(self, command):
        #NSLog(u'handleExpectCommand_')
        seq = command[0]
        name = command[1]
        args = command[2:]
        netrepr = self.netReprCenter.netrepr
        rval = None
        code = None
        if name == 'RemoteConsole.raw_input':
            self.doCallback_sequence_args_(raw_input, seq, args)
        elif name == 'RemoteConsole.write':
            self.doCallback_sequence_args_(sys.stdout.write, seq, args)
        elif name == 'RemoteConsole.displayhook':
            obj = args[0]
            def displayhook_respond(reprobject):
                print(reprobject)
            def displayhook_local(obj):
                if obj is not None:
                    displayhook_respond(repr(obj))
            if isinstance(obj, RemoteObjectReference):
                self.deferCallback_sequence_value_(displayhook_respond, seq, 'repr(%s)' % (netrepr(obj),))
            else:
                self.doCallback_sequence_args_(displayhook_local, seq, args)
        elif name.startswith('RemoteFileLike.'):
            fh = getattr(sys, args[0])
            meth = getattr(fh, name[len('RemoteFileLike.'):])
            self.doCallback_sequence_args_(meth, seq, args[1:])
        elif name == 'RemoteConsole.initialize':
            self.doCallback_sequence_args_(lambda *args:None, seq, args)
        else:
            self.doCallback_sequence_args_(NSLog, seq, [u'%r does not respond to expect %r', self, command,])

    def close(self):
        if self.connection is not None:
            self.writeCode_('raise SystemExit')
        self.pool = None
        self.netReprCenter = None
        self.connection = None
        self.commands = None

RemotePyInterpreter.py

import sys
import traceback
import keyword
import time
from Foundation import *
from AppKit import *
from PyObjCTools import AppHelper


from AsyncPythonInterpreter import *
from ConsoleReactor import *
from netrepr import RemoteObjectReference

def ensure_unicode(s):
    if not isinstance(s, unicode):
        s = unicode(s, 'utf-8', 'replace')
    return s

class RemotePyInterpreterReactor (ConsoleReactor):
    delegate = objc.IBOutlet()

    def handleExpectCommand_(self, command):
        print(command)
        seq = command[0]
        name = command[1]
        args = command[2:]
        netrepr = self.netReprCenter.netrepr
        rval = None
        code = None
        if name == 'RemoteConsole.raw_input':
            prompt = ensure_unicode(args[0])
            def input_received(line):
                self.sendResult_sequence_(line, seq)
            self.delegate.expectCodeInput_withPrompt_(input_received, prompt)
        elif name == 'RemoteConsole.write':
            args = [ensure_unicode(args[0]), u'code']
            self.doCallback_sequence_args_(self.delegate.writeString_forOutput_, seq, args)
        elif name == 'RemoteConsole.displayhook':
            obj = args[0]
            def displayhook_respond(reprobject):
                self.delegate.writeString_forOutput_(ensure_unicode(reprobject) + u'\n', u'code')
            def displayhook_local(obj):
                if obj is not None:
                    displayhook_respond(repr(obj))
            if isinstance(obj, RemoteObjectReference):
                self.deferCallback_sequence_value_(displayhook_respond, seq, 'repr(%s)' % (netrepr(obj),))
            else:
                self.doCallback_sequence_args_(displayhook_local, seq, args)
        elif name.startswith('RemoteFileLike.'):
            method = name[len('RemoteFileLike.'):]
            if method == 'write':
                style, msg = map(ensure_unicode, args)
                args = [msg, style]
                self.doCallback_sequence_args_(self.delegate.writeString_forOutput_, seq, args)

            elif method == 'readline':
                def input_received(line):
                    self.sendResult_sequence_(line, seq)
                self.delegate.expectCodeInput_withPrompt_(input_received, '')

            else:
                self.doCallback_sequence_args_(NSLog, seq, [u'%s does not respond to expect %s', self, command])
        elif name == 'RemoteConsole.initialize':
            def gotTitle(repr_versioninfo, executable, pid):
                self.delegate.setVersion_executable_pid_(
                    u'.'.join(map(unicode, self.netEval_(repr_versioninfo)[:3])),
                    ensure_unicode(executable),
                    pid,
                )
            self.doCallback_sequence_args_(gotTitle, seq, args)
        #    fh = getattr(sys, args[0])
        #    meth = getattr(fh, name[len('RemoteFileLike.'):])
        #    self.doCallback_sequence_args_(meth, seq, args[1:])
        else:
            self.doCallback_sequence_args_(NSLog, seq, [u'%s does not respond to expect %s', self, command])

    def close(self):
        super(RemotePyInterpreterReactor, self).close()
        self.delegate = None


class PseudoUTF8Input(object):
    softspace = 0
    def __init__(self, readlinemethod):
        self._buffer = u''
        self._readline = readlinemethod

    def read(self, chars=None):
        if chars is None:
            if self._buffer:
                rval = self._buffer
                self._buffer = u''
                if rval.endswith(u'\r'):
                    rval = rval[:-1]+u'\n'
                return rval.encode('utf-8')
            else:
                return self._readline(u'\x04')[:-1].encode('utf-8')
        else:
            while len(self._buffer) < chars:
                self._buffer += self._readline(u'\x04\r')
                if self._buffer.endswith('\x04'):
                    self._buffer = self._buffer[:-1]
                    break
            rval, self._buffer = self._buffer[:chars], self._buffer[chars:]
            return rval.encode('utf-8').replace('\r','\n')

    def readline(self):
        if u'\r' not in self._buffer:
            self._buffer += self._readline(u'\x04\r')
        if self._buffer.endswith('\x04'):
            rval = self._buffer[:-1].encode('utf-8')
        elif self._buffer.endswith('\r'):
            rval = self._buffer[:-1].encode('utf-8')+'\n'
        self._buffer = u''

        return rval


DEBUG_DELEGATE = 0
PASSTHROUGH = (
   'deleteBackward:',
   'complete:',
   'moveRight:',
   'moveLeft:',
)

class RemotePyInterpreterDocument (NSDocument):
    """
    PyInterpreter is a delegate/controller for a NSTextView,
    turning it into a full featured interactive Python interpreter.
    """

    commandReactor = objc.IBOutlet()
    interpreter = objc.IBOutlet()
    textView = objc.IBOutlet()

    def expectCodeInput_withPrompt_(self, callback, prompt):
        self.writeString_forOutput_(prompt, u'code')
        self.setCharacterIndexForInput_(self.lengthOfTextView())
        self.p_input_callbacks.append(callback)
        self.flushCallbacks()

    def flushCallbacks(self):
        while self.p_input_lines and self.p_input_callbacks:
            self.p_input_callbacks.pop(0)(self.p_input_lines.pop(0))

    def setupTextView(self):
        self.textView.setFont_(self.font())
        self.textView.setContinuousSpellCheckingEnabled_(False)
        self.textView.setRichText_(False)
        self.setCharacterIndexForInput_(0)

    def setVersion_executable_pid_(self, version, executable, pid):
        self.version = version
        self.pid = pid
        self.executable = executable
        self.setFileName_(executable)

    def displayName(self):
        if not hasattr(self, 'version'):
            return u'Starting...'
        return u'Python %s - %s - %s' % (self.version, self.executable, self.pid)

    def updateChangeCount_(self, val):
        return

    def windowWillClose_(self, window):
        if self.commandReactor is not None:
            self.commandReactor.close()
            self.commandReactor = None
        if self.interpreter is not None:
            self.interpreter.close()
            self.interpreter = None

    def windowNibName(self):
        return u'RemotePyInterpreterDocument'

    def isDocumentEdited(self):
        return False

    def awakeFromNib(self):
        # XXX - should this be done later?
        self.setFont_(NSFont.userFixedPitchFontOfSize_(10))
        self.p_colors = {
            u'stderr': NSColor.redColor(),
            u'stdout': NSColor.blueColor(),
            u'code': NSColor.blackColor(),
        }
        self.setHistoryLength_(50)
        self.setHistoryView_(0)
        self.setInteracting_(False)
        self.setAutoScroll_(True)
        self.setSingleLineInteraction_(False)
        self.p_history = [u'']
        self.p_input_callbacks = []
        self.p_input_lines = []
        self.setupTextView()
        self.interpreter.connect()

    #
    #  Modal input dialog support
    #

    #def p_nestedRunLoopReaderUntilEOLchars_(self, eolchars):
    #    """
    #    This makes the baby jesus cry.

    #    I want co-routines.
    #    """
    #    app = NSApplication.sharedApplication()
    #    window = self.textView.window()
    #    self.setCharacterIndexForInput_(self.lengthOfTextView())
    #    # change the color.. eh
    #    self.textView.setTypingAttributes_({
    #        NSFontAttributeName: self.font(),
    #        NSForegroundColorAttributeName: self.colorForName_(u'code'),
    #    })
    #    while True:
    #        event = app.nextEventMatchingMask_untilDate_inMode_dequeue_(
    #            NSAnyEventMask,
    #            NSDate.distantFuture(),
    #            NSDefaultRunLoopMode,
    #            True)
    #        if (event.type() == NSKeyDown) and (event.window() is window):
    #            eol = event.characters()
    #            if eol in eolchars:
    #                break
    #        app.sendEvent_(event)
    #    cl = self.currentLine()
    #    if eol == u'\r':
    #        self.writeNewLine()
    #    return cl + eol

    def executeLine_(self, line):
        self.addHistoryLine_(line)
        self.p_input_lines.append(line)
        self.flushCallbacks()
        self.p_history = filter(None, self.p_history)
        self.p_history.append(u'')
        self.setHistoryView_(len(self.p_history) - 1)

    def executeInteractiveLine_(self, line):
        self.setInteracting_(True)
        try:
            self.executeLine_(line)
        finally:
            self.setInteracting_(False)

    def replaceLineWithCode_(self, s):
        idx = self.characterIndexForInput()
        ts = self.textView.textStorage()
        s = self.formatString_forOutput_(s, u'code')
        ts.replaceCharactersInRange_withAttributedString_(
            (idx, len(ts.mutableString())-idx),
            s,
        )

    #
    #  History functions
    #

    def addHistoryLine_(self, line):
        line = line.rstrip(u'\n')
        if self.p_history[-1] == line:
            return False
        if not line:
            return False
        self.p_history.append(line)
        if len(self.p_history) > self.historyLength():
            self.p_history.pop(0)
        return True

    def historyDown_(self, sender):
        if self.p_historyView == (len(self.p_history) - 1):
            return
        self.p_history[self.p_historyView] = self.currentLine()
        self.p_historyView += 1
        self.replaceLineWithCode_(self.p_history[self.p_historyView])
        self.moveToEndOfLine_(self)

    def historyUp_(self, sender):
        if self.p_historyView == 0:
            return
        self.p_history[self.p_historyView] = self.currentLine()
        self.p_historyView -= 1
        self.replaceLineWithCode_(self.p_history[self.p_historyView])
        self.moveToEndOfLine_(self)

    #
    #  Convenience methods to create/write decorated text
    #

    def formatString_forOutput_(self, s, name):
        return NSAttributedString.alloc().initWithString_attributes_(
            s,
            {
                NSFontAttributeName: self.font(),
                NSForegroundColorAttributeName: self.colorForName_(name),
            },
        )

    def writeString_forOutput_(self, s, name):
        s = self.formatString_forOutput_(s, name)
        self.textView.textStorage().appendAttributedString_(s)
        if self.isAutoScroll():
            self.textView.scrollRangeToVisible_((self.lengthOfTextView(), 0))

    def writeNewLine(self):
        self.writeString_forOutput_(u'\n', u'code')

    def colorForName_(self, name):
        return self.p_colors[name]

    def setColor_forName_(self, color, name):
        self.p_colors[name] = color

    #
    #  Convenience methods for manipulating the NSTextView
    #

    def currentLine(self):
        return self.textView.textStorage().mutableString()[self.characterIndexForInput():]

    def moveAndScrollToIndex_(self, idx):
        self.textView.scrollRangeToVisible_((idx, 0))
        self.textView.setSelectedRange_((idx, 0))

    def lengthOfTextView(self):
        return len(self.textView.textStorage().mutableString())

    #
    #  NSTextViewDelegate methods
    #

    def textView_completions_forPartialWordRange_indexOfSelectedItem_(self, aTextView, completions, begin_length, index):
        # XXX
        # this will probably have to be tricky in order to be asynchronous..
        # either by:
        #     nesting a run loop (bleh)
        #     polling the subprocess (bleh)
        #     returning nothing and calling self.textView.complete_ later
        begin, length = begin_length
        return None, 0

        if False:
            txt = self.textView.textStorage().mutableString()
            end = begin+length
            while (begin>0) and (txt[begin].isalnum() or txt[begin] in u'._'):
                begin -= 1
            while not txt[begin].isalnum():
                begin += 1
            return self.p_console.recommendCompletionsFor(txt[begin:end])

    def textView_shouldChangeTextInRange_replacementString_(self, aTextView, aRange, newString):
        begin, length = aRange
        lastLocation = self.characterIndexForInput()
        if begin < lastLocation:
            # no editing anywhere but the interactive line
            return False
        newString = newString.replace(u'\r', u'\n')
        if u'\n' in newString:
            if begin != lastLocation:
                # no pasting multiline unless you're at the end
                # of the interactive line
                return False
            # multiline paste support
            #self.clearLine()
            newString = self.currentLine() + newString
            for s in newString.strip().split(u'\n'):
                self.writeString_forOutput_(s + u'\n', u'code')
                self.executeLine_(s)
            return False
        return True

    def textView_willChangeSelectionFromCharacterRange_toCharacterRange_(self, aTextView, fromRange, toRange):
        begin, length = toRange
        if self.singleLineInteraction() and length == 0 and begin < self.characterIndexForInput():
            # no cursor movement off the interactive line
            return fromRange
        else:
            return toRange

    def textView_doCommandBySelector_(self, aTextView, aSelector):
        # deleteForward: is ctrl-d
        if self.isInteracting():
            if aSelector == 'insertNewline:':
                self.writeNewLine()
            return False
        # XXX - this is ugly
        responder = getattr(self, aSelector.replace(':','_'), None)
        if responder is not None:
            responder(aTextView)
            return True
        else:
            if DEBUG_DELEGATE and aSelector not in PASSTHROUGH:
                print(aSelector)
            return False

    #
    #  doCommandBySelector "posers" on the textView
    #

    def insertTabIgnoringFieldEditor_(self, sender):
        # this isn't terribly necessary, b/c F5 and opt-esc do completion
        # but why not
        sender.complete_(self)

    def moveToBeginningOfLine_(self, sender):
        self.moveAndScrollToIndex_(self.characterIndexForInput())

    def moveToEndOfLine_(self, sender):
        self.moveAndScrollToIndex_(self.lengthOfTextView())

    def moveToBeginningOfLineAndModifySelection_(self, sender):
        begin, length = self.textView.selectedRange()
        pos = self.characterIndexForInput()
        if begin + length > pos:
            self.textView.setSelectedRange_((pos, begin + length - pos))
        else:
            self.moveToBeginningOfLine_(sender)

    def moveToEndOfLineAndModifySelection_(self, sender):
        begin, length = self.textView.selectedRange()
        pos = max(self.characterIndexForInput(), begin)
        self.textView.setSelectedRange_((pos, self.lengthOfTextView()))

    def insertNewline_(self, sender):
        line = self.currentLine()
        self.writeNewLine()
        self.executeInteractiveLine_(line)

    moveToBeginningOfParagraph_ = moveToBeginningOfLine_
    moveToEndOfParagraph_ = moveToEndOfLine_
    insertNewlineIgnoringFieldEditor_ = insertNewline_
    moveDown_ = historyDown_
    moveUp_ = historyUp_

    #
    #  Accessors
    #

    def historyLength(self):
        return self.p_historyLength

    def setHistoryLength_(self, length):
        self.p_historyLength = length

    def font(self):
        return self.p_font

    def setFont_(self, font):
        self.p_font = font

    def isInteracting(self):
        return self.p_interacting

    def setInteracting_(self, v):
        self.p_interacting = v

    def isAutoScroll(self):
        return self.p_autoScroll

    def setAutoScroll_(self, v):
        self.p_autoScroll = v

    def characterIndexForInput(self):
        return self.p_characterIndexForInput

    def setCharacterIndexForInput_(self, idx):
        self.p_characterIndexForInput = idx
        self.moveAndScrollToIndex_(idx)

    def historyView(self):
        return self.p_historyView

    def setHistoryView_(self, v):
        self.p_historyView = v

    def singleLineInteraction(self):
        return self.p_singleLineInteraction

    def setSingleLineInteraction_(self, v):
        self.p_singleLineInteraction = v



if __name__ == '__main__':
    AppHelper.runEventLoop(installInterrupt=True)

netrepr.py

import types, itertools

def type_string(obj):
    objType = type(obj)
    if objType is types.InstanceType:
        objType = obj.__class__
    return getattr(objType, '__module__', '-') + '.' + objType.__name__

class NetRepr(object):
    def __init__(self, objectPool):
        self.objectPool = objectPool
        self.cache = {}
        self._identfactory = itertools.count()

    def clear(self):
        self.cache.clear()
        self._identfactory = itertools.count()

    def netrepr_tuple(self, obj):
        return repr(tuple(itertools.imap(self.netrepr, obj)))

    def netrepr_list(self, obj):
        return repr(map(self.netrepr, obj))

    def netrepr_exception(self, e):
        cls = e.__class__
        if cls.__module__ == 'exceptions':
            rval = cls.__name__ + self.netrepr_tuple(e.args)
        else:
            rval = 'Exception(%r)' % ('[Remote] %s.%s %s' % (cls.__module__, cls.__name__, e),)
        return rval

    def netrepr(self, obj):
        if obj is None:
            return 'None'
        objtype = type(obj)
        if objtype is int or objtype is long or objtype is float:
            return repr(obj)
        elif objtype is str or objtype is unicode:
            if True:
                return repr(obj)
            else:
                # "intern" these
                obj_id = id(obj)
                cached = self.get(cache, obj_id, None)
                if cached is None:
                    ident = self._identfactory.next()
                    self.cache[obj_id] = '__cached__(%r)' % (obj_id,)
                    cached = '__cache__(%r, %r)' % (obj_id, obj)
                return cached
        return self.netrepr_default(obj)

    def netrepr_default(self, obj):
        method = getattr(obj, '__netrepr__', None)
        if method is None:
            method = self.objectPool.referenceForObject(obj).__netrepr__
        return method()


class BaseObjectPool(object):
    def __init__(self):
        self.idents = {}
        self.refs = {}
        self.pools = []

    def referenceForIdent(self, ident):
        return self.idents[ident]

    def base_alloc(self, ref, ident):
        self.refs[ref] = ident
        self.idents[ident] = ref

    def base_dealloc(self, ref, ident):
        del self.refs[ref]
        del self.idents[ident]

    def autorelease(self, ref):
        if not self.pools:
            raise RuntimeError("no autoreleasepool for %r" % (ref,))
        pool = self.pools[-1]
        pool[ref] = pool.get(ref, 0) + 1

    def push(self):
        #print "pushed pool"
        self.pools.append({})

    def pop(self):
        if not self.pools:
            raise RuntimeError("popped too many pools")
        #print "popped pool"
        pool = self.pools.pop()
        for ref, count in pool.iteritems():
            ref.release(count)

    def referenceForObject(self, obj):
        raise TypeError("Can not create a reference to %r, the bridge is unidirectional" % (obj,))


class RemoteObjectPool(BaseObjectPool):
    def __init__(self, writecode):
        BaseObjectPool.__init__(self)
        self.writecode = writecode
        self.namespace = {
            'None': None,
            '__ref__': self.referenceForRemoteIdent,
        }

    def referenceForRemoteIdent(self, ident, type_string):
        rval = self.idents.get(ident)
        if rval is None:
            rval = RemoteObjectReference(self, ident, type_string)
        return rval


class ObjectPool(BaseObjectPool):
    def __init__(self):
        BaseObjectPool.__init__(self)
        self._identfactory = itertools.count()
        self.obj_ids = {}
        self.namespace = {
            '__obj__': self.objectForIdent,
        }

    def object_alloc(self, ref, obj_id):
        self.obj_ids[obj_id] = ref

    def object_dealloc(self, ref, obj_id):
        del self.obj_ids[obj_id]

    def objectForIdent(self, ident):
        return self.referenceForIdent(ident).obj

    def referenceForObject(self, obj):
        obj_id = id(obj)
        rval = self.obj_ids.get(obj_id)
        if rval is None:
            ident = self._identfactory.next()
            rval = ObjectReference(self, ident, type_string(obj), obj, obj_id)
            rval = rval.alloc().autorelease()
        return rval


class BaseObjectReference(object):
    def __init__(self, objectPool, ident, type_string):
        self.ident = ident
        self.type_string = type_string
        self.objectPool = objectPool
        self.retainCount = 1

    def retain(self, count=1):
        #print "%r.retain(%d)" % (self, count)
        self.retainCount += count
        return self

    def alloc(self):
        self.objectPool.base_alloc(self, self.ident)
        return self

    def dealloc(self):
        self.objectPool.base_dealloc(self, self.ident)
        self.retainCount = -1

    def release(self, count=1):
        #print "%r.release(%d)" % (self, count)
        newCount = self.retainCount - count
        #print "  newCount = %d" % (newCount,)
        if newCount == 0:
            self.dealloc()
        elif newCount < 0:
            raise ValueError("Reference %r over-released (%r -> %r)" % (self, self.retainCount, newCount))
        self.retainCount = newCount
        return self

    def autorelease(self):
        #print "%s.autorelease()" % (self,)
        self.objectPool.autorelease(self)
        return self

    def __repr__(self):
        return "%s(%r, %r)" % (type(self).__name__, self.ident, self.type_string)


class RemoteObjectReference(BaseObjectReference):
    def __netrepr__(self):
        return "__obj__(%r)" % (self.ident,)


class ObjectReference(BaseObjectReference):
    def __init__(self, objectPool, ident, type_string, obj, obj_id):
        BaseObjectReference.__init__(self, objectPool, ident, type_string)
        self.obj = obj
        self.obj_id = id(obj)

    def alloc(self):
        self = BaseObjectReference.alloc(self)
        self.objectPool.object_alloc(self, self.obj_id)
        return self

    def dealloc(self):
        self.objectPool.object_dealloc(self, self.obj_id)
        self.obj = None
        self.obj_id = -1
        BaseObjectReference.dealloc(self)

    def __netrepr__(self):
        return "__ref__(%r, %r)" % (self.ident, self.type_string)


def test_netrepr():
    import compiler
    pool = ObjectPool()
    pool.push()
    netrepr = NetRepr(pool).netrepr
    assert netrepr("foo") == repr("foo")
    ref = pool.referenceForObject(object)
    assert ref.obj is object
    assert ref is pool.referenceForObject(object)
    assert ref.retainCount == 1
    refrepr = netrepr(ref)
    assert refrepr == netrepr(ref)
    ref.retain()
    assert ref.retainCount == 2
    pool.pop()
    pool.push()
    assert ref.retainCount == 1
    def __ref__(ident, type_string):
        return pool.referenceForIdent(ident)
    netref = eval(refrepr)
    assert netref is ref
    assert netref.obj is object
    ref.release()
    pool.pop()
    assert ref.obj is None

remote_bootstrap.py

__file__ = "<RemotePyInterpreterClient>"
import sys
import os
pool = ObjectPool()
netReprCenter = NetRepr(pool)
netrepr = netReprCenter.netrepr
netrepr_tuple = netReprCenter.netrepr_tuple
netrepr_list = netReprCenter.netrepr_list
netrepr_exception = netReprCenter.netrepr_exception
namespace = globals()
namespace.update(pool.namespace)
__main__ = sys.modules['__main__']
assert namespace is not __main__.__dict__
pipe = RemotePipe(__runsocketcode__, __clientfile__, netReprCenter, namespace, pool)
interp = RemoteConsole(pipe, locals=__main__.__dict__)
interp.interact()

remote_console.py

import sys
import os
try:
    import __builtin__
except ImportError:
    import builtins as __builtin__
import traceback
import keyword
import time
from code import InteractiveConsole, softspace

class RemoteConsole(InteractiveConsole):
    def __init__(self, pipe, **kw):
        self.pipe = pipe
        self.buffer = None
        InteractiveConsole.__init__(self, **kw)
        self.locals['__interpreter__'] = self

    def raw_input(self, prompt=''):
        return self.pipe.expect('RemoteConsole.raw_input', prompt)

    def write(self, msg):
        return self.pipe.expect('RemoteConsole.write', msg)

    def resetbuffer(self):
        self.lastbuffer = self.buffer
        InteractiveConsole.resetbuffer(self)

    def displayhook(self, value):
        if value is not None:
            __builtin__._ = value
        return self.pipe.expect('RemoteConsole.displayhook', value)

    def excepthook(self, type, value, traceback):
        return self.pipe.expect('RemoteConsole.excepthook', type, value, traceback)

    def runcode(self, code):
        try:
            exec(code, self.locals)
        except SystemExit:
            raise
        except:
            self.showtraceback()
        else:
            if softspace(sys.stdout, 0):
                print

    def interact(self):
        old_raw_input = __builtin__.raw_input
        old_displayhook = sys.displayhook
        old_excepthook = sys.excepthook
        old_stdin = sys.stdin
        old_stdout = sys.stdout
        old_stderr = sys.stderr
        old_help = __builtin__.help
        old_quit = __builtin__.quit
        __builtin__.raw_input = self.raw_input
        __builtin__.help = "Close window to exit."
        __builtin__.quit = "Close window to exit."
        sys.displayhook = self.displayhook
        sys.excepthook = self.excepthook
        sys.stdin = self.pipe.stdin
        sys.stdout = self.pipe.stdout
        sys.stderr = self.pipe.stderr
        try:
            self.pipe.expect('RemoteConsole.initialize', repr(sys.version_info), sys.executable, os.getpid())
            InteractiveConsole.interact(self)
        finally:
            __builtin__.raw_input = old_raw_input
            __builtin__.help = old_help
            __builtin__.quit = old_quit
            sys.displayhook = old_displayhook
            sys.excepthook = old_excepthook
            sys.stdin = old_stdin
            sys.stdout = old_stdout
            sys.stderr = old_stderr

    def recommendCompletionsFor(self, word):
        parts = word.split('.')
        if len(parts) > 1:
            # has a . so it must be a module or class or something
            # using eval, which shouldn't normally have side effects
            # unless there's descriptors/metaclasses doing some nasty
            # get magic
            objname = '.'.join(parts[:-1])
            try:
                obj = eval(objname, self.locals)
            except:
                return None, 0
            wordlower = parts[-1].lower()
            if wordlower == '':
                # they just punched in a dot, so list all attributes
                # that don't look private or special
                prefix = '.'.join(parts[-2:])
                check = [
                    (prefix+_method)
                    for _method
                    in dir(obj)
                    if _method[:1] != '_' and _method.lower().startswith(wordlower)
                ]
            else:
                # they started typing the method name
                check = filter(lambda s:s.lower().startswith(wordlower), dir(obj))
        else:
            # no dots, must be in the normal namespaces.. no eval necessary
            check = set(dir(__builtin__))
            check.update(keyword.kwlist)
            check.update(self.locals)
            wordlower = parts[-1].lower()
            check = filter(lambda s:s.lower().startswith(wordlower), check)
        check.sort()
        return check, 0

remote_pipe.py

import itertools

def as_unicode(s, encoding='utf-8'):
    typ = type(s)
    if typ is unicode:
        pass
    elif issubclass(typ, unicode):
        s = unicode(s)
    elif issubclass(typ, str):
        s = unicode(s, encoding, 'replace')
    else:
        raise TypeError('expecting basestring, not %s' % (typ.__name__,))
    return s


def as_str(s, encoding='utf-8'):
    typ = type(s)
    if typ is str:
        pass
    elif issubclass(typ, str):
        s = str(s)
    elif issubclass(typ, unicode):
        s = s.encode(encoding)
    else:
        raise TypeError('expecting basestring, not %s' % (typ.__name__,))
    return s


class RemotePipe(object):
    def __init__(self, runcode, clientfile, netReprCenter, namespace, pool):
        self.runcode = runcode
        self.pool = pool
        self.clientfile = clientfile
        self.namespace = namespace
        self.result = self.namespace['__result__'] = {}
        self.netReprCenter = netReprCenter
        self.netrepr_list = netReprCenter.netrepr_list
        self.sequence = itertools.count()
        self.stdin = RemoteFileLike(self, 'stdin')
        self.stdout = RemoteFileLike(self, 'stdout')
        self.stderr = RemoteFileLike(self, 'stderr')

    def send(self, *args):
        self.clientfile.write(self.netrepr_list(args) + '\n')
        self.clientfile.flush()

    def respond(self, *args):
        self.send('respond', *args)

    def expect(self, *args):
        self.pool.push()
        try:
            return self._expect(*args)
        finally:
            self.pool.pop()

    def _expect(self, *args):
        ident = self.sequence.next()
        self.send('expect', ident, *args)
        while ident not in self.result:
            self.runcode(self.clientfile, self.namespace)
        return self.result.pop(ident)


class RemoteFileLike(object):
    softspace = 0
    closed = False
    encoding = 'utf-8'

    def __init__(self, pipe, ident):
        self.pipe = pipe
        self.ident = ident

    def __iter__(self):
        while True:
            rval = self.readline()
            if not rval:
                break
            yield rval

    def write(self, s):
        s = as_unicode(s, self.encoding)
        self.pipe.expect('RemoteFileLike.write', self.ident, s)

    def writelines(self, lines):
        for line in lines:
            self.write(line)

    def close(self):
        self.closed = True

    def flush(self):
        pass

    def isatty(self):
        return True

    def read(self, size=-1):
        return as_str(
            self.pipe.expect('RemoteFileLike.read', self.ident, size),
            self.encoding,
        )

    def readline(self, size=-1):
        return as_str(
            self.pipe.expect('RemoteFileLike.readline', self.ident, size),
            self.encoding,
        )

    def readlines(self):
        return list(self)

setup.py

"""
Script for building the example.

Usage:
    python3 setup.py py2app
"""
from setuptools import setup

plist = dict(
    CFBundleIdentifier="net.sf.pyobjc.RemotePyInterpreter",
    CFBundleDocumentTypes=[
        dict(
            CFBundleTypeExtensions=[
                "RemotePyInterpreter",
                "*",
            ],
            CFBundleTypeName="RemotePyInterpreter Session",
            CFBundleTypeRole="Editor",
            NSDocumentClass="RemotePyInterpreterDocument",
        ),
    ],
)

REMOTE_REQUIREMENTS = [
    "tcpinterpreter",
    "netrepr",
    "remote_console",
    "remote_pipe",
    "remote_bootstrap"
]

DATA_FILES = ["English.lproj"] + [(s + ".py") for s in REMOTE_REQUIREMENTS]

setup(
    app=["RemotePyInterpreter.py"],
    data_files=DATA_FILES,
    options=dict(py2app=dict(plist=plist)),
    setup_requires=[
        "py2app",
        "pyobjc-framework-Cocoa",
    ]
)

tcpinterpreter.py

#! /usr/bin/env python

"""
    start socket based minimal readline exec server
"""
import sys, socket, os

def runsocketcode(clientfile, g):
    try:
        source = clientfile.readline().rstrip()
    except Exception as e:
        raise SystemExit
    if not source:
        raise SystemExit
    source = eval(source)
    co = compile(source+'\n', '<remote-source>', 'exec')
    exec(co, g)

def serveonce(clientsock, name='stdin'):
    clientfile = clientsock.makefile('r+b', 0)
    g = {
        '__name__': '__socketclient__',
        '__file__': '<%s>' % (name,),
        '__clientsock__': clientsock,
        '__clientfile__': clientfile,
        '__runsocketcode__': runsocketcode,
    }
    try:
        runsocketcode(clientfile, g)
    finally:
        clientfile.close()
        clientsock.close()

def real_main():
    import sys
    hostport = eval(sys.argv[1])
    clientsock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    clientsock.connect(hostport)
    serveonce(clientsock)

def main():
    newglobals = {
        '__builtins__': sys.modules['__builtin__'],
        '__doc__': None,
        '__name__': '__main__',
    }
    sourcefile = __file__
    g = globals()
    g.clear()
    g.update(newglobals)
    serverglobals = {'__name__': '__socketclient__'}
    execfile(sourcefile, serverglobals, serverglobals)

if __name__ == '__main__':
    main()
elif __name__ == '__socketclient__':
    real_main()

test_client.py

from __future__ import print_function
try:
    import fcntl
except:
    fcntl = None
import os
import sys
from subprocess import Popen, PIPE
import socket
from netrepr import NetRepr, RemoteObjectPool, RemoteObjectReference

IMPORT_MODULES = ['netrepr', 'remote_console', 'remote_pipe', 'remote_bootstrap']
source = []
for fn in IMPORT_MODULES:
    for line in open(fn+'.py', 'rU'):
        source.append(line)
    source.append('\n\n')
SOURCE = repr(''.join(source)) + '\n'

def ensure_utf8(s):
    if isinstance(s, unicode):
        s = s.encode('utf-8')
    return s

def bind_and_listen(hostport):
    if isinstance(hostport, str):
        host, port = hostport.split(':')
        hostport = (host, int(port))
    serversock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    # set close-on-exec
    if hasattr(fcntl, 'FD_CLOEXEC'):
        old = fcntl.fcntl(serversock.fileno(), fcntl.F_GETFD)
        fcntl.fcntl(serversock.fileno(), fcntl.F_SETFD, old | fcntl.FD_CLOEXEC)
    # allow the address to be re-used in a reasonable amount of time
    if os.name == 'posix' and sys.platform != 'cygwin':
        serversock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)

    serversock.bind(hostport)
    serversock.listen(5)
    return serversock

def open_connection(executable=sys.executable):
    serversock = bind_and_listen(('127.0.0.1', 0))
    hostport = serversock.getsockname()
    proc = Popen([executable, 'tcpinterpreter.py', repr(hostport)], stdin=PIPE, stdout=PIPE, stderr=PIPE, close_fds=True)
    clientsock, address = serversock.accept()
    serversock.shutdown(2)
    return clientsock, proc

def start_client(clientsock):
    f = clientsock.makefile('r+b', 0)
    f.write(SOURCE)
    f.flush()
    return f

def client_loop(f):
    def writecode(code):
        #print('[code: %r]' % (code,))
        f.write(repr(code) + '\n')
    pool = RemoteObjectPool(writecode)
    netRepr = NetRepr(pool)
    netrepr = netRepr.netrepr
    def neteval(s):
        return eval(s, pool.namespace, pool.namespace)
    while True:
        code = f.readline().rstrip()
        pool.push()
        try:
            if not code:
                break
            command = eval(code)
            basic = eval(command[0])
            if basic == 'expect':
                seq = eval(command[1])
                name = eval(command[2])
                args = map(neteval, command[3:])
                code = None
                rval = None
                if name == 'RemoteConsole.raw_input':
                    try:
                        rval = raw_input(*args)
                    except EOFError:
                        code = 'raise EOFError'
                elif name == 'RemoteConsole.write':
                    sys.stdout.write(ensure_utf8(args[0]))
                elif name == 'RemoteConsole.displayhook':
                    pass
                    obj = args[0]
                    if obj is None:
                        pass
                    elif isinstance(obj, RemoteObjectReference):
                        writecode('interp.write(repr(%s) + "\\n")' % (netrepr(obj),))
                    else:
                        print(repr(obj))
                elif name.startswith('RemoteFileLike.'):
                    fh = getattr(sys, args[0])
                    meth = getattr(fh, name[len('RemoteFileLike.'):])
                    rval = meth(*map(ensure_utf8, args[1:]))
                else:
                    print(name, args)
                if code is None:
                    code = '__result__[%r] = %r' % (seq, rval)
                writecode(code)
        finally:
            pool.pop()

def main():
    clientsock, proc = open_connection()
    f = start_client(clientsock)
    try:
        client_loop(f)
    finally:
        f.close()
        clientsock.close()
        proc.stdin.close()
        print('[stdout]', proc.stdout.read())
        print('[stderr]', proc.stderr.read())

if __name__ == '__main__':
    main()