Using the Open Scripting Architecture from Python


NOTE: this document describes the OSA support that is shipped with the core python distribution. Most users are better of with the more userfriendly appscript library.

OSA support in Python is still not 100% complete, but there is already enough in place to allow you to do some nifty things with other programs from your python program.

In this example, we will look at a scriptable application, extract its “AppleScript Dictionary,” generate a Python interface package from the dictionary, and use that package to control the application. The application we are going to script is Disk Copy, Apple's standard utility for making copies of floppies, creating files that are mountable as disk images, etc. Because we want to concentrate on the OSA details, we won’t bother with a real user-interface for our application.

When we say “AppleScript” in this document we actually mean “the Open Scripting Architecture.” There is nothing AppleScript-specific in the Python implementation. Most of this document focuses on the classic Mac OS; Mac OS X users have some additional tools.

Python OSA architecture

Open Scripting suites and inheritance can be modelled rather nicely with Python packages, so we generate a package for each application we want to script. Each suite defined in the application becomes a module in the package, and the package main module imports everything from all the submodules and glues together all the classes (in Python terminology— events in OSA terminology or verbs in AppleScript terminology).

A suite in an OSA application can extend the functionality of a standard suite. This is implemented in Python by importing everything from the module that implements the standard suites and overriding anything that has been extended. The standard suites live in the StdSuite package.

This all sounds complicated, but the good news is that basic scripting is actually pretty simple. You can do strange and wondrous things with OSA scripting once you fully understand it.

Creating the Python interface package

There is a tool in the standard distribution that can automatically generate the interface packages. This tool is called gensuitemodule.py, and lives in Mac:scripts. It looks through a file for an ‘AETE’ or ‘AEUT’ resource, the internal representation of the AppleScript dictionary, and parses the resource to generate the suite modules. When we start gensuitemodule, it asks us for an input file; for our example, we point it to the Disk Copy executable.

Next, gensuitemodule wants a folder where it will store the package it is going to generate. Note that this is the package folder, not the parent folder, so we navigate to Python:Mac:Demo:applescript, create a folder Disk_Copy, and select that.

We next specify the folder from which gensuitemodule should import the standard suites. Here, we always select Python:Mac:Lib:lib-scriptpackages:StdSuites. (There is one exception to this rule: when you are generating StdSuites itself you select _builtinSuites.)

It starts parsing the AETE resource, and for each AppleEvent suite it finds, gensuitemodule.py prompts us for the filename of the resulting python module. Remember to change folders for the first module—you don't want to clutter up, say, the Disk Copy folder with your python interfaces. If you want to skip a suite, press cancel and the process continues with the next suite.

Summary

  1. Run gensuitemodule.
  2. Select the application (or OSAX) for which you would like a Python interface.
  3. Select the package folder where the interface modules should be stored.
  4. Specify the folder Python:Mac:Lib:lib-scriptpackages:StdSuites to import the standard suites (or _builtinSuites if you are generating StdSuites itself).
  5. Save the generated suites (use cancel to skip a suite).

Notes

The Python interface package contents

Let’s glance at the Disk_Copy package just created. You may want to open Script Editor alongside to see how it interprets the dictionary.

The main package module is in __init__.py. The only interesting bit is the Disk_Copy class, which includes the event handling classes from the individual suites. It also inherits aetools.TalkTo, which is a base class that handles all details on how to start the program and talk to it, and a class variable _signature which is the default application this class will talk to (you can override this in various ways when you instantiate your class, see aetools.py for details).

The Special_Events module is a nice example of a suite module. The Special_Events_Events class is the bulk of the code generated. For each verb, it contains a method. Each method knows what arguments the verb expects, and it makes use of keyword arguments to present a palatable interface to the python programmer. Notice that each method calls some routines from aetools, an auxiliary module living in Mac:Lib. The other thing to notice is that each method calls self.send. This comes from the aetools.TalkTo baseclass.

After the big class, there are a number of little class declarations. These declarations are for the (AppleEvent) classes and properties in the suite. They allow you to create object IDs, which can then be passed to the verbs. For instance, when scripting the popular email program Eudora, you would use mailbox("inbox").message(1).sender to get the name of the sender of the first message in mailbox inbox. It is also possible to specify this as sender(message(1, mailbox("inbox"))), which is sometimes needed because these classes don’t always inherit correctly from baseclasses, so you may have to use a class or property from another suite.

Next we get the enumeration dictionaries, which allow you to pass english names as arguments to verbs, so you don't have to bother with the 4-letter type code. So, you can say diskcopy.create(..., filesystem="Mac OS Standard") as it is called in Script Editor, instead of the cryptic lowlevel diskcopy.create(..., filesystem="Fhfs")

Finally, we get the “table of contents” of the module, listing all classes and such by code, which is used by gensuitemodule itself: if you use this suite as a base package in a later run this is how it knows what is defined in this suite, and what the Python names are.

Notes

Using a Python suite module

Now that we have created the suite module, we can use it in a Python script. In older MacPython distributions this used to be a rather complicated affair, but with the package scheme and with the application signature known by the package it is very simple: you import the package and instantiate the class, e.g. talker = Disk_Copy.Disk_Copy(start=1) You will usually specify the start=1: it will run the application if it is not already running. You may want to omit it if you want to talk to the application only if it is already running, or if the application is something like the Finder. Another way to ensure that the application is running is to call talker._start().

Looking at the sourcefile makedisk.py, we see that it starts with some imports. Naturally, one of these is the Python interface to Disk Copy.

The main program itself is a wonder of simplicity: we create the object (talker) that talks to Disk Copy, create a disk, and mount it. The bulk of the work is done by talker and the Python interface package we just created.

The exception handling does warrant a few comments, though. Since AppleScript is basically a connectionless RPC protocol, nothing happens when we create the talker object. Hence, if the destination application is not running, we will not notice until we send our first command (avoid this as described above). There is another thing to note about errors returned by AppleScript calls: MacOS.Error is raised for all of the errors that are known to be OSErr-type errors, while server generated errors raise aetools.Error.

Scripting Additions

If you want to use any of the scripting additions (or OSAXen, in everyday speech) from a Python program, you can use the same method as for applications, i.e. run gensuitemodule on the OSAX (commonly found in System Folder:Scripting Additions or something similar). There is one minor gotcha: the application signature to use is MACS. You will need to edit the main class in the __init__.py file of the created package and change the value of _signature to MACS, or use a subclass to the same effect.

There are two minor points to watch out for when using gensuitemodule on OSAXen: they appear all to define the class System_Object_Suite, and a lot of them have the command set in multiple dialects. You have to watch out for name conflicts and make sure you select a reasonable dialect (some of the non-English dialects cause gensuitemodule to generate incorrect Python code).

Despite these difficulties, OSAXen offer a lot of possibilities. Take a look at some of the OSAXen in the Scripting Additions folder, or download some from the net.

Further Reading

If you want to look at more involved examples of applescripting, look at the standard modules findertools and nsremote, or (possibly better, as it is more involved) fullbuild from the Mac:scripts folder.

Alternatives

Mac OS X

Under Mac OS X, the above still works, but with some new difficulties. The application package structure can hide the ‘AETE’ or ‘AEUT’ resource from gensuitemodule, so that, for example, it cannot generate an OSA interface to iTunes. Script Editor gets at the dictionary of such programs using a ‘Get AETE’ AppleEvent, if someone wants to donate code to use the same method for gensuitemodule: by all means!

One alternative is available through the Unix command line version of python. Apple has provided the osacompile and osascript tools, which can be used to compile and execute scripts written in OSA languages. See the man pages for more details.