Distribución de librerías Python con setup.py

Python cake Python Cake por Sinsira. CC BY-NC. Imagen con calidad y tamaño reducidos.

En este tutorial se explicará cómo distribuir una librería Python a través del repositorio PyPi usando para esto el archivo setup.py.

Durante el desarrollo de un proyecto de software Python es común implementar utilidades o librerías que facilitan la construcción de la aplicación. Estas utilidades, en la mayoría de los casos, son reutilizadas en más de un proyecto.

En algún punto, el equipo de desarrollo se da cuenta que es muy probable que estas utilidades puedan ayudar a personas y proyectos al exterior de la organización. Es en este momento dónde el equipo de desarrollo decide publicar las utilidades para que otras personas y organizaciones se puedan beneficiar de ellas.

Surge entonces el problema sobre cómo distribuir el código de dichas utilidades, de tal forma que el equipo de desarrollo pueda continuar implementando nuevas características y que las demás organizaciones puedan acceder a las ultimas versiones de la forma mas eficiente.

Es aquí dónde el repositorio de paquetes Python, llamado PyPi, juega un papel importante. Este repositorio contiene una miríada de librerías y utilidades listas para ser descargadas mediante pip y utilizadas en cualquier proyecto de software Python.

Implementación de la librería Python

El primer paso consiste en tener una librería lista para ser distribuida. En este caso se ha preparado una utilidad muy sencilla llamada StarWars Ipsum que permite agregar a cualquier proyecto, texto de relleno basado en la introducción de cada una de las películas de la saga Star Wars. Por el momento solo está disponible la introducción del episodio IV en inglés.

El código fuente completo está disponible en Github y Bitbucket.

La utilidad proporciona dos funciones: paragraphs() y html(). La función paragraphs() retorna un listado con los párrafos leídos del archivo de texto a_new_hope.txt. La función html() retorna código HTML válido del texto leído, listo para ser incluido en una página web.

import markdown
import os

PARENT_DIR = os.path.dirname(os.path.dirname(__file__))

def paragraphs():
    paragraphs = []

    filename = os.path.join(PARENT_DIR, 'data', 'a_new_hope.txt')
    with open(filename, 'r') as file_data:
        paragraph = ''
        for line in file_data:
            if line == '\n':
                paragraphs.append(paragraph)
                paragraph = ''
            else:
                paragraph.append(line.replace('\n', ' '))

    return paragraphs

def html():
    html = ''

    filename = os.path.join(PARENT_DIR, 'data', 'a_new_hope.txt')
    with open(filename, 'r') as file_data:
        html = markdown.markdown(file_data.read())

    return html

El archivo setup.py

Al observar el archivo setup.py, es posible ver la información necesaria para que el repositorio PyPi pueda clasificar la librería correctamente y para que pip pueda instalarla de forma apropiada.

distutils es método tradicional para distribuir librerías y utilidades Python. Este tutorial contiene información sobre setuptools, una implementación mejorada de distutils.

from setuptools import find_packages, setup

setup(
    name='starwars-ipsum',
    version='0.0.1',
    author='Jose Miguel Venegas Mendoza',
    author_email='jvenegas@rukbottoland.com',
    description=('A simple utility that generates placeholder text from Star '
                 'Wars intros.'),
    long_description=get_readme(),
    license='BSD',
    keywords='starwars utilities ipsum',
    url='',
    packages=find_packages(),
    package_data={
        'starwars_ipsum': ['*.txt']
    },
    install_requires=['markdown==2.6.5'],
    classifiers=[
        'Development Status :: 3 - Alpha',
        'License :: OSI Approved :: BSD License',
        'Programming Language :: Python :: 2.7',
        'Topic :: Utilities'
    ]
)

La función setup() necesita saber algunos datos básicos sobre la librería tales como el nombre, la versión, el autor y la descripción. Estos datos por los general son cadenas de texto.

Sin embargo, es posible utilizar funciones que retornen una o varias cadenas de texto. Tal es el caso del atributo long_description al que se le ha asignado la función get_readme().

def get_readme():
    readme = ''
    try:
        import pypandoc
        readme = pypandoc.convert('README.md', 'rst')
    except (ImportError, IOError):
        with open('README.md', 'r') as file_data:
            readme = file_data.read()
    return readme

Debido a que PyPi utiliza reStructuredText en lugar de Markdown para leer el texto en los archivos README y el archivo README de la utilidad esta escrito en Markdown, se hace necesario utilizar la librería pandoc para convertir el formato a reStructuredText para que PyPi lo pueda interpretar correctamente.

Continuando con la descripción de la función setup(), el atributo packages se utiliza para indicarle a pip la ubicación de los paquetes que la librería proporciona para poder instalarlos apropiadamente.

En este caso el valor podría ser simplemente packages=['starwars_ipsum'] pero en su lugar se utiliza la función find_packages(), la cual busca automáticamente todos los paquetes existentes dentro del directorio de la librería.

En el atributo package_data se indican los archivos adicionales que necesita la librería para funcionar correctamente. Debido a que la utilidad lee archivos de texto con el contenido de la introducción de las películas, se hace necesario incluir todos los archivos de texto *.txt.

Estos archivos se deben asignar por paquete, de ahí que se definan mediante un diccionario de datos cuya clave es el nombre del paquete.

Debido a que la utilidad provee una función para convertir el texto a código HTML listo para ser incluido en una página web, es necesario utilizar una librería llamada markdown para que realice la conversión.

Se dice entonces que la utilidad depende de la librería markdown y por lo tanto debe ser instalada al mismo tiempo que la utilidad, para que funcione óptimamente. Estas relaciones de dependencia se asignan a la variable install_requires, la cual es un listado de dependencias que se definen usando el formato requirements.txt.

El atributo classifiers es un listado de cadenas de texto que se utiliza para describir con mas detalle la librería o utilidad. Es posible ver en este caso que la utilidad esta en un estado de desarrollo Alpha, que su licencia es de tipo BSD, que el lenguaje de programación usado para su elaboración es Python 2.7 y que esta clasificada como una utilidad.

El listado completo de classifiers puede consultarse aquí.

Registrando la librería Python en PyPi

Actualmente existen dos repositorios PyPi: uno principal y otro de prueba. El repositorio de prueba fue creado para proporcionar un ambiente adecuado a los desarrolladores que desean probar el sistema de distribución o que desean probar la integración de varias librerías antes de enviarlas al repositorio principal.

Para poder enviar la librería o utilidad al repositorio PyPi principal, es indispensable crear una cuenta en https://pypi.python.org/pypi. Para hacerlo en el repositorio de prueba se debe crear una cuenta en https://testpypi.python.org/pypi.

Una vez registrada una cuenta en cada repositorio, se debe crear un archivo en el directorio home de la maquina donde se encuentra la librería, llamado .pypirc. Este archivo debe contener las credenciales de acceso a los repositorios:

[distutils]
index-servers=
    pypi
    pypitest

[pypi]
repository = https://pypi.python.org/pypi
username = <nombre de usuario>
password = <contraseña>

[pypitest]
repository = https://testpypi.python.org/pypi
username = <nombre de usuario>
password = <contraseña>

Finalmente, para envíar la librería al repositorio deseado, ejecutamos los siguientes comandos en una terminal:

>> Para envíar la libreria al repositorio principal
$ python setup.py register -r pypi
$ python setup.py sdist upload -r pypi

>> Para envíar la libreria al repositorio de prueba
$ python setup.py register -r pypitest
$ python setup.py sdist upload -r pypitest

En este punto, la librería o utilidad ya debe ser visible en la página web del repositorio.

Instalación de la librería Python

Simplemente usamos pip para instalar la librería de la siguiente manera:

$ pip install starwars-ipsum