PyObjCLauncher

A reimplementation of the Python script launcher helper application in PyObjC.

Sources

FileSettings.py

from Foundation import *
from AppKit import *

class FileSettings(NSObject):
    fsdefault_py = None
    fsdefault_pyw = None
    fsdefault_pyc = None
    default_py = None
    default_pyw = None
    default_pyc = None
    factorySettings = None
    prefskey = None
    settings = None

    def getFactorySettingsForFileType_(cls, filetype):
        if filetype == u'Python Script':
            curdefault = cls.fsdefault_py
        elif filetype == u'Python GUI Script':
            curdefault = cls.fsdefault_pyw
        elif filetype == u'Python Bytecode Document':
            curdefault = cls.fsdefault_pyc
        else:
            NSLog(u'Funny File Type: %s\n', filetype)
            curdefault = cls.fsdefault_py
            filetype = u'Python Script'
        if curdefault is None:
            curdefault = FileSettings.alloc().initForFSDefaultFileType_(filetype)
        return curdefault
    getFactorySettingsForFileType_ = classmethod(getFactorySettingsForFileType_)

    def getDefaultsForFileType_(cls, filetype):
        if filetype == u'Python Script':
            curdefault = cls.default_py
        elif filetype == u'Python GUI Script':
            curdefault = cls.default_pyw
        elif filetype == u'Python Bytecode Document':
            curdefault = cls.default_pyc
        else:
            NSLog(u'Funny File Type: %s', filetype)
            curdefault = cls.default_py
            filetype = u'Python Script'
        if curdefault is None:
            curdefault = FileSettings.alloc().initForDefaultFileType_(filetype)
        return curdefault
    getDefaultsForFileType_ = classmethod(getDefaultsForFileType_)

    def newSettingsForFileType_(cls, filetype):
        return FileSettings.alloc().initForFileType_(filetype)
    newSettingsForFileType_ = classmethod(newSettingsForFileType_)

    def initWithFileSettings_(self, source):
        self = super(FileSettings, self).init()
        self.settings = source.fileSettingsAsDict().copy()
        self.origsource = None
        return self

    def initForFileType_(self, filetype):
        defaults = FileSettings.getDefaultsForFileType_(filetype)
        self = self.initWithFileSettings_(defaults)
        self.origsource = defaults
        return self

    def initForFSDefaultFileType_(self, filetype):
        self = super(FileSettings, self).init()
        if type(self).factorySettings is None:
            bndl = NSBundle.mainBundle()
            path = bndl.pathForResource_ofType_(u'factorySettings', u'plist')
            type(self).factorySettings = NSDictionary.dictionaryWithContentsOfFile_(path)
            if type(self).factorySettings is None:
                NSLog(u'Missing %s', path)
                return None
        dct = type(self).factorySettings.get(filetype)
        if dct is None:
            NSLog(u'factorySettings.plist misses file type "%s"', filetype)
            return None

        self.applyValuesFromDict_(dct)
        interpreters = dct[u'interpreter_list']
        mgr = NSFileManager.defaultManager()
        self.settings['interpreter'] = u'no default found'
        for filename in interpreters:
            filename = filename.nsstring().stringByExpandingTildeInPath()
            if mgr.fileExistsAtPath_(filename):
                self.settings['interpreter'] = filename
                break
        self.origsource = None
        return self

    def applyUserDefaults_(self, filetype):
        dct = NSUserDefaults.standardUserDefaults().dictionaryForKey_(filetype)
        if dct:
            self.applyValuesFromDict_(dct)

    def initForDefaultFileType_(self, filetype):
        fsdefaults = FileSettings.getFactorySettingsForFileType_(filetype)
        self = self.initWithFileSettings_(fsdefaults)
        if self is None:
            return self
        self.settings['interpreter_list'] = fsdefaults.settings['interpreter_list']
        self.settings['scriptargs'] = u''
        self.applyUserDefaults_(filetype)
        self.prefskey = filetype
        return self

    def reset(self):
        if self.origsource:
            self.updateFromSource_(self.origsource)
        else:
            fsdefaults = FileSettings.getFactorySettingsForFileType_(self.prefskey)
            self.updateFromSource_(fsdefaults)

    def updateFromSource_(self, source):
        self.settings.update(source.fileSettingsAsDict())
        if self.origsource is None:
            NSUserDefaults.standardUserDefaults().setObject_forKey_(self.fileSettingsAsDict(), self.prefskey)

    def applyValuesFromDict_(self, dct):
        if self.settings is None:
            self.settings = {}
        self.settings.update(dct)

    def commandLineForScript_(self, script):
        cur_interp = None
        if self.settings['honourhashbang']:
            try:
                line = file(script, 'rU').next().rstrip()
            except:
                pass
            else:
                if line.startswith('#!'):
                    cur_interp = line[2:]
        if cur_interp is None:
            cur_interp = self.settings['interpreter']
        cmd = []
        cmd.append('"'+cur_interp.replace('"', '\\"')+'"')
        if self.settings['debug']:
            cmd.append('-d')
        if self.settings['verbose']:
            cmd.append('-v')
        if self.settings['inspect']:
            cmd.append('-i')
        if self.settings['optimize']:
            cmd.append('-O')
        if self.settings['nosite']:
            cmd.append('-S')
        if self.settings['tabs']:
            cmd.append('-t')
        others = self.settings['others']
        if others:
            cmd.append(others)
        cmd.append('"'+script.replace('"', '\\"')+'"')
        cmd.append(self.settings['scriptargs'])
        if self.settings['with_terminal']:
            cmd.append("""&& echo "Exit status: $?" && python -c 'import sys;sys.stdin.readline()' && exit 1""")
        else:
            cmd.append('&')
        return ' '.join(cmd)

    def fileSettingsAsDict(self):
        return self.settings

LaunchServices.py

# Just enough LaunchServices to get what we want.
def _load(g=globals()):
    import objc
    from Foundation import NSBundle
    OSErr = objc._C_SHT
    def S(*args):
        return ''.join(args)

    FUNCTIONS = [
        (u'LSGetApplicationForInfo', 'sII@Io^{FSRef=[80C]}o^@'),
    ]

    bndl = NSBundle.bundleWithPath_(objc.pathForFramework('/System/Library/Frameworks/ApplicationServices.framework'))
    objc.loadBundleFunctions(bndl, g, FUNCTIONS)
globals().pop('_load')()

kLSUnknownType = 0
kLSUnknownCreator = 0
kLSRolesViewer = 2

if __name__ == '__main__':
    err, outRef, outURL = LSGetApplicationForInfo(kLSUnknownType, kLSUnknownCreator, u'txt', kLSRolesViewer)
    print err, outRef.as_pathname(), outURL

MyDocument.py

from Foundation import *
from AppKit import *
from PyObjCTools import NibClassBuilder
import os
from FileSettings import *
from doscript import doscript

class MyDocument(NSDocument):
    commandline = objc.IBOutlet()
    debug = objc.IBOutlet()
    honourhashbang = objc.IBOutlet()
    inspect = objc.IBOutlet()
    interpreter = objc.IBOutlet()
    nosite = objc.IBOutlet()
    optimize = objc.IBOutlet()
    others = objc.IBOutlet()
    scriptargs = objc.IBOutlet()
    tabs = objc.IBOutlet()
    verbose = objc.IBOutlet()
    with_terminal = objc.IBOutlet()

    def init(self):
        self = super(MyDocument, self).init()
        if self is not None:
            self.script = u'<no script>.py'
            self.filetype = u'Python Script'
            self.settings = None
        return self

    def windowNibName(self):
        return u'MyDocument'

    def close(self):
        super(MyDocument, self).close()
        if NSApp().delegate().shouldTerminate():
            NSApp().terminate_(self)

    def load_defaults(self):
        self.settings = FileSettings.newSettingsForFileType_(self.filetype)

    def updateDisplay(self):
        dct = self.settings.fileSettingsAsDict()
        self.interpreter.setStringValue_(dct['interpreter'])
        self.honourhashbang.setState_(dct['honourhashbang'])
        self.debug.setState_(dct['verbose'])
        self.inspect.setState_(dct['inspect'])
        self.optimize.setState_(dct['optimize'])
        self.nosite.setState_(dct['nosite'])
        self.tabs.setState_(dct['tabs'])
        self.others.setStringValue_(dct['others'])
        self.scriptargs.setStringValue_(dct['scriptargs'])
        self.with_terminal.setState_(dct['with_terminal'])
        self.commandline.setStringValue_(self.settings.commandLineForScript_(self.script))

    def update_settings(self):
        self.settings.updateFromSource_(self)

    def run(self):
        cmdline = self.settings.commandLineForScript_(self.script)
        dct = self.settings.fileSettingsAsDict()
        if dct['with_terminal']:
            res = doscript(cmdline)
        else:
            res = os.system(cmdline)
        if res:
            NSLog(u'Exit status: %d', res)
            return False
        return True

    def windowControllerDidLoadNib_(self, aController):
        super(MyDocument, self).windowControllerDidLoadNib_(aController)
        self.load_defaults()
        self.updateDisplay()

    def dataRepresentationOfType_(self, aType):
        return None

    def readFromFile_ofType_(self, filename, typ):
        show_ui = NSApp().delegate().shouldShowUI()
        self.script = filename
        self.filetype = typ
        self.settings = FileSettings.newSettingsForFileType_(typ)
        if show_ui:
            self.updateDisplay()
            return True
        else:
            self.run()
            self.close()
            return False

    @objc.IBAction
    def doRun_(self, sender):
        self.update_settings()
        self.updateDisplay()
        if self.run():
            self.close()

    @objc.IBAction
    def doCancel_(self, sender):
        self.close()

    @objc.IBAction
    def doReset_(self, sender):
        self.settings.reset()
        self.updateDisplay()

    @objc.IBAction
    def doApply_(self, sender):
        self.update_settings()
        self.updateDisplay()

    def controlTextDidChange_(self, aNotification):
        self.update_settings()
        self.updateDisplay()

    def fileSettingsAsDict(self):
        return dict(
            interpreter=self.interpreter.stringValue(),
            honourhashbang=self.honourhashbang.state(),
            debug=self.debug.state(),
            verbose=self.verbose.state(),
            inspect=self.inspect.state(),
            optimize=self.optimize.state(),
            nosite=self.nosite.state(),
            tabs=self.tabs.state(),
            others=self.others.stringValue(),
            with_terminal=self.with_terminal.state(),
            scriptargs=self.scriptargs.stringValue(),
        )

PreferencesWindowController.py

from Foundation import *
from AppKit import *
from PyObjCTools import NibClassBuilder
from FileSettings import *

class PreferencesWindowController(NSWindowController):
    commandline = objc.IBOutlet()
    debug = objc.IBOutlet()
    filetype = objc.IBOutlet()
    honourhashbang = objc.IBOutlet()
    inspect = objc.IBOutlet()
    interpreter = objc.IBOutlet()
    nosite = objc.IBOutlet()
    optimize = objc.IBOutlet()
    others = objc.IBOutlet()
    tabs = objc.IBOutlet()
    verbose = objc.IBOutlet()
    with_terminal = objc.IBOutlet()
    _singleton = None
    settings = None

    def getPreferencesWindow(cls):
        if not cls._singleton:
            cls._singleton = PreferencesWindowController.alloc().init()
        cls._singleton.showWindow_(cls._singleton)
        return cls._singleton
    getPreferencesWindow = classmethod(getPreferencesWindow)

    def init(self):
        return self.initWithWindowNibName_(u'PreferenceWindow')

    def load_defaults(self):
        title = self.filetype.titleOfSelectedItem()
        self.settings = FileSettings.getDefaultsForFileType_(title)

    def updateDisplay(self):
        if self.settings is None:
            return
        dct = self.settings.fileSettingsAsDict()
        self.interpreter.reloadData()
        self.interpreter.setStringValue_(dct['interpreter'])
        self.honourhashbang.setState_(dct['honourhashbang'])
        self.debug.setState_(dct['debug'])
        self.verbose.setState_(dct['verbose'])
        self.inspect.setState_(dct['inspect'])
        self.optimize.setState_(dct['optimize'])
        self.nosite.setState_(dct['nosite'])
        self.tabs.setState_(dct['tabs'])
        self.others.setStringValue_(dct['others'])
        self.with_terminal.setState_(dct['with_terminal'])
        self.commandline.setStringValue_(self.settings.commandLineForScript_(u'<your script here>'))

    def windowDidLoad(self):
        super(PreferencesWindowController, self).windowDidLoad()
        try:
            self.load_defaults()
            self.updateDisplay()
        except:
            import traceback
            traceback.print_exc()
            import pdb, sys
            pdb.post_mortem(sys.exc_info()[2])

    def updateSettings(self):
        self.settings.updateFromSource_(self)

    @objc.IBAction
    def doFiletype_(self, sender):
        self.load_defaults()
        self.updateDisplay()

    @objc.IBAction
    def doReset_(self, sender):
        self.settings.reset()
        self.updateDisplay()

    @objc.IBAction
    def doApply_(self, sender):
        self.updateSettings()
        self.updateDisplay()

    def fileSettingsAsDict(self):
        return dict(
            interpreter=self.interpreter.stringValue(),
            honourhashbang=self.honourhashbang.state(),
            debug=self.debug.state(),
            verbose=self.verbose.state(),
            inspect=self.inspect.state(),
            optimize=self.optimize.state(),
            nosite=self.nosite.state(),
            tabs=self.tabs.state(),
            others=self.others.stringValue(),
            with_terminal=self.with_terminal.state(),
            scriptargs=u'',
        )

    def controlTextDidChange_(self, aNotification):
        self.updateSettings()
        self.updateDisplay()

    def comboBox_indexOfItemWithStringValue_(self, aComboBox, aString):
        if self.settings is None:
            return -1
        dct = self.settings.fileSettingsAsDict()
        return dct['interpreter_list'].indexOfObject_(aString)

    def comboBox_objectValueForItemAtIndex_(self, aComboBox, index):
        if self.settings is None:
            return None
        return self.settings.fileSettingsAsDict()['interpreter_list'][index]

    def numberOfItemsInComboBox_(self, aComboBox):
        if self.settings is None:
            return 0
        return len(self.settings.fileSettingsAsDict()['interpreter_list'])

PyObjCLauncher.py

import objc; objc.setVerbose(1)
from AppKit import *
from Foundation import *
from PyObjCTools import AppHelper
from MyDocument import *
from LaunchServices import *
from PreferencesWindowController import *

PYTHON_EXTENSIONS = [u'py', 'pyw', u'pyc']

FILE_TYPE_BINDING_MESSAGE = u"""
%s is not the default application for all Python script types.  You should fix this with the Finder's "Get Info" command.

See "Changing the application that opens a file" in Mac Help for details.
""".strip()

class MyAppDelegate(NSObject):
    def init(self):
        self = super(MyAppDelegate, self).init()
        self.initial_action_done = False
        self.should_terminate = False
        return self

    @objc.IBAction
    def showPreferences_(self, sender):
        PreferencesWindowController.getPreferencesWindow()

    def applicationDidFinishLaunching_(self, aNotification):
        self.testFileTypeBinding()
        if not self.initial_action_done:
            self.initial_action_done = True
            self.showPreferences_(self)

    def shouldShowUI(self):
        if not self.initial_action_done:
            self.should_terminate = True
        self.initial_action_done = True
        if NSApp().currentEvent().modifierFlags() & NSAlternateKeyMask:
            return True
        return False

    def shouldTerminate(self):
        return self.should_terminate

    def applicationShouldOpenUntitledFile_(self, sender):
        return False

    def testFileTypeBinding(self):
        if NSUserDefaults.standardUserDefaults().boolForKey_(u'SkipFileBindingTest'):
            return
        bndl = NSBundle.mainBundle()
        myURL = NSURL.fileURLWithPath_(bndl.bundlePath())
        myName = bndl.infoDictionary()[u'CFBundleName']
        for ext in PYTHON_EXTENSIONS:
            err, outRef, outURL = LSGetApplicationForInfo(kLSUnknownType, kLSUnknownCreator, u'txt', kLSRolesViewer, None, None)
            if (err or myURL != outURL):
                res = NSRunAlertPanel(
                    u'File type binding',
                    FILE_TYPE_BINDING_MESSAGE % myName,
                    u'OK',
                    u"Don't show this warning again",
                    None)
                if res == 0:
                    NSUserDefaults.standardUserDefaults().setObject_forKey_(u'YES', u'SkipFileBindingTest')
                return

if __name__ == '__main__':
    AppHelper.runEventLoop()

doscript.py

import os

def doscript(cmd):
    OSASCRIPT = '/usr/bin/osascript'
    # not ideal, of course
    scriptcmd = [OSASCRIPT,
        '-e', 'tell application "Terminal"',
        '-e', 'run',
        '-e', 'do script "%s"' % cmd.replace('\\', '\\\\').replace('"', '\\"'),
        '-e', 'end tell']
    return os.spawnv(os.P_WAIT, scriptcmd[0], scriptcmd)

setup.py

"""
Script for building the example.

Usage:
    python setup.py py2app
"""
from setuptools import setup

setup(
    name="PyObjC Launcher",
    app=["PyObjCLauncher.py"],
    data_files=["English.lproj"],
    options=dict(
        py2app=dict(
            plist="Info.plist",
        )
    ),
    setup_requires=[
        "py2app",
        "pyobjc-framework-Cocoa",
    ]
)

Resources

Table Of Contents

Resources

Support development