Prottotipo

Sobre open source y otras yerbas

19 March 2017

Desde hacía mucho tiempo quería comenzar a contribuir en algún proyecto open source (OS), pero la verdad es que no encontraba ninguno con el que me sintiese cómodo. Es bastante común encontrar por ahí artículos o podcasts que hablen de los beneficios de contribuir a proyectos de código abierto pero desde aquí quiero compartir mi humilde experiencia.

Github es, hoy en día, la plataforma de control de versiones en la nube más popular que hay. Mi intención no es la de ofrecer una guía acerca de cómo contribuir en proyectos en esta plataforma, sino más bien comentar porqué creo es es una buena idea hacerlo.

Hace poco más de un año comencé a dar mi primeros pasos programando en Python, y a día de hoy programo a tiempo completo en este lenguaje. Entre las librerías que comencé a utilizar se encuentra Eve, desarrollada por Nicola Iarocci. Eve es un framework que permite construir una API REST de forma sencilla. El motor de Eve es el conocido framework de desarrollo web para Python llamado Flask. La idea principal de Eve es poder construir nuestra API REST simplemente definiendo un schema, como por ejemplo:

people = {
    'schema' = {
        'firstname': {
            'type': 'string',
            'minlength': 1,
            'maxlength': 10,
        },
        'lastname': {
            'type': 'string',
            'minlength': 1,
            'maxlength': 15,
            'required': True,
            'unique': True,
        }
    }
}

Con este schema nuestra API tendrá un recurso llamado people. Para poder realizar un POST en este recurso, la estructura del body deberá verificar ese schema, es decir, deberá contener un campo llamado firstname, de tipo string, con un largo entre 1 y 10 caracteres. De forma similar ocurre con el otro campo, llamado lastname, que es obligatorio (required), y además, único en la base de datos (unique).

Este sistema de validación de schema lo realiza una herramienta llamada Cerberus, también desarrollada por Nicola y también OS.

En los últimos meses estuve usando Cerberus bastante y se me ocurrió una mejora que quizá comente en otro post, pero que básicamente consiste en hacer Cerberus 100% compatible con el estándar para APIs REST llamado OpenAPI, que no es más que la especificación desarrollada por Swagger.

Debido a que esta mejora implicaría bastante tiempo y conocimiento del código de Cerberus, es que decidí comenzar a contribuir de forma más comedida, viendo los issues abiertos en el proyecto.

Al momento de elegir un proyecto al cual contribuir, elige un proyecto que utilices comúnmente.

Esto tiene varias ventajas. La primera es que conocerás más o menos su funcionamiento de antemano. Sabes para qué sirve y cómo se utiliza. Con esto reduces en gran medida la fricción inicial que conlleva corregir un bug o agregar una funcionalidad en un programa que no sabemos muy bien qué hace. Luego está claro que si es una herramienta que utilizas diariamente, estarás corrigiendo errores y agregando funcionalidades a tu propio proyecto de forma indirecta. Esto incluso podría ser un factor a favor si deseas plantear en tu empresa contribuir al proyecto en horario de oficina. Por último, es la mejor forma de agradecer al o los desarrolladores que ofrecen la herramienta de forma gratuita.

Elegir un issue no es una tarea sencilla. Principalmente porque a priori no sabes qué implica corregirlo.

Para comenzar, elige el issue más fácil que encuentres.

Y esto puede ser tan sencillo como corregir un link o un error tipográfico. Esto servirá, principalmente, para familiarizarnos con el flow del proceso y perderle un poco el miedo. Este tipo de errores generalmente no tienen un issue asociado pero es común encontrarlos recorriendo la documentación. Otra tarea muy solicitada por este tipo de proyectos es la de traducción o ampliación de documentación.

En mi caso decidí por comenzar con el issue más sencillo que encontré. Lo primero que hay que hacer es (una vez entendido los pasos básicos que se explican en la guía de Github) leer las “reglas” propias establecidas por el proyecto a la hora de querer enviar tu trabajo. Allí se establecen por ejemplo, reglas de estilo, de comentarios, etc.

El issue que elegí es el siguiente: Cerberus no ofrece soporte de normalización para tuplas (una tupla es, básicamente, una lista inmutable). Esto significa que, si en nuestro esquema definimos un atributo de tipo list y le pasamos un objeto de tipo tuple, recibimos una exception de tipo TypeError:

TypeError: 'tuple' object does not support item assignment

Esto es sencillamente por este trozo de código:

for i in result:
    mapping[field][i] = result[i]

mapping[field] es la tupla a la cual se le está intentando asignar un elemento (normalizado) por posición. Esto lo podemos hacer con una lista, pero no con un objeto inmutable. result es un diccionario con los valores normalizados.

Mi primera solución fue la siguiente:

if type(mapping[field]) is tuple:
    mapping[field] = tuple(result.values())
else:
    mapping[field] = result.values()

El cambio no es más que, en lugar de iterar los valores del diccionario y reasignarlos a la lista, asignar directamente los valores del diccionario mediante el método values(), el cual ya nos devuelve una lista. De esta manera evitamos asignar un valor por posición a la tupla.

Debo destacar en este punto que, la gran mayoría de los proyectos de código abierto, medianamente importantes, dan soporte a las últimas versiones de Python. Python hace unos años hizo un cambio importante en su librería estándar por lo que pasó de la versión 2.x a la versión 3.x. El EOL (End Of Life, es decir, la fecha en la cual dejará de tener soporte) para Python 2.7 será en el año 2020, por lo que la comunidad está moviéndose a Python 3. De todas formas, Cerberus soporta todas las versiones de Python 2 y 3.

Una vez satisfecho con mis cambios realicé el correspondiente pull request en Github (esto es, envié una solicitud de aceptación de mis cambios en el proyecto oficial). Cerberus tiene un sistema de CI (Integración Continua), llamado Travis que se ejecuta cada vez que se realiza un pull request. En este proceso se ejecutan todos los tests para todas las versiones de Python. Una vez enviado mi pull request pude ver, rápidamente, que mis cambios hacía fallar los tests.

CI fallando miserablemente

Usa Tox

En el README del proyecto claramente dicen que debes ejecutar Tox para pasar los tests. Tox es una herramienta que automatiza los tests para diferentes configuraciones de entorno (diferentes librerías instaladas, diferentes versiones de Python, etc.) Como es evidente, yo no hice caso, y ahí está el error.

El problema está en que yo ejecuté los tests solamente para Python 2 python setup.py test, cuando debería haberlo hecho, como mínimo, también para Python 3 python3 setup.py test. En Python 2 el método values() de un diccionario devuelve una lista mientras que en Python 3, el mismo método devuelve un view object. Este cambio, como dicen en la documentación, es para proveer de una vista dinámica de los valores del diccionario, lo que significa que cuando el diccionario cambia, la vista refleja estos cambios.

Python 2.7.12
>>> a_dict = {'name': 'Sebastian'}
>>> elements = a_dict.values()
>>> elements
['Sebastian']
>>> a_dict['name'] = 'Sebastian Rajo'
>>> elements
['Sebastian']
Python 3.5.2
>>> a_dict = {'name': 'Sebastian'}
>>> elements = a_dict.values()
>>> elements
dict_values(['Sebastian'])
>>> a_dict['name'] = 'Sebastian Rajo'
>>> elements
dict_values(['Sebastian Rajo'])

Si queremos una lista, como en Python 2, debemos construirla así list(result.values())

Ahora debía cancelar el pull request, y hacer uno nuevo (o eso creía yo). Procedí a realizar un close del primer pull request, e hice el cambio correspondiente en el código:

if type(mapping[field]) is tuple:
    mapping[field] = tuple(result.values())
else:
    mapping[field] = list(result.values())

Confieso que el código quedó bastante feo, y lo notaba, pero no veía la solución. Abrí el segundo pull request.

Cerberus, además de Nicola, tiene otro gran contributor, funkyfuture (Frank Sachsenheim). Lo primero que me recomendó fue refactorizar el código utilizando únicamente el método type() de la siguiente manera.

mapping[field] = type(mapping[field])(result.values())

Es decir, ¡sustituir las 4 líneas anteriores por solamente esa! Si al método le pasamos un argumento, éste debe ser un objeto, lo que nos devolverá el tipo de un objeto. Esto quiere decir:

>>> list == type([])
True
>>> tuple == type(())
True

En nuestro caso, mapping[field] a veces será una lista y a veces será una tupla. El código dinámicamente creará el tipo que corresponda y, al ejecutar el tipo, hace las veces de constructor, por lo que nos creará la instancia correspondiente.

>>> type([])([1,2,3])
[1, 2, 3]
>>> type(())([1,2,3])
(1, 2, 3)

Otra anotación que me hizo fue que, if type(mapping[field]) is tuple: no funciona para subclases de tuple. Para eso es mejor utilizar isinstance(object, classinfo) como claramente recomiendan en la documentación.

La tercera recomendación (habrá visto que había cancelado el primer pull request) fue que, luego de realizar los cambios, realizara un squash o fixup de mis commits. ¿Cómo? Pues… sí. git tiene más de unos pocos comandos y no siempre los conocemos bien. En mi caso, nunca lo había usado. En realidad el comando en cuestión es el famoso rebase. Entre sus opciones podemos realizar un squash o un fixup. Squash sirve para unir varios commit, pudiéndole corregir a su vez los comentarios. Fixup, es lo mismo, pero eliminando directamente los comentarios de los commits unidos.

De esta experiencia no me llevo más que cosas positivas. He aprendido a utilizar git y Github de mejor forma, así como la librería estándar de Python. He contribuido a un proyecto que utilizo diariamente. He conseguido material para escribir este post. He agregado algo de movimiento a mi cuenta de Github, lo que siempre es importante ya que hay muchas empresas que se fijan en ello, y aunque está claro que no trabajar en proyectos OS no te hace peor candidato, hacerlo puede que sea el factor decisivo al momento de elegirte. Y todo esto, invirtiendo unas pocas horas.

Es importante tener en cuenta que pasamos muchas horas frente al ordenador en nuestra actividad laboral, y seguir programando fuera del horario de oficina es costoso, tanto sicológica como físicamente. Pero aún así creo que es muy provechoso. La nuestra es una actividad en la que nunca paramos de aprender, y si queremos destacar debemos realizar esfuerzos extras en este sentido.

Por último remarcar el tiempo y guía que funkyfuture me ofreció. Para alguien que está comenzando a contribuir es muy importante este tipo de recibimientos. Así que si eres responsable de un proyecto OS, be kind, sé como funkyfuture.