Commits

Bob Ippolito  committed 2bdab51

New ``WebKitInterpreter`` example in ``Examples/Plugins``,
Uses the new WebKit Cocoa plugin API available in Safari 1.3
and later to embed a PyInterpreter in the browser.

  • Participants
  • Parent commits 44c4555
  • Branches pyobjc-ancient

Comments (0)

Files changed (7)

File Examples/00ReadMe.html

 <li><a href="Plugins/SillyBallsSaver">SillyBallsSaver</a><p>A simple screensaver written in Python.</p>
 </li>
 </ul>
+<ul>
+<li><a href="Plugins/WebKitInterpreter">WebKitInterpreter</a><p>Uses the new WebKit Cocoa plugin API available in Safari 1.3
+and later to embed a PyInterpreter in the browser.</p>
+</li>
+</ul>
 <h2><a name="twisted-integration">Twisted Integration</a></h2>
 <p>The directory <a href="Twisted">Twisted</a> contains a number of examples that use
 <a href="http://www.twistedmatrix.com">Twisted (2.0 or later)</a> with Cocoa.</p>

File Examples/00ReadMe.txt

 
 .. __: Plugins/SillyBallsSaver
 
+* `WebKitInterpreter`__
+  
+  Uses the new WebKit Cocoa plugin API available in Safari 1.3
+  and later to embed a PyInterpreter in the browser.
+
+.. __: Plugins/WebKitInterpreter
 
 Twisted Integration
 -------------------

File Examples/Plugins/WebKitInterpreter/WebKitInterpreter.py

+import sys
+import traceback
+import sets
+import keyword
+import time
+from code import InteractiveConsole, softspace
+from StringIO import StringIO
+import objc
+from objc import YES, NO, selector
+from Foundation import *
+from AppKit import *
+from PyObjCTools import AppHelper
+import os
+
+FLT_MAX = 3.40282347e+38
+
+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 = u''
+        self._readline = readlinemethod
+
+    def read(self, chars=None):
+        if chars is None:
+            if self._buffer:
+                rval = self._buffer
+                self._buffer = u''
+                if rval.endswith(u'\r'):
+                    rval = rval[:-1]+u'\n'
+                return rval.encode('utf-8')
+            else:
+                return self._readline(u'\x04')[:-1].encode('utf-8')
+        else:
+            while len(self._buffer) < chars:
+                self._buffer += self._readline(u'\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 u'\r' not in self._buffer:
+            self._buffer += self._readline(u'\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 = u''
+
+        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 = u'Type "help", "copyright", "credits" or "license" for more information.'
+        if banner is None:
+            write(u"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:
+            self.lock = False
+            raise
+        self.lock = False
+
+    def resetbuffer(self):
+        self.lastbuffer = self.buffer
+        InteractiveConsole.resetbuffer(self)
+
+    def runcode(self, code):
+        try:
+            exec code in self.locals
+        except SystemExit:
+            raise
+        except:
+            self.showtraceback()
+        else:
+            if 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:
+                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 = sets.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 = [u'']
+        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
+    #
+
+    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(u'')
+        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):
+                chr = event.charactersIgnoringModifiers()
+                if chr == 'c' and (event.modifierFlags() & NSControlKeyMask):
+                    raise KeyboardInterrupt
+
+            app.sendEvent_(event)
+
+
+    codeString_   = lambda self, s: self._formatString_forOutput_(s, 'code')
+    stderrString_ = lambda self, s: self._formatString_forOutput_(s, 'stderr')
+    stdoutString_ = lambda self, s: self._formatString_forOutput_(s, 'stdout')
+    writeCode_    = lambda self, s: self._writeString_forOutput_(s, 'code')
+    writeStderr_  = lambda self, s: self._writeString_forOutput_(s, 'stderr')
+    writeStdout_  = lambda self, s: 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 setStdoutColor_(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):
+        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(u'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:
+            import traceback
+            traceback.print_exc()
+
+    def doPluginStart(self):
+        dct = self.arguments[u'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()[u'container'] = self.container()
+
+    def objectForWebScript(self):
+        return self
+
+NSLog('loaded WebKitInterpreter')
+
+objc.removeAutoreleasePool()

File Examples/Plugins/WebKitInterpreter/setup.py

+from distutils.core import setup
+import py2app
+
+MIME = 'application/x-pyobjc-demo-webkitinterpreter'
+plist = dict(
+    NSPrincipalClass='WebKitInterpreter',
+    WebPluginName='WebKit PyInterpreter Plug-In',
+    WebPluginDescription='PyObjC demo that embeds a Python interpreter',
+    CFBundlePackageType='WBPL',
+    WebPluginMIMETypes={
+        MIME: dict(
+            WebPluginExtensions=['webkitinterpreter'],
+            WebPluginTypeDescription='WebKit PyInterpreter',
+        ),
+    },
+)
+        
+setup(
+    plugin = ["WebKitInterpreter.py"],
+    options = dict(py2app=dict(plist=plist)),
+)

File Examples/Plugins/WebKitInterpreter/test.html

+<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/1999/REC-html401-19991224/loose.dtd">
+<html>
+<head>
+<title>PyInterpreter WebKit Plugin</title>
+	
+<script language="JavaScript">
+// could do something here...
+function getPlugin() {
+    return document.embeds["pyPlugin"];
+}
+</script>
+
+</head>
+<body>
+
+<center>
+
+<embed name="pyPlugin" width="400" height="300" type="application/x-pyobjc-demo-webkitinterpreter"></embed>
+
+</center>
+
+</body>
+</html>
 <p>An overview of the relevant changes in new, and older, releases.</p>
 <h2><a name="version-1-3-1-2005-04">Version 1.3.1 (2005-04-??)</a></h2>
 <ul>
-<li>Fixed a stupid CFBundleRef reference counting bug in
+<li>New <code><span>WebKitInterpreter</span></code> example in <code><span>Examples/Plugins</span></code>,
+Uses the new WebKit Cocoa plugin API available in Safari 1.3
+and later to embed a PyInterpreter in the browser.</li>
+<li>Fixed a stupid <code><span>CFBundleRef</span></code> reference counting bug in
 Foundation._Foundation.  Symptom of this is usually crashing
 after having loaded a PyObjC-based plugin into an Obj-C app.</li>
-<li>New PyObjCTools.AppHelper functions: callAfter and callLater,
-conveniences for calling Python functions on the main thread
-as soon as possible, or after a delay.</li>
-<li>Twisted examples changed to use threadedselectreactor instead
-of cfreactor.  cfreactor is deprecated.  Needs Twisted newer
-than 2.0 (currently svn r13575 or later).</li>
+<li>New <code><span>PyObjCTools.AppHelper</span></code> functions: <code><span>callAfter</span></code> and
+<code><span>callLater</span></code>, conveniences for calling Python functions on
+the main thread as soon as possible, or after a delay.</li>
+<li>Twisted examples changed to use <code><span>threadedselectreactor</span></code>
+instead of <code><span>cfreactor</span></code>.  <code><span>cfreactor</span></code> is deprecated.
+Needs Twisted newer than 2.0 (currently svn r13575 or later).</li>
 <li><code><span>objc.inject</span></code> now injects on main thread by default,
 takes third useMainThread argument to change this behavior.
 Effectively a complete rewrite which should be more correct
 Version 1.3.1 (2005-04-??)
 --------------------------
 
-- Fixed a stupid CFBundleRef reference counting bug in
+- New ``WebKitInterpreter`` example in ``Examples/Plugins``,
+  Uses the new WebKit Cocoa plugin API available in Safari 1.3
+  and later to embed a PyInterpreter in the browser.
+
+- Fixed a stupid ``CFBundleRef`` reference counting bug in
   Foundation._Foundation.  Symptom of this is usually crashing
   after having loaded a PyObjC-based plugin into an Obj-C app.
 
-- New PyObjCTools.AppHelper functions: callAfter and callLater,
-  conveniences for calling Python functions on the main thread
-  as soon as possible, or after a delay.
+- New ``PyObjCTools.AppHelper`` functions: ``callAfter`` and
+  ``callLater``, conveniences for calling Python functions on
+  the main thread as soon as possible, or after a delay.
 
-- Twisted examples changed to use threadedselectreactor instead
-  of cfreactor.  cfreactor is deprecated.  Needs Twisted newer
-  than 2.0 (currently svn r13575 or later).
+- Twisted examples changed to use ``threadedselectreactor``
+  instead of ``cfreactor``.  ``cfreactor`` is deprecated.
+  Needs Twisted newer than 2.0 (currently svn r13575 or later).
 
 - ``objc.inject`` now injects on main thread by default,
   takes third useMainThread argument to change this behavior.