Scripting

Read preferences from a property list within a Mac OS X Python script

I had a need to read some settings from a Python script on Mac OS X recently. I wanted to be able to change selected parameters for the script — some of which could be site or implementation specific — without embedding them directly in the code. Since the script was Mac OS X-only, using a property list seemed like a good idea.

With customizable settings from a property list, a script could become useful and more customizable for a wider community — or even different internal audiences.

I thought reading preferences was going to take a lot of effort. I was, however, pleasantly surprised at how easily I was able to accomplish it.

Asking others who had been down this route before resulted in some links to Apple docs. I wanted a more concrete example of how it was done in Python, and I got a great one.

That example came from reading the source to munkilib from the open source project, Munki. Since Munki had its own preference file, it needed to read from it, and its example was very enlightening.

Frogor directed me to a specific spot in the Munki code. That spot demonstrated how to read a preferences file with CoreFoundation.

Munki also has its own internal defaults for preferences. The munkilib/munkicommon.py example showed how to implement default settings in their absence in a property list. That provides a fallback position so that you always have some value available. In Munki, setting the defaults was done within a function. It seemed that it would be more generic if those defaults were separated out of the preference-reading function.

My own example of how to read a plist is outlined below. This focuses on just what you need in order to read the preferences and provide default settings for a larger script. A single set of preferences can be shared between scripts, and each script can encode its set own defaults. While you can create your own keys and values, for the example below, I will use the keys “StringPreference,” “BooleanPreference,” “ArrayPreference,” and “DictionaryPreference.”

  1. Create a new property list. There are several ways to do this, including the property list editor that has been rolled into Xcode (and is no longer a standalone application) in v4.
  2. Save the file. The name will be used later.
  3. Set up the basic shell of the script that will read the plist and import CoreFoundation.
    #!/usr/bin/env python

    from Foundation import CFPreferencesCopyAppValue
  4. Add in a variable for the script’s bundle ID. The bundle ID uniquely identifies your script’s preferences. It is written in reverse DNS notation, which should be familiar to almost anyone who has dealt with property lists before. It’s handy to have this defined globally for your script so that you can refer back to it as needed.
    #!/usr/bin/env python

    from Foundation import CFPreferencesCopyAppValue

    this_bundle_id = ‘com.example.your-script-here’
  5. Create a function to read a preference by its bundle ID. The business end of the function is the use of CFPreferencesCopyAppValue get a value for a key.
    #!/usr/bin/env python

    from Foundation import CFPreferencesCopyAppValue

    this_bundle_id = ‘com.example.your-script-here’

    def get_preference(preference_key, bundle_id=this_bundle_id):
        """
        Get the preference value for a given combination of preference
        key and bundle ID. Returns the requested preference value.
        """

        # Get the specified preference key from the specified preference
        # bundle ID using CoreFoundation
        preference_value = CFPreferencesCopyAppValue(preference_key, bundle_id)
        return preference_value
  6. Tie the get_preferences function together with a default_preferences object. This way, any missing preferences will fall back to the defaults encoded in your script. Any preferences that are set in a property list will override the defaults.
    #!/usr/bin/env python

    from Foundation import CFPreferencesCopyAppValue

    this_bundle_id = ‘com.example.your-script-here’
    default_preferences = {
        ‘StringPreference’: False,
        ‘BooleanPreference’: False,
        ‘ArrayPreference’: list(),
        ‘DictionaryPreference’: dict(),
    }

    def get_preference(preference_key, bundle_id=this_bundle_id):
        """
        Get the preference value for a given combination of preference
        key and bundle ID. Returns the preference value.
        """

        # Get the specified preference key from the specified preference
        # bundle ID using CoreFoundation
        preference_value = CFPreferencesCopyAppValue(preference_key, bundle_id)
        # If the value is not set in the property list, get a default value
        # from the default_preferences objects
        if preference_value == None:
            preference_value = default_preferences.get(preference_key)
        return preference_value

The script won’t do anything yet, until we create a main() or other functions to call the get_preferences function. Since you’d be reading preferences as part of a larger script, this is fine for now. However, with what’s written already, you can test out reading preferences interactively with the shell and Python interpreter.

  1. Start by checking the property list in the shell. In my case, I created an example property list that sets two of the four preferences.
    $ defaults read com.example.your-script-here
    {
        BooleanPreference = True;
        StringPreference = abc;
    }
  2. Open Terminal, run “python” at the command prompt, and paste the script code above at the interactive Python “>>>” prompt.
  3. Run the get_preferences function and print its output.
    >>> print(get_preference(‘StringPreference’))
    abc
    >>> print(get_preference(‘BooleanPreference’))
    True
    >>> print(get_preference(‘ArrayPreference’))
    []
    >>> print(get_preference(‘DictionaryPreference’))
    {}

That’s it, the expected results were returned. Notice that the output for StringPreference and BooleanPreference are taken from the property list, while the empty array and dictionary come from the default_preferences in the script.

Update: Greg pointed out that there is a certain degree of danger using #!/usr/bin/env python as the shebang line in the script. Should a system have a non-Apple installation of Python, then /usr/bin/env python might return that. Another Python is probably less likely to be able to bridge CoreFoundation, and thus wouldn’t be able to use the CFPreferencesCopyAppValue call to read preferences.

I think the overall shebang danger is small because few Mac OS X systems will have an alternative Python installed, but clearly your chances of that increase in certain situations. To eliminate this risk, do what Munki does and insert the #!/usr/bin/python shebang instead.

Download Sony GPS Assist data automatically when a memory card mounts with a LaunchAgent

Brix Anderson’s post about Downloading Sony GPS Assist Data Manually includes a Perl script that downloads the GPS Assist data for Sony cameras.

The GPS Assist data is stored on the memory card. When a memory card with GPS Assist data is inserted into a compatible GPS-equipped Sony camera — like the awesome Alpha SLT-A55V I got a few months ago — the camera has much faster lock-on times when searching for its location. The lock-on may take 10 seconds instead of requiring minutes.

I had been working without GPS Assist data since I bought the camera. I didn’t understand its value, but then I had captured photos where most of the shoot was incorrectly geotagged. After investigating this, I found the GPS Assist data should help, but that it required software on the desktop to update that data. Sony did not supply the relevant package for Mac OS X to do this.

Since I wanted to make sure I got updated location information on my photos, I did some searching and came across Brix’s page. I was honestly surprised to find that the GPS Assist data was written to the same memory cards as the photos, but it does make sense after some more thought.

Brix’s Perl script there certainly did the trick; my SLT-A55V picked up on the new GPS Assist data on the first card I tried it with. I have a limited number of cards, so I wrote a wrapper shell script that would run the Perl downloader script if certain volumes were present in /Volumes. I control the names of the cards, so I also control their paths.

I then call that wrapper script via a LaunchAgent. The LaunchAgent is triggered via “StartOnMount,” so it watches for new volumes to be mounted by Mac OS X. When a volume is mounted, the LaunchAgent runs, calls the wrapper script, the wrapper script checks for volume names, and if there is a match, runs the GPS Assist Perl script.

Now, every time I insert my memory cards to download photos, I get freshly-updated GPS Assist data for my camera.

Run Drupal cron with Drush at Site5

After starting to use Drush, I wanted to switch my Drupal cron jobs over to it. I’d previously been running these jobs the standard way, loading a URL for each of my Drupal sites with curl. That curl command was run by the cron; Site5 allows editing of your crontab directly or through its Backstage Web interface.

I started with the basics. Drush was installed in my home directory, with an alias under ~/bin in my Site5 account. I replaced my curl command one-for-one with the following:

$ /home/your_site5_account/bin/drush \
—quiet \
—root=/home/your_site5_account/public_html/your_drupal_site/ \
cron

This didn’t work out well. I was getting a huge number of mail messages, indicating problems with the cron jobs. The messages typically contained “tput: No value for $TERM and no -T specified.” Needless to say, this was rather frustrating, so after some trial and error, I modified the command as follows, and that has been working better for me.

$ /usr/local/bin/php \
-d memory_limit=64M \
/home/your_site5_account/drush/drush.php \
—quiet \
—root=/home/your_site5_account/public_html/your_drupal_site/ \
cron

The biggest change is that I specify PHP, rather than Drush, directly. This was done so that I could increase the PHP memory limit for the cron job significantly, to 64M, while running Drush. I’m sure that this increase was needed partly due to the number of modules I have installed on my main site (which has an even larger default PHP memory limit in its php.ini). My research indicated that I needed to do this for Drush since it doesn’t access the Drupal sites in the same way loading a page does.

The other noticeable change is that I provide the path to the drush.php file rather than pointing to the Drush alias.

Get the display resolution on Mac OS X with PyObjC

I came across this hint about display properties on StackOverflow and thought it was worthwhile to write down for later. If you want to get the screen or Desktop resolution of a Mac via Python, you can do so with PyObjC.

First, let’s get the information about the main screen:

>>> from AppKit import NSScreen
>>> print(NSScreen.mainScreen().frame())
<NSRect origin=<NSPoint x=0.0 y=0.0> size=<NSSize width=1920.0 height=1200.0>>

If you want just the horizontal and vertical resolution from that blob of data, you can pull the width and height out:

>>> print(NSScreen.mainScreen().frame().size.width)
1920.0
>>> print(NSScreen.mainScreen().frame().size.height)
1200.0
>>> width, height = NSScreen.mainScreen().frame().size.width, NSScreen.mainScreen().frame().size.height
>>> width, height
(1920.0, 1200.0)

This might be useful in situations where you don’t have any of the “hundred of portable libs in Python that give you access to that information” — such as in your stock Mac OS X Python installation. To clarify: I’m in no way meaning to belittle that there are portable libraries that would let you do the same thing, but you also have to program for your audience and its constraints. One of the reasons I appreciate Python over some scripting languages is that you get so much capability in the Standard Library. However, on Mac OS X, you don’t get modules like pygame by default (yet … and maybe never) while you do get PyObjC.

Generate version numbers for Mac OS X package installers with Mercurial and Semantic Versioning

I’ve had occasion to create some installer packages lately, and the topic of assigning a version number to them has rattled around in my head. The version number of an installer package is shown in the Finder’s Get Info windows, and in the preview pane of column view, so it has some usefulness just for telling one installer apart from another. And, importantly, the version number provides a way for Installer to determine whether a package has previously been installed and simply needs to be upgraded.

At first, I was just going to assign the date in YYYYMMDD format as the version, just to “keep things simple” by using:

$ date +%Y%m%d
20100324

Then I was rebuilding the packages several times a day, and wanted to distinguish between the builds, so I switched to YYYYMMDD.hhmm.

$ date +%Y%m%d.%H%M
20100324.2240

This seemed ridiculous after a day. Although I could distinguish between builds, the installers I was building weren’t really changing per se. They were the same payload and scripts and resources, just rebuilt by the command line packagemaker tool using The Luggage.

I crowdsourced my question about versioning, and Steve Losh helpfully responded with the idea to use Mercurial tags along with Semantic Versioning.

My package sources are already checked into Mercurial repositories, so this made a good deal of sense. It meant that I’d have to use tags to identify a version number — it would no longer be as automatic as using the current date. However, certain parts of the version string would be automatic, and the only manual part would be tagging revisions in Hg.

I had already bookmarked Semantic Versioning a while back, but had forgotten about it and never done much of anything with it. If I followed the Semantic Versioning standard, I’d create a “semver” tag to set up the repository, and then a new “vX.Y.Z” tag for actual releases thereafter. Steve further suggested the use of the “latest tag distance” and “short node hash” for the Hg revision, and that information is automatically available from Hg.

Semantic Versioning states, “when tagging releases in a version control system, the tag for a version MUST be ’vX.Y.Z’ e.g. ’v3.1.0’.” I felt I would have to strip the “v” from the version tag to use it cleanly as a package version. This results in commands like the following, where I’ve used sed to remove that character:

# Full Semantic Version with further identifying information from Mercurial
$ hg log -r. —template ‘{latesttag}+{latesttagdistance}-{node|short}\n’
v1.0.0+1-df34ebfc12c9
# Strip the "v" with sed
$ hg log -r. —template ‘{latesttag}+{latesttagdistance}-{node|short}\n’ | sed -e ’s/^v//’
1.0.0+1-df34ebfc12c9
# Basic Semantic Version
$ hg log -r. —template ‘{latesttag}\n’
v1.0.0
# Strip the "v" with sed
$ hg log -r. —template ‘{latesttag}\n’ | sed -e ’s/^v//’
1.0.0

This is something I can build into makefiles for use with The Luggage, so I’m going to try it out. On first glance, I think it even fits in nicely with the concept of The Luggage. It automates much of the version-assignment process. And, it takes advantage of version control for both repeatable results and peer review.

Remove filename extensions with os.path.splitext instead of the {lr}strip or replace methods in Python

Jesper Noehr explains why {l,r}strip are considered harmful for removing extensions from filenames with Python. I think he’s absolutely right on that score, and I would agree. The lstrip() and rstrip() methods shouldn’t be used for this purpose.

However, like the only commenter on that post, I’d also recommend os.path.splitext() as the proper tool for the extension-removing job.

Let’s take some example filenames you might come across on Mac OS X Snow Leopard:

>>> a = [ ‘Document.pages’, ‘Property List.plist’, ‘Application.app’, ‘Word.docx’, VPN (Cisco VPN).networkConnect’, ‘Dated Spreadsheet 2010.02.17.xlsx’]

If we had a list of filenames (or file paths) like this — perhaps created by os.walk() or some other generator-based process — we couldn’t easily use Jesper’s recommended solution. The replace() string method would give us a much harder time dealing with the range of filenames and extensions in that list. In the case where you don’t know the filename extensions in advance, replace() breaks down. The replace() method would have to be looped with many possible filename extensions.

What we need is a way to split filename from extension, even if we don’t know the extension beforehand. The os.path.splitext() alternative does just that, returning a tuple. Here, I’ll import the os module and then use a list comprehension to run os.path.splitext() through every filename in the list above.

>>> import os
>>> [os.path.splitext(x) for x in a]
[(‘Document’, ‘.pages’), (‘Property List’, ‘.plist’), (‘Application’, ‘.app’), (‘Word’, ‘.docx’), (VPN (Cisco VPN)’, ‘.networkConnect’), (‘Dated Spreadsheet 2010.02.17’, ‘.xlsx’)]

It becomes a simple matter to get just the filename from the tuple, as I do here by modifying the list comprehension to just get the zeroth item from it:

>>> [os.path.splitext(x)[0] for x in a]
[‘Document’, ‘Property List’, ‘Application’, ‘Word’, VPN (Cisco VPN)’, ‘Dated Spreadsheet 2010.02.17’]

Note that several interesting conditions are handled by os.path.splitext():

  • long filename extensions, including “.pages” and “.networkConnect”
  • spaces in filenames, as in “Property List.plist”
  • parentheses, as in “VPN (Cisco VPN).networkConnect”
  • periods within the filename, as seen in “Dated Spreadsheet 2010.02.17.xlsx”

AppleScript date and time format parsing change in Snow Leopard

I had a handy script I wrote on a plane years ago that let me block off my Entourage calendar at specific times each weekday for a given week. The times for these events were created by concatenating some strings and converting the result into an AppleScript date object. I mention that merely for background, and because it was an incredibly geeky way to automate the tedious process of blocking off lunch on my calendar (without resorting to recurring events).

I found that my script didn’t work in Snow Leopard — despite flawless operation across several successive major versions of Mac OS X. The dates themselves remained correct, but the times were all coming up as 12:00 a.m. instead of what was expected.

For example, here’s a simplified reproduction scenario you can try on Snow Leopard:

set vEventDateString to "Friday, August 28, 2009"
set vEventStartTime to "10:00 a.m."
log vEventStartTime
(*10:00 a.m.*)
set vEventStartDate to the (date (vEventDateString & " " & vEventStartTime))
log vEventStartDate
(*date Friday, August 28, 2009 12:00 AM*)

It turns out that the fix is easy if not exactly obvious: remove the periods from “a.m.” and “p.m.” before converting strings to date objects. (I use the periods because I follow the Associated Press Stylebook!)

set vEventDateString to "Friday, August 28, 2009"
set vEventStartTime to "10:00 am"
log vEventStartTime
(*10:00 am*)
set vEventStartDate to the (date (vEventDateString & " " & vEventStartTime))
log vEventStartDate
(*date Friday, August 28, 2009 10:00 AM*)

So, there is a workaround in the unlikely event you encounter the same problem with your scripts.

Core Graphics bindings on 64-bit Python in Snow Leopard

Mac OS X Snow Leopard does not include Core Graphics bindings (CGBindings) for 64-bit Python.

The SWIG-based Python CGBindings originally shipped with Mac OS X 10.3, which bundled Python 2.3. Since that time, these bindings — specific to the system’s bundled framework build of Python — had allowed access to Core Graphics objects and commands from within scripts.

They were one of the reasons I decided to use Python in the first place. I thought they would be fun to learn and use, particularly with the then-new PDF Services feature of Mac OS X. The Core Graphics bindings also provided much, much more power than the command line sips tool and had an advantage over other alternatives by being bundled with the operating system. I thought they offered the possibility of growing with Mac OS X’s graphics hardware acceleration. I even found a way to use them to create better screenshots with drop shadows, a task where I’d previously employed Ambrosia’s Snapz Pro X.

Here’s an example of what you’ll see on Snow Leopard if you try to “import CoreGraphics” in 64-bit Python:

# Explicitly enable 64-bit execution for Python
$ export VERSIONER_PYTHON_PREFER_32_BIT=no  
$ python
Python 2.6.1 (r261:67515, Jul  7 2009, 23:51:51)
[GCC 4.2.1 (Apple Inc. build 5646)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> from CoreGraphics import *
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/BinaryCache/CoreGraphicsBindings/CoreGraphicsBindings-26~139/Root/System/Library/Frameworks/Python.framework/Versions/2.6/Extras/lib/python/CoreGraphics/__init__.py", line 7, in <module>
ImportError: /System/Library/Frameworks/Python.framework/Versions/2.6/Extras/lib/python/CoreGraphics/_CoreGraphics.so: no appropriate 64-bit architecture (see "man python" for running in 32-bit mode)
>>> ^D

With 32-bit Python on Snow Leopard:

# Explicitly enable 32-bit execution for Python
$ export VERSIONER_PYTHON_PREFER_32_BIT=yes
$ python                                  
Python 2.6.1 (r261:67515, Jul  7 2009, 23:51:51)
[GCC 4.2.1 (Apple Inc. build 5646)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> from CoreGraphics import *
>>> ^D

While the CGBindings are still available to 32-bit Python in Snow Leopard, you must use PyObjC to replace their functionality for 64-bit Python. Since 64-bit Python is the default in Snow Leopard, it makes sense to transition from the bindings to PyObjC as soon as possible. This means there is some porting work for scripts that used the Core Graphics bindings. I guess I’m glad I didn’t do as much with them as I’d planned.

I see this change as something of a loss. (Is this what Carbon developers are experiencing? Hm.) The Core Graphics bindings were relatively easy to use and felt reasonably Pythonic, even if the documentation was almost nonexistent. PyObjC feels more foreign to me when I attempt to use it — even though it’s clearly the future.

Python 32-bit execution on Snow Leopard

The default installation of Python on Mac OS X Snow Leopard is version 2.6.1. According to the man page for Python on Snow Leopard, Python 2.6 executes as a 64-bit application by default.

If, for some reason, you need to run it as a 32-bit application, this can be changed at the command line:

# Prefer 32-bit execution for Python 2.6.1 on Snow Leopard
$ defaults write com.apple.versioner.python Prefer-32-Bit -bool yes

The preference can be set in either the User or Local filesystem domain in Mac OS X, following the normal precedence rules. To unset it, presumably you would change the boolean to “no” — or perhaps even delete the “Prefer-32-Bit” key.

There is also an environment variable that can override this preference.

Updating Acquia Drupal versions in a Mercurial repository

After using Acquia Drupal for a while, I took advantage of a trial subscription to the Acquia Network. The network’s services showed me that I had files present in my install that the agent could not account for.

I suspected this was happening because of the way I manage my Acquia Drupal installation with Mercurial. So, I’ve modified my previous process (and updated my instructions) to extract the downloaded tar archive with the —recursive-unlink option. This option appears to successfully remove the contents of every directory before putting new files back into them.

$ tar —strip-components=1 —directory=acquia_drupal —recursive-unlink -zxvf acquia-drupal-1.2.12.5047.tar.gz
acquia-drupal-1.2.8/
acquia-drupal-1.2.8/robots.txt

acquia-drupal-1.2.8/INSTALL.txt

When the archive is extracted in this way, my repository’s working directory shows modified, unknown, and deleted files. This allows me to treat each category of files individually before I commit the changes for a Drupal update as a revision.

$ hg status

The modified files will be tracked normally because they’ve already been added to the Mercurial repository, so I don’t need to do anything special for them.

The unknown files are ones that are completely new, and have not appeared in the same position in a previous revision. They have yet to be tracked by Mercurial, so I have to add them to the repository. To add just those unknown files, then, I have to pick them out from the status listing:

$ hg status --unknown

In order to operate just on those files to add them to the repository, I run a for loop:

$ for FILEPATH in `hg status —unknown —no-status`
for> do
for>    hg add "$FILEPATH"
for> done

This changes the “?” status to “A,” because the files were successfully being tracked by Mercurial.
I use the “—no-status” flag on the “status” command so that just the file paths are printed; the actual status code is not, which is appropriate for the target of the “add” command in the loop.

I do the same basic steps with deleted files. These are files that were in the previous revisions but have been deleted by the —recursive-unlink option from the tar extraction and not replaced with the extraction of the new Acquia Drupal tar archive. If the deleted files had been replaced by the tar extraction, they would either be unchanged (which would not show up in the “status” output) or marked as modified.

To remove the files that are marked as deleted from the repository’s working directory:

$ for FILEPATH in `hg status —deleted —no-status`
for> do
for>    hg remove "$FILEPATH"
for> done

However, that may be the same as simply using the following, which I have to explore further:

$ hg remove --after

So, to follow all of these changes in the repository, I run the loop for the uknown files and the loop for the deleted files. The modified files are already tracked, so I don’t need to do anything additional for them. After that, a “commit” will record all of the changes — modifications, additions, and deletions — in the repo.

These commands are based on my current understanding of Mercurial, and they do work for me right now. There could certainly be another better way to do this in one fell swoop — or at least fewer steps. I would welcome that, so if you’re aware of a way, feel free to comment or contact me.

Update: I found that the “hg addremove” command cleanly replaces all of the shell loops I mentioned above. Therefore, I recommend using it instead of the “for” loops I described.

Syndicate content