PyInterpreter¶
A PyObjC Example without documentation
Sources¶
PyInterpreter.py¶
import keyword
import sys
import time
from code import InteractiveConsole
from functools import partial
from Cocoa import (
NSApplication,
NSAttributedString,
NSBundle,
NSColor,
NSControlKeyMask,
NSDate,
NSDefaultRunLoopMode,
NSFont,
NSFontAttributeName,
NSForegroundColorAttributeName,
NSKeyDown,
NSObject,
NSTextView,
NSUIntegerMax,
)
from objc import NO, YES, IBOutlet, super # noqa: A004
from PyObjCTools import AppHelper
import objc
try:
sys.ps1
except AttributeError:
sys.ps1 = ">>> "
try:
sys.ps2
except AttributeError:
sys.ps2 = "... "
class PseudoUTF8Output:
softspace = 0
def __init__(self, writemethod):
self._write = writemethod
def write(self, s):
if not isinstance(s, str):
s = s.decode("utf-8", "replace")
self._write(s)
def writelines(self, lines):
for line in lines:
self.write(line)
def flush(self):
pass
def isatty(self):
return True
class PseudoUTF8Input:
softspace = 0
def __init__(self, readlinemethod):
self._buffer = ""
self._readline = readlinemethod
def read(self, chars=None):
if chars is None:
if self._buffer:
rval = self._buffer
self._buffer = ""
if rval.endswith("\r"):
rval = rval[:-1] + "\n"
return rval.encode("utf-8")
else:
return self._readline("\x04")[:-1].encode("utf-8")
else:
while len(self._buffer) < chars:
self._buffer += self._readline("\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 "\r" not in self._buffer:
self._buffer += self._readline("\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 = ""
return rval
class AsyncInteractiveConsole(InteractiveConsole):
lock = False
buffer = None
def __init__(self, *args, **kwargs):
InteractiveConsole.__init__(self, *args, **kwargs)
self.locals["__interpreter__"] = self
def asyncinteract(self, write=None, banner=None):
if self.lock:
raise ValueError("Can't nest")
self.lock = True
if write is None:
write = self.write
cprt = 'Type "help", "copyright", "credits" or "license" for more information.'
if banner is None:
write(
"Python %s in %s\n%s\n"
% (
sys.version,
NSBundle.mainBundle().objectForInfoDictionaryKey_("CFBundleName"),
cprt,
)
)
else:
write(banner + "\n")
more = 0
_buff = []
try:
while True:
if more:
prompt = sys.ps2
else:
prompt = sys.ps1
write(prompt)
# yield the kind of prompt we have
yield more
# next input function
yield _buff.append
more = self.push(_buff.pop())
except: # noqa: E722, B001
self.lock = False
raise
self.lock = False
def resetbuffer(self):
self.lastbuffer = self.buffer
InteractiveConsole.resetbuffer(self)
def runcode(self, code):
try:
exec(code, self.locals)
except SystemExit:
raise
except: # noqa: E722, B001
self.showtraceback()
else:
if sys.stdout.softspace:
print()
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: # noqa: E722, B001
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 = list(
filter(lambda s: s.lower().startswith(wordlower), dir(obj))
)
else:
# no dots, must be in the normal namespaces.. no eval necessary
check = set(dir(__builtins__))
check.update(keyword.kwlist)
check.update(self.locals)
wordlower = parts[-1].lower()
check = list(filter(lambda s: s.lower().startswith(wordlower), check))
check.sort()
return check, 0
DEBUG_DELEGATE = 0
PASSTHROUGH = ("deleteBackward:", "complete:", "moveRight:", "moveLeft:")
class PyInterpreter(NSObject):
"""
PyInterpreter is a delegate/controller for a NSTextView,
turning it into a full featured interactive Python interpreter.
"""
textView = IBOutlet()
#
# Outlets - for documentation only
#
_NIBOutlets_ = ((NSTextView, "textView", "The interpreter"),)
#
# NSApplicationDelegate methods
#
def applicationDidFinishLaunching_(self, aNotification):
self.textView.setFont_(self.font())
self.textView.setContinuousSpellCheckingEnabled_(False)
self.textView.setRichText_(False)
self._executeWithRedirectedIO_args_kwds_(self._interp, (), {})
#
# NIB loading protocol
#
def awakeFromNib(self):
self = super().init()
self._font = NSFont.userFixedPitchFontOfSize_(10)
self._stderrColor = NSColor.redColor()
self._stdoutColor = NSColor.blueColor()
self._codeColor = NSColor.blackColor()
self._historyLength = 50
self._history = [""]
self._historyView = 0
self._characterIndexForInput = 0
self._stdin = PseudoUTF8Input(self._nestedRunLoopReaderUntilEOLchars_)
# self._stdin = PseudoUTF8Input(self.readStdin)
self._stderr = PseudoUTF8Output(self.writeStderr_)
self._stdout = PseudoUTF8Output(self.writeStdout_)
self._isInteracting = False
self._console = AsyncInteractiveConsole()
self._interp = partial(next, self._console.asyncinteract(write=self.writeCode_))
self._autoscroll = True
#
# Modal input dialog support
#
def _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.codeColor(),
}
)
while True:
event = app.nextEventMatchingMask_untilDate_inMode_dequeue_(
NSUIntegerMax, NSDate.distantFuture(), NSDefaultRunLoopMode, True
)
if (event.type() == NSKeyDown) and (event.window() == window):
eol = event.characters()
if eol in eolchars:
break
app.sendEvent_(event)
cl = self.currentLine()
if eol == "\r":
self.writeCode_("\n")
return cl + eol
#
# Interpreter functions
#
def _executeWithRedirectedIO_args_kwds_(self, fn, args, kwargs):
old = sys.stdin, sys.stdout, sys.stderr
if self._stdin is not None:
sys.stdin = self._stdin
sys.stdout, sys.stderr = self._stdout, self._stderr
try:
rval = fn(*args, **kwargs)
finally:
sys.stdin, sys.stdout, sys.stderr = old
self.setCharacterIndexForInput_(self.lengthOfTextView())
return rval
def executeLine_(self, line):
self.addHistoryLine_(line)
self._executeWithRedirectedIO_args_kwds_(self._executeLine_, (line,), {})
self._history = list(filter(None, self._history))
self._history.append("")
self._historyView = len(self._history) - 1
def _executeLine_(self, line):
self._interp()(line)
self._more = self._interp()
def executeInteractiveLine_(self, line):
self.setIsInteracting_(True)
try:
self.executeLine_(line)
finally:
self.setIsInteracting_(False)
def replaceLineWithCode_(self, s):
idx = self.characterIndexForInput()
ts = self.textView.textStorage()
ts.replaceCharactersInRange_withAttributedString_(
(idx, len(ts.mutableString()) - idx), self.codeString_(s)
)
#
# History functions
#
def historyLength(self):
return self._historyLength
def setHistoryLength_(self, length):
self._historyLength = length
def addHistoryLine_(self, line):
line = line.rstrip("\n")
if self._history[-1] == line:
return False
if not line:
return False
self._history.append(line)
if len(self._history) > self.historyLength():
self._history.pop(0)
return True
def historyDown_(self, sender):
if self._historyView == (len(self._history) - 1):
return
self._history[self._historyView] = self.currentLine()
self._historyView += 1
self.replaceLineWithCode_(self._history[self._historyView])
self.moveToEndOfLine_(self)
def historyUp_(self, sender):
if self._historyView == 0:
return
self._history[self._historyView] = self.currentLine()
self._historyView -= 1
self.replaceLineWithCode_(self._history[self._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: getattr(self, name + "Color")(),
},
)
def _writeString_forOutput_(self, s, name):
self.textView.textStorage().appendAttributedString_(
getattr(self, name + "String_")(s)
)
window = self.textView.window()
app = NSApplication.sharedApplication()
st = time.time()
now = time.time
if self._autoscroll:
self.textView.scrollRangeToVisible_((self.lengthOfTextView(), 0))
while app.isRunning() and now() - st < 0.01:
event = app.nextEventMatchingMask_untilDate_inMode_dequeue_(
NSUIntegerMax,
NSDate.dateWithTimeIntervalSinceNow_(0.01),
NSDefaultRunLoopMode,
True,
)
if event is None:
continue
if (event.type() == NSKeyDown) and (event.window() == window):
char = event.charactersIgnoringModifiers()
if char == "c" and (event.modifierFlags() & NSControlKeyMask):
raise KeyboardInterrupt
app.sendEvent_(event)
def codeString_(self, s):
return self._formatString_forOutput_(s, "code")
def stderrString_(self, s):
return self._formatString_forOutput_(s, "stderr")
def stdoutString_(self, s):
return self._formatString_forOutput_(s, "stdout")
def writeCode_(self, s):
return self._writeString_forOutput_(s, "code")
def writeStderr_(self, s):
return self._writeString_forOutput_(s, "stderr")
def writeStdout_(self, s):
return self._writeString_forOutput_(s, "stdout")
#
# Accessors
#
def more(self):
return self._more
def font(self):
return self._font
def setFont_(self, font):
self._font = font
def stderrColor(self):
return self._stderrColor
def setStderrColor_(self, color):
self._stderrColor = color
def stdoutColor(self):
return self._stdoutColor
def setStdoutColor_(self, color):
self._stdoutColor = color
def codeColor(self):
return self._codeColor
def setCodeColor_(self, color):
self._codeColor = color
def isInteracting(self):
return self._isInteracting
def setIsInteracting_(self, v):
self._isInteracting = v
def isAutoScroll(self):
return self._autoScroll
def setAutoScroll_(self, v):
self._autoScroll = v
#
# 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 characterIndexForInput(self):
return self._characterIndexForInput
def lengthOfTextView(self):
return len(self.textView.textStorage().mutableString())
def setCharacterIndexForInput_(self, idx):
self._characterIndexForInput = idx
self.moveAndScrollToIndex_(idx)
#
# NSTextViewDelegate methods
#
def textView_completions_forPartialWordRange_indexOfSelectedItem_(
self, aTextView, completions, word_range, index
):
(begin, length) = word_range
txt = self.textView.textStorage().mutableString()
end = begin + length
while (begin > 0) and (txt[begin].isalnum() or txt[begin] in "._"):
begin -= 1
while not txt[begin].isalnum():
begin += 1
return self._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 NO
newString = newString.replace("\r", "\n")
if "\n" in newString:
if begin != lastLocation:
# no pasting multiline unless you're at the end
# of the interactive line
return NO
# multiline paste support
# self.clearLine()
newString = self.currentLine() + newString
for s in newString.strip().split("\n"):
self.writeCode_(s + "\n")
self.executeLine_(s)
return NO
return YES
def textView_willChangeSelectionFromCharacterRange_toCharacterRange_(
self, aTextView, fromRange, toRange
):
return toRange
begin, length = toRange
if length == 0 and begin < self.characterIndexForInput():
# no cursor movement off the interactive line
return fromRange
return toRange
def textView_doCommandBySelector_(self, aTextView, aSelector):
# deleteForward: is ctrl-d
if self.isInteracting():
if aSelector == "insertNewline:":
self.writeCode_("\n")
return NO
responder = getattr(self, aSelector.replace(":", "_"), None)
if responder is not None:
responder(aTextView)
return YES
else:
if DEBUG_DELEGATE and aSelector not in PASSTHROUGH:
print(aSelector)
return NO
#
# 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.writeCode_("\n")
self.executeInteractiveLine_(line)
moveToBeginningOfParagraph_ = moveToBeginningOfLine_
moveToEndOfParagraph_ = moveToEndOfLine_
insertNewlineIgnoringFieldEditor_ = insertNewline_
moveDown_ = historyDown_
moveUp_ = historyUp_
if __name__ == "__main__":
objc.setVerbose(1)
AppHelper.runEventLoop()
setup.py¶
"""
Script for building the example.
Usage:
python3 setup.py py2app
"""
from setuptools import setup
plist = {"NSMainNibFile": "PyInterpreter"}
setup(
name="PyInterpreter",
app=["PyInterpreter.py"],
data_files=["PyInterpreter.nib"],
options={"py2app": {"plist": plist}},
setup_requires=["py2app", "pyobjc-framework-Cocoa"],
)