26 diciembre 2015

Manteniendo sicronizado un fork de Github con respecto al repositorio original

Estupendo, has solicitado tu primer pull request y te lo han aceptado junto con algunos otros pull requests de otros  desarrolladores, o puede ser que todavía estes trabajando en tu propia versión pero la original esté recibiendo actualizaciones que te convenga tener en tu fork. En ambos casos lo que te interesará es importar todos los commits recibidos por el fork original en el tuyo.

Borrar tu fork y reclonar el original no es una solución porque podrías tener código inacabado que perderías si borrases tu repositorio. Trabajar de esa manera es ineficiente y tendente a errores.


Hay otra manera mucho mejor que esa pero para ser sincero no es evidente si se mira el interfaz web de Github. El truco es importar los commits del repositorio original en la copia local de nuestro fork y acto seguido hacer un merge de los cambios en local para luego subirlo todo a Github con un push.

A primera vista podría parecer excesivamente complicado pero tiene sentido si consideramos nuestro repositorio de Github como el lugar donde debemos dejar nuestros cambios definitivos mientras que dejamos nuestra copia local como el lugar donde cacharrear.


Como dije antes, para hacerlo así no he encontrado ninguna manera evidente a través del interfaz web de Github. Incluso Pycharm carece de una de la de las funcionalidades que necesitamos para hacer todo el proceso a través del GUI, por lo cual tendremos que recurrir a la consola en algún paso. Por suerte todo el asunto se resuelve con sólo unos pocos comandos.

El primer paso supone añadir el repositorio original de Github como una fuente remota de nuestro repositorio local. Voy a mostrar cómo hacerlo usando mi fork de vdist. Este paso es precisamente el que no podemos hacer visualmente a través del GUI de Pycharm. Para ver qué fuentes tenemos en nuestro repositorio de Git tendremos que teclear el siguiente comando:

dante@Camelot:~/vdist$ git remote -v origin https://github.com/dante-signal31/vdist.git (fetch) origin https://github.com/dante-signal31/vdist.git (push)


Como se puede ver, llamo origin a mi fork en Github. Ahora vamos a añadir el repositorio original a mis fuentes de git. Digamos que queremos llamar upstream al repositorio original:

dante@Camelot:~/vdist$ git remote add upstream https://github.com/objectified/vdist.git dante@Camelot:~/vdist$ git remote -v origin https://github.com/dante-signal31/vdist.git (fetch) origin https://github.com/dante-signal31/vdist.git (push) upstream https://github.com/objectified/vdist.git (fetch) upstream https://github.com/objectified/vdist.git (push)

Se puede ver que el repositorio original se encuentra ahora entre las fuentes de mi repositorio local de git.

Para descargar cualquier cambio en el repositorio original sin aplicarlo en el repositorio local debemos usar el comando git fetch:

dante@Camelot:~/vdist$ git fetch upstream remote: Counting objects: 1, done. remote: Total 1 (delta 0), reused 0 (delta 0), pack-reused 1 De https://github.com/objectified/vdist * [new branch] master -> upstream/master

Se puede hacer este último paso desde el GUI de Pycharm usando la entrada de menu: "VCS-> Git-> Fetch".

Los cambios descargados se guardan por defecto en la rama upstream/master. Una vez revisados los cambios, y si se está de acuerdo con ellos, se pueden aplicar a la rama master local haciendo:

dante@Camelot:~/vdist$ git checkout master Switched to branch 'master' dante@Camelot:~/vdist$ git merge upstream/master


Con lo anterior la rama master local y la del repositorio original quedarán sincronizados.  Ahora, lo que quedaría por hacer es actualizar nuestro repositorio de Github con un push:

dante@Camelot:~/vdist$ git push origin master


La buena noticia es que sólo hay que añadir una vez el repositorio original a nuestras fuentes de git, tras lo cual se puede usar git como hasta el momento, incluso a través del interfaz de Pycharm, ya que este último detecta las nuevas ramas remotas en cuanto hacemos un fetch desde su menú.

12 julio 2015

Effective Python

Cuando aprendes un lenguaje hay un punto en el que los libros para novatos no te aportan ya nada nuevo, es un punto en el que puedes desarrollar casi cualquier cosa con lo que ya sabes pero ese nivel de conocimiento ya no es suficiente porque quieres ser dominar el lenguaje y mejorar tus habilidades un poquito más cada día.

"Effective Python" es la clase de libro que leer cuando llegas a ese punto. No es un libro para empezar en Python sino para desarrolladores que quieran ser realmente "pythonicos".

Escrito por un ingeniero de Google, cubre muchas áreas del desarrollo como funciones, clases, metaclases, concurrencia, colaboration, o puesta en producción del código, todo ello a partir de muchas recetas y ejemplos. Se puede leer este libro secuencialmente o no. Tiene muchas semejanzas con libros como "Python Cookbook".  Algunos temas pueden ser conocidos, otros serán interesantes y nuevos. Al final este libro se convertirá en una referencia cuando nos topemos con situaciones como las descritas en este libro.

En mi opinión, el dinero invertido para obtener este libro está bien invertido. Me sirvió para conocer muchos ejemplos y trucos sobre temas y posibilidades realmente útiles para mis desarrollos y muchos meses después de leerlo sigo volviendo a este libro en busca de referencias.

26 abril 2015

Google Code anuncia su cierre

No es una noticia fresca, pero el mes pasado Google anunció que van a cerrar Google Code tras 9 años de existencia.

Ese servicio comenzó en 2006 con el objetivo de facilitar una manera segura y escalable de alojar proyectos open source. En ese tiempo, millones de personas contribuyeron con proyectos open source alojados en esa página. Sin embargo, el tiempo ha pasado y otras opciones como GitHub o BitBuket han superado en popularidad a Google Code.

Nadie puede negar que a Google le gusta probar nuevas tendencias y tecnologías, pero el hecho es que tienen que mantener contentos a sus accionistas por lo que acaban cerrando aquellos servicios que no se mantienen altos en popularidad. Muchos servicios de alta calidad han sido cerrados antes que Google Code: Notes o Wave son los primeros que me vienen a la cabeza. El problema es que Google cierra servicios porque no son populares, pero algunos empiezan a pensar que no son tan populares porque la gente y las compañías empiezan a tener miedo de que Google cierre esos servicios a la primera de cambio, por lo que no los utilizan. Quién sabe...

Lo cierto es que desde Marzo no se ha podido añadir ningún nuevo proyecto a Google Code y aquellos que ya existen en él podrán mantener su funcionalidad completa sólo hasta Agosto de este año. A partir de ahí, los datos de los proyectos serán sólo de lectura y el sitio se cerrará el año próximo aunque los datos de los proyectos seguirán disponibles para descarga en formato fichero.

Aquellos que como yo tengan proyectos en Google Code cuentan con el exportador de Google Code para migrar su proyecto a GitHub y documentación para migrar a otros servicios manualmente.

Muchos de mis proyectos están bastante desactualizados pero supongo que los importaré a mis repositorios privados en Bitbucket para refactorizarlos a fondo y después hacerlos disponibles a través de GitHub.

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.

08 enero 2015

Herramientas de cobertura de tests en Python

Supongo que hay muchas metricas para saber como de efectivos son nuestras pruebas unitarias a la hora de cubrir todas las casuísticas posibles en nuestros desarrollos. En el futuro tengo pensado formalizar mis conocimientos en TDD, pero por ahora sólo estoy jugando con los conceptos por lo que sólo sigo una métrica muy simple: si mis test ejecutan todas las líneas de código de mi programa, entonces debo estar enfocando bien mis tests.

Pero ¿cómo saber si tus tests ejecutan todo tu código?. Conforme crece el tamaño de tu código, también lo hace la cantidad de tus tests, por lo que es fácil dejar por olvido un fragmento de código sin probar. Ahí es donde entran en juego las herramientas de cobertura de tests.

Esas herramientas hacen seguimiento de la ejecución de tus tests y toman nota de las líneas de código visitadas. De esa manera, tras la ejecución de los tests se pueden ver las estadísticas sobre qué porcentage del código dse prueba en realidad y cual no.

Vamos a ver dos maneras de analizar la cobertura de tests de tus proyectos: desde la consola y desde tu IDE.

En caso de estar en un equipo Ubuntu, deberíamos instalar el paquete "python3-coverage" para obtener la herremienta de consola:

dante@Camelot:~/project-directory$ sudo aptitude install python3-coverage


Una vez instalado, python3-covergae debe ser ejecutado para llamar a tus pruebas unitarias:

dante@Camelot:~/project-directory$ python3-coverage run --omit "/usr/*" ./run_tests.py

Utilizo el parámetro "--omit" para mantener la carpeta "/usr" fuera de mis informes de cobertura. Antes de usar ese parámetro, las librerías externas que mi programa iba llamando se incluían en el informe. Obviamente, no creo tests para las librerías externas dado que no toco su implementación, por lo que incluirlas en el informe de cobertura sólo haría al mismo más difícil de leer. El script "run_test.py" es el mismo que expliqué en mi artículo sobre unittest.

Supongamos que todos nuestros tests se ejecutan correctamente. Se puede generar un informe de cobertura tanto en html como en xml. Para generar un informe en formato html, sólo hay que
ejecutar:

dante@Camelot:~/project-directory$ python3-coverage html


Este comendo genera una carpeta llamada "htmlcov" donde el se guarda el informe generado en html (su página de entrada es index.html). Lo único un poco molesto es que la carpeta htmlcov debe ser vorrada manualmente antes de generar un nuevo informe. Además, es bastante aburrido tener que buscar el fichero index.html para pichar en él y abrir el informe. Esa es la razón por la que prefiero usar un script que automatiza todas esas aburridas tareas.

#!/usr/bin/env bash

python3-coverage run --omit "/usr/*" ./run_tests.py
echo "Removing previous reports..."
rm -rf htmlcov/*
rmdir htmlcov
echo "Removed."
echo "Building coverage report..."
python3-coverage html
echo "Coverage report built."
echo "Let's show report."
(firefox htmlcov/index.html &> /dev/null &)
echo "Bye!"

Al ejecutar ese script (lo suelo llamar "run_tests_and_coverage.sh") consigo que el navegador Firefox se habra mostrando el informe de cobertura recién creado.

Si usas un IDE, lo más probable es que ya incluya algún tipo de herramienta de análisis de cobertura. PyCharm incluye un analizador de cobertura en su versión profesional. En realidad, con PyCharm se consigue más o menos lo mismo que con las herramientas de consola que vimos antes, pero  de manera integrada con el editor y por tanto mucho más cómoda de utilizar.

A nivel de aplicación, la configuración por defecto debería ser suficiente:



Supongo que si no se tiene instalado el paquete de sistema "python3-coverage" se debería marcar la opción "Use bundled coverage.py" para usar así la herramienta que trae integrada de manera nativa PyCharm. En mi caso, no he notado diferencia alguna en marcar o desmarcar dicha opción (aunque obviamente yo ya tengo el paquete "python3-coverage" instalado).

El único truco a recordar es que ejecutar los tests con soporte para análisis de cobertura implica usar el botón dedicado a esa tarea. Ese butón está localizado junto a los de "Run" y los de "Debug":



Tras usar ese botón, aparecerá un panel resumen a la derecha del editor principal con los porcentajes de cobertura de cada una de las carpetas del proyecto. El panel esquemático de la izquierda mostrará los niveles de cobertura de cada uno de los ficheros de código fuente. Además, el panel principal de edición marcará con color verde las líneas de código cubiertas y en rojo las que no.

Hacer click en la imagen para mostrarla a tamaño original.

Mantener la cobertura de los códigos tan cerca como sea posible del 100% es unos de los mejores indicadores de que tus tests está bien diseñados. Llevar a cabo ese análisis desde la consola o desde el IDE es una cuestión de gustos, pero ambas opciones son lo suficientemente cómodas y potentes como para usarlas a menudo.