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
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
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
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
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