3ENGINE

Programación y otros cachivaches

Archivo

Página 7/104

Tecnologia

Cómo preparar y distribuir un paquete Python


Supongamos que después de un laborioso esfuerzo has terminado una aplicación o módulo en Python y quieres compartirlo. En este artículo intento explicar cómo preparar y distribuir un paquete Python.
packpy

Preparación básica de tu paquete

Normalmente yo me organizo el código así pero por supuesto eres libre de hacer lo que quieras 🙂

/demo
/test
/[src]
LICENSE
README.rst
MANIFEST.in
setup.py

carpetas

  • /[src] con el nombre de tu proyecto de la que cuelga todo el código fuente y ficheros relacionados
  • /demo con una o varias demos de mi aplicación o módulo
  • /test donde mediante test unitarios, técnicas de mocking y code coverage me aseguro que mi código es indestructible X-) y así garantizo que la víctima persona que haga uso de mi código no se lleve sorpresas.

setup.py

setuptools deja tu aplicación o módulo preparado para distribuirlo. Es un módulo incorporado en Python que permite descargar, construir, instalar, actualizar y desinstalar paquetes de Python. Necesitarás crear un setup.py.

Veamos un ejemplo y luego comento:

from setuptools import setup, find_packages

setup(
name = 'iniconfig.py',
version = ‘1.0.0’,
author = 'David Miro',
author_email = 'lite.3engine@gmail.com',
description = 'A more convenient and practical approach to manage .ini files',
long_description = open('README.rst').read(),
license = open('LICENSE').read(),
url = 'https://github.com/dmiro/iniconfig',
packages = find_packages(),
test_suite = 'tests',
classifiers = [
    'Development Status :: 5 - Production/Stable',
    'Intended Audience :: Developers',
    'Intended Audience :: Information Technology',
    'Programming Language :: Python',
    'Programming Language :: Python :: 2.6',
    'Programming Language :: Python :: 2.7',
    'Programming Language :: Python :: 3.2',
    'Programming Language :: Python :: 3.3',
    'Programming Language :: Python :: 3.4',
    'Programming Language :: Python :: 3.5',
    'Topic :: Software Development :: Libraries :: Python Modules',
    'License :: OSI Approved :: MIT License'
    ]
)
  • name: es el nombre de tu paquete, si tienes pensado subirlo a PyPi comprueba antes que el nombre no exista.
  • long_description: descripción detallada de lo que hace el paquete, en texto plano o en formato reStructuredText (.rst). Si la documentación la tienes en otro formato prueba pandoc para convertila a .rst
  • license: es el texto con describe el tipo de licencia que has elegido.
  • url: la web del autor o en este caso donde reside el código
  • packages: son las dependencias a otros paquetes que necesita para funcionar. Puedes añadirlo manualmente o dejar que find_packages() busque las dependencias en tu código automáticamente por ti.
  • test_suite: opcional, nombre de la carpeta que contiene los tests unitarios. Para que todo funcione automáticamente se tienen que cumplir algunas convenciones de nomenclatura. (1) los archivos que contiene los test deben acabar en _text.py (2) dentro de cada archivo los test deben heredar de la clase unittest.TestCase (3) dentro cada clase los test a ejecutar debe empezar por def test_. También es importante que la carpeta /tests contenga un __init__.py. Para saber mas lee unittest

    ejemplo:

    /tests
    /tests/__init__.py
    /tests/textcase_test.py
    

    testcase_test.py

    import unittest
    
    class Test(unittest.TestCase):
    
        def test_upper(self):
            self.assertEqual('foo'.upper(), 'FOO')
    
  • classifiers: sirve para clasificar tu paquete, imprescindible si tienes pensado subir tu paquete a PyPi.

otros ficheros

  • LICENSE: contiene el texto de la licencia. Lo que hacemos es leer su contenido desde setup.py.
  • README.rst: contiene la descripción detallada de lo que hace el paquete. Al igual que el fichero de licencia, leemos el contenido desde setup.py
  • MANIFEST.in: setuptools incluye todos los archivos referenciados en setup.py – modulos, paquetes, readme.txt y todo lo que cuelga de /test. Si deseas incluir algún otro archivo, entonces debes incluir la referencia en el archivo manifest.in.

    ejemplo:

    include LICENSE
    include README.rst
    

Generar y distribuir tu paquete con PyPi

Esta chuleta (cheat sheet) resume los comandos básicos para preparar y distribuir tu paquete, así como los comandos básicos para instalar paquetes de terceros.

Cómo preparar y distribuir un paquete Python

Siempre y cuando tu paquete sea de código abierto, la manera oficial es subirlo a PyPi. De este modo tu aplicación o módulo estará disponible a través de la herramienta de administración pip que Python 2.7.9 y Python 3.4 incorporan. Si tu versión Python es anterior averigua cómo instalar pip.

Algunas de las ventajas de pip es que antes de instalar descarga todos los paquetes ahorrando problemas a mitad de instalación, tiene un mecanismo de desinstalación de paquetes y además ofrece soporte para instalar directamente de git, mercurial o bazaar.

Pasos

  1. Date de alta en PyPi
  2. Registra tu paquete en PyPi

    python setup.py register
    
  3. Subelo a PyPi

    Esto lo puedes hacer en un paso:

    python setup.py sdist upload
    

    o en dos pasos:
    python setup.py sdist
    twine upload dist/*
    

    La ventaja de twine es que utiliza seguridad en la capa de transporte (TLS) para proteger tu subida.

Otros comandos básicos

  • testear tu paquete:
    python setup.py test
    
  • lista de paquetes instalados:
    pip freeze
    
  • desinstalar un paquete:
    pip uninstall <nombre_del_paquete>
    

    Curiosamente, setuptools no provee un comando para desinstalar un paquete, aunque siempre puedes ir a /site-packages y borrar a mano las carpetas, si te atreves. Para obtener la lista de paquetes instalados:

    python setup.py install --record files.txt
    
  • instalar directamente de github
    pip install <url_git>
    



Tecnologia

Cómo mostrar una imagen de progreso con jquery ajax (ajax loader)


Para mostrar una imagen de progreso con jquery ajax antes de nada tenemos que conseguir un gif animado. Por ejemplo podemos obtener de ajaxload.info un gif como este:

loading

Lo que yo quiero conseguir es que aparezca el gif animado sea cual sea la llamada jquery ajax. Esto se consigue con el siguiente código en jquery. Básicamente lo que hace es mostrar un div con id=ajaxBusy que contiene el gif, al iniciar una llamada jquery ajax y esconder el div al finalizar la misma:

$(document).ajaxStart(function () {
    $('#ajaxBusy').show();
}).ajaxStop(function () {
    $('#ajaxBusy').hide();
});

El código HTML con el gif es el siguiente:

<div id="ajaxBusy">
  <p>
    <img src="loading.gif">
  </p>
</div>

Que necesita su correspondiente css:

#ajaxBusy
{
    display: none; 
    margin: 0px;
    paddingLeft: 0px;
    paddingRight: 0px;
    paddingTop: 0px;
    paddingBottom: 0px;
    position: absolute;
    top: 50%;
    left: 50%;
    marginTop: -50px;
    marginLeft: -50px;
    width: 100px;
    height: 100px;
}

Para probar que todo esto funciona he escrito una demo. En la demo he adaptado un servidor HTTP muy simple escrito en Python que explique en la entrada Método rápido para servir archivos a través de HTTP (Servidor HTTP) con SimpleHTTPServer. La adaptación ha consistido en añadir en el método do_POST() un «echo» de modo que el servidor retorne un HTML con el mensaje enviado, en un tiempo opcionalmente parametrizable. En mi caso, esto es util para testear que el gif animado aparece:

from SimpleHTTPServer import SimpleHTTPRequestHandler
import SocketServer
import time
import urlparse
from cgi import parse_header, parse_multipart

class MyRequestHandler(SimpleHTTPRequestHandler):

    def __init__(self, *args):
        SimpleHTTPRequestHandler.__init__(self, *args)

    def parse_POST(self):
        ctype, pdict = parse_header(self.headers['content-type'])
        if ctype == 'multipart/form-data':
            postvars = parse_multipart(self.rfile, pdict)
        elif ctype == 'application/x-www-form-urlencoded':
            length = int(self.headers['content-length'])
            postvars = urlparse.parse_qs(
                    self.rfile.read(length), 
                    keep_blank_values=1)
        else:
            postvars = {}
        return postvars
    
    def do_GET(self):
        print self.command
        print self.path
        return SimpleHTTPRequestHandler.do_GET(self)

    def do_POST(self):
        print self.command
        print self.path
        parse = urlparse.urlparse(self.path)
        fields = self.parse_POST()
        self.send_response(200) 
        self.send_header('Content-type', 'text/html')
        self.end_headers()
        if parse.path == '/echo':
            if fields['delay']:
                seconds = float(fields['delay'][0]) / 1000
                time.sleep(seconds)
            if fields['html']:        
                self.wfile.write(fields['html'][0])
        else:
            self.wfile.write('<ul>')
            for key, value in fields.iteritems():
                self.wfile.write('<li>%s: %s</li>' % (key, value))
            self.wfile.write('</ul>')

PORT = 8000
Handler = MyRequestHandler
httpd = SocketServer.TCPServer(("", PORT), Handler)

print "serving at port", PORT
httpd.serve_forever()

Al final tienes esto:

demo

Enlace para descargar la demo: demo.zip




Tecnologia

Método rápido para servir archivos a través de HTTP (Servidor HTTP) con SimpleHTTPServer


Python viene con un módulo llamado SimpleHTTPServer que permite montar un sencillo Servidor HTTP al que acceder través de localhost. Esto es útil a la hora de desarrollar porque hay cosas que no funcionan con ficheros. Por ejemplo las URLs en los navegadores web.

En la documentación de Python explica que también es posible «lanzar» directamente el servidor HTTP utilizando el modificador -m del intérprete y como argumento opcional el número de puerto.

Ejemplo:

python -m SimpleHTTPServer 8000

La clase encarga de hacer todo esto es SimpleHTTPRequestHandler que implementa la interface BaseHTTPRequestHandler. Esta clase es capaz de servir archivos del directorio actual y de cualquier archivo que esté por debajo, asi como la cartografía de la estructura de directorios en una petición HTTP:

Servidor HTTP

Un problemilla: SimpleHTTPRequestHandler no admite HTTP POST

Tengo form.html que contiene un formulario con un submit action de tipo POST:

<!DOCTYPE html>
<html>
<head>
</head>
<body>
  <img src="python.png"/>
  <form action="action_page.php" method="post">
    First name:<br>
    <input type="text" name="firstname" value="Mickey"><br>
    Last name:<br>
    <input type="text" name="lastname" value="Mouse"><br><br>
    <input type="submit" value="Submit">
  </form>
</body>
</html>

Con el siguiente resultado:

form

Ahora bien si hago submit me encuentro con una desagradable sorpresa:

error

El problema es que SimpleHTTPRequestHandler implementa las funciones do_GET() y do_HEAD(), pero NO implementa do_POST() aunque si lo pensamos tiene cierta lógica.

Implementación de do_POST() con el objetivo de testear una aplicación

Si el objetivo es comprobar que información POST esta llegando al servidor esta implementación puede servirnos:

from SimpleHTTPServer import SimpleHTTPRequestHandler
import SocketServer
import time
import urlparse
from cgi import parse_header, parse_multipart

class MyRequestHandler(SimpleHTTPRequestHandler):

    def __init__(self, *args):
        SimpleHTTPRequestHandler.__init__(self, *args)

    def parse_POST(self):
        ctype, pdict = parse_header(self.headers['content-type'])
        if ctype == 'multipart/form-data':
            postvars = parse_multipart(self.rfile, pdict)
        elif ctype == 'application/x-www-form-urlencoded':
            length = int(self.headers['content-length'])
            postvars = urlparse.parse_qs(
                    self.rfile.read(length), 
                    keep_blank_values=1)
        else:
            postvars = {}
        return postvars
    
    def do_GET(self):
        print self.command
        print self.path
        return SimpleHTTPRequestHandler.do_GET(self)

    def do_POST(self):
        print self.command
        print self.path
        fields = self.parse_POST()
        self.send_response(200) 
        self.send_header('Content-type', 'text/html')
        self.end_headers()
        self.wfile.write('<ul>')
        for key, value in fields.iteritems():
            self.wfile.write('<li>%s: %s</li>' % (key, value))
        self.wfile.write('</ul>')

PORT = 8000
Handler = MyRequestHandler
httpd = SocketServer.TCPServer(("", PORT), Handler)

print "serving at port", PORT
httpd.serve_forever()

Si lanzo el script, ahora al hacer el submit de form.htm este es el resultado:

resultpost

Espero que os pueda servir.
demo: server.zip




Tecnologia

Python y PyQt: cómo agrupar una lista de radio buttons (QRadioButtonGroupBox)


QT, al contrario de otras librerias centradas en el desarrollo de interfaces gráficas de usuario, no implementa un widget que permita agrupar una lista de radio buttons. Crear un nuevo Widget que permita esto, no es muy complicado. A continuación el código:

Agrupar una lista de radio buttons


from PyQt4.QtGui import *
from PyQt4.QtCore import *


class QRadioButtonGroupBox(QGroupBox):

    selectItemChanged = pyqtSignal(int)

    def __buttonGroupClickedSlot(self, index):
        self.selectItemChanged.emit(index)

    def __init__(self, *args):
        QGroupBox.__init__(self, *args)
        self.box = QFormLayout(parent=self)
        self.buttonGroup = QButtonGroup()
        QObject.connect(self.buttonGroup, SIGNAL("buttonClicked(int)"), 
                        self.__buttonGroupClickedSlot)

    def selectedItem(self):
        return self.buttonGroup.checkedId()

    def setChecked_(self, index, isChecked=True):
        self.buttonGroup.button(index).setChecked(isChecked)

    def addItem(self, text, isChecked=False):
        radioButton = QRadioButton(text)
        index = len(self.buttonGroup.buttons())
        self.buttonGroup.addButton(radioButton, index)
        self.buttonGroup.button(index).setChecked(isChecked)
        self.box.addRow(radioButton)
        return index

    def addItems(self, *items):
        for item in items:
            self.addItem(item)

    def removeItems(self):
        for button in self.buttonGroup.buttons():
            self.buttonGroup.removeButton(button)
            self.box.removeWidget(button)

selectedItem retorna el número de radiobutton seleccionada, setChecked_ permite seleccionar uno de los radiobuttons de la lista, addItem y addItems sirve para añadir radiobuttons y removeItems para eliminar la lista completa de radiobutton. También incluye el signal selectItemChanged que se dispara cuando el usuario selecciona un radiosbutton de la lista.

A continuación una demo:

def selectedItemChangedSlot(index):
    print 'ha seleccionado:', index

app = QApplication(sys.argv)
main = QRadioButtonGroupBox('Mi color favorito')
main.selectItemChanged.connect(selectedItemChangedSlot)

main.addItem('1')
main.addItem('2')
main.addItem('3')
main.removeItems()

main.addItem('Rojo')
main.addItem('Azul', isChecked=True)
main.addItem('Amarillo')
main.addItems('Marron', 'Naranja', 'Verde')

main.show()
sys.exit(app.exec_())

Resultado:

agrupar una lista de radio buttons

Punto Extra

A veces puede ser interesante tener la posibilidad de acompañar al radio button de un widget extra. El siguiente código permite esto:

from PyQt4.QtGui import *
from PyQt4.QtCore import *
import sys


class QRadioButtonGroupBox(QGroupBox):

    selectItemChanged = pyqtSignal(int, QWidget)

    def __setEnabledWidget(self, index):
        widget = None
        for button in self.buttonGroup.buttons():
            if button.buddie:
                if index == self.buttonGroup.id(button):
                    button.buddie.setEnabled(True)
                    widget = button.buddie
                else:
                    button.buddie.setEnabled(False)
        return widget

    def __buttonGroupClickedSlot(self, index):
        widget = self.__setEnabledWidget(index)
        self.selectItemChanged.emit(index, widget)

    def __init__(self, *args):
        QGroupBox.__init__(self, *args)
        self.box = QFormLayout(parent=self)
        self.buttonGroup = QButtonGroup()
        QObject.connect(self.buttonGroup, SIGNAL("buttonClicked(int)"),
                        self.__buttonGroupClickedSlot)

    def selectedItem(self):
        return self.buttonGroup.checkedId()

    def buddie(self, index):
        button = self.buttonGroup.button(index)
        return button.buddie

    def setChecked_(self, index):
        self.__setEnabledWidget(index)
        self.buttonGroup.button(index).setChecked(True)

    def addItem(self, text, widget=None, isChecked=False):
        radioButton = QRadioButton(text)
        index = len(self.buttonGroup.buttons())
        self.buttonGroup.addButton(radioButton, index)
        self.buttonGroup.button(index).setChecked(isChecked)
        radioButton.buddie = widget
        if widget:
            self.box.addRow(radioButton, widget)
            widget.setEnabled(isChecked)
        else:
            self.box.addRow(radioButton)
        return index

    def addItems(self, *items):
        for item in items:
            self.addItem(item)

    def removeItems(self):
        for button in self.buttonGroup.buttons():
            self.buttonGroup.removeButton(button)
            if button.buddie:
                self.box.removeWidget(button.buddie)
                button.buddie = None
            self.box.removeWidget(button)

Una demo:

def selectedItemChangedSlot(index, widget):
    print 'ha seleccionado:', index
    print 'widget asociado:', widget

app = QApplication(sys.argv)
main = QRadioButtonGroupBox('Mi color favorito')
main.selectItemChanged.connect(selectedItemChangedSlot)
main.addItem('Rojo')
main.addItem('Azul', isChecked=True)
main.addItem('Amarillo')
main.addItems('Marron', 'Naranja', 'Verde')
colores = QComboBox()
colores.addItems(['violeta', 'fucsia', 'lima'])
main.addItem('otro color', widget=colores)
main.addItem('mas colores:', QLineEdit())
main.show()
sys.exit(app.exec_())

Resultado:

agrupar una lista de radio buttons