pre-commit para mejorar las revisiones de código

Cuando se desarrolla software generalmente se trabaja en equipos, en donde se hacen diferentes cambios por diferentes personas, muchos de estos cambios se transforman en PR(pull request) donde otros programadores los revisan, aquí generalmente se puede perder tiempo  en:

  • Debatiendo si el código debe llevar ;  o no
  • Agregando comentarios sobre el formato del código  "debe ir aquí un espacio en blanco", "debe ser más pythonico" etc.
  • Dentro del código encontrarse con print o console.log

cuando se hace revisión de código el tiempo debería dedicarse enteramente a entender la lógica y la calidad del código, mas no a los formatos y estándares pues ya existen muchas herramientas que pueden formatear el código automáticamente o generar recomendaciones de cambios y con ello evitar enfrascarse en discusiones que podrían no llegar a ningún lado o generar un ambiente poco agradable. Para automatizar esto existen los llamados git hooks

Git hooks

Figura 1:  Ubicación de los git hooks en un repositorio local

Consisten en acciones basadas en código (scripts) que se pueden lanzar del lado del cliente y del servidor, estos hooks dependen de cuando se realice un evento o comando dentro de git, podríamos pensar en un flujo normal de realizar un commit el cual se vería como en la Figura 2, ahora podríamos ver el mismo flujo pero con hooks los cuales funcionan ejecutando algún tipo de script Ver Figura 3 y estos scripts pueden dar una respuesta positiva o negativa (retornando un nonzero value) y a partir de esta respuesta seguir en el  flujo del "caso feliz" o devolverle al working directory. En este artículo nos enfocaremos en hook pre-commit que consiste en ejecutar un script durante la ejecución del evento commit

Figura 2:  Flujo normal al realizar un commit
Figura 3: Flujo al realizar un commit con el hook pre-commit

Herramientas

Necesitaremos de algunas herramientas con las cuales trabajaremos para implementar las validaciones empleadas en la Figura 3  por ello a continuación mencionare que herramientas se van a utilizar

pre-commit framework

Esta es una librería especialmente desarrollada para gestionar el hook pre-commit, tiene una lista de "scripts" que se van a agregar a un archivo de configuración y ejecutar cuando se llame al comando commit en git esta herramienta está pensada para ser usada con varios lenguajes no sólo python. Cada vez que se inicializa un repositorio utilizando el comando git init se crea un folder .git este repositorio contiene toda la configuración de ramas, tags y commits de los archivos contenidos en ese repositorio, hay una carpeta especial llamada .git/hooks/ esta contiene todos los hooks que se van a ejecutar, pre-commit se encargará por defecto de gestionar el hook llamado pre-commit que no es más que un script bash generado por esta herramienta donde podemos ver el codigo a continuacion

#!/usr/bin/env python3.7
# File generated by pre-commit: https://pre-commit.com
# ID: 138fd403232d2ddd5efb44317e38bf03
import os
import sys

# we try our best, but the shebang of this script is difficult to determine:
# - macos doesn't ship with python3
# - windows executables are almost always `python.exe`
# therefore we continue to support python2 for this small script
if sys.version_info < (3, 3):
    from distutils.spawn import find_executable as which
else:
    from shutil import which

# work around https://github.com/Homebrew/homebrew-core/issues/30445
os.environ.pop('__PYVENV_LAUNCHER__', None)

# start templated
INSTALL_PYTHON = '/home/jairo/.cache/pypoetry/virtualenvs/automate-preccomit-cZHKYASo-py3.7/bin/python'
ARGS = ['hook-impl', '--config=.pre-commit-config.yaml', '--hook-type=pre-commit']
# end templated
ARGS.extend(('--hook-dir', os.path.realpath(os.path.dirname(__file__))))
ARGS.append('--')
ARGS.extend(sys.argv[1:])

DNE = '`pre-commit` not found.  Did you forget to activate your virtualenv?'
if os.access(INSTALL_PYTHON, os.X_OK):
    CMD = [INSTALL_PYTHON, '-mpre_commit']
elif which('pre-commit'):
    CMD = ['pre-commit']
else:
    raise SystemExit(DNE)

CMD.extend(ARGS)
if sys.platform == 'win32':  # https://bugs.python.org/issue19124
    import subprocess

    if sys.version_info < (3, 7):  # https://bugs.python.org/issue25942
        raise SystemExit(subprocess.Popen(CMD).wait())
    else:
        raise SystemExit(subprocess.call(CMD))
else:
    os.execvp(CMD[0], CMD)

para instalarla usaremos poetry con el siguiente comando

poetry add pre-commit

pre-commit usa un punto de entrada que es una rchivo en el directorio raíz de nuestro proyecto llamado .pre-commit-config.yaml , podemos generarlo utilizando el siguiente comando

poetry run pre-commit sample-config

esto genera un archivo con el siguiente formato

repos:
-   repo: https://github.com/pre-commit/pre-commit-hooks
    rev: v3.2.0
    hooks:
    -   id: trailing-whitespace
    -   id: end-of-file-fixer
    -   id: check-yaml
    -   id: check-added-large-files

el formato estándar de la estructura anterior lo describire a continuacion especificando por ahora los items necesarios

  • repo es el repositorio de la herramienta que se va a ejecutar en el hook (algunos permiten utilizar el repositorio oficial de la herramienta)
  • rev hace referencia la tag que se va a ejecutar dentro de ese repositorio (los tags están contenidos en el repositorio linkeado anteriormente)
  • id los hooks dentro de esta herramienta tienen un id que se pueden encontrar en el siguiente enlace
  • language_version dentro de cada hook podemos especificar una versión del lenguaje a ejecutar por defecto se toma la del entorno virtual pero al especificar una la herramienta en python crea un entorno virtual con esta versión de python que debe estar instalada en la máquina

y finalmente el framework se debe instalar ejecutando el siguiente comando

poetry run pre-commit install 

black

Es una herramienta que permite formatear código utilizando el concepto de black code style que es basado en la PEP8

flake8

es básicamente un conjunto de varias herramientas en donde no solo permite validar código estándar, si no también chequear errores o problemas en los imports y también algunos temas relacionados con complejidad ciclomática

mypy

Es un herramienta utilizada para hacer chequeo de tipos estáticos en python más conocido como type checking es una herramienta de gran potencia, que dado el auge con el uso de los typing permite realizar validaciones sobre ellos

Ejemplo

Para el contexto del ejemplo utilizaremos como formateador de código black para que todo cambio que se realice pase por un formateo, adicionalmente agregaremos flake8 para que se encargue de revisar  las importaciones y los posibles errores en el código, y finalmente dentro del ejemplo al usar typing agregare mypy para que se encargue del chequeo de los tipos, para ello el archivo de configuración mencionado anteriormente  .pre-commit-config.yaml quedaría de la siguiente forma

repos:
- repo: https://github.com/ambv/black
  rev: 21.5b2
  hooks:
    - id: black
      language_version: python3.8
-   repo: https://gitlab.com/pycqa/flake8
    rev: 3.7.9
    hooks:
    - id: flake8

-   repo: https://github.com/pre-commit/mirrors-mypy
    rev: 'v0.901'  
    hooks:
    -   id: mypy

agregare el siguiente codigo en un archivo main.py para probar la validacion

from typing import List

def show_payments(payments: List[int]) -> str:
    result_payments: str = ""
    for payment in payments:
        if len(result_payments)==0:
            result_payments = str(payment)
        else:
            result_payments = result_payments+ " - " +str(payment)
    return result_payments


print(show_payments([100000,200000,400000, "100000"]))

agrego el cambio y realizo el commit

git add -A
git commit -m "feat: added show payments"

cuando se ejecuta el evento commit se  puede apreciar la siguiente salida en consola

Figura 4:  Mensaje de error de las herramientas ejecutadas dentro del hook

como se puede ver el hook está utilizando las 3 herramientas que se agregaron las cuales mypy y black están fallando en la consola el mensaje explica la razón:

  • black debido a que el código que se intenta agregar no cumple con el black code style, por lo tanto se genera un formateo, es decir se  modifica el archivo que se quería agregar a staging
  • mypy la herramienta detecta un error en la siguiente  línea de código
print(show_payments([100000, 200000, 400000, "100000"]))

debido a que la función show_paymernts recibe una lista de int y en este caso se está pasando una lista de varios tipos, esto enmarca el flujo que se mostró en la Figura 3 y ejecutando git status se puede apreciar que el archivo aún no ha pasado a staging

Figura 5: Resultado de ejecutar git status

para poder avanzar con el commit se realizan las correcciones sugeridas por la herramienta del hook (algunas correcciones son automáticas como las de black), se vuelven a agregar los cambios con git add y se ejecuta de nuevo el evento commit, el código con los cambios quedaría de la siguiente forma

from typing import List


def show_payments(payments: List[int]) -> str:
    result_payments: str = ""
    for payment in payments:
        if len(result_payments) == 0:
            result_payments = str(payment)
        else:
            result_payments = result_payments + " - " + str(payment)
    return result_payments


print(show_payments([100000, 200000, 400000]))

se ejecuta git add y git commit, esta vez se obtiene la siguiente salida en consola

como se puede ver en este caso las validaciones pasaron correctamente y por lo tanto el comit se agregó al repositorio local de manera satisfactoria cumpliendo el ciclo mostrado en la Figura 3, adicionalmente se pueden agregar archivos de configuración para las herramientas por ejemplo para black en el archivo pyproject.toml

[tool.black]
line-length = 79
include = '\.pyi?$'
exclude = '''
/(
    \.git
  | \.hg
  | \.mypy_cache
  | \.tox
  | \.venv
  | _build
  | buck-out
  | build
  | dist
)/
'''

y para la herramienta flake8 se puede crear un archivo llamado .flake8 con la siguiente configuración

ignore = E203, E266, E501, W503, F403, F401
max-line-length = 79
max-complexity = 18

ahora cuando se ejecute el commit, automáticamente las herramientas usadas dentro del hook tomarán la respectiva configuración, estos archivos ayudan bastante sobre todo evitando que las herramientas analizan cambios en archivos que no deberían, brindando a si una mayor rapidez en su ejecución

Ya para finalizar quiero recalcar que existen una gran cantidad de hooks muy interesantes como por ejemplo el hook commit-message que permite capturar el mensaje que va dentro del commit, el cual es muy importante en revisiones de histórico a futuro, por lo tanto se podría usar este hook para cumplir el conventional commit un estándar bastante interesante para escribir "buenos commits" pero eso ya quedará para otro post.

Conclusión

Utilizar el hook pre-commit brinda una gran ventaja en el desarrollo debido a que al poner estas barreras que además están automatizadas permiten al desarrollador tener un feedback temprano antes de pasar el codigo a revision, cumpliendo asi de antemano unos estándares que seguramente potenciarán la tarea de aprobación de cambios en sus respectivos entornos