De .py a PyPI

7 minute read

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:

  1. 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).
  2. 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 y author_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 de README.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

Tags:

Categories:

Updated:

Comments