Twisted i PyGTK

data dodania: 2008-08-20 22:33:36
etykiety: pygtk python twisted

Twisted to framework sterowany zdarzeniami. Umożliwia on pisanie programów, które wykonują kilka(dziesiąt) operacji jednocześnie. Charakterystyczną cechą Twisted jest to, że nie wykorzystuje on do tego wątków (chociaż można ich używać przy pomocy deferToThread), zamiast tego używa własnej pętli zdarzeń.

Sercem tak napisanego programu jest obiekt "twisted.internet.defer.Deferred". W skrócie pisanie programu w twisted można opisać tak (jest to skrót ale pokazuje główną zasadę): Kiedy piszemy funkcję której wynik może być dostępny po dłuższym czasie, zamiast czekać i blokować wykonywanie programu, zwracamy odrazu obiekt klasy Deferred. Do takiego obiektu dodajemy wywołanie przy pomocy addCallback(self, callback, *args, kw), które wykona się w momencie gdy wynik będzie dostępny.

Twisted to rozbudowany framework o bardzo dużych możliwościach, dlatego polecam zapoznanie się z dokumentacją

Przykład programu w twisted z interfejsem graficznym w pygtk (z którym bardzo dobrze współpracuje). Chciałbym zaznaczyć, że dopiero uczę się Twisted i nie gwarantuje, że wszystko jest tu "zrobione tak jak należy", jeśli ktoś ma jakieś uwagi to pisać :)

#!/usr/bin/python
# -*- coding: utf-8 -*-

from twisted.internet import gtk2reactor
# instalujemy specjalną wersję reactora która jest stowrzona do obsługi programów z interfejsem w pygtk
gtk2reactor.install()
from twisted.internet import reactor
from twisted.web.client import HTTPDownloader

import pygtk
pygtk.require('2.0')
import gtk

import os.path
import urlparse
import time


class MyHTTPDownloader(HTTPDownloader):
    # HTTPDownloader to klasa która pobiera dane i zapisuje je do pliku
    # MyHTTPDownloader dziedziczy po niej i dodaje do niektórych metod dodatkową funkcjonalność
    # jak np aktualizacje progressbara
    progressBar = None
    size = None
    sizeDelta = 999
    time = 0
    speed = 0
    def buildProtocol(self, addr):
        self.protocolInstance = HTTPDownloader.buildProtocol(self, addr)
        return self.protocolInstance

    def pagePart(self, data):
        # metoda wywoływan po pobraniu fragmentu danych, fragment ten znajduje sie w data
        HTTPDownloader.pagePart(self, data)
        if self.size != None and self.progressBar:

            size = self.size/1048576.0
            length = self.protocolInstance.length/1048576.0
            downloaded = size-length

            t = time.time()
            dt = t - self.time 
            if dt >= 2.0:
                self.sizeDelta =  (self.sizeDelta - length)*1024
                self.speed = self.sizeDelta/dt
                self.time = t
                self.sizeDelta = length

            # ustawiamy wypełnienie progresbara na odpowiednią wartość
            # i wpisujemy rozmair pobranego pliku, szybkość poberania itp
            self.progressBar.set_fraction((downloaded/size))
            self.progressBar.set_text( "%0.1f/%0.1fMB\t%0.1f%%\t%0.1fkB/s" % (downloaded, size, downloaded/size*100.0, self.speed ) )
        elif self.progressBar:
            self.progressBar.pulse()

    def gotHeaders(self, headers):
        HTTPDownloader.gotHeaders(self, headers)
        # mamy pobrany nagłówek, rozmiar pliku jest w self.length, jeśli serwer nie podaje długości pliku length = None
        # zapamiętujemy tę wartość, ponieważ length zmienia się (jeśli jest rózne od None) o rozmiar fragmentu pobranych danych (aż do zera)
        self.size = self.protocolInstance.length
        if self.size == None:
            # jeśli nie znamy rozmiaru pliku zakładamy że zakończnie transmisji oznacza pobranie całego pliku
            self.value = True
            if self.progressBar:
                self.progressBar.set_text( "Please wait..." )
        else:
            range_ = self.headers.get("range", None)
            if range_:
                self.size += float(range_.split("=")[1][:-1])


    def pageEnd(self):
        # metoda wywoływana gdy cały plik został pobrany
        if self.protocolInstance.length != None and self.protocolInstance.length == 0:
            # jeśli znamy rozmiar pliku a length jest równe 0 oznacza to że pobraliśmy cały plik
            self.value = True
        # metoda klasy bazowej musi być wywołana po ewentualnym ustawieniu self.value
        # ponieważ pageEnd wywołuje zdarzenie: self.deferred.callback(self.value) jeśli pobieranie powiodło się
        # self.deferred.errback(failure.Failure()) w innym przypadku
        HTTPDownloader.pageEnd(self)


def getFile(url, dest, progressBar=None):
    # funkcja pomocnicza do tworzenia instancji klasy pobierającej plik i tworzenia połączenia
    factory = MyHTTPDownloader(url,
                               os.path.join(dest, urlparse.urlparse(url)[2].split("/")[-1]),
                               supportPartial=1,
                               )
    factory.progressBar = progressBar
    reactor.connectTCP(urlparse.urlparse(url)[1], 80, factory)
    return factory


class TG(object):
    def __init__(self):
        self.factory = None

        self.window = gtk.Window(gtk.WINDOW_TOPLEVEL)
        self.window.set_title("TG")
        self.window.connect("delete_event", self.delete_event)
        self.window.connect("destroy", self.destroy)

        self.box = gtk.VBox()
        self.box.show()

        self.url_entry = gtk.Entry()
        self.url_entry.set_text("http://python.org/ftp/python/2.5.2/Python-2.5.2.tgz")
        self.box.add(self.url_entry)
        self.url_entry.show()

        self.dest_entry = gtk.Entry()
        self.dest_entry.set_text("/tmp/")
        self.box.add(self.dest_entry)
        self.dest_entry.show()        

        self.progressbar = gtk.ProgressBar()
        self.progressbar.set_pulse_step(0.01)
        self.box.add(self.progressbar)
        self.progressbar.show()

        self.button_box = gtk.HBox()
        self.button_box.show()
        self.box.add(self.button_box)

        self.start_button = gtk.Button("Start")
        self.button_box.add(self.start_button)
        self.start_button.connect("clicked", self.start)
        self.start_button.show()

        self.pause_button = gtk.Button("Pause")
        self.button_box.add(self.pause_button)
        self.pause_button.connect("clicked", self.pause_or_resume)        
        self.pause_button.show()

        self.cancel_button = gtk.Button("Stop")
        self.button_box.add(self.cancel_button)
        self.cancel_button.connect("clicked", self.stop)        
        self.cancel_button.show()

        self.window.add(self.box)
        self.window.show()

    def start(self, widget):
        if self.factory:
            return
        # zaczynamy pobieranie pliku
        self.factory = getFile(url=self.url_entry.get_text(),
                               dest=self.dest_entry.get_text(),
                               progressBar=self.progressbar)

        def show_success(value):
            # jesli value wynosi False oznacza to że transmisja zakończyła się poprawnie, lecz nie pobraliśmy całego pliku
            # taka sytuacja ma miejsce np gdy wciśniemy przycisk "Stop" przerywając tym samym pobieranie pliku
            if not value: return
            d = gtk.MessageDialog(parent=self.window,
                                  flags=gtk.DIALOG_DESTROY_WITH_PARENT,
                                  buttons=gtk.BUTTONS_CLOSE,
                                  message_format = "File downloaded successfully."
                                  )
            d.connect("response", lambda x,y,z:d.destroy(), None)
            d.run()
            self.factory = None

        def show_fail(error):
            d = gtk.MessageDialog(parent=self.window,
                                  flags=gtk.DIALOG_DESTROY_WITH_PARENT,
                                  type=gtk.MESSAGE_ERROR,
                                  buttons=gtk.BUTTONS_CLOSE,
                                  message_format = str(error.getErrorMessage())
                                  )
            d.connect("response", lambda x,y,z:d.destroy(), None)
            d.run()
            self.factory = None

        # funkcja show_success zostanie wywołana gdy pobieranie strony zakończy się sukcesem
        # zostanie jej przekazana wartość znajdująca sie w self.factory.value
        self.factory.deferred.addCallback(show_success)
        # funkcja show_fail zostanie wywołana gdy poberanie pliku z jakiegoś powodu nie powiedzie się
        # zostanie jej przekazany błąd który wystapił
        self.factory.deferred.addErrback(show_fail)


    def pause_or_resume(self, widget):
        if not self.factory: return 
        if self.factory.protocolInstance.paused:
            self.factory.protocolInstance.resumeProducing()
            self.pause_button.set_label("Pause")
        else:
            self.factory.protocolInstance.pauseProducing()
            self.pause_button.set_label("Resume")

    def stop(self, widget):
        if not self.factory: return
        # ustawiamy wartość self.factory.value na False, aby przy nieznanej długości pliku,
        # przerwanie pobierania przez użytkownika nie było wzięte za pomyślne zakończenie transmisji 
        self.factory.value = False
        self.factory.protocolInstance.stopProducing()
        self.factory = None
        self.progressbar.set_fraction(0)
        self.progressbar.set_text( "" )

    def delete_event(self, widget, event):
        return False

    def destroy(self, widget):
        if self.factory:
            self.factory.value = False
        reactor.stop()


if __name__ == "__main__":
    tg = TG()
    # zamiast głownej pętli pygtk uruchamiamy reactor twisted
    reactor.run()


Dodaj komentarz: