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:
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:
#!/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()