Skip to main content

Ralsina.Me — Roberto Alsina's website

Posts about pyqt (old posts, page 11)

Charla: aplicaciones extensibles con PyQt

Span­ish on­ly, since it's about a video in span­ish ;-)

Acá es­tá, gra­cias a la gente de Junín, un video de mi char­la "Apli­ca­ciones ex­ten­si­bles us­an­do PyQt", en la que in­ten­to mostrar co­mo de­sar­rol­lar una apli­cación con PyQt y yap­sy.

No es una char­la con la que es­té muy con­tento. La otra sal­ió mejor, pero no se filmó, así que quedará so­lo en la memo­ria de los cu­a­tro gatos lo­cos que es­tábamos ahí ;-)

El resto de las char­las: http://un­no­ba.blip.tv/

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.

eBooks and PyQt: a good match

I have been putting lots of love in­to Aran­du­ka an eBook man­ager, (which is look­ing very good late­ly, thanks!), and I did­n't want it to al­so be an eBook read­er.

But then I thought... how hard can it be to read ePub? Well, it's freak­ing easy!

Here's a good start at stack­over­flow.­com but the short of it is... it's a zip with some XML in it.

One of those XML files tells you where things are, one of them is the TOC, the rest is just a small stat­ic col­lec­tion of HTM­L/C­SS/im­ages.

So, here are the in­gre­di­ents to rol­l-y­our-own ePub read­er wid­get in 150 LOC:

  • Use python's zip­­file li­brary to avoid ex­­plod­ing the zip (that's lame)

  • Use El­e­­ment Tree to parse said XML files.

  • Use PyQt's QtWe­bKit to dis­­­play said col­lec­­tion of XM­L/C­SS/Im­ages

  • Use this recipe to make QtWe­bKit tell you when it wants some­thing from the zip­­file.

Plug some things to oth­er­s, shake vig­or­ous­ly, and you end up with this:

Share photos on twitter with Twitpic

Here's the code (as of to­day) and the UI file you need.

Miss­ing stuff:

  • It does­n't dis­­­play the cov­­er.

  • It on­­ly shows the top lev­­el of the ta­ble of con­­tents.

  • I on­­ly test­ed it on two books ;-)

  • It sure can use a lot of refac­­tor­ing!

Nei­ther should be ter­ri­bly hard to do.

Introducing Aranduka

Yes, it's yet an­oth­er pro­gram I am work­ing on. But hey, the last few I start­ed are ac­tu­al­ly pret­ty func­tion­al al­ready!

And... I am not do­ing this one alone, which should make it more fun.

It's an eBook (or just any book?) man­ager, that helps you keep your PDF/­Mo­bi/F­B2/what­ev­er or­ga­nized, and should even­tu­al­ly sync them to the de­vice you want to use to read them.

What works now? See the video!

In case that makes no sense to you:

  • You can get books from Feed­­Book­s. Those books will get down­load­­ed, added to your database, tagged, the cov­­er fetched, etc. etc.

  • You can im­­port your cur­rent fold­er of books in bulk.

    Aran­­du­­ka will use google and oth­­er sources to try to guess (from the file­­name) what book that is and fill in the ex­­tra da­­ta about it.

  • You can "guess" the ex­­tra da­­ta.

    By mark­ing cer­­tain da­­ta (say, the ti­tle) as re­li­able, Aran­­du­­ka will try to find some pos­si­ble books that match then you can choose if it's right.

    Of course you can al­­so ed­it that da­­ta man­u­al­­ly.

And that's about it. Planned fea­tures:

  • Way too many to list.

The goals are clear:

  • It should be beau­ti­­ful (I know it is­n't!)

  • It should be pow­er­­ful (not yet!)

  • It should be bet­ter than the "com­pe­ti­­tion"

If those three goals are not achieved, it's fail­ure. It may be a fun fail­ure, but it would still be a fail­ure.

Very pythonic progress dialogs.

Sometimes, you see a piece of code and it just feels right. Here's an example I found when doing my "Import Antigravity" session for PyDay Buenos Aires: the progressbar module.

Here's an example that will teach you enough to use progressbar effectively:

progress = ProgressBar()
for i in progress(range(80)):
    time.sleep(0.01)

Yes, that's it, you will get a nice ASCII progress bar that goes across the ter­mi­nal, sup­ports re­siz­ing and moves as you it­er­ate from 0 to 79.

The progressbar module even lets you do fancier things like ETA or fie transfer speeds, all just as nicely.

Is­n't that code just right? You want a progress bar for that loop? Wrap it and you have one! And of course since I am a PyQt pro­gram­mer, how could I make PyQt have some­thing as right as that?

Here'show the out­put looks like:

progress

You can do this with ev­ery toolk­it, and you prob­a­bly should!. It has one ex­tra fea­ture: you can in­ter­rupt the it­er­a­tion. Here's the (short) code:

# -*- coding: utf-8 -*-
import sys, time
from PyQt4 import QtCore, QtGui

def progress(data, *args):
    it=iter(data)
    widget = QtGui.QProgressDialog(*args+(0,it.__length_hint__()))
    c=0
    for v in it:
        QtCore.QCoreApplication.instance().processEvents()
        if widget.wasCanceled():
            raise StopIteration
        c+=1
        widget.setValue(c)
        yield(v)

if __name__ == "__main__":
    app = QtGui.QApplication(sys.argv)

    # Do something slow
    for x in progress(xrange(50),"Show Progress", "Stop the madness!"):
        time.sleep(.2)

Have fun!


Contents © 2000-2020 Roberto Alsina