Ir al contenido principal

Ralsina.Me — El sitio web de Roberto Alsina

Publicaciones sobre pyqtbyexample

Shipping your PyQt app for windows

Sys­tem Me­ss­age: ERRO­R/3 (<s­­tri­n­­g>, li­­ne 1)

Do­cu­ment or sec­tion may not be­gin wi­th a tran­si­tio­n.


I ha­ve wri­tten about this in the pas­t, wi­th the ge­ne­ral con­clu­sion being "i­t's a pain in the ass".

So, no­w, he­re is how it's do­ne.

  1. Start wi­­th a wo­­­rking Py­­Qt appli­­ca­­tio­­n. In this exa­m­­ple, I wi­­ll use de­­vi­­cen­­zo­­.­­py mo­s­­tly be­­­cau­se:

    1. It is a wo­­­­­rking Py­­­Qt appli­­­ca­­­tio­­­n.

    2. It uses a big chunk of Py­­­Qt

    3. It's ea­sy to test

  2. Now you need a se­tu­p.­py. He­re's one that wo­rks, wi­th ex­ten­si­ve co­m­m­men­ts.

# 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 py­thon se­tu­p.­py py2exe and get a dist fol­der fu­ll of bi­na­ry good­ness.

And tha­t's it. Ex­cept of cour­se, tha­t's not it.

What this wi­ll do is crea­te a bi­na­ry se­t, ei­ther a fol­der fu­ll of things, or a sin­gle EXE fi­le. And tha­t's not enou­gh. You ha­ve to con­si­der at least the fo­llo­win­g:

  1. Put eve­­r­­y­­thing in re­­sou­r­­ce fi­­le­s: ima­­ges, qss fi­­le­s, ico­n­s, etc. Eve­­ry fi­­le your app nee­­d­s? Put it in a re­­sou­r­­ce fi­­le and load it from the­­re. That way you do­­n't ha­­ve to ca­­re about them if you go the "o­­­ne exe" road.

  2. Co­m­­pi­­le .ui fi­­les to .py (s­a­­me rea­­so­­n)

  3. Fi­­gu­­re out if you use Qt's plu­­gi­n­s, and make them wo­­­rk. This in­­clu­­des: using Pho­­­no­­n, using QtS­­Q­­L, and using any ima­­ge fo­r­­ma­­ts other than PN­­G.

After you ha­ve tha­t, are you do­ne? NO!

Your win­do­ws user wi­ll want an ins­ta­lle­r. I am not going to go in­to de­tail­s, but I had a good ti­me using Bi­tRo­ck's Ins­ta­ll­Buil­der for Qt. It's a ni­ce tool, and it wo­rks. Tha­t's a lot in this fiel­d.

But is that all? NO!

You ha­ve to take ca­re of the Vi­sual Stu­dio Runti­me. My su­gges­tio­n? Get a co­py of the 1.1MB vcre­dis­t_­x86.exe (not the lar­ger one, the 1.1MB one), and ei­ther te­ll peo­ple to ins­ta­ll it ma­nua­ll­y, or add it to your ins­ta­lle­r. You are le­ga­lly allo­wed (A­FAIK) to re­dis­tri­bu­te that thing as a who­le. But not wha­t's in it (un­le­ss you ha­ve a VS li­cen­se).

And we are do­ne? NO!

On­ce you run your app "ins­ta­lle­d", if it ever prin­ts an­y­thing to stde­rr, you wi­ll get ei­ther a dia­log te­lling you it di­d, or wor­se (if you are in ay­thing newer than XP), a dia­log te­lling you it can't wri­te to a log fi­le, and the app wi­ll ne­ver wo­rk agai­n.

This is be­cau­se py2exe ca­tches stde­rr and tries to save it on a lo­gfi­le. Whi­ch it tries to crea­te in the sa­me fol­der as the bi­na­r­y. Whi­ch is usua­lly not allo­wed be­cau­se of per­mis­sion­s.

So­lu­tio­n? Your app should ne­ver wri­te to stde­rr. Wri­te an ex­cep­thook and ca­tch tha­t. And then re­mo­ve stde­rr or re­pla­ce it wi­th a log fi­le, or so­me­thin­g. Just do­n't let py2exe do it, be­cau­se the way py2exe does it is bro­ken.

And is that it?

We­ll, ba­si­ca­lly ye­s. Of cour­se you should get 4 or 5 di­ffe­rent ver­sions of win­do­ws to test it on, but you are pre­tty mu­ch free to ship your app as you wis­h. Oh, mind you, do­n't upload it to do­wn­load­s.­com be­cau­se they wi­ll wrap your ins­ta­ller in a lar­ger one that ins­ta­lls bloa­twa­re and cra­p.

So, the­re you go.

Modularizando tu aplicación: Yapsy

Una ma­ne­ra (si es­tás pro­gra­man­do en Py­tho­n) es usar Yap­sy..

Yap­sy es asom­bro­so. Tam­bién, ca­re­ce com­ple­ta­men­te de do­cu­men­ta­ción en­ten­di­ble. Vea­mos si es­te post arre­gla un po­co esa par­te y de­ja só­lo lo asom­bro­so.

Up­da­te: No ha­bía vis­to la do­cu­men­ta­ción nue­va de Yap­s­y. Es mu­cho me­jor que la que ha­bía an­tes :-)

Es­ta es la idea ge­ne­ral con yap­s­y:

  • Creás un Plu­­gin Ma­­na­­ger que pue­­de en­­co­n­­trar y ca­r­­gar plu­­gins de una lis­­ta de lu­­ga­­res (por eje­m­­plo, de ["/us­­r/s­ha­­re/a­­pp­­na­­me/­­plu­­gi­n­s", "~/.a­­pp­­na­­me/­­plu­­gi­n­s"]).

  • Una ca­­te­­go­­­ría de plu­­gins es una cla­­se.

  • Hay un ma­­peo en­­tre no­m­­bres de ca­­te­­go­­­ría y cla­­ses de ca­­te­­go­­­ría.

  • Un plu­­gin es un mó­­­du­­lo y un ar­­chi­­vo de me­­ta­­da­­ta. El mó­­­du­­lo de­­fi­­ne una cla­­se que he­­re­­da de una cla­­se de ca­­te­­go­­­ría, y pe­r­­te­­ne­­ce a esa ca­­te­­go­­­ría.

    El ar­­chi­­vo de me­­ta­­da­­ta tie­­ne co­­sas co­­­mo el no­m­­bre del plu­­gi­n, la des­­cri­p­­ció­­n, la UR­­L, ve­r­­sió­­n, etc.

Una de las me­jo­res co­sas de Yap­sy es que no es­pe­ci­fi­ca de­ma­sia­do. Un plu­gin va a ser sim­ple­men­te un ob­je­to Py­tho­n, po­dés po­ner lo que quie­ras ahí, o lo po­dés li­mi­tar de­fi­nien­do la inte­faz en la cla­se de ca­te­go­ría.

De he­cho, lo que ven­go ha­cien­do con las cla­ses de ca­te­go­ría es:

  • Arran­­co con una cla­­se va­­cía

  • Im­­ple­­men­­to dos plu­­gins de esa ca­­te­­go­­­ría

  • Los pe­­da­­zos en co­­­mún los mue­­vo den­­tro de la ca­­te­­go­­­ría.

Pe­ro créan­me, es­to va a ser mu­cho más cla­ro con un ejem­plo :-)

Lo voy a ha­cer con una apli­ca­ción grá­fi­ca en Py­Q­t, pe­ro Yap­sy fun­cio­na igual de bien pa­ra apli­ca­cio­nes "head­le­ss" o pa­ra lí­neas de co­man­do.

Co­men­ce­mos con al­go sim­ple: un edi­tor HT­ML con un wi­dget pre­view.

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

Un edi­tor sim­ple con pre­view

Es­te es el có­di­go de la apli­ca­ció­n, que es real­men­te sim­ple (no pue­de guar­dar ar­chi­vos ni na­da in­te­re­san­te, es só­lo un ejem­plo­):

edi­to­r1.­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()

No­ta

De aho­ra en más los lis­ta­dos no in­clu­yen la fun­ción main por­que no cam­bia.

Pe­ro es­ta apli­ca­ción tie­ne un ob­vio lí­mi­te: hay que es­cri­bir HT­M­L! Por qué no es­cri­bir py­thon y que lo mues­tre re­sal­ta­do en HT­M­L? O ma­rkup de Wiki! O reS­truc­tu­red tex­t!

Uno po­dría, en prin­ci­pio, im­ple­men­tar to­dos esos mo­do­s, pe­ro es­tás asu­mien­do la res­pon­sa­bi­li­dad de so­por­tar ca­da co­sa-­que-se-­con­vier­te-en-HT­M­L. Tu apli­ca­ción se­ría un mo­n­oli­to. Ahí en­tra Yap­s­y.

Cree­mos en­ton­ces una ca­te­go­ría de plu­gin­s, lla­ma­da "For­ma­tte­r" que to­ma tex­to pla­no y de­vuel­ve HT­M­L. Des­pués agre­gue­mos co­sas en la UI pa­ra que el usua­rio pue­da ele­gir que for­ma­tter usar, e im­ple­men­te­mos un pa­r.

Es­ta es la cla­se de ca­te­go­ría de plu­gin­s:

ca­te­go­rie­s.­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 su­pues­to que no sir­ve de na­da sin plu­gin­s! Asi que cree­mos un pa­r.

Pri­me­ro, un plu­gin qye to­ma có­di­go py­thon y de­vuel­ve HT­M­L, usan­do pyg­men­ts.

plu­gin­s/­p­yg­men­ti­ze.­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))

Co­mo ven, eso va en una car­pe­ta plu­gin­s. Des­pués le de­ci­mos a Yap­sy que bus­que los plu­gins ahi aden­tro.

Pa­ra ser re­co­no­ci­do co­mo un plu­gi­n, ne­ce­si­ta me­ta­da­ta:

plu­gin­s/­p­yg­men­ti­ze.­yap­s­y-­plu­gin

[Core]
Name = Python Code
Module = pygmentize

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

Y real­men­te, eso es to­do lo que hay que ha­cer pa­ra ha­cer un plu­gi­n. Acá hay otro pa­ra com­pa­ra­r, que usa do­cu­tils pa­ra for­ma­tear reS­truc­tu­red Tex­t:

plu­gin­s/­res­t.­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­gin­s/­res­t.­yap­s­y-­plu­gin

[Core]
Name = Restructured Text
Module = rest

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

Y acá es­tán en ac­ció­n:

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

reSt mo­de

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

Py­thon mo­de

Of cour­se using ca­te­go­ries you can do things like a "Tool­s" ca­te­go­r­y, whe­re the plu­gins get added to a Tools me­nu, too.

Es­te es el có­di­go del la­do de la apli­ca­ció­n:

edi­to­r2.­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)

Re­su­mien­do: es fá­ci­l, y te lle­va a me­jo­rar la es­truc­tu­ra in­ter­na de tu apli­ca­ción y ter­mi­nás con me­jor có­di­go.

Có­di­go fuen­te de to­do.

Posteo poco porque escribo mucho.

¿Qué li­bro? ¡Un li­bro de py­tho­n, por su­pues­to! Se lla­ma "P­y­thon no muer­de".

Aho­ra bien, soy el pri­me­ro en de­cir­lo: no soy un gran pro­gra­ma­do­r. Y no soy un gran es­cri­tor tam­po­co. Pe­ro ten­go mu­cho pa­ra de­ci­r. Si con­si­go or­ga­ni­zar­lo, a ve­ces has­ta tie­ne sen­ti­do cuan­do lo di­go.

En­ton­ce­s, le es­toy dan­do una opor­tu­ni­dad a es­to de es­cri­bi­r-­co­sas-­lar­ga­s.

Por su­pues­to, co­mo soy un nerd open sour­ce, no pue­do ha­cer na­da a la ma­ne­ra tra­di­cio­na­l, así que el li­bro es li­bre ba­jo Crea­ti­ve Co­m­mon­s. Y co­mo soy un pro­gra­ma­do­r, ar­mé una (si se me per­mi­te la in­mo­des­tia) es­truc­tu­ra de­cen­te pa­ra ma­ne­jar mi es­cri­tu­ra.

  1. Es­­cri­­bo en res­­tru­c­­tu­­red tex­­t.

  2. Uso rs­­t2­­pdf pa­­ra crear PDFs de los ca­­pí­­tu­­los in­­di­­vi­­dua­­les y de to­­­do el li­­bro.

  3. Uso res­­t2web pa­­ra crear el si­­to.

  4. Uso me­r­­cu­­rual (en google­­co­­­de) pa­­ra ma­­ne­­jar co­n­­trol de re­­vi­­sio­­­nes e his­­to­­­ria­­l.

  5. Uso make pa­­ra co­n­­tro­­­lar la re­­con­s­­tru­c­­ción de ca­­pí­­tu­­los cuan­­do hay ca­m­­bios en el có­­­di­­go, se ac­­tua­­li­­za una ima­­gem etc.

Por su­pues­to que es un po­co más com­pli­ca­do que eso, los PDFs es­tán en el si­tio, que se su­be via rs­yn­c, to­do se dis­pa­ra con los push de hg, y así si­gue.

En cual­quier ca­so, tal vez pos­tee un par de ve­ces acer­ca de co­mo fun­cio­na to­da es­ta co­sa, acá es­tá la sali­da de la ma­qui­na­ria:

http://­no­muer­de.­ne­t­ma­na­ger­s.­co­m.ar

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

Ade­más gra­cias a:

  • ZeD que en­­contró un bug en la se­­sión 2 y es­­cri­­bió un ar­­chi­­vo de co­n­­fi­­gu­­ra­­ción sphi­nx (fun­­cio­­­na muy bien­­!)

  • Ema­­nue­­le Ra­m­­pi­­chi­­ni que en­­contró un bug (que to­­­da­­vía no arre­­glé)

  • Si­­mon Edwa­r­­ds que po­s­­teó so­­­bre es­­ta se­­rie en su blog y in­­di­­có al­­gu­­nas di­­fe­­ren­­cias si quie­­ren usar PyK­­DE en vez de Py­­Q­­t.


Contents © 2000-2020 Roberto Alsina