Part of the reason that I haven’t been posting very actively to Beckism.com or Tagamac is that I’ve recently been getting exceedingly involved in a number of personal projects involving Python. Python itself is super fantastic and learning it has been a blast, but one project in particular has been causing me undue amounts of head-to-desk contact thanks to its reliance on the oh-so-cool but insufficiently documented PyObjC, and I’d like to share some of the things that I’ve discovered for other would-be programmers who want to extend their favorite Mac OS X applications with PyObjC powered plugins.
For those of you who prefer source to exposition, the project that I’ve been working on is TEA for Espresso and you can find the full source code over at the TEA for Espresso project on GitHub. As you may have surmised, it’s a plugin for the upcoming Espresso text editor created because I wanted to play with Espresso but am addicted to Textmate-style HTML actions.
My personal preference for Python is to use four spaces rather than a tab for indentation, so keep that in mind if you’re trying to execute the code samples below and your document is using tabs.
Using py2app to create semi-standalone code
Py2app allows you to work completely in Python without ever needing to boot up Xcode and touch Objective-C. There’s plenty of information around the web about how to setup a basic py2app setup.py file, but one of the things that I had to discover via trial and error was how to make my code semi-standalone.
Semi-standalone is an option you can enable with py2app that makes your code reliant on the version of Python that is installed with the OS. By default, py2app tries to bundle any dependencies for your project into your bundle, but if you’re only distributing the bundle/plugin/app/whatever to people using the same major OS version this makes for about 15 MB minimum of unnecessary junk getting tossed into your bundle.
Turning on semi-standalone is as simple as adding a key to your py2app options dictionary, but what you might not know is that you also need to enable site-packages, as well (which apparently encourages py2app to create the links to Python necessary for getting the bundle up and running, although it’s only supposed to tell it to include the system and user site-packages in the system path). So to get a semi-standalone bundle, your code needs to look something like this:
setup( name='My Plugin Bundle', plugin = ['MyPluginBundle.py'], options=dict(py2app=dict( extension='.bundle', semi_standalone = True, site_packages = True )), )
One of the most frustrating aspects of working with py2app for me was the lack of any decent documentation on the main py2app site. Fortunately, there’s a much more comprehensive page of documentation available in the MacPython py2app SVN repository, which is where I discovered the site-packages option.
Automatically including project data files
Odds are since you’re using Python you’ll need to include some arbitrary data files with your application, be they NIB files, extra Python scripts and classes, or whatever else. However, py2app’s method of declaring what files you want included with your project is to list them explicitly in a data_files array. This is a major pain, since every time you add or remove a file you have to remember to edit setup.py. No thanks.
Fortunately, Python comes with some powerful file system modules that allow you to traipse around your file system, making note of files and folders as you go. The following code snippet, if included in your setup.py script, will automatically parse through a directory of files and add them to your data_files array without you needing to lift a finger (assuming that you configure the first couple variables to fit your project, that is). Files that start with a period (hidden files) will be ignored. If you’re using SVN, you may also need to add some logic to exclude folders that start with a period (TEA for Espresso is using Git, so this hasn’t been an issue for me).
The downside to using this code, of course, is that you’ll need to nest all your files in the directory structure that you want in your final bundle. So, for example, in order to include something in the Resources folder for TEA for Espresso, I have to nest it in
src/Contents/Resources/ whereas I otherwise could have placed it in the root project directory. If you’re doing a simple project with very few files, it might be more worth your while to define data_files differently.
import os # Sets what directory to crawl for files to include # Relative to location of setup.py; leave off trailing slash includes_dir = 'src' # Set the root directory for included files # Relative to the bundle's Resources folder, so '../../' targets bundle root includes_target = '../../' # Initialize an empty list so we can use list.append() includes =  # Walk the includes directory and include all the files for root, dirs, filenames in os.walk(includes_dir): if root is includes_dir: final = includes_target else: final = includes_target + root[len(includes_dir)+1:] + '/' files =  for file in filenames: if (file != '.'): files.append(os.path.join(root, file)) includes.append((final, files)) setup( name='My Plugin Bundle', plugin = ['MyPluginBundle.py'], data_files = includes, options=dict(py2app=dict( extension='.bundle' )), )
Avoid release statements in Python
I’m sure this is extremely obvious for anyone with significant PyObjC experience, but TEA for Espresso lay fallow for over a month after I started it because I couldn’t get the example plugin ported from Objective-C to Python. It turns out that almost everything in Objective-C can be easily ported to Python using the simple conversion rules (colons to underscores, etc.; see the PyObjC intro for more info), except for any calls to release objects. For instance, here’s the code that was breaking my project:
Ported into Python (don’t do this!)
PyObjC auto-releases absolutely everything, so don’t port lines like the one above. They will break your project. This is actually described in the PyObjC introduction if you’d like a more technical explanation; apparently I just missed the significance of it when I read the intro the first time.
Handling **NSError arguments
If you’re implementing a bundle that will be loaded into another application, odds are you’ll need to implement an Objective-C protocol in Python, and Objective-C functions occasionally have an **NSError parameter. If you’re not going to be returning any error information, you can safely ignore the **NSError in your Python code. The bridge automatically handles it. If you might return an error, then you may find this thread on the PyObjC-dev mailing list useful.
For example, for TEA for Espresso I needed to implement this Objective-C method in Python:
- (BOOL)performActionWithContext:(id)context error:(NSError **)outError;
Since I’m never returning any error information, in Python this translates to:
def performActionWithContext_error_(self, context):
Although woefully out of date in some respects, one of the most useful and understandable sources of information about using **NSError with PyObjC I found was Apple’s PyObjC for Developing Cocoa Apps page.
Implementing Objective-C interfaces without a protocol
An interesting problem that can arise if your bundle is implementing an Objective-C interface (rather than conforming to a protocol) is that when your code is loaded you may find that Objective-C can’t find your class methods. To solve this, usually all you need to do is explicitly define the type encoding of your class method. For instance, TEA for Espresso’s main bundle class has the following method:
class TEAforEspresso(NSObject): @objc.signature('B@:@') def canPerformActionWithContext_(self, context):
The @objc.signature() decorator tells PyObjC that this particular method returns a bool (“B”), is an object method (“@” means object, and “:” means method selector), and accepts one argument which should be a generic object (“@” again meaning object). For a full list of available encoding characters, see the Objective-C Runtime Programming Guide.
You can find more information about this bit of weirdness over at Jim Matthews Blog or, to a very limited extent, in the PyObjC class wrapping documentation.
Thanks for this! Very helpful as I start working with pyobjc
Posted 6:41 AM on Dec. 17, 2016 ↑