Dugan Chen's Homepage

Various Things

A PyQt implementation of JQuery’s Promises and Deferreds

I tried to implement jQuery’s Promises and Deferreds in PyQt. I was moderately successful.

The use case here is chaining QNetworkAccessManager calls. When you have the name of an artist, it takes two calls to fetch the artist’s photo from last.fm. The first call fetches a reply that has the image URL. The second call fetches the image.

I want to see how feasible it would be to do it the way you would in jQuery. Here is an actual line that you’re about to see:

1
2
service.getArtistInfo(artist).then(getUrl)\
    .then(fetchArt)

In the above, getUrl and fetchArt are both callbacks.

The following example builds on my last entry, only with the artist’s bio replaced with the artist’s photo.

As mentioned, I was only moderately successful. When you write PyQt, you’re writing C++ in Python, and I had to fight the two languages the entire way. To deal with the fact that C++ is strongly-typed, I made the signals and callbacks always pass and take unicode strings. I also had to pay attention to Qt’s memory management model, where objects are deleted when their parents are.

The resulting example, with all its callbacks, is arguably even uglier and less readable than normal PyQt code would be. And no, I’m not going to try to unit test it. Yes, I realize that “I’m not going to try to unit test it” actually means “this isn’t actually usable.”

Nevertheless, here it is:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
#!/usr/bin/env python
 
from sip import setapi
setapi("QDate", 2)
setapi("QDateTime", 2)
setapi("QTextStream", 2)
setapi("QTime", 2)
setapi("QVariant", 2)
setapi("QString", 2)
setapi("QUrl", 2)
 
 
from PyQt4.QtCore import (QObject, Qt, QUrl,
                          pyqtSignal)
from PyQt4.QtGui import (QApplication, QComboBox,
                         QLabel, QPixmap,
                         QMainWindow,
                         QVBoxLayout, QWidget)
from PyQt4.QtNetwork import (QNetworkAccessManager,
                             QNetworkRequest)
import base64
import json
import urlparse
import sys
 
 
def main():
 
    app = QApplication(sys.argv)
    view = BigFourView()
    service = ArtistInfoService()
    presenter = BigFourPresenter(view, service)
    view.artistIndexChanged.connect(
        presenter.loadArtistAtIndex)
    presenter.loadArtistAtIndex(0)
    view.show()
    sys.exit(app.exec_())
 
 
class BigFourView(QMainWindow):
 
    artistIndexChanged = pyqtSignal(int)
 
    def __init__(self, parent=None):
        super(BigFourView, self).__init__(parent)
 
        layout = QVBoxLayout()
 
        artistBox = QComboBox()
        artistBox.currentIndexChanged.connect(
            self._onArtistIndexChanged)
        artistBox.addItem('Anthrax')
        artistBox.addItem('Megadeth')
        artistBox.addItem('Metallica')
        artistBox.addItem('Slayer')
        layout.addWidget(artistBox)
        self._artistBox = artistBox
 
        self._photoLabel = QLabel()
        self._photoPixmap = QPixmap()
        self._photoLabel.setPixmap(
            self._photoPixmap)
        layout.addWidget(self._photoLabel)
 
        widget = QWidget()
        widget.setLayout(layout)
        self.setCentralWidget(widget)
        text = self.tr('Artist Bios')
        self.setWindowTitle(text)
 
    def artistData(self, index):
        return self._artistBox.itemData(
            index, Qt.DisplayRole)
 
    def artistIndex(self):
        return self._artistBox.currentIndex()
 
    def _onArtistIndexChanged(self, index):
        self.artistIndexChanged.emit(index)
 
    def setPixmapData(self, data):
        self._photoPixmap.loadFromData(data)
        self._photoLabel.setPixmap(
            self._photoPixmap)
 
 
class BigFourPresenter(object):
 
    def __init__(self, view, artistInfoService):
        self._view = view
        self._artistInfoService = artistInfoService
 
    def loadArtistAtIndex(self, index):
        artist = self._view.artistData(index)
 
        service = self._artistInfoService
 
        def getUrl(promise, data):
            deserialized = json.loads(data)
            artist = deserialized['artist']
            url = artist['image'][-1]['#text']
            promise._resolved.emit(url)
            promise.deleteLater()
 
        def fetchArt(promise, url):
 
            # Prevent the Promise from being
            # garbage-collected.
            promise.setParent(self._view)
 
            def artCallback(data):
                self._view.setPixmapData(data)
                promise.deleteLater()
            service.getArtistPhotoReply(
                url, artCallback)
 
        # A chain of network calls. The line that
        # justifies this example.
        service.getArtistInfo(artist)\
            .then(getUrl).then(fetchArt)
 
    def _onArtistDataReceive(self, data):
        bio = data['artist']['bio']['content']
        self._view.setBioHtml(bio)
 
 
class Promise(QObject):
 
    _resolved = pyqtSignal(unicode)
 
    def __init__(self, callback, parent=None):
        super(Promise, self).__init__(parent)
        self._callback = callback
 
    def then(self, callback):
        promise = Promise(callback, self)
        self._resolved.connect(promise.resolve)
        return promise
 
    def resolve(self, data):
        self._callback(self, data)
 
 
class ArtistInfoService(QObject):
 
    def __init__(self, parent=None):
        super(ArtistInfoService, self).__init__(
            parent)
        self._apiKey = base64.b64decode(
            'Mjk1YTAxY2ZhNjVmOWU1MjFiZGQyY2Mz'
            'YzM2ZDdjODk=')
 
        self._network = QNetworkAccessManager(self)
 
    def getArtistInfo(self, artist):
 
        qurl = QUrl()
        url = urlparse.urlunsplit([
            'http', 'ws.audioscrobbler.com',
            '2.0/', '', ''])
        qurl.setEncodedUrl(url)
        qurl.addEncodedQueryItem('method',
                                 'artist.getinfo')
        qurl.addEncodedQueryItem('artist', artist)
        qurl.addEncodedQueryItem('api_key',
                                 self._apiKey)
        qurl.addEncodedQueryItem('format', 'json')
        request = QNetworkRequest(qurl)
        reply = self._network.get(request)
 
        def callback(myPromise, replyText):
            myPromise._resolved.emit(replyText)
            myPromise.deleteLater()
 
        promise = Promise(callback, self)
 
        def handleReply():
            newReply = self.sender()
            newReply.deleteLater()
            data = reply.readAll().data()
            promise.resolve(data)
 
        reply.finished.connect(handleReply)
 
        return promise
 
    def getArtistPhotoReply(self, url, callback):
        qurl = QUrl.fromEncoded(url)
        request = QNetworkRequest(qurl)
        reply = self._network.get(request)
 
        def handleReply():
            newReply = self.sender()
            newReply.deleteLater()
            data = newReply.readAll()
            callback(data)
        reply.finished.connect(handleReply)
 
if __name__ == '__main__':
    main()