WebKitInterpreterPlugin¶
Uses the new WebKit Cocoa plugin API available in Safari 1.3 and later to embed a PyInterpreter in the browser.
This example requires Python 2.7 because the code has not been ported to Py3K yet.
Sources¶
WebKitInterpreter.py¶
import keyword
import sys
import time
import traceback
from code import InteractiveConsole
import objc
from Cocoa import (
NSZeroRect,
NSControlKeyMask,
NSViewWidthSizable,
NSAnyEventMask,
NSApplication,
NSAttributedString,
NSBundle,
NSColor,
NSDate,
NSDefaultRunLoopMode,
NSFont,
NSFontAttributeName,
NSForegroundColorAttributeName,
NSKeyDown,
NSLog,
NSObject,
NSView,
NSScrollView,
NSViewHeightSizable,
NSTextView,
)
from objc import NO, YES, super
try:
from code import softspace
except ImportError:
softspace = None
FLT_MAX = 3.402_823_47e38
try:
unicode
except NameError:
unicode = str
try:
sys.ps1
except AttributeError:
sys.ps1 = ">>> "
try:
sys.ps2
except AttributeError:
sys.ps2 = "... "
class PseudoUTF8Output(object):
softspace = 0
def __init__(self, writemethod):
self._write = writemethod
def write(self, s):
if not isinstance(s, unicode):
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(object):
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 softspace is not None and softspace(sys.stdout, 0):
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 = 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 = 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 = objc.ivar("textView")
def initWithTextView_(self, textView):
self = super(PyInterpreter, self).init()
self.textView = textView
self.textView.setDelegate_(self)
self.awakeFromNib()
return self
def interpreterLocals(self):
return self._console.locals
#
# NSApplicationDelegate methods
#
def applicationDidFinishLaunching_(self, aNotification):
self.textView.setFont_(self.font())
self.textView.setContinuousSpellCheckingEnabled_(False)
self.textView.setRichText_(False)
self._executeWithRedirectedIO(self._interp)
#
# NIB loading protocol
#
def awakeFromNib(self):
self = super(PyInterpreter, self).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 = self._console.asyncinteract(write=self.writeCode_).next
self._autoscroll = True
self.applicationDidFinishLaunching_(None)
#
# 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_(
NSAnyEventMask, 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
#
@objc.python_method
def _executeWithRedirectedIO(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(self._executeLine_, line)
self._history = 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_(
NSAnyEventMask,
NSDate.dateWithTimeIntervalSinceNow_(0.01),
NSDefaultRunLoopMode,
True,
)
if event is None:
continue
if (event.type() == NSKeyDown) and (event.window() == window):
chars = event.charactersIgnoringModifiers()
if chars == "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, begin_length, index
):
begin, length = begin_length
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_
class WebKitInterpreter(NSView):
arguments = objc.ivar("arguments")
pyInterpreter = objc.ivar("pyInterpreter")
scrollView = objc.ivar("scrollView")
textView = objc.ivar("textView")
def container(self):
return self.arguments.get("WebPluginContainer")
def pluginViewWithArguments_(cls, arguments):
self = super(WebKitInterpreter, cls).alloc().initWithFrame_(NSZeroRect)
NSLog("pluginViewWithArguments:")
NSLog(arguments)
self.arguments = arguments
return self
pluginViewWithArguments_ = classmethod(pluginViewWithArguments_)
def pluginStart(self):
NSLog("pluginStart")
try:
self.doPluginStart()
except: # noqa: E722, B001
traceback.print_exc()
def doPluginStart(self):
dct = self.arguments["WebPluginAttributes"]
w, h = [float(dct.get(k, 0)) for k in ("width", "height")]
self.setFrame_(((0.0, 0.0), (w, h)))
scrollView = NSScrollView.alloc().initWithFrame_(self.frame())
scrollView.setHasVerticalScroller_(True)
scrollView.setHasHorizontalScroller_(False)
scrollView.setAutoresizingMask_(NSViewWidthSizable | NSViewHeightSizable)
contentSize = scrollView.contentSize()
textView = NSTextView.alloc().initWithFrame_(((0, 0), scrollView.contentSize()))
textView.setMinSize_((0, contentSize.height))
textView.setMaxSize_((FLT_MAX, FLT_MAX))
textView.setVerticallyResizable_(True)
textView.setHorizontallyResizable_(False)
textView.setAutoresizingMask_(NSViewWidthSizable)
textView.textContainer().setContainerSize_((contentSize.width, FLT_MAX))
textView.textContainer().setWidthTracksTextView_(True)
scrollView.setDocumentView_(textView)
self.addSubview_(scrollView)
self.pyInterpreter = PyInterpreter.alloc().initWithTextView_(textView)
self.pyInterpreter.interpreterLocals()["container"] = self.container()
def objectForWebScript(self):
return self
NSLog("loaded WebKitInterpreter")
objc.removeAutoreleasePool()
setup.py¶
"""
Script for building the example.
Usage:
python2 setup.py py2app
"""
from setuptools import setup
MIME = "application/x-pyobjc-demo-webkitinterpreter"
plist = {
"NSPrincipalClass": "WebKitInterpreter",
"WebPluginName": "WebKit PyInterpreter Plug-In",
"WebPluginDescription": "PyObjC demo that embeds a Python interpreter",
"CFBundlePackageType": "WBPL",
"WebPluginMIMETypes": {
"MIME": {
"WebPluginExtensions": ["webkitinterpreter"],
"WebPluginTypeDescription": "WebKit PyInterpreter",
}
},
}
setup(
name="WebKitInterpreter",
plugin=["WebKitInterpreter.py"],
options={"py2app": {"plist": plist}},
setup_requires=["py2app", "pyobjc-framework-Cocoa", "pyobjc-framework-WebKit"],
)