Next article: Friday Q&A 2009-11-27: Using Accessors in Init and Dealloc
Previous article: Deconstructing Apple's Copyright and Trademark Guidelines
Tags: cocoa hack objectivec pyobjc python
It's another Friday and time for another Friday Q&A. This week, fellow Amoeba Jeff Johnson suggested talking about using Cocoa from the command line using Python and PyObjC.
I assume everybody reading this knows what Cocoa is, but may not know what the other two parts are:
- Python: A clean, fairly modern "scripting" language. Its object model is much like Smalltalk, in that pretty much everything is an object and there are no primitives, but the syntax is more C-like. Significantly, for the purposes of this article, Python provides a nice, friendly command-line interpreter that you can access by typing
python
into your nearest terminal window. - PyObjC: A language bridge between Python and Objective-C. It will translate or proxy objects from one language into objects in the other language, bi-directionally. Allows you to write Cocoa apps partly or entirely in Python, and also lets you poke at Cocoa from Python's interpreter.
PyObjC gets implicitly loaded whenever you load one of the Python modules that need it. Most system frameworks have a corresponding Python module. To load such a module, you can enter
import FrameworkName
at the Python command line. However, since Python supports namespaces, this requires putting FrameworkName.
before any symbol inside that framework that you want to use. This is usually a good thing for "real" code, but if we're just going to experiment with things from the command line, it's better to avoid that. You can tell Python to import everything in the framework into the top-level namespace instead with from FrameworkName import *
. Then you can use symbols from the framework directly. Example:
>>> from Foundation import *
>>> NSFileManager
<objective-c class NSFileManager at 0x7fff7101eb18>
>>> NSFileManager.defaultManager()
<NSFileManager: 0x100257900>
>>> NSFileManager.defaultManager().displayNameAtPath_('/')
u'Fear'
>>> NSFileManager.defaultManager().fileAttributesAtPath_traverseLink_('/', True)
{
NSFileCreationDate = "2006-08-18 08:33:34 -0400";
NSFileExtensionHidden = 0;
NSFileGroupOwnerAccountID = 80;
NSFileGroupOwnerAccountName = admin;
NSFileModificationDate = "2009-11-16 23:58:45 -0500";
NSFileOwnerAccountID = 0;
NSFileOwnerAccountName = root;
NSFilePosixPermissions = 1021;
NSFileReferenceCount = 38;
NSFileSize = 1360;
NSFileSystemFileNumber = 2;
NSFileSystemNumber = 234881026;
NSFileType = NSFileTypeDirectory;
}
>>> 'abc'.length()
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: 'str' object has no attribute 'length'
>>> NSString.stringWithString_('abc').length()
3
len('abc').)
Errors
Cocoa methods that return NSError
instances by reference get special treatment by PyObjC. Python cleanly supports returning multiple values from a method, but doesn't cleanly support return-by-reference, so PyObjC translates the NSError
return by reference into a multiple return. You just assign two variables to the result of the method, and then pass None
(Python's version of nil
for the NSError
argument. Example:
>>> string, error = NSString.stringWithContentsOfFile_encoding_error_('/', NSUTF8StringEncoding, None)
>>> string
>>> error.description().encode('utf8')
'Error Domain=NSCocoaErrorDomain Code=257 UserInfo=0x11994b610 "The file \xe2\x80\x9cFear\xe2\x80\x9d couldn\xe2\x80\x99t be opened because you don\xe2\x80\x99t have permission to view it." Underlying Error=(Error Domain=NSPOSIXErrorDomain Code=13 "The operation couldn\xe2\x80\x99t be completed. Permission denied")'
error
directly, Python will complain that its description can't be converted to ASCII, so I have to manually get the description and convert it to UTF-8 for printing.
Arrays and Dictionaries
A Python array can be written like this:
['a', 'b', 'c']
>>> data, error = NSPropertyListSerialization.dataFromPropertyList_format_errorDescription_(['hello', 'world'], NSPropertyListXMLFormat_v1_0, None)
>>> NSString.alloc().initWithData_encoding_(data, NSUTF8StringEncoding)
u'<?xml version="1.0" encoding="UTF-8"?>\n<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">\n<plist version="1.0">\n<array>\n\t<string>hello</string>\n\t<string>world</string>\n</array>\n</plist>\n'
Dictionaries are written like this:
{ 'key' : 'value', 'key2' : 'value2' }
>>> data, error = NSPropertyListSerialization.dataFromPropertyList_format_errorDescription_({ 'key' : 'value', 'key2' : 'value2' }, NSPropertyListXMLFormat_v1_0, None)
>>> NSString.alloc().initWithData_encoding_(data, NSUTF8StringEncoding) u'<?xml version="1.0" encoding="UTF-8"?>\n<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">\n<plist version="1.0">\n<dict>\n\t<key>key</key>\n\t<string>value</string>\n\t<key>key2</key>\n\t<string>value2</string>\n</dict>\n</plist>\n'
Custom Frameworks
A fun thing with PyObjC is that it's not limited to system frameworks. You can load your own frameworks! It's trivial to do, because you can do it exactly as you would do it in a Cocoa program: just use NSBundle to load the framework and start getting and manipulating classes. Here's an example of doing this with a system framework, but it works just the same for your own:
>>> bundle = NSBundle.bundleWithPath_('/System/Library/Frameworks/WebKit.framework')
>>> bundle.principalClass()
<objective-c class WebPlaceholderModalWindow at 0x7fff7095cc40>
>>> bundle.classNamed_('WebView').alloc().init()
<WebView: 0x100212490>
Note that on 10.6 and 64-bit capable machines, Python loads as 64-bit by default, so if your framework is 32-bit only then this won't work. You can load Python in 32-bit mode by starting it with the following command (assuming you use the default bash shell):
VERSIONER_PYTHON_PREFER_32_BIT=yes python
AppKit
So far we've just been working with Foundation-like objects, but PyObjC supports AppKit as well:
>>> from AppKit import *
>>> NSApplicationLoad()
True
>>> window = NSWindow.alloc().init()
>>> window.makeKeyAndOrderFront_(None)
>>> image = NSImage.alloc().initWithContentsOfFile_('brakes.jpg')
>>> image
<NSImage 0x1198b6f70 Size={483, 450} Reps=(
"NSBitmapImageRep 0x1198bcfa0 Size={483, 450} ColorSpace=(not yet loaded) BPS=8 BPP=(not yet loaded) Pixels=483x450 Alpha=NO Planar=NO Format=(not yet loaded) CurrentBacking=nil (faulting) CGImageSource=0x1198bc220"
)>
>>> scaledImage = NSImage.alloc().initWithSize_((32, 32))
>>> scaledImage.lockFocus()
>>> image.drawInRect_fromRect_operation_fraction_(((0, 0), (32, 32)), NSZeroRect, NSCompositeCopy, 1.0)
>>> scaledImage.unlockFocus()
>>> scaledImage.TIFFRepresentation().writeToFile_atomically_('/tmp/brakes_thumb.tiff', True)
True
Conclusion
This article barely scratches the surface of what's possible with PyObjC, but it should get you started. You can manipulate QuickTime movies, put up windows, test frameworks, and even write full-blown applications. For further reading, check out the PyObjC documentation and the Python documentation. This is a valuable tool to have in your kit, whether for testing, rapid prototyping, or just trying things out.
That's it for this week. Come back in seven days for another exciting edition. Until then, send in your ideas for topics to cover. Friday Q&A is driven by your ideas, so send them in!
Comments:
I frequently find myself firing up Python to see if a particular API works the way I expect, or just to figure out something without needless code-compile-debug cycles.
Another useful addition is using the ipython shell (http://ipython.scipy.org/moin/) which adds tab completion, improved history, and some other niceties. This makes a very easy environment for testing out new ideas quickly.
Also it's important to note that PyObjC is unfortunately not garbage collection safe, so if you are working with a GC only framework this method is off the table. (See this thread from the PyObjC list: http://bit.ly/6GqADe).
I have been toying with the same concept, but using MacRuby instead of PyObjC. I was able to get most of you examples running in MacRuby as well. This approach makes a lot of sense for prototyping and for exploring the System libraries.
Again, thanks for the post.
Jose Vazquez: There are a lot of options here, and Python+PyObjC is only one. Ruby, Perl, Nu, F-Script, are all other reasonable choices. I do Python simply because it's what I know best.
bundle.classNamed_('WebView')
#!/usr/bin/python
from AppKit import *
from Foundation import *
import objc
NSApplicationLoad()
True
bundle = NSBundle.bundleWithPath_('/Library/Frameworks/MyFramework.framework')
controller = bundle.classNamed_('MyController').alloc().initWithWindowNibName_('MyWindow')
controller.showWindow_(0)
Comments RSS feed for this page
Add your thoughts, post a comment:
Spam and off-topic posts will be deleted without notice. Culprits may be publicly humiliated at my sole discretion.