PyKDE4: new style signals and slots

Those who use PyQt and PyKDE4 are certainly familiar with the syntax used to connect signals and slots:

from PyQt4 import QtCore
from PyQt4 import QtGui
from PyKDE4 import kdeui

class MyGUI(QtGui.QWidget):

    def __init__(self, parent=None):
        super(MyGUI, self).__init__(parent)
        self.pushbutton = kdeui.KPushButton()
        self.pushbutton.setText("Push me!")

        QObject.connect(self.pushbutton, QtCore.SIGNAL("clicked()"),
                               self.button_pushed)

    def button_pushed(self):
        print "Button clicked"

The main advantage of this syntax is that it’s very close to the C++ equivalent, and so you can translate easily from C++ to Python. Unfortunately the advantages of this syntax end here. The disadvantages, at least from a Python coding perspective, outweigh the advantages:

  • It’s extremely error-prone: make a typo, and not only your signal won’t be connected, but you won’t even get a warning, your program will simply do nothing;
  • In case you have overloaded signals, you have to type the exact signature, going back to the first problem;
  • It’s not Pythonic at all.

So, in recent PyQt versions (and thus also in PyKDE4) a new style approach was introduced (although the old style is always present should it be the need to). Using the new style, the signals become a property of the object that emits them. and then you use the connect function of that property. Here’s the example using the new style-signals:

from PyQt4 import QtCore
from PyQt4 import QtGui
from PyKDE4 import kdeui

class MyGUI(QtGui.QWidget):

    def __init__(self, parent=None):
        super(MyGUI, self).__init__(parent)
        self.pushbutton = kdeui.KPushButton()
        self.pushbutton.setText("Push me!")
        # New style
        self.pushbutton.clicked.connect(self.button_pushed)

    def button_pushed(self):
        print "Button clicked"

As you can see it’s much clearer, and much more Pythonic. Also, typos will trigger an AttributeError, which means you’ll be able to track where the problem is.

What about overloaded signals? Normally the first defined is the default, but you can use a dictionary-like syntax to access other overloads (signal names are completely made up here):

# One signal is without arguments, the other has a bool

# Signal without arguments
self.my_widget.connected.connect(self.handle_errors)
# Signal with a book
self.my_widget.connected[bool].connect(self.handle_errors)

Signals are emitted with the emit() function and disconnected with the disconnect() function:

# Emit a signal
self.pushbutton.clicked.emit()
# Emit a signal with a value (an int)
self.my_widget.valueChanged.emit(int)
# Disconnect another
self.my_tabwidget.currentIndexChanged.disconnect()

To define new signals, you can use the pyqtSignal function, specifying which values will the signal take (if any): just define that as a class constant (like in the example) and then you can access them like the wrapped ones:


class MyWidget(QWidget):

    # Signal with no arguments
    operationPerformed = QtCore.pyqtSignal()

    # Signal that takes arguments
    valueChanged = QtCore.pyqtSignal(int)

I merely scratched the surface with this. For more information, check out PyQt’s reference manual, which also covers other cases.

The world of KIO metadata – checking the HTTP response from a server

Recently, I investigated how to perform some checks on web addresses using KIO for Danbooru Client. My old code was synchronous, so it blocked the application while checking, thus causing all sort of troubles (UI freezing, etc.). Therefore, making the switch to KIO was the best solution. However, I had one problem: how could I check the HTTP response?

I knew already that the various ioslaves can store metadata, consisting of key-value pairs which are specific on the slave used. Normally you can get the whole map by accessing the metaData function of the job you have used, in the slot connected from the result signal. For some reason, however, in PyKDE4 calling metaData() triggers an assert in SIP, which ends in a crash (at least in my application; I stil need to debug further). KIO jobs have also the queryMetaData function, which returns the value of the key you have queried. Unfortunately, there was no way I could find the name.

Thus began my search for the right key. Googling didn’t help, and on IRC I got the first answers I needed but not enough to reach the goal. Until I saw a commit by David Faure in trunk/kdelibs/kio/ which touched a file called DESIGN.metadata (link is for the branch version). After checking with webSVN, that was exactly the thing I was looking for! It lists all the keys for the metadata, indicating also to which ioslave they begin. After that, the solution was easy.

Of course I’m not leaving you hanging there and now I’ll show you how, in PyKDE4, you can quickly check for the server response:

from PyKDE4.kio import KIO
from PyQt4.QtCore import SIGNAL
[...]

class my_widget(QWidget):
[...]

    def check_address(self, url):

        # You can add optional flags such as KIO.HideProgressInfo
        job = KIO.get(KUrl(url))
        self.connect(job, SIGNAL("result (KJob *)"), self.slot_result)

    def slot_result(self, job):

        if job.error():
            # Bail out if there's an error
            return    

        # Get the HTTP response through queryMetaData
        http_response = job.queryMetaData("responsecode")
        print "Got response: %s" % unicode(http_response)

This snippet does a few things. Firstly, it gets the specified URL, using KIO.get (KIO.stat doesn’t set the required metadata). Notice that the call is not wrapped in the new-style PyQt API because result (KJob *) isn’t wrapped like that (there’s a bug open for that). In any case, the signal passes to the connecting slot (slot_result) where we first check if there’s an error (perhaps the address didn’t exist?) and then we use queryMetaData("responsecode") to get the actual response code.

If you want to do error checking basing on the result, bear in mind that KIO operates asynchronously, so you should use a signal to tell your application that the result is what it expected or not.

I wonder if this should be documented in Techbase…

Learning by example

With my brand-new SVN account, I just committed some code to kdeexamples, KDE’s example code module. In particular, I committed a simple example which shows how to use KConfigXT via PyKDE4, a simplified version of what I wrote about here.

As most of KDE is C++, and the Python API docs are translated directly from the C++ API docs, it is essential to have good examples to help newcomers learn faster. There are some PyKDE4 examples in the kdebindings module already, but I put mine in kdeexamples for a number of reasons:

  • Clear purpose: kdeexamples is meant exactly for this – example code;
  • Visibility: A central place to find KDE examples even for bindings is optimal, makes easier to find what one is looking for.

Visibility is also important as currently the examples are rather buried inside kdebindings, and as far as I know they aren’t included in the packages of some distributions (at least not openSUSE; YMMV).

I decided to take this route because PyKDE4 is basically the job of one person (Simon Edwards): he does already a great job, but the work is too much for a single person to handle. And due to shortage of human resources, PyKDE4 lacks examples and documentation, and thus it’s not always easy to understand how to use the C++ API in Python. Writing snippets of working code, with extensive comments, is a step in the good direction. And also an opportunity to contribute back to KDE after all these years!

For now there’s just KConfigXT, but I plan on tackling KIO next, as soon as I have time. Of course, help is welcome!

Danbooru Client 0.5 is out

Sometimes answering apparently harmless questions on instant messaging can have unexpected results. In particular, I was telling about Danbooru Client to someone and a question popped up "Why don’t you support pages?". It seemed a nice idea, so I branched off the code (yay for git!) and started working on it.

Well, it took me more than a month to get this thing done… I didn’t spend every day coding, but it was a challenge. Glad it’s over now, which means that Danbooru Client 0.5 is finally available. Grab it at the usual place on kde-apps.org.

Changes in this version:

  • Massive code refactoring and documentation
  • Support for multiple pages: the same query can be repeated multiple pages (shown in a tabbed interface), kind of like browsing the actual Danbooru board;
  • Rating information added to the API;
  • Support for translations (thanks to Pino "pinotree" Toscano for the help): the tarball now contains a .pot file which can be used for translating Danbooru Client. If you make a translation, send the .po file my way and I’ll include it in the next version.

Improvements that I have in the queue:

  • Suppport for pools (every board out there changes the API, so it will require some work);
  • Support for storing password/username using KWallet (through python-keyring, so it works even without KWallet installed);
  • Review usability of the dialogs (I have a separate branch for that);
  • Improve the image download dialog.

On recent KDE SC versions (4.4 beta 2 and onwards) there are some painting issues with regards to the thumbnails, but I’m not sure if the fault is in PyKDE4 or in the underlying libraries. Nothing too bad, luckily: hovering the thumbnails or giving focus to the thumbnail view should be what’s needed.

Here’s a screenshot of the new interface (click to enlarge):

Screenshot of the new interface

Comments and suggestions are always welcome, so don’t hesitate to drop me a line.

As a final word, some thoughts on the work required to get this out of the door. My largest issues are related to garbage collection: Python’s reference counting based GC got a lot in the way, at least because of how the underlying C++ structures work. I had to work a bit to keep references to objects around so I wouldn’t get crashes (accessing an already deleted object). All is well now, and I think my Python/PyQt/PyKDE4 knowledge gained from it. I keep telling myself that I should be writing some tutorials one day…