pyproject.toml en esteroides con Hatch
Si alguna vez te has visto en la necesidad de compartir tu código con el resto del internet, es muy probable que te haya tocado interactuar con un archivo llamado setup.py
.
Pues bien, de un tiempo acá esta era la forma que se usaba para empaquetar y distribuir paquetes en Python, sin embargo con la llegada del PEP-518 se introdujo un nuevo archivo de nombre pyproject.toml
que trataría de suplir o cumplir las mismas funcionalidades que el setup.py
, a su vez este archivo tendría mejoras significativas gracias al PEP-517 y el PEP-660 donde se trataría de estandarizar la interfaz para el empaquetado, continuando con elPEP-621 y el PEP-631 para formalizar el formato lingüístico a utilizar en Python.
Como resultado de todos estos cambios la ejecución o el uso del archivo setup.py
esta ahora deprecado, y aunque esto sonara a un apocalipsis, por que si hablas con algún desarrollador en Python con algunos años de experience, te vas a encontrar con alguna historia llena de errors un tanto difíciles de encontrar, por que durante el proceso de empaquetaniento también es el momento adecuado para que nuevas herramientas surjan y mejoren la forma en que se trabaja hoy en día.
Usando hatch
Descrito a si mismo como un administrador de proyectos de Python moderno y extensible, es el nuevo amigo en el vecindario. Se caracteriza por tener un sistema de empaquetamiento estandarizado con compilaciones reproducibles de forma predeterminada, así como una gestión robusta de entornos virtuales con soporte para scripts personalizados, entre otros.
A manera personal se siente como la evolución de proyectos como poetry
o flit
, su interfaz de línea de comandos es bastante robusta y de fácil adopción, así como la capacidad de generar entornos virtuales de manera isolada y la ejecución de XXX que te ayudaran a mejorar o simplificar las necesidades de tu proyecto.
Vamos a tomar como suposición que el código que estas tratando de migrar hace uso de setup.py
, para ello tomaremos como ejemplo mi paquete para saber los códigos postales de Nicaragua, y a como se puede notar contiene lo siguiente dentro del archivo setup.py
:
from setuptools import setup, find_packages
with open('README.md') as readme_file:
readme = readme_file.read()
with open('HISTORY.md') as history_file:
history = history_file.read()
setup_requirements = ['pytest-runner', ]
test_requirements = ['pytest', ]
setup(
author="Oscar Cortez",
author_email='om.cortez.2010@gmail.com',
name='postalcodes_ni',
version='1.2.0',
classifiers=[
'Development Status :: 5 - Production/Stable',
'Intended Audience :: Developers',
'Topic :: Software Development :: Libraries',
'License :: OSI Approved :: MIT License',
'Natural Language :: English',
'Natural Language :: Spanish',
'Programming Language :: Python :: 3',
'Programming Language :: Python :: 3.4',
'Programming Language :: Python :: 3.5',
'Programming Language :: Python :: 3.6',
'Programming Language :: Python :: 3.7',
],
description="Python package for handle Nicaragua postal codes",
license="MIT license",
long_description=readme + '\n\n' + history,
long_description_content_type="text/markdown",
include_package_data=True,
python_requires='>=3',
keywords='postalcodes nicaragua',
packages=find_packages(include=['postalcodes_ni']),
setup_requires=setup_requirements,
test_suite='tests',
tests_require=test_requirements,
project_urls={
'Documentation': 'https://postalcodes-ni.readthedocs.io',
'Funding': 'https://donate.pypi.org',
'Say Thanks!': 'http://saythanks.io/to/oscarmcm',
'Source': 'https://github.com/oscarmcm/postalcodes-ni/',
'Tracker': 'https://github.com/oscarmcm/postalcodes-ni/issues',
},
url='https://github.com/oscarmcm/postalcodes-ni',
zip_safe=False,
)
La manera mas fácil de migrar un proyecto antiguo es ejecutando hatch new —-init
para generar un nuevo archivo pyproject.toml
a partir de la información que tengamos en nuestro setup.py
, en el caso de este ejemplo se ve de la siguiente manera:
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[project]
name = "postalcodes-ni"
dynamic = ["version"]
description = "Python package for handle Nicaragua postal codes"
readme = "README.md"
license = "MIT license"
requires-python = ">=3"
authors = [
{ name = "Oscar Cortez", email = "demo@mail.com" },
]
keywords = [
"nicaragua",
"postalcodes",
]
classifiers = [
"Development Status :: 5 - Production/Stable",
"Intended Audience :: Developers",
"License :: OSI Approved :: MIT License",
"Natural Language :: English",
"Natural Language :: Spanish",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.4",
"Programming Language :: Python :: 3.5",
"Programming Language :: Python :: 3.6",
"Programming Language :: Python :: 3.7",
"Topic :: Software Development :: Libraries",
]
[project.urls]
Documentation = "https://postalcodes-ni.readthedocs.io"
Funding = "https://donate.pypi.org"
Homepage = "https://github.com/oscarmcm/postalcodes-ni"
"Say Thanks!" = "http://saythanks.io/to/oscarmcm"
Source = "https://github.com/oscarmcm/postalcodes-ni/"
Tracker = "https://github.com/oscarmcm/postalcodes-ni/issues"
[tool.hatch.version]
path = "postalcodes_ni/__init__.py"
[tool.hatch.build.targets.sdist]
include = [
"/postalcodes_ni",
]
Pero en un dado caso que se este iniciando desde cero, hatch
tiene la capacidad de crear un proyecto nuevo usando hatch new —-interactive
, ahi se responderán un par de preguntas y automáticamente creara la estructura para el nuevo proyecto de Python.
Project name: postalcodes
Description []: Nicaragua postal codes
postalcodes
├── postalcodes
│ ├── __about__.py
│ └── __init__.py
├── tests
│ └── __init__.py
├── LICENSE.txt
├── README.md
└── pyproject.toml
De igual manera el archivo pyproject.toml
generado en el proyecto desde cero, es casi parecido a el que se genero a partir de setup.py
:
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[project]
name = "postalcodes"
description = 'Nicaragua postal codes'
readme = "README.md"
requires-python = ">=3.7"
license = "MIT"
keywords = []
authors = [
{ name = "Oscar Cortez", email = "om.cortez.2010@gmail.com" },
]
classifiers = [
"Development Status :: 4 - Beta",
"Programming Language :: Python",
"Programming Language :: Python :: 3.7",
"Programming Language :: Python :: 3.8",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: Implementation :: CPython",
"Programming Language :: Python :: Implementation :: PyPy",
]
dependencies = []
dynamic = ["version"]
[project.urls]
Documentation = "https://github.com/unknown/postalcodes#readme"
Issues = "https://github.com/unknown/postalcodes/issues"
Source = "https://github.com/unknown/postalcodes"
[tool.hatch.version]
path = "postalcodes/__about__.py"
[tool.hatch.envs.default]
dependencies = [
"pytest",
"pytest-cov",
]
[tool.hatch.envs.default.scripts]
cov = "pytest --cov-report=term-missing --cov-config=pyproject.toml --cov=postalcodes --cov=tests {args}"
no-cov = "cov --no-cov {args}"
[[tool.hatch.envs.test.matrix]]
python = ["37", "38", "39", "310", "311"]
[tool.coverage.run]
branch = true
parallel = true
omit = [
"postalcodes/__about__.py",
]
[tool.coverage.report]
exclude_lines = [
"no cov",
"if __name__ == .__main__.:",
"if TYPE_CHECKING:",
]
Los entornos y comandos en hatch
Una vez tengamos generado nuestro archivo pyproject.py
y gracias a que usamos hatch
tenemos a la disposición la herramienta para la creación de entornos virtuales totalmente isolados que pueden ser usados para los procesos de pruebas o de documentación, si tener que agregar validaciones a las dependencias del proyecto.
Todos los entornos se definen como secciones dentro de la tabla tool.hatch.envs
de la siguiente manera [tool.hatch.envs.<ENV_NAME>]
:
[tool.hatch.envs.test]
dependencies = [
"coverage[toml]",
"pytest",
"pytest-cov",
"pytest-mock",
]
Gracias a ello ya puedes ir eliminando los archivos de requerimientos para pruebas o para desarrollo, menos carga cognitiva, mas centralización.
Comandos
Y si también tienes esos grandes archivos Makefile
con varios comandos, hatch
ofrece la misma solución pero de manera centralizada y que se pueden ejecutar solo en un entorno en especifico:
[tool.hatch.envs.test]
dependencies = [
"coverage[toml]",
"pytest",
"pytest-cov",
"pytest-mock",
]
[tool.hatch.envs.test.scripts]
run-coverage = "pytest --cov-config=pyproject.toml --cov=pkg --cov=tests"
check-cov = "run-coverage --no-cov"
En el ejemplo de arriba podemos notar que hay una nueva tabla que contiene una lista de comandos, estos solo están disponibles en el entorno llamado test
y para ejecutarlos basta con hacer lo siguiente:
hatch run test:check-cov
Lo cual sera expandido a:
pytest --cov-config=pyproject.toml --cov=pkg --cov=tests --no-cov
Los cambios
Y ya para ir cerrando este post introductorio a pyproject.toml
y hatch
hay algunas cosas que cambian cuando decides usar este como, primero los archivos setup.py
, setup.cfg
, y Manifest.in
ya no serán necesarios; si haces uso de los scripts
entonces también puedes eliminar Makefile
de tu proyecto, de igual manera los archivos de requerimientos para prueba o desarrollo.
Ya no sera necesario ejecutar python setup.py install
o python setup.py develop
en contraposición basta con usar pip install
o pip install -e .
en la raíz del proyecto.
Es muy probable que puedas eliminar la dependencia de tox
si haces uso de las matrices en hatch
.
Hasta aca el blog post de hoy, en otra entrada veremos mas a detalle como sacarle provecho a hatch y el uso de las matrices para entornos de prueba.
Y eso es todo, espero que te haya gustado este post.