Por supuesto, podría hacer más, pero hasta yo tengo mis standards!
Salvo eso, hice algunos trucos sucios, pero, en este momento, es un browser bastante completo en 127 líneas de código según sloccount, así que ya jugué suficiente y mañana tengo trabajo que hacer.
Pero antes, consideremos como se implementaron algunos features (voy a cortar las líneas para que la página quede razonablemente angosta), y veamos también las versiones "normales" de lo mismo. La versión "normal" no está probada, avisen si está rota ;-)
Esto noes algo que deba aprenderse. De hecho es casi un tratado en como no hacer las cosas. Es el código menos pitónico y menos claro que vas a ver esta semana.
Es corto, es expresivo, pero es feo feo.
Voy a comentar sobre esta versión.
Soporte deProxy
Un browser no es gran cosa si no se puede usar con proxy. Por suerte el stack de red de Qt tiene buen soporte de proxy. El chiste es configurarlo.
De Vicenzo soporta proxies HTTP y SOCKS parseando la variable de entorno http_proxy
y seteando el proxy a nivel aplicación en Qt:
proxy_url = QtCore.QUrl(os.environ.get('http_proxy', ''))
QtNetwork.QNetworkProxy.setApplicationProxy(QtNetwork.QNetworkProxy(\
QtNetwork.QNetworkProxy.HttpProxy if unicode(proxy_url.scheme()).startswith('http')\
else QtNetwork.QNetworkProxy.Socks5Proxy, proxy_url.host(),\
proxy_url.port(), proxy_url.userName(), proxy_url.password())) if\
'http_proxy' in os.environ else None
Como es la versión normal de esa cosa?
if 'http_proxy' in os.environ:
proxy_url = QtCore.QUrl(os.environ['http_proxy'])
if unicode(proxy_url.scheme()).starstswith('http'):
protocol = QtNetwork.QNetworkProxy.HttpProxy
else:
protocol = QtNetwork.QNetworkProxy.Socks5Proxy
QtNetwork.QNetworkProxy.setApplicationProxy(
QtNetwork.QNetworkProxy(
protocol,
proxy_url.host(),
proxy_url.port(),
proxy_url.userName(),
proxy_url.password()))
Los abusos principales contra python son el uso del operador ternario para hacer un if de una línea (y anidarlo) y el largo de línea.
Cookies Persistentes
Esto es necesario porque querés permanecer logueado en los sitios de una sesión a otra. Para esto, primero tuve que hacer un pequeño mecanismo de persistencia, y guardar/leer los cookies de ahí.
Acá está como hice la persistencia (settings is una instancia de QSettings
global):
def put(self, key, value):
"Persist an object somewhere under a given key"
settings.setValue(key, json.dumps(value))
settings.sync()
def get(self, key, default=None):
"Get the object stored under 'key' in persistent storage, or the default value"
v = settings.value(key)
return json.loads(unicode(v.toString())) if v.isValid() else default
No es código muy raro, salvo por usar el operador ternario al final. El uso de json me asegura que mientras meta cosas razonables, voy a obtener lo mismo de vuelta, con el mismo tipo, sin necesidad de convertirlo o llamar métodos especiales.
¿Entonces, como guardo/leo los cookies? Primero se necesita acceder el "cookie jar". No encontré si hay uno global o por view, así que creé un QNetworkCookieJar
en la línea 24 y la asigno a cada página en la línea 107.
# Save the cookies, in the window's closeEvent
self.put("cookiejar", [str(c.toRawForm()) for c in self.cookies.allCookies()])
# Restore the cookies, in the window's __init__
self.cookies.setAllCookies([QtNetwork.QNetworkCookie.parseCookies(c)[0]\
for c in self.get("cookiejar", [])])
Confieso mi crimen de usar comprensiones de listas cuando la herramienta correcta era un for
.
Uso el mismo truco al restaurar los tabs abiertos, con el moco agregado de usar una comprensión de lista y descartar el resultado:
# get("tabs") is a list of URLs
[self.addTab(QtCore.QUrl(u)) for u in self.get("tabs", [])]
Propiedades y Señales al crear un objeto
Este feature está en versiones recientes de PyQt: si pasás nombres de propiedades como argumentos con nombre, se les asigna el valor. Si pasás una señal como argumento con nombre, se conectan al valor.
Es un feature excelente, que te ayuda a crear código claro, local y conciso, y me encanta tenerlo. Pero si te querés ir a la banquina, es mandada a hacer.
Esto está por todos lados en De Vicenzo, éste es sólo un ejemplo (sí, es una sola línea):
QtWebKit.QWebView.__init__(self, loadProgress=lambda v:\
(self.pbar.show(), self.pbar.setValue(v)) if self.amCurrent() else\
None, loadFinished=self.pbar.hide, loadStarted=lambda:\
self.pbar.show() if self.amCurrent() else None, titleChanged=lambda\
t: container.tabs.setTabText(container.tabs.indexOf(self), t) or\
(container.setWindowTitle(t) if self.amCurrent() else None))
Por adonde empiezo...
Hay expresiones lambda
usadas para definir los callbacks en el lugar en vez de conectarse con una función o método "de verdad".
Hya lambdas con el operador ternario:
loadStarted=lambda:\
self.pbar.show() if self.amCurrent() else None
Hay lambdas que usan or
o una tupla para engañar al intérprete y que haga más de una cosa en un solo lambda!
loadProgress=lambda v:\
(self.pbar.show(), self.pbar.setValue(v)) if self.amCurrent() else\
None
No voy ni a intentar desenredar esto con fines educativos, pero digamos que esa línea contiene cosas que deberían ser 3 métodos separados, y debería estar repartida en 6 líneas o mas.
Download Manager
Llamarlo un manager es exagerar porque no se puede parar una descarga después que empieza, pero bueno, te deja bajar cosas y seguir browseando, y te da un reporte de progreso!
Primero, en la línea 16
creé un diccionario bars
para llevar registro de los downloads.
Después, tenía que delegar el contenido no soportado al método indicado, y eso se hace en las líneas 108 and 109
Básicamente, con eso cada vez que hacés click en algo que WebKit no puede manejar, se llama al método fetch
con el pedido de red como argumento.
def fetch(self, reply):
destination = QtGui.QFileDialog.getSaveFileName(self, \
"Save File", os.path.expanduser(os.path.join('~',\
unicode(reply.url().path()).split('/')[-1])))
if destination:
bar = QtGui.QProgressBar(format='%p% - ' +
os.path.basename(unicode(destination)))
self.statusBar().addPermanentWidget(bar)
reply.downloadProgress.connect(self.progress)
reply.finished.connect(self.finished)
self.bars[unicode(reply.url().toString())] = [bar, reply,\
unicode(destination)]
No hay mucho golf acá salvo las líneas largas, pero una vez que metés enters es la manera obvia de hacerlo:
Pedí un nombre de archivo
Creás un progressbar, lo ponés en el statusbar, y lo conectas a las señales de progreso de la descarga.
Entonces, por supuesto, está el slot progress
que actualiza la barra:
progress = lambda self, received, total:\
self.bars[unicode(self.sender().url().toString())][0]\
.setValue(100. * received / total)
Sí, definí un método como lambda para ahorrar una línea. [facepalm]
Y elslot finished
para cuando termina el download:
def finished(self):
reply = self.sender()
url = unicode(reply.url().toString())
bar, _, fname = self.bars[url]
redirURL = unicode(reply.attribute(QtNetwork.QNetworkRequest.\
RedirectionTargetAttribute).toString())
del self.bars[url]
bar.deleteLater()
if redirURL and redirURL != url:
return self.fetch(redirURL, fname)
with open(fname, 'wb') as f:
f.write(str(reply.readAll()))
hasta soporta redirecciones correctamente! Más allá d eso, nada más esconde la barra, guarda los datos, fin del cuentito. La línea larga ni siquiera es mi culpa!
Hay un problema en que el archivo entero se mantiene en memoria hasta el fin de la descarga. Si te bajás un DVD, te va a doler.
Usar el with
ahorra una línea y no pierde un file handle, comparado con las alternativas.
Impresión
De nuevo Qt me salva las papas, porque hacer esto a mano debe ser difícil. Sin embargo, resulta que el soporte de impresión... está hecho. Qt, especialmente usado vía PyQt es tan completo!
self.previewer = QtGui.QPrintPreviewDialog(\
paintRequested=self.print_)
self.do_print = QtGui.QShortcut("Ctrl+p",\
self, activated=self.previewer.exec_)
No necesité nada de golf. Eso es exactamente el código que se necesita, y es la manera recomendada de enganchar "Ctrl+p" con la impresión de la página.
Otros Trucos
No hay otros trucos. Todo lo que queda es crear widgets, conectar unas cosas con otras, y disfrutar la increíble experience de programar PyQt, donde podés escribir un web browser entero (salvo el motor) en 127 líneas de código.