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.

http://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 = http://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 = http://ralsina.me
Description = Formats restructured text

Y acá están en acción:

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

reSt mode

http://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.

Comentarios

Comments powered by Disqus