Gente haciendo cosas útiles con mis juguetes
¡Pero de golpe alguien fué y lo hizo hacer algo útil! Específicamente, para tener previews cuando edita documentos en sphinx
Está bueno :)
¡Pero de golpe alguien fué y lo hizo hacer algo útil! Específicamente, para tener previews cuando edita documentos en sphinx
Está bueno :)
I have written about this in the past, with the general conclusion being "it's a pain in the ass".
So, now, here is how it's done.
Start with a working PyQt application. In this example, I will use devicenzo.py mostly because:
It is a working PyQt application.
It uses a big chunk of PyQt
It's easy to test
Now you need a setup.py
. Here's one that works, with extensive commments.
# We will be using py2exe to build the binaries. # You may use other tools, but I know this one. from distutils.core import setup import py2exe # Now you need to pass arguments to setup # windows is a list of scripts that have their own UI and # thus don't need to run in a console. setup(windows=['devicenzo.py'], options={ # And now, configure py2exe by passing more options; 'py2exe': { # This is magic: if you don't add these, your .exe may # or may not work on older/newer versions of windows. "dll_excludes": [ "MSVCP90.dll", "MSWSOCK.dll", "mswsock.dll", "powrprof.dll", ], # Py2exe will not figure out that you need these on its own. # You may need one, the other, or both. 'includes': [ 'sip', 'PyQt4.QtNetwork', ], # Optional: make one big exe with everything in it, or # a folder with many things in it. Your choice # 'bundle_files': 1, } }, # Qt's dynamically loaded plugins and py2exe really don't # get along. data_files = [ ('phonon_backend', [ 'C:\Python27\Lib\site-packages\PyQt4\plugins\phonon_backend\phonon_ds94.dll' ]), ('imageplugins', [ 'c:\Python27\lib\site-packages\PyQt4\plugins\imageformats\qgif4.dll', 'c:\Python27\lib\site-packages\PyQt4\plugins\imageformats\qjpeg4.dll', 'c:\Python27\lib\site-packages\PyQt4\plugins\imageformats\qsvg4.dll', ]), ], # If you choose the bundle above, you may want to use this, too. # zipfile=None, )
Run python setup.py py2exe
and get a dist
folder full of binary goodness.
And that's it. Except of course, that's not it.
What this will do is create a binary set, either a folder full of things, or a single EXE file. And that's not enough. You have to consider at least the following:
Put everything in resource files: images, qss files, icons, etc. Every file your app needs? Put it in a resource file and load it from there. That way you don't have to care about them if you go the "one exe" road.
Compile .ui files to .py (same reason)
Figure out if you use Qt's plugins, and make them work. This includes: using Phonon, using QtSQL, and using any image formats other than PNG.
After you have that, are you done? NO!
Your windows user will want an installer. I am not going to go into details, but I had a good time using BitRock's InstallBuilder for Qt. It's a nice tool, and it works. That's a lot in this field.
But is that all? NO!
You have to take care of the Visual Studio Runtime. My suggestion? Get a copy of the 1.1MB vcredist_x86.exe (not the larger one, the 1.1MB one), and either tell people to install it manually, or add it to your installer. You are legally allowed (AFAIK) to redistribute that thing as a whole. But not what's in it (unless you have a VS license).
And we are done? NO!
Once you run your app "installed", if it ever prints anything to stderr, you will get either a dialog telling you it did, or worse (if you are in aything newer than XP), a dialog telling you it can't write to a log file, and the app will never work again.
This is because py2exe catches stderr and tries to save it on a logfile. Which it tries to create in the same folder as the binary. Which is usually not allowed because of permissions.
Solution? Your app should never write to stderr. Write an excepthook and catch that. And then remove stderr or replace it with a log file, or something. Just don't let py2exe do it, because the way py2exe does it is broken.
And is that it?
Well, basically yes. Of course you should get 4 or 5 different versions of windows to test it on, but you are pretty much free to ship your app as you wish. Oh, mind you, don't upload it to downloads.com because they will wrap your installer in a larger one that installs bloatware and crap.
So, there you go.
Cliente Twitter client (no soporta identi.ca en la primera versión)
Con estos features: http://pastebin.lugmen.org.ar/6464
Implementado antes del 4/4
Menos de 16384 bytes (de python). Puede ser mas grande por iconos y cosas así.
Veremos qué sale :-)
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.
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.
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.
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.
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.
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.
Si no querés leer eso de nuevo, la idea es ver cuánto código falta para convertir el motor WebKit de Qt en un browser "en serio".
Para ello, me puse una meta completamente arbitraria de 128 líneas de código. En este momento lo declaro feature-complete (pero buggy).
Los nuevos features son:
Tabbed browsing (se puede agregar/sacar tabs)
Bookmarks (se pueden agregar/sacar y elegir de una lista)
Esto es lo que ya funcionaba:
Zoom in (Ctrl++)
Zoom out (Ctrl+-)
Reset Zoom (Ctrl+=)
Buscar (Ctrl+F)
Esconder búsqueda (Esc)
Botones de atrás/adelante y recargar
Entrada de URL que coincide con la página + autocompletado desde la historia + arregla la URL puesta a mano (agrega http://, esas cosas)
Plugins (incluído flash, que hay que bajar aparte ;-)
El título de la ventana muestra el título de la página (sin propaganda del browser)
Barra de progreso para la carga de la página
Barra de estado que muestra el destino de los links cuando pasas el mouse
Toma una URL en la línea de comando (o abre http://python.org
Multiplataforma (funciona donde funciona QtWebKit)
Y cuanto código es eso? 87 LINEAS.
O si preferís la versión que cumple con la PEP8: 115 LINEAS.
Me atajo antes que alguien lo diga: sí, el motor de rendering y el toolkit son enormes. Lo que escribí es el "chrome" alrededor de eso, igual que hacen Arora, Rekonq, Galeon, Epiphany, y muchos otros browsers.
Es un chrome simple y minimalista, pero funciona bastante bien, creo yo.
Aquí está el demo (buggy):
Mas o menos hace lo que esperaba que se puediera lograr, pero le faltan arreglos.
Para ver el código, vayan a su home page: http://devicenzo.googlecode.com