Combining Python and AppleScript

You may remember this post from last June, in which I had to rewrite a script that printed out the current iTunes track. The original script was written in Python and used Hamish Sanderson’s appscript library; the replacement was written in AppleScript.

I had to do the rewrite because an update to iTunes had broken the way appscript gets at an application’s AppleScript dictionary. Hamish had stopped developing appscript because Apple had deprecated the Carbon libraries he used to develop it and hadn’t replaced them with Cocoa equivalents.

That post generated many thousands of words of commentary, most of it by Hamish and most of the rest by Matt Neuburg. Although Matt came up with a clever workaround to Ruby-appscript’s access to application dictionaries, and I thought seriously about mimicking his work for Python-appscript, eventually I decided that I should just abandon appscript.1 Because Apple has no proprietary interest in appscript, it will almost certainly continue to make changes that undermine it.

Ferreting out all my appscript-using programs and changing them into pure AppleScript or some Python/AppleScript hybrid wasn’t appealing, so I decided to just wait until a script broke before rewriting it. Recently, my script for automatically generating invoice emails broke, and I rewrote it into a combination of two AppleScripts and one Python script. It worked, but I wasn’t happy with the results—it seemed both kludgy and fragile. What I needed was a more general way to run AppleScript code from within my Python scripts.

I’ve touched on this topic before. Back then, I thought Kenneth Reitz’s envoy module was the solution. I still like the idea of envoy, but the GitHub page has no real documentation, and Kenneth’s own site seems to have been purged of most of his coding work in favor of writing, photography, and music. Besides, envoy is a bit more general-purpose than I need. Basically, I just want one or two wrapper functions around Python’s subprocess module that will allow me to

  1. Write an AppleScript as a Python string.
  2. Run it from within my Python program.
  3. Collect any output it generates.

With this, I’ll be able to keep all the code in one script instead of artificially breaking it up into separate AppleScript and Python parts.

Here’s the module, applescript.py:

python:
 1:  #!/usr/bin/python
 2:  
 3:  import subprocess
 4:  
 5:  def asrun(ascript):
 6:    "Run the given AppleScript and return the standard output and error."
 7:  
 8:    osa = subprocess.Popen(['osascript', '-'],
 9:                           stdin=subprocess.PIPE,
10:                           stdout=subprocess.PIPE)
11:    return osa.communicate(ascript)[0]
12:  
13:  def asquote(astr):
14:    "Return the AppleScript equivalent of the given string."
15:    
16:    astr = astr.replace('"', '" & quote & "')
17:    return '"{}"'.format(astr)

There are just two functions: asrun, which takes the AppleScript string as its only argument, runs it, and returns the output, if any; and asquote, which reconfigures any string into a string that AppleScript can parse.

There’s not much to either one of these functions, but I can think of two things worth a little explanation. You’ll note that the Popen in asrun doesn’t change the stderr parameter from its default value of None. That’s because I wanted any AppleScript errors that arise to propagate out into the surrounding script and get handled like any other Python error—shutting the program down unless it’s in a try block. And instead of simply backslash-escaping double quotes in asquote, I do the more verbose thing of splitting the string at the double quotes and reconcatenating it with quotes. Doing it this way seemed more AppleScripty, but maybe that’s just me. You could certainly change Line 16 to

python:
16:    astr = astr.replace('"', r'\"')

if you think that’s better. The double backslash is necessary to get around Python’s escaping rules. The raw string gets around Python’s escaping rules.

I have applescript.py saved in /Library/Python/2.7/site-packages so it’s available to all my scripts. I have a feeling I’ll be changing it as I use it and find that it fails under certain conditions. So far, though, it’s done what I want.

Here’s a short script using both asrun and asquote:

python:
 1:  #!/usr/bin/python
 2:  
 3:  from applescript import asrun, asquote
 4:  
 5:  subject = 'A new email'
 6:  
 7:  body = '''This is the body of my "email."
 8:  I hope it comes out right.
 9:  
10:  Regards,
11:  Dr. Drang
12:  '''
13:  ascript = '''
14:  tell application "Mail"
15:    activate
16:    make new outgoing message with properties {{visible:true, subject:{0}, content:{1}}}
17:  end tell
18:  '''.format(asquote(subject), asquote(body))
19:  
20:  print ascript
21:  asrun(ascript)

This does pretty much what you’d expect: after printing out the AppleScript source, it runs it through osascript to create a new message in Mail with the Subject and Content fields filled. Except for the format placeholders, and the doubled braces that format requires, the AppleScript in Lines 14-17 is exactly as I’d write it in the AppleScript Editor. I know Clark Goble will disagree, but I prefer this to the appscript syntax, which I found awkward because it didn’t feel like real Python.

Since Hamish Sanderson and Matt Neuburg inadvertently contributed to this post, I should recommend their AppleScript books. Hamish’s is the book I reach for now when I have an AppleScript question; Matt’s is more concise and has excellent sections on the structure and philosophy of AppleScript. And if you’re interested in scripting Mail, this tutorial by Ben Waldie at MacTech is a great place to start and may well be where you finish.


  1. A couple of months ago, Matt sent me an email: iTunes 11 “unbroke” the appscript connection to its AppleScript dictionary, and my original script would probably work again. An ironic twist, but it didn’t change my mind. 


8 Responses to “Combining Python and AppleScript”

  1. Jonathan Lundell says:

    A pythonic detail:

    16 astr = astr.replace('"', '\\"')

    is equivalent to:

    16 astr = astr.replace('"', r'\"')

    (And s/reconcatinating/reconcatenating/, but that’s a quibble about a nice post and an elegant solution, for which thanks.) (Mnemonic: same root as “catenary”.)

  2. Dr. Drang says:

    Both good points, and both incorporated in the post. Thanks, Jonathan!

    And you cut me to the quick with the reference to the catenary. Concatenation is chaining things together, just as a catenary is the shape of a hanging chain. I should never get that wrong.

  3. has says:

    Code munging is evil. And wrong. It’s also evil and wrong. Happily, I’m not a complete scuzz, so left a demonstration of how to pass Python strings to AppleScript via argv here:

    http://appscript.sourceforge.net/osascript.html

    Or, if you’re feeling slightly more ambitious and don’t mind PyObjC as a dependency, it’d be pretty simple to put a pure Python wrapper around NSAppleScript that knows how to pack and unpack most of the common types (bool, int, float, str, list, DateTime and maybe dict [with caveats] too). You’d just need to pick out the relevant bits of objc-appscript’s AEMCodecs class and CallAppleScriptHandler project and write the equivalent PyObjC code.

    Or you could always try SB via PyObjC - it’s a second-rate bridge with a regular habit of blowing up or just being a general PITA on stuff that AppleScript (and appscript) are perfectly happy with, but if it’s a simple task and you can abide the crappy syntax (and gnawing self-guilt at such quisling behavior) then it might suffice.

    Lastly, I believe Clark Goble also spent a bit of time looking into accessing AppleScript+AppleScriptObjC from Python+PyObjC. I’ve bugged him a couple times already to publish his results (as I’m too lazy to investigate myself), though seeing as he’s now with babby I suspect it might be a while. But again, might be worth exploring since ASOC gives you a bunch of bridging for free.

  4. Lauri Ranta says:

    Don’t you also have to escape backslashes? subject = 'A new \\email' wouldn’t currently work.

    I also prefer using run handlers:

    subject="-aa\\bb"
    content="'a\"\\n"
    osascript -e 'on run {s, c}
    tell app "Mail"
    activate
    make new outgoing message with properties {visible:true, subject:s, content:c}
    end
    end' -- "$subject" "$content"
    
  5. Dr. Drang says:

    Building separately compiled AppleScripts that get called from Python is exactly what I wanted to avoid. Cluttering my disk with scripts that are really subroutines makes no sense to me.

    Also, demonstration scripts with one or two arguments are nice, but as the number of arguments grows, the repetition of item n of argv is both wearing and makes the code hard to read. We can’t always loop through argv, applying the same operation to each item.

  6. has says:

    @Drang:

    1. Passing args via ARGV works equally well when piping the AppleScript code to STDIN as you did. No need for code munging or external script files.

    2. Assuming ARGV is fixed length, you don’t need to write ‘item n of argv’ throughout your code. Like Python2, AppleScript supports multiple assignment within handler parameters. Just declare the ‘run’ handler like this:

      on run {arg1, arg2, arg3} — (assumes ARGV contains 3 values) … end run

    and AppleScript will assign each item in ARGV directly to a variable for you.

    Of course, what AppleScript really needs is a proper optparse-style library to chew ARGV for you, but the lack of a built-in library loader and the failure of 3rd-party efforts to gain any community traction means that’s pretty much a non-starter, alas. Still, it’s fine for basic argument passing as-is, and quite painless once you take advantage of what features the language does possess.

  7. has says:

    Ugh, screwy Markdown formatting; trying one more time:

    on run {arg1, arg2, arg3} -- (assumes ARGV contains 3 values)
        …
    end run
    
  8. has says:

    Drang: “I prefer this to the appscript syntax, which I found awkward because it didn’t feel like real Python.”

    As the appscript documentation always took pains to point out, appscript syntax never intended to be ‘real Python/Ruby/ObjC/etc’. All of the bridges that attempted this (gensuitemodule, aeve, RubyOSA, SB, etc) ended up crippling their functionality and breaking application compatibility to one degree or another. The impedance mismatch between Python/Ruby/ObjC’s OO model and Apple events’ RPC+relational query model is simply too great to be swept under the rug like that. (Relational database ORMs frequently run into similar problems for the same reason, not to mention the messes made by SOAP, Rails, et-al bashing square HTTP pegs into round OO holes.) i.e. The whole point of appscript was to avoid repeating others’ mistakes.

    Appscript is pure unadulterated Apple event semantics with a very thin skin of syntactic sugar on top of its query building and RPC dispatching mechanics to make your code acceptably clean and easy to read and write. If you drop down to appscript’s lower-level AEM API, you can see what an OO-ified version of the Apple Event Manager’s procedural APIs actually looks like: functional but still pretty ugly. All the topmost appscript layer does is hide the four-char codes and verbose names under human-readable names and whatever syntactic prettification the host language allows.

    Funny enough, it was the Ruby and ObjC users who were most likely to moan about appscript’s design, since they had the most deeply ingrained preconceptions about how things should work and most resistance to accepting anything else (OO as the one true religion). I think they often fight with other non-OO technologies like relational databases and HTTP for much the same reason. Ultimately, they’re just making rods for their own backs.

    OTOH, those who approached appscript on its own terms - i.e. how to speak Apple events fluently with minimal fuss - seemed to love it like nothing else before or since (with the possible exception of UserTalk, which was also pretty good at this). It still bums me out that I didn’t do more to drive appscript’s adoption both inside and outside Apple, but I fell into the usual FOSS trap of “it scratches my itch now, so I’ve done enough”. Hard lesson learned.