Python For AppleScripters
Volume Number: 20 (2004)
Issue Number: 11
Column Tag:
Programming
Python For AppleScripters
by Ryan Wilcox
Introduction By Comparison
A Tale of Two Languages
The advent of OS X brought with it a wealth (some might say invasion) of
tools from the Unix world: the command shells, grep, sed, ls, cat, less,
vi, emacs all these utilities and countless others. It also brought
with it programming languages probably the most popular one being
Perl. OS X 10.2, however, added to its repertoire a scripting language
called Python.
Introduction
Python is easy to use,
simple, powerful, and chock-full of great modules (similar to
AppleScripts you load via the load script command). The design of the
language "just makes sense," the modules are well thought out, and best
of all the language has many similarities to AppleScript. Every day it
seems I find more uses for Python than I could have imagined. I use
Python along with BBEdit to automate all sorts of common text-based
tasks: I have scripts to help me resolve CVS conflicts, to convert
decimal to hexadecimal (and back), to encode selected text into URL
encoded format and more. Python's readable structure and multitude of
included modules lends itself to quick one-off utilities, often with
less pain than a similar AppleScript - at least in this author's
opinion. In this article I'll discuss how Python and AppleScript are
similar, and how they differ. Then we'll walk through an example script
in both languages. Finally, I'll conclude showing how you can use Python
and AppleScript simultaneously in your projects.
AppleScript and Python share many similar traits
Looking at a Python script should be a vaguely familiar experience
for an AppleScripter - the same indented flow, and understandable
syntax. When you are creating a Python script you often try out little
chunks of code first, make sure they work, then put them in a larger
whole - just like you might when adding code to an AppleScript. Let's
tackle these similarities in more detail:
Whitespace matters
In AppleScript whitespace is automatically added by the compiler so
that nested commands (such as those in an if, repeat, try, tell, etc)
are always indented properly. In Python whitespace is also important -
in fact whitespace tells the compiler that a line or block of code is
nested. When the indentation stops, the block of code has ended.
Contrast this to AppleScript's approach, where blocks of code are ended
with end statements. The key difference with Python is that it does not
automatically add whitespace as AppleScript does. This isn't as much of
an issues as it sounds, as most text editors auto-indent when you type a
return. It's worth mentioning here that statements that mark a block of
code (such as for loops, if statements, and even functions) require a
colon at the end of the "parent" line. The sample script presented later
in the article shows several indented blocks of code.
Having whitespace matter is both a good thing and a bad thing. The
good news: every Python script you run into will have a similar style,
indentation-wise. The bad news: the compiler will complain if you mix
spaces and tabs to indent, and it's annoying to have to debug something
you can't see. For this reason using a text editor that can Show
Invisibles is so very important(maybe even a requirement) while
programming in Python. As a sidenote, in cross-platform scripts, using 4
spaces to indent is recommended over using a tab, as spaces are not so
easily mangled by unsavvy text editors.
"simple" syntax
Python's syntax is very straightforward, and often compared to
pseudo-code. For contrast, look at the sample script later in this
article, implemented first in AppleScript, then in Python. The Python
version, while it is not as readable as the near-English AppleScript,
reads like English plus a bit of 8th grade algebra. AppleScript's
approach of English-Like-Syntax-Wherever-Possible often results in extra
typing. Compare if you will AppleScript's:
set end of myList to "the end"
to Python's:
myList.append("the end").
Python is not AppleScript
or some, the apparent similarity stops there. Python brings its own
unique flavor to the language party, differing from AppleScript in some
key areas: cross-platformness, case sensitivity, and Python's
(significantly) different approach to types are some of what make Python
a unique scripting language when compared to AppleScript.
Python is Cross-Platform
Python runs on most major platforms - both flavors of Mac OS,
Windows, Linux, Unix, even the PalmOS. Much of Python's functionality
knows what platform a script is currently running under, and adjusts
platform specific things. For example, the linesep attribute of the os
module will return the line separator character(s) for the current
platform. There are certain times when you want to use a platform
specific API, and that's perfectly acceptable as well. One Python rule
of thumb is "We're all consenting adults here," meaning that the
language won't try to prevent you from doing something potentially
"naughty" if you want to.
Case Matters
In AppleScript, the compiler changes the case of a variable to be the
same as the first instance of that variable. So, while case matters, the
compiler takes care of it for you. In Python case also matters, except
there is no automatic correction - what you type is what you get.
What do you contain? Types Matter
As any experienced scripter knows, AppleScript plays fast-and-loose
with type. Sometimes you can't be sure exactly what you will get back.
This has its advantages as well as its disadvantages. Take this line of
AppleScript for example:
set firstNum to "1"
set testVar to firstNum + 1
If you know that firstNum can always be converted into number, this
works great - it saves everybody some typing. But here's the puzzle:
what is testVar? Is it a string? A number? Without a specific
declaration, AppleScript will automatically coerce all of the values to
the same type, but the question still remains: what type of object will
you end up with? (To those of you who answered that the result will be a
number, go to the head of the class.) However, as scripts grow in
complexity, being explicit regarding what type a variable is becomes
essential - you end up almost fighting the implicit coercion you used
(and loved) with your smaller script.
With Python, there is no implicit coercion - instead, variables have
a very strict sense about what type they are, and what they can do. (For
those of you versed in programming terminology, Python is dynamically,
but strongly, typed. You can create a variable without caring what type
it will be, but Python keeps track of what kind of data that variable
currently has in it. Here is that same sample in Python:
testVar = int("1") + 1
This is how Python does coercion - instead of AppleScript's as xxxxx
notation, Python uses xxxx(), as C/C++ does. Trying to run "1" + 1 in
python will give a runtime error, as you can not concatenate 'str' and
'int' objects. Python has no idea what to do (it could do two things:
cast "1" to an integer, or cast 1 to a string. One answer will result in
2, while the other gives "11"). One of the guidelines (Zens) of Python
says: "When faced with ambiguity, resist the temptation to guess." The
"Zens of Python" guide both the development of Python as a language and
provide a good framework for writing your own scripts and modules. To
read more about the culture of Python, and the Zen/Design Principles of
Python, visit the following URL:
http:
//www.python.org/dev/culture.html
Batteries Included
Like AppleScript, Python has a small core language, while external
modules provide additional functionality. In AppleScript, these external
modules come in the form of Scripting Additions and Scriptable
Applications (created by Apple and third parties). There are a few
Scripting Additions that come preinstalled with every Mac OS
installation (Standard Additions, URL Access Scripting, Image Capture
Scripting, among others), and several of the apps that come preinstalled
are scriptable. All Scriptable Applications and Scripting Additions are
written in languages like C/C++ or Objective-C. In Python the focus is
not so much on applications as it is on modules - collections of Python
routines or objects, usually written in Python, that perform certain
tasks. These modules are similar in style to AppleScript's script
libraries. While some Python modules include C/C++ code, these seem to
be the exception, rather than the rule. Python comes with a huge
collection of modules called the Standard Library, so instead of asking
the Finder for the size of a file, you would call a function in the
Standard Library.
It's in the __doc__s
In AppleScript, there is always some human readable documentation:
the dictionary of the application or scripting addition. Sometimes the
dictionary is not enough but it is always there, on your machine. When I
am writing AppleScript, I always have at least one or two dictionaries
open, referring to them as I write my script, like a cheat-sheet right
there on my desktop.
Python, on the other hand, takes more of a "reference book" approach
to documentation - it is available in a number of different formats,
(downloadable from http://python.org), but like any reference book, you
hope the documentation is up to date, complete, and that it describes
the method you want to use. There have been several utilities written to
reduce the risk of these mistakes in the documentation happening, and
the Python documentation is usually of high quality. Still, the
possibility of out of date documentation exists. The Mac Python IDE
includes a module browser, letting you explore different modules like
you do an AppleScript dictionary, but it's often not as helpful. As
mentioned before, most Python modules are coded in Python itself, and
you can usually view the source code for a module, trying to figure out
what a function actually does.
The standard Python practice is to add a string literal describing
the function and parameters it takes as the first line of the function.
This string is called a "docstring." If you view a module in the Mac
Python IDE's Module Browser, this string will be described as __doc__
(pronounced "under under doc under under") - however this __doc__ string
is what is rendered for the documentation - meaning that if the
documentation is poor, the __doc__ will probably be as well.
Here's an example of a function with a docstring. But first it is
also important to note Python's string literal functionality. If you
have a character in a string literal that you would ordinarily have to
escape, for example a quote character, you can instead triple-quote the
string literal - the string is considered everything enclosed in triple
quotes ("""I'm in triple quotes""", for example, is a perfectly valid
string literal.)
def addValues(value1, value2):
"""addValues adds two numbers. Simple. value1 is
the first value to add, value2 is the second. Returns
these two values added together"""
return (value1 + value2)
This standard practice is a great practice to adopt for your own
methods. Adopting this documentation convention will help you remember
what a function does, why you need it, and what the parameters do when
you revisit the function at a later date. AppleScript is without such a
standard practice; everybody has their own styles of documenting an
AppleScript method, if they do it at all.
An IDE and an example: Kicking the tires
Python makes a great multi-purpose language. Internally we use it
from everything from creating shell programs, to making BBEdit Unix
filters, creating throw-away one-time scripts, or designing custom CGI
scripts for our clients. You can even use Python in conjunction with
Apple's Cocoa application framework using PyObjC. With some additional
modules, you can use Python just like you would AppleScript - to display
simple GUIs, talk to other applications, and do other user
administration tasks.
Starting at the beginning: Installing a GUI friendly Python
While you can use Python on the command line, the command line
program gives you everything you would expect from a Unix based tool: no
GUI capabilities, no IDE and no graphical debugger. In short, it's not
the best environment for Mac people who are used to such niceties.
In the pre-OS X days, a Mac OS 9 version of Python, including an IDE,
was provided by Jack Jansen. The IDE and all the Mac specific modules
from those days still work under OS X, but their appearance has not been
updated for OS X. Those looking for prettier IDEs on OS X shouldn't fret
- there are several that show promise, but as of this writing most are
still in the early stages of development.
You can download the MacPython package at
http://homepages.cwi.nl/
~jack/macpython/
This package will install the PythonIDE application (found in your
Applications/MacPython-2.3 folder) along with some other things. Double
click on the Python IDE and you should get something similar to
this:
Figure 1.
Got it? Does it look something like this? Good. Let's go to
work.
A simple illustration, line by line
Let's start things off with a simple example - a script that accepts
user input and appends it to a file. It should be noted here that simple
AppleScript display dialog like interfaces aren't Python's strong suit.
While the MacPython package helps, it's still not as easy as
AppleScript's display dialog. This (and inter-application communication)
are two of the things that Python does poorly, however there are two
packages currently competing to become the de facto standard for
inter-application communication in Python, so the tide (at least on that
front) should turn rather quickly.
First, the AppleScript:
set filepath to choose file with prompt "select a file to append to"
set fileRef to open for access filepath with write permission
repeat
set dialogResult to display dialog "enter a line" default answer "line" buttons -
{"No More", "Enter"} default button 2
if button returned of dialogResult is "Enter" then
set textReturned to text returned of dialogResult
write textReturned & return to fileRef
else
exit repeat
end if
end repeat
close access fileRef
Now, the Python:
import EasyDialogs, os
filepath = EasyDialogs.AskFileForOpen("select a file to append to")
if filepath:
fileRef = open(filepath, 'w')
while True:
textReturned = EasyDialogs.AskString(
prompt = "enter a line", default = "line",
ok="Enter", cancel = "No more")
if textReturned:
fileRef.write(textReturned + os.linesep)
else:
break
fileRef.close()
Let's take the Python sample line by line:
import EasyDialogs, os
As mentioned before, Python organizes sets of functionality into
modules. Import loads these modules into your script. Here we import
both the EasyDialogs module (a Mac specific module) and the
cross-platform os module.
filepath = EasyDialogs.AskFileForOpen("file to append
to please")
This line calls the AskFileForOpen method in the EasyDialogs module,
which will ask the user to select a file. By comparison, AppleScript
searches all of the installed scripting additions for you, looking for
the command, and sometimes it "helpfully" finds the wrong one. This is
what often causes a terminology conflict. If AppleScript required you to
specify where to get the terminology from, you might have to write
something like set filepath to standard addition's choose file which may
be more typing, but would remove any potential ambiguity. Sadly,
AppleScript does not support this style of reference.
if filepath:
In AppleScript, if the user presses cancel in a choose file dialog,
AppleScript raises an error and terminates the script (unless you handle
the error in an on error block). Python's AskFileForOpen function does
no such thing - it just returns None and keeps on executing the script.
We must explicitly test the value of filepath for its existence
(filepath would be None if the user pressed the "Cancel" button on the
dialog).
In Python variables that are None are simply considered false. Truth
in Python is a tricky thing, but best explained by the following web
page:
http://www.users.csbsju.edu/~clusena/python/fundamentals/node10.html
fileRef = open(filepath, 'w')
Again, similar looking to the AppleScript - open the file at filepath
with write permissions.
while True:
Here the aforementioned Zen of Python "when faced with ambiguity,
resist the temptation to guess" returns. The above line shows how deeply
this statement is ingrained in the Python culture. The equivalent
AppleScript statement is just "repeat" - to which Pythonistas would ask
"repeat what?". Here Python explicitly says "do the following as long as
this statement is true". The True must be capitalized - True means true,
while true means nothing. Got it? Good.
te xtReturned = EasyDialogs.AskString(
prompt = "enter a line", default = "line",
ok="Enter", cancel = "No more")
By reading the documentation I found this method, and figured out
what parameters to pass to it. These parameters are self-explanatory,
but it did take a bit of hunting in the documentation (and maybe even a
read of the source) to learn exactly how to construct this line.
if textReturned:
Here again we test the value of textReturned - if it contains
anything, the if executes. Same as the if filepath line above. It is
worth repeating that lines that begin blocks of indented code, such as
this line, need a colon at the end.
fileRef.write(textReturned + os.linesep)
Here we write the text the user entered, and a line separator (of
whatever platform we're on) to the file. As mentioned before, os.linesep
will return the end-of-line character(s) for whatever platform the
script is on.
else:
break
Here we come to the end of the if textReturned block. If textReturned
is None, as belabored in more detail above, the user pressed the cancel
button - we should abort our while loop.
fileRef.close()
Always close our file - in this case, by calling fileRef object's
close() method. Note the indentation level of this line - it is on the
same level indentation wise, as the while statement. This signals the
end of the while loop - the indentation level changed. While this was
mentioned previously in the article, in the "whitespace matters"
section, it deserves repeating here.
Two Worlds Collide: AppleScript, meet Python
Even if you don't want to use Python as your main scripting language,
you can slowly move parts of your AppleScripts into Python - for
instance having your Python scripts do things that are hard to do (or
slow to do) in AppleScript, but easy in Python. Here's an example that
will find a string inside a string (or return 0 if it does not). This
task is easy to do in AppleScript (using the offset of functionality),
but it can be very slow. Instead of using offset of we use a Python
script to do it.
Python script: substr.py:
#!/usr/bin/env python
#first line tells us where to find python.
#a # character means the rest of the
#line is a comment, just like AppleScript's --
import sys
findWord = sys.argv[1] #get the first command line argument
thestring = sys.argv[2] #get the string
print thestring.find(findWord) + 1
#AppleScript strings start at 1, python's @ 0. Adjust the answer for AS.
Create the above Python script your favorite text editor, and save
it. Make sure the line endings are set to Unix line endings, just to be
safe.
Now, create the following AppleScript, and save it in the same folder
as the above Python script, in Application format.
on run
display dialog ( "world has been found at character: "
& pythonSubStr("world", "hello world") )
end run
to pythonSubStr(toFind, theString)
set myContainer to getContainerofMe()
set myResult to do shell script "python " & myContainer
& "substr.py " & " \"" & toFind & "\"" & " \"" &
theString & "\""
-- tell python what script to open up, and what params to pass
-- also note that the quotes we put around both strings are to prevent the shell from
-- breaking them into lots of different arguments (the shell sees a space
-- as an argument separator)
-- this is usually not what we want to do. These will be removed
-- automatically by Python.
return myResult
end pythonSubStr
on getContainerofMe()
tell application "Finder"
set dest to path to me
set temp_container to container of dest as alias
return (quoted form of POSIX path of temp_container)
--POSIX = unix path
end tell
end getContainerofMe
However, it's worth noting here that do shell script on my test
machine (400Mhz Powerbook G4) takes about .5 seconds to execute. This is
not because Python is slow, but rather do shell script can take a while
to do its initialization and termination routines. This slowness,
however, may just beat out a vanilla AppleScript using offset of,
depending on the data.
Using Python, you can sometimes build functionality into your scripts
that normally would require third party OSAXen in AppleScript. Complex
string manipulations, regular expressions, even sending email. Using do
shell script to merge AppleScript and Python code might just provide
that extra oomph for your script, or may just speed up your development
process.
Conclusion
With it's familiar-feeling language, cross-platform abilities, large
standard library, and simple, readable syntax, you might find Python an
interesting choice for your next project - even if it's only a part of
it. Feel free to experiment with the built-in Python interpreter. Fire
up Terminal.app and enter the command python to be taken into the
command line Python's interactive mode (Control-D to get out). For those
of you of the GUI persuasion, see the Python Interactive window in the
Python IDE. Learn more about Python by visiting the Python website athttp://www.python.org , in particular
the Introduction section
(http://www.python.org/doc/Intros.html).
References
For additional information on Python, see
http://www.python.org. For additional information on using Python on
the Mac, see
http://www.pythonmac.org/ . Thanks go to Matthew Strange and Jared
Barden for reviewing this article.
Ryan Wilcox is the founder of
Wilcox Development Solutions
(www.wilcoxd.com) specializing in
carbonization, cross-platform application development and e-commerce
solutions. He often has a hard time thinking of witty things to say in
these blurbs. You can reach him at
rwilcox@wilcoxd.com.