Skip to main content

Ralsina.Me — Roberto Alsina's website

Posts about pyqtbyexample

Shipping your PyQt app for windows

I have writ­ten about this in the past, with the gen­er­al con­clu­sion be­ing "it's a pain in the as­s".

So, now, here is how it's done.

  1. Start with a work­ing PyQt ap­­pli­­ca­­tion. In this ex­am­­ple, I will use de­vi­­cen­­zo.py most­­ly be­­cause:

    1. It is a work­ing PyQt ap­­­pli­­­ca­­­tion.

    2. It us­es a big chunk of PyQt

    3. It's easy to test

  2. Now you need a set­up.py. Here's one that work­s, with ex­ten­sive com­m­ments.

# 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 set­up.py py2exe and get a dist fold­er full of bi­na­ry good­ness.

And that's it. Ex­cept of course, that's not it.

What this will do is cre­ate a bi­na­ry set, ei­ther a fold­er full of things, or a sin­gle EXE file. And that's not enough. You have to con­sid­er at least the fol­low­ing:

  1. Put ev­ery­thing in re­­source files: im­ages, qss files, icon­s, etc. Ev­ery file your app need­s? Put it in a re­­source file and load it from there. That way you don't have to care about them if you go the "one ex­e" road.

  2. Com­pile .ui files to .py (same rea­­son)

  3. Fig­ure out if you use Qt's plu­g­in­s, and make them work. This in­­­cludes: us­ing Phonon, us­ing Qt­SQL, and us­ing any im­age for­­mats oth­­er than PNG.

Af­ter you have that, are you done? NO!

Your win­dows us­er will want an in­stall­er. I am not go­ing to go in­to de­tail­s, but I had a good time us­ing Bi­tRock­'s In­stall­Builder for Qt. It's a nice tool, and it work­s. That's a lot in this field.

But is that al­l? NO!

You have to take care of the Vis­ual Stu­dio Run­time. My sug­ges­tion? Get a copy of the 1.1MB vcre­dis­t_x86.exe (not the larg­er one, the 1.1MB one), and ei­ther tell peo­ple to in­stall it man­u­al­ly, or add it to your in­stall­er. You are legal­ly al­lowed (AFAIK) to re­dis­tribute that thing as a whole. But not what's in it (un­less you have a VS li­cense).

And we are done? NO!

Once you run your app "in­stalled", if it ev­er prints any­thing to stder­r, you will get ei­ther a di­a­log telling you it did, or worse (if you are in ay­thing new­er than XP), a di­a­log telling you it can't write to a log file, and the app will nev­er work again.

This is be­cause py2exe catch­es stderr and tries to save it on a log­file. Which it tries to cre­ate in the same fold­er as the bi­na­ry. Which is usu­al­ly not al­lowed be­cause of per­mis­sion­s.

So­lu­tion? Your app should nev­er write to stder­r. Write an ex­cepthook and catch that. And then re­move stderr or re­place it with a log file, or some­thing. Just don't let py2exe do it, be­cause the way py2exe does it is bro­ken.

And is that it?

Well, ba­si­cal­ly yes. Of course you should get 4 or 5 dif­fer­ent ver­sions of win­dows to test it on, but you are pret­ty much free to ship your app as you wish. Oh, mind you, don't up­load it to down­load­s.­com be­cause they will wrap your in­stall­er in a larg­er one that in­stalls bloat­ware and crap.

So, there you go.

Making your app modular: Yapsy

That a plug­in ar­chi­tec­ture for a com­plex app is a good idea is one of those things that most peo­ple kin­da agree on. One thing we don't quite agree is how the heck are we go­ing to make out app mod­u­lar?

One way to do it (if you are cod­ing python) is us­ing Yap­sy.

Yap­sy is awe­some. Al­so, yap­sy is a bit un­der­doc­u­ment­ed. Let's see if this post fix­es that a bit and leaves just the awe­some.

Up­date: I had not seen the new Yap­sy doc­s, re­leased a few days ago. They are much bet­ter than what was there be­fore :-)

Here's the gen­er­al idea be­hind yap­sy:

  • You cre­ate a Plug­in Man­ag­er that can find and load plug­ins from a list of places (for ex­am­­ple, from ["/us­r/share/ap­p­­name/­­plu­g­in­s", "~/.ap­p­­name/­­plu­g­in­s"]).

  • A plug­in cat­e­­go­ry is a class.

  • There is a map­ping be­tween cat­e­­go­ry names and cat­e­­go­ry class­es.

  • A plug­in is a mod­­ule and a meta­­da­­ta file. The mod­­ule de­fines a class that in­­her­its from a cat­e­­go­ry class, and be­­longs to that cat­e­­go­ry.

    The meta­­da­­ta file has stuff like the plug­in's name, de­scrip­­tion, URL, ver­­sion, etc.

One of the great things about Yap­sy is that it does­n't spec­i­fy too much. A plug­in will be just a python ob­jec­t, you can put what­ev­er you want there, or you can nar­row it down by spec­i­fy­ing the cat­e­go­ry class.

In fac­t, the way I have been do­ing the cat­e­go­ry class­es is:

  • Start with an em­p­­ty class

  • Im­­ple­­ment two plug­ins of that cat­e­­go­ry

  • If there is a chunk that's much alike in both, move it in­­­to the cat­e­­go­ry class.

But trust me, this will all be clear­er with an ex­am­ple :-)

I will be do­ing it with a graph­i­cal PyQt ap­p, but Yap­sy works just as well for head­less of CLI app­s.

Let's start with a sim­ple ap­p: an HTML ed­i­tor with a pre­view wid­get.

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

A sim­ple ed­i­tor with pre­view

Here's the code for the ap­p, which is re­al­ly sim­ple (it does­n't save or do any­thing, re­al­ly, it's just an ex­am­ple):

ed­i­tor1.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()

Note

From now on list­ings will not in­clude the main func­tion, be­cause it nev­er changes.

But this ap­pli­ca­tion has an ob­vi­ous lim­it: you have to type HTML in it. Why not type python code in it and have it con­vert to HTML for dis­play? Or Wi­ki markup, or re­struc­tured tex­t?

You could, in prin­ci­ple, just im­ple­ment all those mod­es, but then you are as­sum­ing the re­spon­s­abil­i­ty of sup­port­ing ev­ery thing-that-­can-be-­turned-in­to-HTM­L. Your app would be a mono­lith. That's where yap­sy en­ters the scene.

So, let's cre­ate a plug­in cat­e­go­ry, called "For­mat­ter" which takes plain text and re­turns HTM­L. Then we add stuff in the UI so the us­er can choose what for­mat­ter he wants, and im­ple­ment two of those.

Here's our plug­in cat­e­go­ry class:

cat­e­gories.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

Of course what good is a plug­in ar­chi­tec­ture with­out any plug­ins for it? So, let's cre­ate two plug­ins.

First: a plug­in that takes python code and re­turns HTM­L, thanks to pyg­ments.

plu­g­in­s/pyg­men­tize.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))

See how it goes in­to a plug­ins fold­er? Lat­er on we will tell yap­sy to search there for plug­ins.

To be rec­og­nized as a plug­in, it needs a meta­da­ta file, too:

plu­g­in­s/pyg­men­tize.yap­sy-­plu­g­in

[Core]
Name = Python Code
Module = pygmentize

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

And re­al­ly, that's all there is to mak­ing a plug­in. Here's an­oth­er one for com­par­ison, which us­es do­cu­tils to for­mat re­Struc­tured Tex­t:

plu­g­in­s/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

plu­g­in­s/rest.yap­sy-­plu­g­in

[Core]
Name = Restructured Text
Module = rest

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

And here they are in ac­tion:

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

reSt mode

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

Python mode

Of course us­ing cat­e­gories you can do things like a "Tool­s" cat­e­go­ry, where the plug­ins get added to a Tools menu, too.

And here's the ap­pli­ca­tion code:

ed­i­tor2.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)

In short: this is easy to do, and it leads to fix­ing your ap­pli­ca­tion's in­ter­nal struc­ture, so it helps you write bet­ter code.

Full source code for ev­ery­thing.

I am posting very little because I am writing a lot.

I am just not writ­ing here. I am writ­ing a book in­stead.

What book am I writ­ing? A book about python pro­gram­ming, of course! It's called "Python No Muerde" (Python Does­n't Bite) and it's in span­ish.

Now, I am the first to ad­min: I am not a great pro­gram­mer. And I am not a great writ­er. But I have lots of things to say. If I can or­ga­nize them cor­rect­ly, they even make sense some­times!

So, I am giv­ing this write-­long-stuff thing a try.

Of course since I am an open source nerd, I can't do things the usu­al way, there­fore, the book is un­der Cre­ative Com­mon­s. And be­cause I am a pro­gram­mer, I hacked to­geth­er a (if I may say so my­self) de­cent struc­ture to han­dle book-writ­ing.

  1. I write in re­struc­­tured text

  2. I use rst2pdf to cre­ate PDFs both of in­­di­vid­u­al chap­ters and the whole thing.

  3. I use rest2web to cre­ate a we­b­site

  4. I use mer­cu­ri­al (at google­­code) to han­­dle re­vi­­sion con­trol and his­­to­ry.

  5. I use make to con­trol re­build­ing of chap­ters when code changes, or im­ages get up­­­dat­ed, etc.

Of course it's more com­pli­cat­ed than that, the PDFs are in the site, the site is up­load­ed via rsync, the up­loads and re­builds are trig­gered by hg push, and so on.

In any case, I may post a few times about how this whole thing work­s, here is the out­put of the ma­chin­ery:

http://no­muerde.net­man­ager­s.­com.ar

PyQt by Example (Session 4) in spanish! (and some thanks)

Thanks to Leonar­do De Lu­ca, ses­sion 4 is now avail­able in span­ish

Al­so, thanks to:

  • ZeD who point­ed out a bug in ses­­sion 2 and wrote a sphinx con­­fig file (works very well!)

  • Emanuele Rampi­chi­ni who point­ed out a bug (which I have not fixed yet)

  • Si­­mon Ed­wards who post­ed about this se­ries in his blog and point­ed out some dif­fer­­ences if you want to use PyKDE in­­stead of PyQt.