PyObjC in Snow Leopard

As some of you may have noticed, several of our applications were not compatible with Mac OS X 10.6 “Snow Leopard” when it was first released. We’ve since issued updates for them all — but why was this necessary? Well, I’ll explain… Note: If you’re one of our customers, this probably isn’t terribly important info — but if you’re a Mac developer who’s run into the same issues, these pointers may be helpful!

Most of our Mac applications are written in Python, using the PyObjC bridge. Mac OS X 10.6 brought an updated version of PyObjC, from 2.0.1 to 2.2b3. So really, this is about breaking changes in PyObjC rather than Snow Leopard itself, but since users pretty much never update PyObjC independently, it amounts to the same thing.

The first issue we ran into was methods which take variable argument lists, eg:

python

variable arguments example
class MyExampleClass(NSObject): def exampleMethod(self, argument, *extraArgs, **keywordArgs): print argument, extraArgs, keywordArgs

Now, it’s understandable that this method wouldn’t be automatically bridged and callable from Objective C, since that language doesn’t support this kind of method call. Unfortunately, in PyObjC 2.2b3 you can’t declare a method like this at all in a subclass of NSObject, without throwing an exception at startup.

Quite why, I’m not sure — it simply decides you must be too stupid to be allowed to play with such things, and bans you from using this (perfectly respectable) Python technique even when calling the Python method from some other part of Python. Unfortunately, we use this in several places, such as networking infrastructure and animation code.

There are several possible workarounds, with varying degrees of ugliness; if you run in to this, you may be able to refactor your application to avoid such calls — certainly we did in some places. A “quick and dirty” hack is to move the method out of the class, as it’s fine to call module-scope methods of this type. Another path is to replace the *extraArgs and **keywordArgs with a simple list and dictionary, making the whole thing more explicit, if slightly clunkier — and of course, you’ll need to chase down all the calls to it and change them to match.

A better solution would be if there was a decorator to declare that you knew what you were doing and could PyObjC stop nannying you please? I couldn’t find one, but if you know better, do let us know

Secondly, instance variables: In PyObjC, an object’s instance variables are not visible to Objective C unless you explicitly declare them, like so:

python

instance variables example
class MySecondExampleClass(NSObject): counter = 0 counterEnabled = objc.ivar()

In this example, counter is not visible from Objective C, but counterEnabled is. However, the documentation states that counter is still accessible from Objective C by using Key-Value Coding.

Unfortunately, in Snow Leopard, this isn’t strictly true. You can access the members by KVC, but Key-Value Observing seems to be broken in this instance. Specifically, if you change a value using KVC, then any bindings attached to it will not observe the change. If you then change the value a second time, the first value is then observed! The result is that, for example, any user-interface bound to the instance variable will be out of sync, “one click behind” the user all the time. So if you have any bindings hooked up to a Python member, you must declare the variable properly.

Finally — and you should be doing this anyway — ensure you change any Observed instance methods using PyObjC’s automagic KVC access system:

python

KVC access
self._.counterEnabled = True

The extra _ in there causes counterEnabled to be set via KVC, triggering any appropriate KVO notifications. Like I say, you should be doing this anyway, but sometimes under Leopard it would still work even if you forgot. Not so under Snow Leopard.