17 abril 2015

Empaquetando programas Python - Paquetes para PyPI


Una vez que hayamos finalizado nuestro programa lo más probable es que queramos compartirlo. Hay varias maneras de compartir un programa Python:
  • Comprimir el programa y mandarlo por email: Sólo usaría este programa para scripts muy cortos que sólo utilizasen bibliotecas incluidas por defecto en Python.
  • Colgar el programa en un repositorio público como GitHub o BitBucket: Es una buena opción si queremos compartir nuestro programa con otros desarrolladores para dejarles hacer contribuciones al código. Revisamos esa opción en mi tutorial sobre Mercurial.
  • Ponerlo en el Python Package Index (PyPI): Es la opción obvia si queremos compartir nuestro programa con otros desarrolladores de Python no para que lo modifiquen sino para que lo usen como librería en sus propios desarrollos de Python. La parte buena de esta opción es que se pueden instalar dependencias de manera automática.
  • Empaquetarlo como un paquete nativo de una distribución de Linux: En otro capítulo explicaré maneras de empaquetar nuestro programa Python en un paquete RPM (para Fedora/RedHat/Suse) o un paquete DEB (para Ubuntu/Debian
En este artículo voy a explicar el tercer método: usar PyPI como repositorio.

PyPI es el repositorio oficial de paquetes de Python y está mantenido por la Python Software Foundation. Por eso es el repositorio central de referencia para practicamente todos los paquetes de Python públicamente disponibles. Pypi tiene una página web desde la que se puede descargar cualquier paquete manualmente pero generalmente lo mejor es usar pip o easy_install para instalar los paquetes desde la línea de comandos. Yo prefiero pip porque está incluido por defecto en las últimas versiones de Python 3.x y además es la herramienta recomendada por la Python Packaging Authority (PyPA), la principal fuente sobre buenas prácticas sobre empaquetamiento de aplicaciones python. Con pip se puede descargar e instalar cualquier paquete python desde los repositorios PyPI, junto con cualquier dependencia necesaria.

Se pueden subir dos tipos principales de paquetes python a PyPI:
  • Paquetes Wheel: Es un paquete de tipo binario que está pensado para reemplazar a los antiguos paquetes Egg. Es el formato recomendado para subir paquetes a PyPI.
  • Paquetes source: Es un tipo de paquete que tiene que ser compilado en el extremo del usuario. El formato wheel es más rápido y fácil de instalar porque ya viene precompilado desde el extremo del empaquetador. Una razón para utilizar paquetes source en vez de wheel sería cuando queramos incluir extensiones en C, dado que estas tienen que ser compiladas en el extremo del usuario.
En general los paquetes wheel son los más recomendables para ser distribuidos al común de los usuarios pero los paquetes source son útiles para hacer paquetes nativos para distintas distribuciones de linus (paquetes DEB para Debian/Ubuntu, paquetes RPM para Red Hat/Fedora/Suse, etc). En este capítulo vamos a ver cómo generar ambos tipos.

Lo primero que tenemos que hacer para asegurarnos de que nuestra aplicación está lista para ser empaquetada es organizarla en una estructura de carpetas estándar. Puedes organizar los ficheros de tu proyecto como quieras pero si le echas un ojo a cualquiera de los proyectos de python populares en Bitbucket o GitHub te darás cuenta de que siguen una estructura similar a la hora de organizar sus ficheros. Dicha estructura es una buena práctica que se ha ido generalizando con el tiempo. Para verlo más claro, se puede consultar el esqueleto de estructura de proyecto desarrollado por PyPA como ejemplo de esta buena práctica. Según la misma, lo habitual es poner el script de instalación y todos los ficheros que describan el proyecto en la carpeta raíz del mismo. Los ficheros de la aplicación (los que desarrollamos) deberían ir dentro de una carpeta llamada como el proyecto, pero colgando de la raíz. Otro ficheros relacionados con el desarrollo, pero que no formen parte del desarrollo mismo de la aplicación, deberían colocarse en sus propias carpetas al mismo nivel que la carpeta de desarrollo de la aplicación, pero no dentro de ella.


Veamos un ejemplo, el repositorio en GitHub de Geolocate. Se puede ver que puse los scripts de compilación, empaquetado e instalación en el nivel razi, junto con ficheros como el README.txt o el de REQUIREMENTS.txt que descirben la aplicación. Sin embargo los ficheros de desarrollo de la aplciación están dentro de una carpeta llamada "geolocate". Algunos prefieren meter los tests unitarios en su propia carpeta fuera de la de aplicación y otros prefieren meterlos dentro. Si no se pretende distribuir los tests a los usuarios finales, creo que es mejor mantenerlos fuera de la carpeta de aplicación. En el caso de geolocate están dentro porque tuve una serie de problemas con los imports y fue la primera manera que encontré de resolverlos, sin embargo ahora que creo que tengo claras las causas de los problemas creo que mantendrá los tests en su propia carpeta colgando directamente del nivel raíz.

Una vez organizado el proyecto en una estructura de carpetas estándar, suele ser buena idea crear un virtualenv para ejecutar la aplicación. Si no sabes lo que es un virtualenv, te recomiendo que le eches un ojo a este tutorial. Además, ese virtualenv nos permitirá definir la lista de paquetes de python que nuestra aplicación necesita como dependencias y exportar dicha lista a un fichero REQUIREMENTS.txt, tal y como se explica aquí. Ese fichero será realmente útil a la hora de escribir nuestro script setup.py, tal y como vamos a ver.

El script setup.py es el fichero más importante pra crear paquetes compatibles con PyPI. Tiene dos funciones principales:
  • Es el fichero donde se configuran los diferentes aspectos de nuestro proyecto. El script contiene una función global: setup(). Los argumentos que se le pasen a dicha función definirán determinados detalles de nuestra aplicación: autor, versión, descripción, etc.
  • Es el interfaz para ejecutar desde la línea de comandos diversas órdenes relacionadas con las tareas de empaquetamiento.
Se puede obtener una lista de los comando disponibles a través de setup.py haciendo:

dante@Camelot:~/project-directory$ python setup.py --help-commands

Setup.py depende del paquete de python setuptools, así que hay que asegurarse de tenerlo instalado.

Vamos a explicar como escribir un script setup.py funcional usando como guía
usando como guía el fichero setup.py de geolocate. Como se puede ver en ese ejemplo, el fichero en si es relativamente simple: se importa la función setup.py del paquete setuptools y se le llama. La configuración como tal viene con los parámetros que le pasamos a setup(). Veamos esos parámetros:
  • "name": Es el nombre del paquete tal y como va a ser identificado en PyPI. Lo mejor es comprobar que el nombre que deseamos ponerle a nuestra aplicación no esté siendo usado ya en PyPI. En mi caso desarrollé geolocate sólo para encontrarme con que el nombre ya estaba siendo usado por otro paquete, por lo que tuve que llamar al paquete glocate aunque el ejecutable de dentro aún se llamase geolocate. Una solución muy sucia pero la próxima vez lo haré mejor desde el principio.
  • "version": Versión del paquete. Intenta mantenerlo para facilitar que los usuarios puedan actualizar su copia descargándosela de PyPI.
  • "description": Es la descripción corta que se mostrará en la página del paquete en PyPI. Lo más recomendable es mantenerla corta y descriptiva.
  • "long_description": Esta es la versión larga de la descipción. Aquí se puede añadir más detalle. Dos o tres párrafos está bien.
  • "author": Tu nombre (real o nickname).
  • "author_email": Correo electrónico donde quieres que te contacten para cosas relativas a esta aplicación.
  • "license": Nombre de la licencia elegida para este programa.
  • "url": URL del sitio web de esta aplicación.
  • "download_url": Aquí suelo poner la URL desde donde se pueden descargar los paquetes de instalación para cada una de las distribuciones de linux.
  • "classifiers":Categorías en las que clasificar la aplicación. Es importante definirlas para que sirvan de ayuda a los usuarios a para buscar la aplicación que realmente necesitan dentro de la base de datos de PyPI. Se puede encontrar la lista completa de clasificadores aquí.
  • "keywords": Losta de palabras clave que describen tu aplicación.
  • "install_requires": Aquí es donde se ponen la lista dependencias que exportamos en su momento a REQUIREMENTS.txt.
  • "zip_safe": Una posible optimización que ofrecen los paquetes PyPI es la de poder ser instalados en formato comprimido de manera que consuman menos espacio en disco duro. El problema es que alguna aplicaciones no funcionan bien así, por lo que yo prefiero fijarlo siempre en "false".
  • "packages": Es necesario para enumerar la lista de packetes del proyecto a ser incluidos dentro del paquete instalable de tu proyecto. Aunque pueden ser especificados manualmente, yo prefiero usar la función setuptools.find_packages() para encontrarlos automáticamente.En esta función, la palabra clave "exclude" se supone que permite omitir paquetes que no necesitamos que sean incluidos en el instalable. el problema es que, en realidad, esa palabra clave no funciona debido a un bug. Más adelante veremos como solventar ese problema.
  • "entry_points" Aquí se define qué funciones de dentro de nuestros scripts serán llamadas directamente por el usuario. Yo lo utilizo para definir "console_scripts". Usando "console_scripts" setuptool compila el script fijado en un ejecutable de linux. Por ejemplo, si definimos la función main() dentro de nuestro script.py obtendremos un ejecutable sin extensión .py que podrá ser llamado ejecutado por el usuario directamente.
  • "package_data": Por defecto, setup.py sólo incluye ficheros  de python en el instalable. Si nuestra aplicación contiene otros tipos de ficheros que deben ser llamados por nuestros paquetes python, entonces deberíamos usar esta palabra clave para hacer que sean incluidos. Se define package_data con un diccionario. Cada clave es un paquete de la aplicación y su correspondiente valor es una lista de rutas relativas a los nombres de fichero que deberían incluirse dentro del instalable. Las rutas se consideran relativas a la carpeta que contiene el paquete. Setup.py no es capaz de crear carpetas vacióas para poner en ellas ficheros creados tras la instalación, por eso el truco es crear ficheros vacíos en dichas carpetas y acto seguido incluir dichos ficheros en el instalable usando esta palabra clave.
Algunos utilizan la palabra clave "data_files" para incluir ficheros que no están localizados en ninguno de los paquetes python de su aplicación. El problema que he encontrado con esta aproximación es que esos ficheros acaban instalados en rutas que dependen de la plataforma de instalación, por eso es realmente difícil hacer que tus scripts localicen esos ficheros cuando son ejecutados tras la instalación. Esa es la razón por la que prefiero poner mis ficheros dentro de mis paquetes y uso la palabra clave "package_data" en su lugar.

Una vez escrito el fichero setup.py ya se puede compilar el instalable, pero puede ser que queremos comprobar que setup.py está bien configurado. Pyroma es una aplicación que analiza la configuración de setup.py para comprobar que se cumplen las buenas prácticas reconocidas sobre empaquetado y alertándonos si no es así.

Una vez que estemos satisfechos con nuestra configuración de setup.py podemos crear un paquete source haciendo:

dante@Camelot:~/project-directory$ python setup.py sdist

Mientras que para crear un paquete wheel sólo hay que hacer:
dante@Camelot:~/project-directory$ python setup.py bdist_wheel

Cuando se usa setup.py para crear tus paquetes, se generará una carpeta dist (en la misma carpeta que setup.py) y se ubicarán ahí los instalables.

El problema surge cuando se intenta usar la función find_packages(), junto al argumento exclude y con la palabra clave packages. Me he dado cuenta de que en ese caso particular, el argumento exclude no funciona para omitir los ficheros no deseados de tu instalable, por lo que estos acaban siendo distribuidos. Según parece, este comportamiento es un bug y en tanto en cuanto lo resuelven la solución implica crear primero un paquete source y acto seguido crear un paquete whell del source con el siguiente comando:

dante@Camelot:~/project-directory$ pip wheel --no-index --no-deps --wheel-dir dist dist/*.tar.gz

El paquete generado se puede instalar localmente usando pip:
dante@Camelot:~/project-directory$ pip install dist/your_package.whl

Intentar instalar localmente tu paquete en un entorno virtualenv recién creado es una buena manera de probar si el instalable funciona como era de esperar.

Para compartir el paquete se puede enviar por email o hacerlo públicamente disponible desde un servidor web, pero la manera "pythonica" de hacerlo es subirlo a PyPI.

PyPI tiene dos sitios web.

  • PyPI test: Este sitio se limpia de una manera semi regular. Antes de subir tu paquete a al sitio principal de PyPI es mejor probarlo cacharreando con el sitio de tests. Se puede intentar instalar el paquete desde el sitio de tests de PyPI pero puede ocurrir que la instalación falle porque en dicho sitio no estén presentes todos los paquetes necesarios para solventar las dependencias. Eso se debe porque el sitio de test no cuenta con toda la base datos de paquetes sino con el subconjunto de paquetes que la gente haya subido para cacharrear.
  • PyPI main site: Este es el sitio web que tiene la base de datos de paquetes completa. Si alguna de nuestras dependencias no puede ser descargada de este lugar, entonces es que estamos haciendo algo mal.
Para usar cualquiera de estos sitios es necesario registrarse. Dado que la base de datos de usuario tampoco está compartida entre los sitios, habrá que registrarse en cada uno de ellos. En la página web de registro es donde se rellena el formulario dando los detalles de nuestro paquete. No hay que agobiarse, cualquier cosa que pongamos en dicho formulario puede modificarse después.

Tras el registro, se pueden subir los paquetes instaladores a través del interfaz web de PyPI pero lo más probable es que prefiramos mayor grado de automatización. Para enviar los ficheros desde nuestra consola (o desde un script), habrá que crear un fichero .pypirc en nuestra carpeta home (ojo al punto al comienzo del nombre) y darle este contenido:
[distutils]
index-servers=pypi 
[pypi]
repository = https://pypi.python.org/pypi
username =
password =

Después se puede ejecutar el comando twine para subir los paquetes a PyPI:
dante@Camelot:~/project-directory$ twine upload dist/*


Puede ser que tengamos que instalar twine ,con pip, antes de usarlo si se da el caso de que nuestro sistema no lo tenga instalado. Twine utilizará las credenciales que hayamos puesto en ~/.pypirc para conectarse a PyPI y subir nuestros paquetes. Twine usa cifrado SSL para proteger nuestras credenciales por lo que es una opción más segura que otras, como usar "python setup.py upload".

Tras lo anterior, nuestros paquetes estará disponibles a través de PyPI y cualquiera podrá instalárselos haciendo simplemente:
dante@Camelot:~/project-directory$ pip install your_package



14 marzo 2015

Clean Code

A lo largo de la vida no hay muchos libros que cambien realmente tu manera de pensar y hacer las cosas. En mi caso puedo contar con los dedos de una mano los libros de ese tipo que me he encontrado: "Computer Networking: a Top Down Approach" de Kurose y Ross, "Security Engineering: A Guide to Building Dependable Distributed systems" de Ross J. Anderson, y el libro del que trata este artículo "Clean Code: a Handbook of agile Software Craftmanship" de Robert C. Martin.

Supe de este libro en una de las conferencias de la PyConEs de 2013. En aquella conferencia hablaron de como hacer código sostenible en el tiempo. El tema me interesaba porque estaba preocupado por un fenómeno que todo programados se encuentra tarde o temprano: incluso con Python, cuando tu código crece se hace más difícil de mantener. Había programado aplicaciones que me resultaban difíciles de entender cuando tenía que revisarlas apenas unos meses después. Muchos años antes me había pasado a Python para evitar ese mismo problema en Java y Perl. En la conferencia aseguraban que los principios explicados en el libro ayudaban a prevenir ese problema. Esa es la razón por la que lo leí y he de admitir que tenían razón.

La lectura de este libro puede ser chocante. Hay tantas prácticas que sumimos correctas pero que sin embargo está terriblemente equivocadas que lo normal es leer por primera vez ciertos pasajes con una mezcla de sorpresa e incredulidad. Cosas como afirmar que los comentarios en el código son un reconocimiento de tu fracaso para hacer comprensible tu código suenan extrañas la primera vez que se leen pero luego se acaba comprendiendo que el autor está en lo cierto.

Los ejemplos del libro no están en Python sino en Java, sin embargo no creo que ningún programador de Python tenga problema alguno en comprender los conceptos que allí se explican. Algunos de los conceptos son un poco "javeros" pero muchos otros son útiles para desarrolladores en Python. Algunos de los conceptos principales son:

  • Los nombres de tus funciones deberían explicar claramente lo que hace la función. No se permiten abreviaturas en dichos nombres.
  • Una función sólo debería hacer una única cosa. Una función sólo debería tener un propósito. Por supuesto, una función puede tener muchos pasos pero el conjunto de ellos debería estar enfocado a conseguir el objetivo de la función y cada paso debería implementarse en su propia función. Una consecuencia de este enfoque es que es más fácil desarrollar tests unitarios para este tipo de funciones.
  • Las funciones deberían ser cortas: 2 líneas es estupendo, 5 está bien, 10 regular y 15 es muy pobre.
  • Los comentarios de código debería utilizarse únicamente para explicar decisiones de diseño pero nunca para explicar lo que hace el código.
  • No se deben mezclar niveles de abstracción en la misma función, lo que significa que no se debe llamar directamente a la API de Python mientras que otros pasos de la función llaman a tus propias funciones. En vez de eso es mejor meter tu llamada a la API dentro de una de tus propias funciones.
  • Ordena el código de manera que se pueda leer el conjunto de las implementaciones desde arriba en sentido descendente.
  • Hay que reducir todo lo pasible la cantidad de argumentos que se pasan a las funciones. Funciones con 1 argumento están bien, con 2 regular y con 3 probablemente deberías darle otra vuelta.
  • No te repitas (DRY, Don't Repeat Yourself, aunque este concepto ya lo conocía antes de leer este libro).
  • Las clases deberían ser pequeñas.
  • Las clases sólo deberían tener una razón para cambiar (Principio de Responsabilidad Única). En mi opinión este principio es una extensión lógica del de "un único propósito" de las funciones.
  • Idealmente, los atributos de las clases deberían ser utilizados por todos los métodos de la clase. Si nos encontramos atributos que sólo utilicen un pequeño subconjunto de métodos, deberíamos preguntarnos si esos atributos y esos métodos podrían ir en una clase separada.
  • Las clases deberían estar abiertas a extensiones pero cerradas para modificaciones. Eso significa que deberíamos incorporar nuevas funcionalidades heredando las clases existentes no modificándolas. De esa manera se reduce el riesgo de romper las programas cuando incluimos nuevas funcionalidades.
  • Haz TDD o condénate a ti mismo al infierno de meter modificaciones en tu código con la incertidumbre de si no romperás algo sin darte cuenta.
Hay muchos más conceptos, todos completamente explicados con ejemplos, pero estos que he explicado son los que tengo en la cabeza cuando escribo código.

Para probar si los principios de este libro estaban en lo cierto, desarrollé una aplicación llamada Geolocate, siguiendo estos conceptos y los de TDD. Al principio fue difícil cambiar mis hábitos a la horas de escribir código, pero conforme mi código fue creciendo me fui dando cuenta resultaba mucho más fácil encontrar errores y corregirlos de lo que me había resultado en proyectos anteriores. Además, cuando mi aplicación adquirió un tamaño respetable, la dejé aparte durante cinco meses para ver cómo de fácil resultaba retomar el desarrollo después de que pasase tanto tiempo. Fue asombroso. Aunque en mis anteriores proyectos habría necesitado varios días para comprender del todo un código tan grande, esta vez comprendí el funcionamiento del código en apenas una hora.

Mi conclusión es que este libro es imprescindible, dado que te permite mejorar dramáticamente la calidad de tu código y tu paz mental a la hora de mantenerlo después.