Eneko Alonso

un Navarro en California

Projects

¿Eres español y vives fuera de España? ¿Estás pensando en salir una temporada a trabajar o estudiar en el extranjero? Si es así, no dejes de visitar Spaniards.es, la Comunidad de Españoles en el Mundo
spaniards.es

Recent comments

11:42 America/Los_Angeles


Playing with Cocoa-Python

Today I have spent the whole morning playing with Cocoa-Python. When I upgraded to Leopard and XCode 3.0 back in December, I discovered the new Cocoa-Python and Cocoa-ruby projects (later I learned Pyobjc has been around for a few years now).

At first I though it was cool to have alternatives to Objective-C when developing software for Macs with the Cocoa library. But then I thought why would you want to use something else, when ObjC works so great.

So it has been now that I have started playing with Python when I realized that I was looking at it the wrong way: Cocoa-Python is not intended to replace ObjectiveC in any way, but intended to bring to you Python scripts the most beautiful user interface available: the Cocoa framework.

My first test

One of the first things I wanted to know is if all the scripts I have created lately, which use different libraries like htmllib, urllib2, cookielib or sqlite3 would work on my XCode project. And the answer is yes, everything seems to work! All Python libraries are available :)

The script I have been using today is a script to dump cookies from the sqlite database which Firefox 3 uses for storage to a plain text file (actually the file created is a netscape formatted cookiejar file). I created this code based on code I found on Internet:

#!/usr/bin/python # -*- coding: utf-8 -*- import sqlite3 as db # Hardcoded path to Firefox 3 cookie database cookie_file = "/Users/[your user name]/Library/Application Support/Firefox/Profiles/[your profile].default/cookies.sqlite" output_file = 'cookies.txt' conn = db.connect(cookie_file) cur = conn.cursor() cur.execute("SELECT host, path, isSecure, expiry, name, value FROM moz_cookies") f = open(output_file, 'w') ftstr = ["FALSE","TRUE"] for row in cur.fetchall(): s = "%s\t%s\t%s\t%s\t%s\t%s\t%s\n" % ( row[0], ftstr[row[0].startswith('.')], row[1], ftstr[row[2]], row[3], row[4], row[5].encode("ascii", "ignore")) f.write(s) f.close() conn.close()

So now let's make it work on XCode. At first, the new Cocoa-Python project will look like this:

# # FirefoxCookieDumperAppDelegate.py # FirefoxCookieDumper # from Foundation import * from AppKit import * class FirefoxCookieDumperAppDelegate(NSObject): def applicationDidFinishLaunching_(self, sender): NSLog("Application did finish launching.")

The original script was command line driven, so it didn't require any extra user action. For the Cocoa based version, we want to use the interface, so we will load the cookies after clicking a button and we will show the cookies on a nice table (instead of saving the data to a fileNote our application doesn't include the code for writing cookies to file.). But first, let's add the script code to our application:

# # FirefoxCookieDumperAppDelegate.py # FirefoxCookieDumper # from Foundation import * from AppKit import * import sqlite3 as db cookie_file = "/Users/[your user name]/Library/Application Support/Firefox/Profiles/[your profile].default/cookies.sqlite" class FirefoxCookieDumperAppDelegate(NSObject): def dumpCookies_(self, sender): conn = db.connect(cookie_file) cur = conn.cursor() cur.execute("SELECT host, path, isSecure, expiry, name, value FROM moz_cookies") for row in cur.fetchall(): # Display cookies here :) pass conn.close()

At this point, the application has all the functionality we need to read cookies from Firefox, but still doesn't do anything, since it doesn't interact with the user interface yet.

Interacting with the user interface

When you program in Cocoa with Interface Builder, you have to define some Outlets and Actions on your AppController class. On Objective-C you do this on the header (.h) file. So I was a little bit lost, since Python doesn't have header files.

The way to do it is directly in between you class code. Outlets are varibles declared as myOutlet = objc.IBOutlet(), which will let Interface Builder know about their existence. In order to define class methods as Actions, the line @objc.IBAction has to be added before the function itself.

We want to interact with an NSArrayController, which will pass our cookies to the NSTableView. Before, our cookies will be stored on a Python list of dictionariesI use a dict/zip combination with the list of fields and list of values to create each dictionary., each containing the content of one cookie. The keys used in the dictionary will be later used on the NSTableView columns and othe NSTextfields.

# # FirefoxCookieDumperAppDelegate.py # FirefoxCookieDumper # from Foundation import * from AppKit import * import sqlite3 as db cookie_file = "/Users/[your user name]/Library/Application Support/Firefox/Profiles/[your profile].default/cookies.sqlite" class FirefoxCookieDumperAppDelegate(NSObject): myCookieArray = objc.IBOutlet() cookies = [] @objc.IBAction def dumpCookies_(self, sender): conn = db.connect(cookie_file) cur = conn.cursor() cur.execute("SELECT host, path, isSecure, expiry, name, value FROM moz_cookies") fields = ['host', 'path', 'isSecure', 'expiry', 'name', 'value'] for row in cur.fetchall(): self.cookies.append(dict(zip(fields, row))) conn.close() def cookieArray(self): return self.cookies; cookieArray = objc.accessor(cookieArray) def setCookieArray_(self, value): self.cookies[:] = value setCookieArray_ = objc.accessor(setCookieArray_)

The last two functions are accessors that would let us link our NSArrayController to our python's cookies list. So we are doneExcept for a little bug we need to fix on this side. Now it's time to build the user interface.

Binding the user interface

Our application has a very simple interface, with NSTableView to scroll through the cookies and a fieldset to show all cookie fields.

2008-05-03_1924

Now we need to bind the table and all fields to the array controller. Next, we need to bind the array controller to our FirefoxCookieDumperAppDelegate cookieArray accessor. Don't forget to bind the Load Cookies button and we are done!

Picture 114 Picture 115 Picture 116 Picture 117 Picture 118 Picture 120

Final touch

So now you may be wondering why did we need an outlet for the NSArrayController. Well, there is a problem with our application (I'm not sure about if this is a bug or not). If we run the application and click on Load Cookies button, the screen will still empty. our cookies will be loaded, though. But the screen won't refresh.

In order to make the screen refresh properly I have used a little trick: add an object calling NSArrayController.addObject and deleting it. It's not very pretty, but it works. Please, if anyone knows how to do this better let me know :)

# # FirefoxCookieDumperAppDelegate.py # FirefoxCookieDumper # from Foundation import * from AppKit import * import sqlite3 as db cookie_file = "/Users/[your user name]/Library/Application Support/Firefox/Profiles/[your profile].default/cookies.sqlite" class FirefoxCookieDumperAppDelegate(NSObject): myCookieArray = objc.IBOutlet() cookies = [] @objc.IBAction def dumpCookies_(self, sender): conn = db.connect(cookie_file) cur = conn.cursor() cur.execute("SELECT host, path, isSecure, expiry, name, value FROM moz_cookies") fields = ['host', 'path', 'isSecure', 'expiry', 'name', 'value'] self.cookies = [] for row in cur.fetchall(): self.cookies.append(dict(zip(fields, row))) conn.close() # Trick to refresh TableView self.myCookieArray.addObject_(dict(host='ignore',path='this')) self.myCookieArray.remove_(self) def cookieArray(self): return self.cookies; cookieArray = objc.accessor(cookieArray) def setCookieArray_(self, value): self.cookies[:] = value setCookieArray_ = objc.accessor(setCookieArray_)

Here is the final capture, with all cookies dumped:

2008-05-03_1941

Conclusions

As you can see, it is very, very easy to add a Cocoa user interface to your Python scripts. It takes a while to figure it out at the beginning, but soon everything starts making sense :)

Hope you enjoyed the post!

PS: I have let the Clear button implementation for you :)

Post new comment

The content of this field is kept private and will not be shown publicly.
CAPTCHA
This question is for testing whether you are a human visitor and to prevent automated spam submissions.
Image CAPTCHA
Enter the characters shown in the image.