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() |