PyQt4 : QNetworkAccessManager, QEventLoop et synchronisation

parse error

Je me suis pas mal emm..amusé ces derniers jours avec PyQt4. L'énoncé du problème était pourtant assez simple : récupérer des infos d'un service web tout en gardant l'interface réactive. J'ai finalement trouvé la solution hier soir, et je me suis dit que ça pourrait peut-être en aider certains.

La première étape consiste à lancer une requête vers un service web et à récupérer le résultat. Qt4 facilite pas mal la tâche au développeur avec une classe appelée QNetworkAccessManager (du module QtNetwork) qui permet -entre autre- de faire ce genre de choses grâce à sa méthode get.

Petite difficulté à appréhender : la requête est asynchrone. Elle va donc se faire en arrière plan, et son résultat ne sera disponible (et exploitable) qu'au moment où notre QNetworkAccessManager émettra le signal finished. Cela signifie aussi que, pendant ce temps, les instructions suivantes du programme seront executées. Et, ô malheur, si ces instructions font appel au résultat de la requête... elles vont planter, tout simplement (éh oui, le résultat n'est pas forcément encore disponible) !

En rendant la requête totalement synchrone, on crée un autre problème : le programme reste bloqué jusqu'à l'obtention de la réponse à la requête, ce qui, dans certains cas, peut durer un certain moment.

Alors ? Alors la solution consiste bien à rendre notre requête synchrone (on a pas trop le choix) mais en l'executant dans une nouvelle boucle d'évènements (QEventLoop). Celle-ci aura pour effet de bloquer le programme jusqu'à la sortie de cette boucle, mais sans pour autant bloquer les évènements qui pourraient survenir dans la boucle d'évènements principale (celle dans laquelle s'execute notre interface graphique). Notre GUI reste donc réactive (elle ne freeze pas) et le programme attend sagement les données venant de la requête. Pari gagné !

Cette méthode est proche d'une méthode connue appelée busy waiting ou spinning, qui consiste à attendre, dans une boucle "infinie", qu'une condition soit remplie. Lorsque la condition est remplie, on sort de la boucle et on continue à exécuter les instructions suivantes. Cette méthode est très coûteuse en CPU, puisque le programme vérifie sans cesse si la condition est remplie ou non. Elle est donc à éviter.

Dans notre cas, Qt4 nous permet de réaliser une opération similaire mais beaucoup moins coûteuse, grâce à sa QEventLoop et aux signaux. La condition qui permet de poursuivre l'exécution du programme n'a pas besoin d'être vérifiée sans cesse : elle envoie un signal quand elle est remplie !

Bien entendu, j'ai mis tout ça dans une petite classe. Notez au passage que le QNetworkAccessManager ne dispose pas de système de timeout. Il faut l'implémenter soit même !

from PyQt4.QtCore import pyqtSlot, QObject, QEventLoop, QUrl, QTimer
from PyQt4.QtNetwork import QNetworkAccessManager, QNetworkRequest
from myown.Exceptions import NetworkException

class HttpRequest(QObject):
    """
    """
    def __init__(self):
        """
        """
        QObject.__init__(self)
        self.manager = QNetworkAccessManager()
        self.timer = QTimer()
        self.loop = QEventLoop()
        self.reply = None

        self.timer.setSingleShot(True)

        self.timer.timeout.connect(self.loop.quit)
        self.manager.finished.connect(self.loop.quit)

    def request(self, url):
        """
        Sends an HTTP request to the provided URL.
        """
        self.reply = self.manager.get(QNetworkRequest(QUrl(url)))
        self.reply.error.connect(self.handleError)

        self.timer.start(5000)
        self.loop.exec_()

        if self.timer.isActive():
            # Download completed before the end of the timer.
            self.timer.stop()
            r = self.handleReply()
        else:
            # Timeout.
            raise NetworkException("Timeout error.")

        return r

    @pyqtSlot()
    def handleError(self):
        """
        """
        raise NetworkException(self.reply.errorString())
        self.reply.deleteLater()

    def handleReply(self):
        """
        """
        raw_data = self.reply.readAll()
        self.reply.deleteLater()

        return raw_data

# Utilisation :

req = HttpRequest()

reply = req.request("http://francois.kubler.org/")

# Convert reply (a QByteArray) into unicode string and print :
print unicode(reply)

Et voilà ! Mon apprentissage de Python et de Qt4 se poursuit de jour en jour... Et je dois dire que je prends énormément de plaisir à travailler avec !