En 128 líneas de código entra exactamente ESTE browser.
Por supuesto, podría hacer más, pero hasta yo tengo mis standards!
No usar
;
No usar
if whatever: f()
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.
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:
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!
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.
Doesn't QtWebKit have built-in proxy support that "just works" if you configure it in the system control panel?
Good question. If there is, I couldn't find it in the docs.
Great examples on different topics, printing, network (with redirect), cookie handling, proxy and of course, the power of Python :)
Awesome. Just happend to read through wikipedia's "list of browsers" - This one is missing. I'd love to do all the tests and post it there..
Sure, go ahead :-)
(Py)Qt is powerful indeed, and you are a master to showcase them. The only thing I really miss in Qt and PyQt is built in Poppler support, I got it working on Windows with MinGW but it was somewhat painful.
Anyway, what's your next code golfing challenge? Your public is eager to know! :-)
A twitter/identi.ca client apparently!
Well, the write-up is truly the freshest on this laudable topic.
Hi, I think there is a bug (maybe with Qt). If you open the application to a page that plants a cookie, then open a tab, close that tab then try to open another one, you get:
Traceback (most recent call last):
File "./qtwk.py", line 15, in <lambda>
self.tabs.setCornerWidget(QtGui.QToolButton(self, text="New Tab", icon=QtGui.QIcon.fromTheme("document-new"), clicked=lambda: self.addTab().url.setFocus(), shortcut="Ctrl+t"))
File "./qtwk.py", line 77, in addTab
self.tabs.setCurrentIndex(self.tabs.addTab(Tab(url, self), ""))
File "./qtwk.py", line 105, in __init__
self.wb.page().networkAccessManager().setCookieJar(container.cookies)
RuntimeError: wrapped C/C++ object of type QNetworkCookieJar has been deleted
Traceback (most recent call last):
File "./qtwk.py", line 62, in closeEvent
self.put("cookiejar", [str(c.toRawForm()) for c in self.cookies.allCookies()])
RuntimeError: wrapped C/C++ object of type QNetworkCookieJar has been deleted
Looks like a race condition where I am trying to set properties in the already closed tab. Easy-ish to work around, though (just try/except it)
Hi Roberto, the try / except works, but then you don't pick up the persistent cookies. I've tried to fix it myself, but not found a way so far.
It looks like we're destroying the self.cookies instance when we close a tab, meaning it can't be picked up again and you loose the cookies saved application wide.
Makes sense. I admit the cookiejar is not very tested ;-)
Yep, I'm not trying to knock holes in it, just cool to get it fully working :)
Also trying to get new window to work with tabs. It's odd how easy some hard things are and how hard a few easy things are with QtWebKit; like new windows, cookies etc.
Also noticed that if you open a tab, close it then try and click on a link on the original tab, you get a segmentation fault.