De .py a PyPI
Post basado en la charla del mismo nombre (PyConAr 2017), actualizado a 2020
Tenemos nuestro script, módulo o paquete Python que consideramos útil y queremos compartirlo con el mundo. Para ello necesitamos seguir una serie de pasos de tal manera que todo aquel al que le resulte interesante nuestro proyecto, pueda obtenerlo, instalarlo y usarlo. Y por qué no, eventualmente contribuir para mejorarlo.
Independientemente del lenguaje, la necesidad de reusar código es un escenario usual: nos ahorra escribir el mismo código múltiples veces y/o le sirve a otros para resolver un determinado problema y poder concentrarse en lo que realmente quieren construir. Incluso en un mismo proyecto es útil a nivel organizativo, permitiendo mantener cada parte por separado.
En este post, la idea es contar, a grandes rasgos, cómo empaquetar código (principalmente librerías o herramientas), para su distribución a través de pip
y PyPI. Nótese que los usuarios de un paquete en PyPI van a necesitar Python y pip
, y por lo tanto no es el método ideal para distribuir una aplicación independiente (hay otras herramientas para esto, buscar por ejemplo sobre pyinstaller o py2exe).
Algunas definiciones
paquete
En Python la palabra paquete tiene distintos significados según el contexto:
- m. archivo que agrupa paquetes, módulos y otros recursos para ser distribuidos como un todo. Es lo que el usuario final va a obtener e instalar (distribution package).
- m. espacio de nombres que contiene otros módulos o paquetes (import package).
A lo largo de este artículo nos concentraremos en los paquetes de la primera definición.
PyPI
Sigla, en inglés, para Python Package Index (Índice de Paquetes Python). Es el nombre del repositorio oficial. Cuenta con una interfaz web para la búsqueda y obtención de información de los paquetes registrados. Se encuentra en https://pypi.org.
No confundir con PyPy!
Formatos de paquete
Source distribution (sdist)
Se trata básicamente de un archivo .tar.gz
que provee la metadata y los archivos fuente necesarios para generar un paquete ‘built’ (ver abajo). Permite la instalación en cualquier plataforma (si las dependencias están disponibles, claro).
Built distribution (wheel)
Es un .zip
(de extensión .whl
) que contiene los archivos y metadata ya “buildeados” (en particular para aquellos paquetes que usan extensiones que requieren un paso de compilación), de tal forma que solamente se requiere moverlos a la ubicación adecuada en el sistema de archivos para su instalación.
Instalar un wheel es notablemente más rápido que un sdist. La contra es que puede hacer falta crear varios wheels para cubrir las distintas plataformas.
Al momento de instalación, si pip
no encuentra un wheel para tu sistema, va a intentar obtener el sdist, armar el wheel particular para el caso, e instalarlo.
Herramientas
Por lo general uno instalará estas herramientas en el entorno en el que desarrolla el proyecto. Si antes de cada release corremos este comando, nos aseguraremos de tener las versiones más recientes para la tarea:
$ pip install -U pip setuptools wheel twine
pip
nos permite instalar paquetes; setuptools
, armarlos; wheel
, construir el paquete homónimo; y twine
, subirlo y publicarlo en PyPI.
Estructura usual de un paquete
La siguiente no es la única forma de organizar los archivos de un paquete a distribuir, pero es una posible, y frecuente.
dist-package-name/
docs/
package_name/
__init__.py
module.py
tests/
__init__.py
test_module.py
CHANGELOG
CONTRIBUTING
HACKING
LICENSE
MANIFEST.in
README.md
requirements.txt
setup.py
Para la estructura descripta, nuestro paquete se instalaría de la siguiente manera:
pip install dist-package-name
y se importaría como:
import package_name
Un ejemplo de un paquete simple que sigue este esquema es fpt-cli.
setup.py
Es el archivo más importante a la hora de empaquetar. El principal objetivo de este módulo es llamar a la función setup()
de setuptools
, que toma por argumentos los detalles específicos que describen a nuestro paquete.
Un ejemplo mínimo de cómo podría lucir nuestro setup.py
sería:
from setuptools import setup, find_packages
setup(
name='dist-package-name',
version='1.1',
packages=find_packages(),
)
packages
lista los import packages
(en nuestro caso, ['package_name']
) a incluir en el distribution package
. En lugar de listar cada paquete a mano, usamos find_packages()
que lo calcula automáticamente por nosotros.
Además, una vez que tenemos nuestro setup.py
, se habilitan una serie de comandos relacionados al empaquetado (que para el propósito de este post casi no vamos a necesitar):
$ python setup.py --help-commands
Volviendo a los parámetros que recibe la función setup()
, veamos cuáles son los más comunes:
name
es el nombre del paquete (distribution package name); puede contener letras, números,_
y-
. Y obviamente, no tiene que estar registrado por alguien más.version
es la versión del paquete (ver más detalles aquí).author
yauthor_email
se usan para identificar al autor del paquete.description
es una descripción corta del paquete, de una línea.long_description
es la descripción detallada; se muestra en la página del paquete en PyPI (usualmente se popula con el contenido deREADME.md
).long_description_content_type
indica el tipo de markup usado en el campo anterior (en este caso,Markdown
).url
la URL a la página de nuestro proyecto (por ejemplo, un link a GitHub, GitLab, o similar).classifiers
permiten definir metadata adicional de nuestro paquete; se espera que al menos se liste las versiones de Python y sistemas operativos soportados, y la licencia. La lista completa de tags está aquí: https://pypi.org/classifiers/.
Y hay varios más aún, que se pueden revisar en la respectiva documentación.
Un ejemplo de cómo quedaría un setup.py
más completo, aunque todavía simple, se puede ver aquí.
MANIFEST.in
A veces es necesario incluir archivos en nuestro paquete que no se incluyen automáticamente (ej. README). Si además queremos que esos archivos se instalen con nuestro paquete, será necesario pasar include_package_data=True
en nuestra llamada a setup()
(sólo afecta paquetes sdist, habría que usar package_data
en setup()
for bdist).
Un ejemplo de MANIFEST.in
. Para mayores precisiones sobre este tema, conviene revisar la documentación correspondiente de setuptools.
Algunas recomendaciones
- Elegir un esquema de versionado (semántico, basado en fechas, secuencial, híbridos)
- Siempre liberar bajo alguna licencia! Esto define bajo qué términos un usuario puede usar nuestro paquete. Una vez elegida la licencia, copiar el texto de la misma en el archivo LICENSE. Ante la duda, vale la pena ver https://choosealicense.com/
- Incluir un README (reStructuredText o markdown, encodeado en UTF-8) que describa de qué se trata nuestro proyecto, y que se muestre en la página del mismo en PyPI y/o en el sitio que hosteemos el código
- Al listar las dependencias, idealmente especificar versiones mínimas (que permitan a los usuarios instalar actualizaciones de seguridad)
- En lo posible y si tiene sentido, agregar información y documentación adicional (docs, CHANGELOG, CONTRIBUTING, HACKING, etc)
Empaquetamos
En este punto sólo resta correr este comando en el directorio donde tenemos nuestro setup.py
:
python3 setup.py sdist bdist_wheel
(para construir el wheel universal, pasar la opción --universal
al final; nota importante: hoy, universal supone que nuestro paquete es compatible con Python 2 y Python 3… pero, siendo un paquete nuevo, no habría porqué soportar Python 2 :-)).
Después de ver pasar alguna cantidad de texto por la pantalla, al completarse la corrida, deberíamos tener un par de archivos en el directorio dist
:
dist/
dist_package_name-1.1-py3-none-any.whl
dist-package-name-1.1.tar.gz
Llegando al público
Una buena idea antes de hacer el release oficial es hacerlo en el servidor de testing. Para ello hay que registrar un usuario, que será totalmente independiente del servidor real (de hecho cada tanto se resetea el estado del servidor de testing, así que puede ser necesario registrarse otra vez luego de un tiempo).
A continuación agregamos un archivo ~/.pypirc
con el siguiente contenido:
[distutils]
index-servers=
test
[test]
repository = https://test.pypi.org/legacy/
username = <nombre de usuario>
Ya estamos en condiciones de subir nuestro paquete, usando twine
:
$ twine upload -r test dist/dist_package_name-1.1*
(con -r
pasamos el nombre del server a utilizar, y luego los archivos correspondientes a los paquetes que creamos).
Si todo anduvo bien, deberíamos poder instalarlo de la siguiente manera (ojo que si tenemos dependencias esto podría fallar porque no todo paquete está subido en el servidor de testing):
$ pip install -i https://testpypi.python.org/pypi dist-package-name
También podemos confirmar que la descripción en la página del paquete luce bien, y que la metadata del proyecto es la correcta.
Hagámoslo oficial
Después de registrarnos en el PyPI de producción, actualizamos nuestro ~/.pypirc
para agregar un nuevo servidor (pypi
):
[distutils]
index-servers=
pypi
test
[test]
repository = https://test.pypi.org/legacy/
username = <nombre de usuario>
[pypi]
username = __token__
En este caso vamos a configurar un token para hacer los uploads (aunque también podría usarse el nombre de usuario al igual que en el caso anterior), por ser más seguro (podemos definir diferentes nivel de acceso, revocarlos, no exponemos nuestras credenciales).
Para usar el token creado, dejamos el username
definido como se muestra arriba (__token__
), y cuando nos pida el password, introducimos el token que generamos (asegurarse de guardarlo bien, porque no se puede recuperar).
Finalmente:
$ twine upload -r pypi dist/dist_package_name-1.1*
Y desde este momento, nuestro paquete está a un pip install
de distancia para cualquiera que desee instalarlo!
Referencias et al
Python Packaging Guide
Documentación oficial de empaquetado en Python
PyPA sample project
Esqueleto de un paquete Python de PyPA (Python Package Authority)
setup.py
Ejemplo de setup.py
, con varios patrones y convenciones más avanzados
Otras herramientas
pyroma
Evalúa tus habilidades de empaquetado
check-manifest
Valida nuestro MANIFEST.in
Cookiecutter
Cookiecutter template para un paquete Python
Flit
Herramienta para simplificar el subir un paquete a PyPI
Poetry
Un modelo diferente de empaquetado y manejo de dependencias
Comments