My team has been working on porting some PyQt stuff to Mac OSX, and we have run
into several Qt bugs, sadly. Here are two, and the workarounds we found.
When you use Widgets inside the items of a QTreeWidget (which I know, is not a common case, but
hey, it happens), the widgets don't scroll with the items.
Solution: use the -graphicssystem raster options. You can even inject them into
argv if the platform is darwin.
Three years ago, I started a series of long posts called "PyQt by Example". It reached five posts
before I abandoned for a series of reasons that don't matter anymore. That series is coming back
starting next week, rewritten, improved and extended.
It will do so in a new site, and the "old" posts will be retired to an archive page. Why? Well,
the technologies used in some of them are obsolete or don't quite work nowadays. So, the new
versions will be the preferred ones.
And while I am not promising anything, I have enough written to make this something quite longer,
more nicely layouted, more interesting and make it cover more ground. BUT, while doing some
checks on the traffic statistics for the old posts, some things popped out.
This was very popular
About 60% of my site's traffic goes to those five posts. Out of about 1200 posts over
12 years, 60% of the viewers go to the 0.4% of the pages. That is a lot.
It's a long tail
The traffic has not decreased in three years. If anything, it has increased
So, all this means there is a desire for PyQt documentation that is not satisfied. I am not
surprised: PyQt is great, and the recommended book is not free, so there is bound to be a lot
of demand.
And, here's the not-so-rosy bit: I had unobtrusive, relevant, out-of-the-way-but-visible ads
in those pages for more than two years. Of the 70000 unique visitors, not even one clicked on
an ad. Don't worry, I was not expecting to get money out of them (although I would love to
some day collect a $100 check instead of having google hold my money for me ad eternum).
But really? Not even one ad click? In more than two years, thousands of people? I have to
wonder if I just attract cheap people ;-)
One of the nice things about working at Canonical is that we produce open source software.
I, specifically, work in the team that does the desktop clients for Ubuntu One which is a really cool job, and a really cool piece
of software. However, one thing not enough people know, is that we offer damn nice APIs
for developers. We have to, since all our client code is open source, so we need those
APIs for ourselves.
So, here is a small tutorial about using some of those APIs. I did it using Python and
PyQt for several reasons:
Both are great tools for prototyping
Both have good support for the required stuff (DBus, HTTP, OAuth)
It's what I know and enjoy. Since I did this code on a sunday, I am
not going to use other things.
Having said that, there is nothing python-specific or Qt-specific in the code. Where
I do a HTTP request using QtNetwork, you are free to use libsoup, or whatever.
So, on to the nuts and bolts. The main pieces of Ubuntu One, from a infrastructure
perspective, are Ubuntu SSO Client, that handles user registration and login, and
SyncDaemon, which handles file synchronization.
To interact with them, on Linux, they offer DBus interfaces. So, for example, this is
a fragment of code showing a way to get the Ubuntu One credentials (this would normally
be part of an object's __init__):
# Get the session busbus=dbus.SessionBus():::# Get the credentials proxy and interfaceself.creds_proxy=bus.get_object("com.ubuntuone.Credentials","/credentials",follow_name_owner_changes=True)# Connect to signals so you get a call when something# credential-related happensself.creds_iface=dbus.Interface(self.creds_proxy,"com.ubuntuone.CredentialsManagement")self.creds_proxy.connect_to_signal('CredentialsFound',self.creds_found)self.creds_proxy.connect_to_signal('CredentialsNotFound',self.creds_not_found)self.creds_proxy.connect_to_signal('CredentialsError',self.creds_error)# Call for credentialsself._credentials=Noneself.get_credentials()
You may have noticed that get_credentials doesn't actually return the credentials. What
it does is, it tells SyncDaemon to fetch the credentials, and then, when/if they are there,
one of the signals will be emitted, and one of the connected methods will be called. This
is nice, because it means you don't have to worry about your app blocking while SyncDaemon
is doing all this.
But what's in those methods we used? Not much, really!
defget_credentials(self):# Do we have them already? If not, get'emifnotself._credentials:self.creds_proxy.find_credentials()# Return what we've got, could be Nonereturnself._credentialsdefcreds_found(self,data):# Received credentials, save them.print"creds_found",dataself._credentials=data# Don't worry about get_quota yet ;-)ifnotself._quota_info:self.get_quota()defcreds_not_found(self,data):# No credentials, remove old ones.print"creds_not_found",dataself._credentials=Nonedefcreds_error(self,data):# No credentials, remove old ones.print"creds_error",dataself._credentials=None
So, basically, self._credentials will hold a set of credentials, or None. Congratulations, we
are now logged into Ubuntu One, so to speak.
So, let's do something useful! How about asking for how much free space there is in
the account? For that, we can't use the local APIs, we have to connect to the servers, who
are, after all, the ones who decide if you are over quota or not.
Access is controlled via OAuth. So, to access the API, we need to sign our requests. Here
is how it's done. It's not particularly enlightening, and I did not write it, I just use it:
defsign_uri(self,uri,parameters=None):# Without credentials, return unsigned URLifnotself._credentials:returnuriifisinstance(uri,unicode):uri=bytes(iri2uri(uri))print"uri:",urimethod="GET"credentials=self._credentialsconsumer=oauth.OAuthConsumer(credentials["consumer_key"],credentials["consumer_secret"])token=oauth.OAuthToken(credentials["token"],credentials["token_secret"])ifnotparameters:_,_,_,_,query,_=urlparse(uri)parameters=dict(cgi.parse_qsl(query))request=oauth.OAuthRequest.from_consumer_and_token(http_url=uri,http_method=method,parameters=parameters,oauth_consumer=consumer,token=token)sig_method=oauth.OAuthSignatureMethod_HMAC_SHA1()request.sign_request(sig_method,consumer,token)print"SIGNED:",repr(request.to_url())returnrequest.to_url()
And how do we ask for the quota usage? By accessing the https://one.ubuntu.com/api/quota/ entry point
with the proper authorization, we would get a JSON dictionary with total and used space.
So, here's a simple way to do it:
# This is on __init__self.nam=QtNetwork.QNetworkAccessManager(self,finished=self.reply_finished):::defget_quota(self):"""Launch quota info request."""uri=self.sign_uri(QUOTA_API)url=QtCore.QUrl()url.setEncodedUrl(uri)self.nam.get(QtNetwork.QNetworkRequest(url))
Again, see how get_quota doesn't return the quota? What happens is that get_quota will
launch a HTTP request to the Ubuntu One servers, which will, eventually, reply with the data.
You don't want your app to block while you do that. So, QNetworkAccessManager will call
self.reply_finished when it gets the response:
What else would be nice to have? How about getting a call whenever the status of syncdaemon
changes? For example, when sync is up to date, or when you get disconnected? Again, those are
DBus signals we are connecting in our __init__:
self.status_proxy=bus.get_object('com.ubuntuone.SyncDaemon','/status')self.status_iface=dbus.Interface(self.status_proxy,dbus_interface='com.ubuntuone.SyncDaemon.Status')self.status_iface.connect_to_signal('StatusChanged',self.status_changed)# Get the status as of right nowself._last_status=self.process_status(self.status_proxy.current_status())
The process_status function is boring code to convert the info from
syncdaemon's status into a human-readable thing like "Sync is up-to-date". So we
store that in self._last_status and update the menu.
What menu? Well, a QSystemTrayIcon's context menu! What you have read are the main pieces
you need to create something useful: a Ubuntu One tray app you can use in KDE, XFCE or openbox.
Or, if you are on unity and install sni-qt, a Ubuntu One app indicator!
So, you are writing a PyQt app, and you want it to support command line arguments. So you do something like this:
opt_parser=OptionParser()opt_parser.add_option("-q",dest="quickly",action="store_true",help="Do it quickly (default=False)")(options,args)=opt_parser.parse_args(sys.argv)app=QApplication(sys.argv):::
Or maybe even QApplication([]). Ok, you are doing it wrong. And this is wrong in most tutorials, too. Why? Because Qt (and thus PyQt) supports a bunch of useful command line options already. So if you do it like in the first listing, and pass "-style=oxygen" or whatever, one of the following will happen.
OptParser is going to tell you it's not a valid option and abort
You will ignore the option and not do anything useful with it
You will have your own -style option and do two things with it
All three outcomes are less than ideal.
The right way to do this is:
opt_parser=OptionParser()opt_parser.add_option("-q",dest="quickly",action="store_true",help="Do it quickly (default=False)")app=QApplication(sys.argv)(options,args)=opt_parser.parse_args(app.arguments()):::
This way, you give PyQt a chance to process the options it recognizes, and, then, you get to handle the rest, because app.arguments() has all Qt options removed.
The bad side of this is, you will make --help slightly slower, since it will have to build a QApplication to do nothing, and you will have undocumented options. Solution for both problems left as an exercise.
Some of you may know I have written about 30% of a book, called "Python No Muerde", available at http://nomuerde.netmanagers.com.ar (in spanish only).That book has stagnated for a long time.
On the other hand, I wrote a very popular series of posts, called PyQt by Example, which has (you guessed it) stagnated for a long time.
The main problem with the book was that I tried to cover way too much ground. When complete, it would be a 500 page book, and that would involve writing half a dozen example apps, some of them in areas I am no expert.
The main problem with the post series is that the example is lame (a TODO app!) and expanding it is boring.
¡So, what better way to fix both things at once, than to merge them!
I will leave Python No Muerde as it is, and will do a new book, called PyQt No Muerde. It will keep the tone and language of Python No Muerde, and will even share some chapters, but will focus on developing a PyQt app or two, instead of the much more ambitious goals of Python No Muerde. It will be about 200 pages.
I have acquired permission from my superiors (my wife) to work on this project a couple of hours a day, in the early morning. So, it may move forward, or it may not. This is, as usual, an experiment, not a promise.