Publicaciones sobre pyqtbyexample

2011-07-24 21:20

Shipping your PyQt app for windows

Disculpen, solo en inglés, no me da el cuerpo para ponerme a traducir :-(


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.

  1. Start with a working PyQt application. In this example, I will use devicenzo.py mostly because:
    1. It is a working PyQt application.
    2. It uses a big chunk of PyQt
    3. It's easy to test
  2. 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,
)
  1. 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:

  1. 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.
  2. Compile .ui files to .py (same reason)
  3. 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.

2010-10-01 15:12

Modularizando tu aplicación: Yapsy

Casi todos estamos más o menos de acuerdo en que hacer que las aplicaciones sean modulares es buena idea. Una cosa en la que no hay tanto acuerdo es como miércoles hacemos eso.

Una manera (si estás programando en Python) es usar Yapsy..

Yapsy es asombroso. También, carece completamente de documentación entendible. Veamos si este post arregla un poco esa parte y deja sólo lo asombroso.

Update: No había visto la documentación nueva de Yapsy. Es mucho mejor que la que había antes :-)

Esta es la idea general con yapsy:

  • Creás un Plugin Manager que puede encontrar y cargar plugins de una lista de lugares (por ejemplo, de ["/usr/share/appname/plugins", "~/.appname/plugins"]).

  • Una categoría de plugins es una clase.

  • Hay un mapeo entre nombres de categoría y clases de categoría.

  • Un plugin es un módulo y un archivo de metadata. El módulo define una clase que hereda de una clase de categoría, y pertenece a esa categoría.

    El archivo de metadata tiene cosas como el nombre del plugin, la descripción, la URL, versión, etc.

Una de las mejores cosas de Yapsy es que no especifica demasiado. Un plugin va a ser simplemente un objeto Python, podés poner lo que quieras ahí, o lo podés limitar definiendo la intefaz en la clase de categoría.

De hecho, lo que vengo haciendo con las clases de categoría es:

  • Arranco con una clase vacía
  • Implemento dos plugins de esa categoría
  • Los pedazos en común los muevo dentro de la categoría.

Pero créanme, esto va a ser mucho más claro con un ejemplo :-)

Lo voy a hacer con una aplicación gráfica en PyQt, pero Yapsy funciona igual de bien para aplicaciones "headless" o para líneas de comando.

Comencemos con algo simple: un editor HTML con un widget preview.

//ralsina.me/static/yapsy/editor1.jpeg

Un editor simple con preview

Este es el código de la aplicación, que es realmente simple (no puede guardar archivos ni nada interesante, es sólo un ejemplo):

editor1.py

from PyQt4 import QtCore, QtGui, QtWebKit
import os, sys

class Main(QtGui.QWidget):
    def __init__(self):
        QtGui.QWidget.__init__(self)
        self.layout = QtGui.QVBoxLayout()
        self.editor = QtGui.QPlainTextEdit()
        self.preview = QtWebKit.QWebView()
        self.layout.addWidget(self.editor)
        self.layout.addWidget(self.preview)
        self.editor.textChanged.connect(self.updatePreview)
        self.setLayout(self.layout)

    def updatePreview(self):
        self.preview.setHtml(self.editor.toPlainText())

def main():
    # Again, this is boilerplate, it's going to be the same on
    # almost every app you write
    app = QtGui.QApplication(sys.argv)
    window=Main()
    window.show()
    # It's exec_ because exec is a reserved word in Python
    sys.exit(app.exec_())

if __name__ == "__main__":
    main()

Nota

De ahora en más los listados no incluyen la función main porque no cambia.

Pero esta aplicación tiene un obvio límite: hay que escribir HTML! Por qué no escribir python y que lo muestre resaltado en HTML? O markup de Wiki! O reStructured text!

Uno podría, en principio, implementar todos esos modos, pero estás asumiendo la responsabilidad de soportar cada cosa-que-se-convierte-en-HTML. Tu aplicación sería un monolito. Ahí entra Yapsy.

Creemos entonces una categoría de plugins, llamada "Formatter" que toma texto plano y devuelve HTML. Después agreguemos cosas en la UI para que el usuario pueda elegir que formatter usar, e implementemos un par.

Esta es la clase de categoría de plugins:

categories.py

class Formatter(object):
    """Plugins of this class convert plain text to HTML"""

    name = "No Format"

    def formatText(self, text):
        """Takes plain text, returns HTML"""
        return text

Por supuesto que no sirve de nada sin plugins! Asi que creemos un par.

Primero, un plugin qye toma código python y devuelve HTML, usando pygments.

plugins/pygmentize.py

from pygments import highlight
from pygments.lexers import PythonLexer
from pygments.formatters import HtmlFormatter

from categories import Formatter

class Pygmentizer(Formatter):
    name = "Python Code"

    def formatText(self, text):
        return highlight(text, PythonLexer(), HtmlFormatter(full=True))

Como ven, eso va en una carpeta plugins. Después le decimos a Yapsy que busque los plugins ahi adentro.

Para ser reconocido como un plugin, necesita metadata:

plugins/pygmentize.yapsy-plugin

[Core]
Name = Python Code
Module = pygmentize

[Documentation]
Author = Roberto Alsina
Version = 0.1
Website = //ralsina.me
Description = Highlights Python Code

Y realmente, eso es todo lo que hay que hacer para hacer un plugin. Acá hay otro para comparar, que usa docutils para formatear reStructured Text:

plugins/rest.py

from categories import Formatter
import docutils.core
import docutils.io


class Rest(Formatter):
    name = "Restructured Text"

    def formatText(self, text):
        output = docutils.core.publish_string(
            text, writer_name = 'html'
        )
        return output

plugins/rest.yapsy-plugin

[Core]
Name = Restructured Text
Module = rest

[Documentation]
Author = Roberto Alsina
Version = 0.1
Website = //ralsina.me
Description = Formats restructured text

Y acá están en acción:

//ralsina.me/static/yapsy/editor2.jpeg

reSt mode

//ralsina.me/static/yapsy/editor3.jpeg

Python mode

Of course using categories you can do things like a "Tools" category, where the plugins get added to a Tools menu, too.

Este es el código del lado de la aplicación:

editor2.py

from categories import Formatter
from yapsy.PluginManager import PluginManager

class Main(QtGui.QWidget):
    def __init__(self):
        QtGui.QWidget.__init__(self)
        self.layout = QtGui.QVBoxLayout()
        self.formattersCombo = QtGui.QComboBox()
        self.editor = QtGui.QPlainTextEdit()
        self.preview = QtWebKit.QWebView()

        self.layout.addWidget(self.formattersCombo)
        self.layout.addWidget(self.editor)
        self.layout.addWidget(self.preview)

        self.editor.textChanged.connect(self.updatePreview)
        self.setLayout(self.layout)

        # Create plugin manager
        self.manager = PluginManager(categories_filter={ "Formatters": Formatter})
        self.manager.setPluginPlaces(["plugins"])

        # Load plugins
        self.manager.locatePlugins()
        self.manager.loadPlugins()

        # A do-nothing formatter by default
        self.formattersCombo.addItem("None")
        self.formatters = {}
        print self.manager.getPluginsOfCategory("Formatters")
        for plugin in self.manager.getPluginsOfCategory("Formatters"):
            print  "XXX"
            # plugin.plugin_object is an instance of the plugin
            self.formattersCombo.addItem(plugin.plugin_object.name)
            self.formatters[plugin.plugin_object.name] = plugin.plugin_object

    def updatePreview(self):
        # Check what the current formatter is
        name =  unicode(self.formattersCombo.currentText())
        text = unicode(self.editor.toPlainText())
        if name in self.formatters:
            text = self.formatters[name].formatText(text)
        self.preview.setHtml(text)

Resumiendo: es fácil, y te lleva a mejorar la estructura interna de tu aplicación y terminás con mejor código.

Código fuente de todo.

2010-04-11 05:12

Posteo poco porque escribo mucho.

Nada más que no estoy escribiendo acá. Estoy escribiendo un libro.

¿Qué libro? ¡Un libro de python, por supuesto! Se llama "Python no muerde".

Ahora bien, soy el primero en decirlo: no soy un gran programador. Y no soy un gran escritor tampoco. Pero tengo mucho para decir. Si consigo organizarlo, a veces hasta tiene sentido cuando lo digo.

Entonces, le estoy dando una oportunidad a esto de escribir-cosas-largas.

Por supuesto, como soy un nerd open source, no puedo hacer nada a la manera tradicional, así que el libro es libre bajo Creative Commons. Y como soy un programador, armé una (si se me permite la inmodestia) estructura decente para manejar mi escritura.

  1. Escribo en restructured text.
  2. Uso rst2pdf para crear PDFs de los capítulos individuales y de todo el libro.
  3. Uso rest2web para crear el sito.
  4. Uso mercurual (en googlecode) para manejar control de revisiones e historial.
  5. Uso make para controlar la reconstrucción de capítulos cuando hay cambios en el código, se actualiza una imagem etc.

Por supuesto que es un poco más complicado que eso, los PDFs están en el sitio, que se sube via rsync, todo se dispara con los push de hg, y así sigue.

En cualquier caso, tal vez postee un par de veces acerca de como funciona toda esta cosa, acá está la salida de la maquinaria:

http://nomuerde.netmanagers.com.ar

2009-04-12 10:32

PyQt en Ejemplos (Sesión 4) in castellano! (y agradecimientos)

Gracias a Leonardo De Luca, la sesiń 4 se encuentra disponible en castellano

Además gracias a:

  • ZeD que encontró un bug en la sesión 2 y escribió un archivo de configuración sphinx (funciona muy bien!)
  • Emanuele Rampichini que encontró un bug (que todavía no arreglé)
  • Simon Edwards que posteó sobre esta serie en su blog y indicó algunas diferencias si quieren usar PyKDE en vez de PyQt.

2009-03-17 21:10

Traducciones casi listas

La traducción al español de la mayoria de las sesiones de "PyQt en Ejemplos" está casi lista, gracias al trabajo de varios voluntarios.

Mañana voy a subir por lo menos una.

Si alguien quiere traducir a otros lenguajes, me encantaría incluirlos también.

2009-03-12 00:59

Sesión 5 ligeramente demorada

Tenía planeado tenerla para hoy, pero mi trabajo de consultoría actual (migrar una organización de Exchange a FLOSS) se complicó un poco (más que nada por el Outlook 2000) (Sí, ya sé).

Tal vez para el sabado, tal ve para el martes que viene, no sé todavía.

Contents © 2000-2019 Roberto Alsina