PyInterpreter.py
author Kevin Mitchell <kam@kamit.com>
Thu Feb 07 01:15:46 2008 -0600 (17 months ago)
changeset 18 174a615be869
parent 21100c92536d2
permissions -rw-r--r--
Add copyright notices, license, and readme.
     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()