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