Twisted i PyGTK
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()