PyInterpreter.py
author Kevin Mitchell <kam@kamit.com>
Sun Jan 27 11:52:08 2008 -0600 (7 months ago)
changeset 5 b546054c3b17
parent 21100c92536d2
permissions -rw-r--r--
- Turn the InterpreterKeyController on only when evaluating
interactive output.

- Add encoding and full set of methods to PseudoUTF8Output and
PseudoUTF8Input, in a bid to eventually get iPython working.

- Make this more of a generic controller rather than the
NSApplicationDelegate, since we already have one of those.
        1 import sys
        2 import traceback
        3 import sets
        4 import keyword
        5 import time
        6 import errno
        7 import posix
        8 from code import InteractiveConsole, softspace
        9 from StringIO import StringIO
       10 from objc import YES, NO, selector
       11 from Foundation import *
       12 from AppKit import *
       13 from PyObjCTools import NibClassBuilder, AppHelper
       14 
       15 from EmbeddedInterpreterPlugIn import InterpreterKeyController
       16 
       17 NibClassBuilder.extractClasses("PyInterpreter.nib")
       18 
       19 try:
       20     sys.ps1
       21 except AttributeError:
       22     sys.ps1 = ">>> "
       23 try:
       24     sys.ps2
       25 except AttributeError:
       26     sys.ps2 = "... "
       27 
       28 class PseudoUTF8Output(object):
       29     softspace = 0
       30     encoding = 'utf8'
       31 
       32     def __init__(self, writemethod):
       33         self._write = writemethod
       34 
       35     def write(self, s):
       36         if not isinstance(s, unicode):
       37             s = s.decode('utf-8', 'replace')
       38         self._write(s)
       39 
       40     def writelines(self, lines):
       41         for line in lines:
       42             self.write(line)
       43 
       44     def flush(self):
       45         pass
       46 
       47     def isatty(self):
       48         return True
       49 
       50 class PseudoUTF8Input(object):
       51     softspace = 0
       52     encoding = 'utf8'
       53 
       54     def __init__(self, readlinemethod):
       55         self._buffer = u''
       56         self._readline = readlinemethod
       57 
       58     def write(self, s):
       59         raise IOError(errno.EBADF, posix.strerror(errno.EBADF))
       60 
       61     def read(self, chars=None):
       62         if chars is None:
       63             if self._buffer:
       64                 rval = self._buffer
       65                 self._buffer = u''
       66                 if rval.endswith(u'\r'):
       67                     rval = rval[:-1]+u'\n'
       68                 return rval.encode('utf-8')
       69             else:
       70                 return self._readline(u'\x04')[:-1].encode('utf-8')
       71         else:
       72             while len(self._buffer) < chars:
       73                 self._buffer += self._readline(u'\x04\r')
       74                 if self._buffer.endswith('\x04'):
       75                     self._buffer = self._buffer[:-1]
       76                     break
       77             rval, self._buffer = self._buffer[:chars], self._buffer[chars:]
       78             return rval.encode('utf-8').replace('\r','\n')
       79 
       80     def readline(self):
       81         if u'\r' not in self._buffer:
       82             self._buffer += self._readline(u'\x04\r')
       83         if self._buffer.endswith('\x04'):
       84             rval = self._buffer[:-1].encode('utf-8')
       85         elif self._buffer.endswith('\r'):
       86             rval = self._buffer[:-1].encode('utf-8')+'\n'
       87         self._buffer = u''
       88 
       89         return rval
       90 
       91     def flush(self):
       92         pass
       93 
       94 class AsyncInteractiveConsole(InteractiveConsole):
       95     lock = False
       96     buffer = None
       97 
       98     def __init__(self, *args, **kwargs):
       99         InteractiveConsole.__init__(self, *args, **kwargs)
      100         self.locals['__interpreter__'] = self
      101 
      102     def asyncinteract(self, write=None, banner=None):
      103         if self.lock:
      104             raise ValueError, "Can't nest"
      105         self.lock = True
      106         if write is None:
      107             write = self.write
      108         cprt = u'Type "help", "copyright", "credits" or "license" for more information.'
      109         if banner is None:
      110             write(u"Python %s in %s\n%s\n" % (
      111                 sys.version,
      112                 NSBundle.mainBundle().objectForInfoDictionaryKey_('CFBundleName'),
      113                 cprt,
      114             ))
      115         else:
      116             write(banner + '\n')
      117         more = 0
      118         _buff = []
      119         try:
      120             while True:
      121                 if more:
      122                     prompt = sys.ps2
      123                 else:
      124                     prompt = sys.ps1
      125                 write(prompt)
      126                 # yield the kind of prompt we have
      127                 yield more
      128                 # next input function
      129                 yield _buff.append
      130                 more = self.push(_buff.pop())
      131         except:
      132             self.lock = False
      133             raise
      134         self.lock = False
      135 
      136     def resetbuffer(self):
      137         self.lastbuffer = self.buffer
      138         InteractiveConsole.resetbuffer(self)
      139 
      140     def runcode(self, code):
      141         try:
      142             InterpreterKeyController.setEnabled_(True);
      143             exec code in self.locals
      144         except SystemExit:
      145             InterpreterKeyController.setEnabled_(False);
      146             raise
      147         except:
      148             InterpreterKeyController.setEnabled_(False);
      149             self.showtraceback()
      150         else:
      151             InterpreterKeyController.setEnabled_(False);
      152             if softspace(sys.stdout, 0):
      153                 print
      154 
      155 
      156     def recommendCompletionsFor(self, word):
      157         parts = word.split('.')
      158         if len(parts) > 1:
      159             # has a . so it must be a module or class or something
      160             # using eval, which shouldn't normally have side effects
      161             # unless there's descriptors/metaclasses doing some nasty
      162             # get magic
      163             objname = '.'.join(parts[:-1])
      164             try:
      165                 obj = eval(objname, self.locals)
      166             except:
      167                 return None, 0
      168             wordlower = parts[-1].lower()
      169             if wordlower == '':
      170                 # they just punched in a dot, so list all attributes
      171                 # that don't look private or special
      172                 prefix = '.'.join(parts[-2:])
      173                 check = [
      174                     (prefix+_method)
      175                     for _method
      176                     in dir(obj)
      177                     if _method[:1] != '_' and _method.lower().startswith(wordlower)
      178                 ]
      179             else:
      180                 # they started typing the method name
      181                 check = filter(lambda s:s.lower().startswith(wordlower), dir(obj))
      182         else:
      183             # no dots, must be in the normal namespaces.. no eval necessary
      184             check = sets.Set(dir(__builtins__))
      185             check.update(keyword.kwlist)
      186             check.update(self.locals)
      187             wordlower = parts[-1].lower()
      188             check = filter(lambda s:s.lower().startswith(wordlower), check)
      189         check.sort()
      190         return check, 0
      191 
      192 DEBUG_DELEGATE = 0
      193 PASSTHROUGH = (
      194    'deleteBackward:',
      195    'complete:',
      196    'moveRight:',
      197    'moveLeft:',
      198 )
      199 
      200 class PyInterpreter(NibClassBuilder.AutoBaseClass):
      201     """
      202     PyInterpreter is a delegate/controller for a NSTextView,
      203     turning it into a full featured interactive Python interpreter.
      204     """
      205 
      206     #
      207     #  Outlets - for documentation only
      208     #
      209 
      210     _NIBOutlets_ = (
      211         (NSTextView,    'textView',         'The interpreter'),
      212     )
      213 
      214     #
      215     #  NIB loading protocol
      216     #
      217 
      218     def awakeFromNib(self):
      219         self = super(PyInterpreter, self).init()
      220         self._font = NSFont.userFixedPitchFontOfSize_(10)
      221         self._stderrColor = NSColor.redColor()
      222         self._stdoutColor = NSColor.blueColor()
      223         self._codeColor = NSColor.blackColor()
      224         self._historyLength = 50
      225         self._history = [u'']
      226         self._historyView = 0
      227         self._characterIndexForInput = 0
      228         self._stdin = PseudoUTF8Input(self._nestedRunLoopReaderUntilEOLchars_)
      229         #self._stdin = PseudoUTF8Input(self.readStdin)
      230         self._stderr = PseudoUTF8Output(self.writeStderr_)
      231         self._stdout = PseudoUTF8Output(self.writeStdout_)
      232         self._isInteracting = False
      233         self._console = AsyncInteractiveConsole()
      234         self._interp = self._console.asyncinteract(
      235             write=self.writeCode_,
      236         ).next
      237         self._autoscroll = True
      238         self.textView.setFont_(self.font())
      239         self.textView.setContinuousSpellCheckingEnabled_(False)
      240         self.textView.setRichText_(False)
      241         self._executeWithRedirectedIO(self._interp)
      242 
      243     #
      244     #  Modal input dialog support
      245     #
      246 
      247     def _nestedRunLoopReaderUntilEOLchars_(self, eolchars):
      248         """
      249         This makes the baby jesus cry.
      250 
      251         I want co-routines.
      252         """
      253         app = NSApplication.sharedApplication()
      254         window = self.textView.window()
      255         self.setCharacterIndexForInput_(self.lengthOfTextView())
      256         # change the color.. eh
      257         self.textView.setTypingAttributes_({
      258             NSFontAttributeName:self.font(),
      259             NSForegroundColorAttributeName:self.codeColor(),
      260         })
      261         while True:
      262             event = app.nextEventMatchingMask_untilDate_inMode_dequeue_(
      263                 NSAnyEventMask,
      264                 NSDate.distantFuture(),
      265                 NSDefaultRunLoopMode,
      266                 True)
      267             if (event.type() == NSKeyDown) and (event.window() == window):
      268                 eol = event.characters()
      269                 if eol in eolchars:
      270                     break
      271             app.sendEvent_(event)
      272         cl = self.currentLine()
      273         if eol == '\r':
      274             self.writeCode_('\n')
      275         return cl+eol
      276 
      277     #
      278     #  Interpreter functions
      279     #
      280 
      281     def _executeWithRedirectedIO(self, fn, *args, **kwargs):
      282         old = sys.stdin, sys.stdout, sys.stderr
      283         if self._stdin is not None:
      284             sys.stdin = self._stdin
      285         sys.stdout, sys.stderr = self._stdout, self._stderr
      286         try:
      287             rval = fn(*args, **kwargs)
      288         finally:
      289             sys.stdin, sys.stdout, sys.stderr = old
      290             self.setCharacterIndexForInput_(self.lengthOfTextView())
      291         return rval
      292 
      293     def executeLine_(self, line):
      294         self.addHistoryLine_(line)
      295         self._executeWithRedirectedIO(self._executeLine_, line)
      296         self._history = filter(None, self._history)
      297         self._history.append(u'')
      298         self._historyView = len(self._history) - 1
      299 
      300     def _executeLine_(self, line):
      301         self._interp()(line)
      302         self._more = self._interp()
      303 
      304     def executeInteractiveLine_(self, line):
      305         self.setIsInteracting(True)
      306         try:
      307             self.executeLine_(line)
      308         finally:
      309             self.setIsInteracting(False)
      310 
      311     def replaceLineWithCode_(self, s):
      312         idx = self.characterIndexForInput()
      313         ts = self.textView.textStorage()
      314         ts.replaceCharactersInRange_withAttributedString_(
      315             (idx, len(ts.mutableString())-idx), self.codeString_(s))
      316 
      317     #
      318     #  History functions
      319     #
      320 
      321     def historyLength(self):
      322         return self._historyLength
      323 
      324     def setHistoryLength_(self, length):
      325         self._historyLength = length
      326 
      327     def addHistoryLine_(self, line):
      328         line = line.rstrip('\n')
      329         if self._history[-1] == line:
      330             return False
      331         if not line:
      332             return False
      333         self._history.append(line)
      334         if len(self._history) > self.historyLength():
      335             self._history.pop(0)
      336         return True
      337 
      338     def historyDown_(self, sender):
      339         if self._historyView == (len(self._history) - 1):
      340             return
      341         self._history[self._historyView] = self.currentLine()
      342         self._historyView += 1
      343         self.replaceLineWithCode_(self._history[self._historyView])
      344         self.moveToEndOfLine_(self)
      345 
      346     def historyUp_(self, sender):
      347         if self._historyView == 0:
      348             return
      349         self._history[self._historyView] = self.currentLine()
      350         self._historyView -= 1
      351         self.replaceLineWithCode_(self._history[self._historyView])
      352         self.moveToEndOfLine_(self)
      353 
      354     #
      355     #  Convenience methods to create/write decorated text
      356     #
      357 
      358     def _formatString_forOutput_(self, s, name):
      359         return NSAttributedString.alloc().initWithString_attributes_(
      360             s,
      361             {
      362                 NSFontAttributeName:self.font(),
      363                 NSForegroundColorAttributeName:getattr(self, name+'Color')(),
      364             },
      365         )
      366 
      367     def _writeString_forOutput_(self, s, name):
      368         self.textView.textStorage().appendAttributedString_(getattr(self, name+'String_')(s))
      369 
      370         window = self.textView.window()
      371         app = NSApplication.sharedApplication()
      372         st = time.time()
      373         now = time.time
      374 
      375         if self._autoscroll:
      376             self.textView.scrollRangeToVisible_((self.lengthOfTextView(), 0))
      377 
      378         while app.isRunning() and now() - st < 0.01:
      379             event = app.nextEventMatchingMask_untilDate_inMode_dequeue_(
      380                 NSAnyEventMask,
      381                 NSDate.dateWithTimeIntervalSinceNow_(0.01),
      382                 NSDefaultRunLoopMode,
      383                 True)
      384 
      385             if event is None:
      386                 continue
      387 
      388             if (event.type() == NSKeyDown) and (event.window() == window):
      389                 chr = event.charactersIgnoringModifiers()
      390                 if chr == 'c' and (event.modifierFlags() & NSControlKeyMask):
      391                     raise KeyboardInterrupt
      392 
      393             app.sendEvent_(event)
      394 
      395 
      396     codeString_   = lambda self, s: self._formatString_forOutput_(s, 'code')
      397     stderrString_ = lambda self, s: self._formatString_forOutput_(s, 'stderr')
      398     stdoutString_ = lambda self, s: self._formatString_forOutput_(s, 'stdout')
      399     writeCode_    = lambda self, s: self._writeString_forOutput_(s, 'code')
      400     writeStderr_  = lambda self, s: self._writeString_forOutput_(s, 'stderr')
      401     writeStdout_  = lambda self, s: self._writeString_forOutput_(s, 'stdout')
      402 
      403     #
      404     #  Accessors
      405     #
      406 
      407     def more(self):
      408         return self._more
      409 
      410     def font(self):
      411         return self._font
      412 
      413     def setFont_(self, font):
      414         self._font = font
      415 
      416     def stderrColor(self):
      417         return self._stderrColor
      418 
      419     def setStderrColor_(self, color):
      420         self._stderrColor = color
      421 
      422     def stdoutColor(self):
      423         return self._stdoutColor
      424 
      425     def setStdoutColor_(self, color):
      426         self._stdoutColor = color
      427 
      428     def codeColor(self):
      429         return self._codeColor
      430 
      431     def setStdoutColor_(self, color):
      432         self._codeColor = color
      433 
      434     def isInteracting(self):
      435         return self._isInteracting
      436 
      437     def setIsInteracting(self, v):
      438         self._isInteracting = v
      439 
      440     def isAutoScroll(self):
      441         return self._autoScroll
      442 
      443     def setAutoScroll(self, v):
      444         self._autoScroll = v
      445 
      446 
      447     #
      448     #  Convenience methods for manipulating the NSTextView
      449     #
      450 
      451     def currentLine(self):
      452         return self.textView.textStorage().mutableString()[self.characterIndexForInput():]
      453 
      454     def moveAndScrollToIndex_(self, idx):
      455         self.textView.scrollRangeToVisible_((idx, 0))
      456         self.textView.setSelectedRange_((idx, 0))
      457 
      458     def characterIndexForInput(self):
      459         return self._characterIndexForInput
      460 
      461     def lengthOfTextView(self):
      462         return len(self.textView.textStorage().mutableString())
      463 
      464     def setCharacterIndexForInput_(self, idx):
      465         self._characterIndexForInput = idx
      466         self.moveAndScrollToIndex_(idx)
      467 
      468     #
      469     #  NSTextViewDelegate methods
      470     #
      471 
      472     def textView_completions_forPartialWordRange_indexOfSelectedItem_(self, aTextView, completions, (begin, length), index):
      473         txt = self.textView.textStorage().mutableString()
      474         end = begin+length
      475         while (begin>0) and (txt[begin].isalnum() or txt[begin] in '._'):
      476             begin -= 1
      477         while begin < end and not txt[begin].isalnum():
      478             begin += 1
      479         return self._console.recommendCompletionsFor(txt[begin:end])
      480 
      481     def textView_shouldChangeTextInRange_replacementString_(self, aTextView, aRange, newString):
      482         begin, length = aRange
      483         lastLocation = self.characterIndexForInput()
      484         if begin < lastLocation:
      485             # no editing anywhere but the interactive line
      486             return NO
      487         newString = newString.replace('\r', '\n')
      488         if '\n' in newString:
      489             if begin != lastLocation:
      490                 # no pasting multiline unless you're at the end
      491                 # of the interactive line
      492                 return NO
      493             # multiline paste support
      494             #self.clearLine()
      495             newString = self.currentLine() + newString
      496             for s in newString.strip().split('\n'):
      497                 self.writeCode_(s+'\n')
      498                 self.executeLine_(s)
      499             return NO
      500         return YES
      501 
      502     def textView_willChangeSelectionFromCharacterRange_toCharacterRange_(self, aTextView, fromRange, toRange):
      503         return toRange
      504         begin, length = toRange
      505         if length == 0 and begin < self.characterIndexForInput():
      506             # no cursor movement off the interactive line
      507             return fromRange
      508         return toRange
      509 
      510     def textView_doCommandBySelector_(self, aTextView, aSelector):
      511         # deleteForward: is ctrl-d
      512         if self.isInteracting():
      513             if aSelector == 'insertNewline:':
      514                 self.writeCode_('\n')
      515             return NO
      516         responder = getattr(self, aSelector.replace(':','_'), None)
      517         if responder is not None:
      518             responder(aTextView)
      519             return YES
      520         else:
      521             if DEBUG_DELEGATE and aSelector not in PASSTHROUGH:
      522                 print aSelector
      523             return NO
      524 
      525     #
      526     #  doCommandBySelector "posers" on the textView
      527     #
      528 
      529     def insertTabIgnoringFieldEditor_(self, sender):
      530         # this isn't terribly necessary, b/c F5 and opt-esc do completion
      531         # but why not
      532         sender.complete_(self)
      533 
      534     def insertTab_(self, sender):
      535         # this isn't terribly necessary, b/c F5 and opt-esc do completion
      536         # but why not
      537         sender.complete_(self)
      538 
      539     def moveToBeginningOfLine_(self, sender):
      540         self.moveAndScrollToIndex_(self.characterIndexForInput())
      541 
      542     def moveToEndOfLine_(self, sender):
      543         self.moveAndScrollToIndex_(self.lengthOfTextView())
      544 
      545     def moveToBeginningOfLineAndModifySelection_(self, sender):
      546         begin, length = self.textView.selectedRange()
      547         pos = self.characterIndexForInput()
      548         if begin+length > pos:
      549             self.textView.setSelectedRange_((pos, begin+length-pos))
      550         else:
      551             self.moveToBeginningOfLine_(sender)
      552 
      553     def moveToEndOfLineAndModifySelection_(self, sender):
      554         begin, length = self.textView.selectedRange()
      555         pos = max(self.characterIndexForInput(), begin)
      556         self.textView.setSelectedRange_((pos, self.lengthOfTextView()))
      557 
      558     def insertNewline_(self, sender):
      559         line = self.currentLine()
      560         self.writeCode_('\n')
      561         self.executeInteractiveLine_(line)
      562 
      563     moveToBeginningOfParagraph_ = moveToBeginningOfLine_
      564     moveToEndOfParagraph_ = moveToEndOfLine_
      565     insertNewlineIgnoringFieldEditor_ = insertNewline_
      566     moveDown_ = historyDown_
      567     moveUp_ = historyUp_
      568 
      569 
      570 if __name__ == '__main__':
      571     AppHelper.runEventLoop()