El horrible mundo del empaquetado para PyPi (quejas y manual)

8 minutos de lectura Publicado:

Hace poco tuve la desastrosa idea de que estaría bien hacer accesible el programa gnusrss, del cual he sacado una nueva versión hace poco, por cierto. Si miráis este último link, podréis ver que he tenido que hacer un versionado absurdo. Por si da pereza mirarlo, aquí va una captura.

Se pueden ver unos commits que inspiran mucho amor y devoción. Y al lado, unos tags con la versión. v0.2.1.5 ha sido la última, y la que yo pretendía que fuese era la v0.2. Explicaré el motivo cuando llegue el momento.

A continuación, el proceso de empaquetado dando por supuesto que tenemos un repositorio git, un programa de lo que sea, da igual el lenguaje, y queremos subirlo a PyPi, El repositorio del que se instala al ejecutar pip install pycurl. Tendréis que crearos una cuenta ahí y además en otro lado, PyPi-test. Esta última web es identica a la original, pero sirve para testear. Hay que imaginar la de tonterías que tiene el empaquetado para que la web tenga una versión para hacer pruebas. Se puede registrar via web sin más o se puede hacer a través del fichero setup.py. Recomiendo la primera, la segunda no usa SSL y el usuario y contraseña que se escoa podrá verse en plano por internet.

Manual

Aquí va el directorio que tengo de gnusrss cómo ejemplo, para plantar una base de lo que habia antes del proceso de empaquetado:

drymer % torre ~/Instalados/Proyectos/gnusrss $ ls -l
total 84
-rw-r--r-- 1 drymer drymer 14576 dic  5 11:09 gnusrss.py
-rw-r--r-- 1 drymer drymer   620 dic  5 11:06 LICENSE
-rw-r--r-- 1 drymer drymer 12944 dic  7 05:46 README
-rw-r--r-- 1 drymer drymer 13684 dic  7 05:45 README.md
-rw-r--r-- 1 drymer drymer 12710 dic  7 05:45 README.org

El primer paso será crear un setup.py. Con este fichero interactuaremos con el índice PyPi. Se pueden usar dos librerías para ello. setuptools o docutils. docutils viene por defecto y ni el desarrollado ni quien lo use tendrá que descargar un paquete extra al usar el programa empaquetado. Pero si se quiere que este programa instale las dependencias que tenga de manera automática, escogeremos el segundo. Además, es uno de esos paquetes bastante básicos, se suele tener instalado. Entonces, el fichero setup.py podría ser tal que así:

#!/usr/bin/env python3

from setuptools import setup

VERSION = '0.2.1.5'

setup(name='gnusrss',
      version=VERSION,
      description='Post feeds to GNU Social.',
      long_description=open('README').read(),
      author='drymer',
      author_email='drymer@autistici.org',
      url='http://daemons.it/drymer/gnusrss/about/',
      download_url='http://daemons.it/drymer/gnusrss/snapshot/gnusrss-' + VERSION + '.tar.gz',
      scripts=['gnusrss.py'],
      license="GPLv3",
      bugtrack_url="https://notabug.org/drymer/gnusrss/issues",
      install_requires=[
          "feedparser>=5.0",
          "pycurl>=7.0",
          ],
      classifiers=["Development Status :: 4 - Beta",
                   "Programming Language :: Python",
                   "Programming Language :: Python :: 3",
                   "Programming Language :: Python :: 3.4",
                   "Operating System :: OS Independent",
                   "Operating System :: POSIX",
                   "Intended Audience :: End Users/Desktop"]
      )

Cuidar la identación si se copia tal cual. A continuación explicaré algunas líneas menos obvias.

  • long_description debería contener el README. Cómo este suele ser largo y estar escrito en otro fichero, recordando que esto es un programa, podemos simplemente leerlo. En este caso lee el fichero README.
  • download_url la tengo puesta, pero probablemente la quite. Este string debería tener cómo valor una url que lleve a un *.tar.gz o similar.
  • scripts contiene los ficheros que hay que subir, más allá del propio README.
  • install_requires contiene las dependencias y su versión.
  • classifiers es eso, clasificadores. Si se mira en la propia web de PyPi se puede ver cuales hay. Inventárselos está feo. Yo aviso.

Ahora se creará el paquete tar.gz para subirlo. Para hacerlo sólo hay que ejecutar python setup.py sdist. Hay otras opciones, cómo crear paquete para winsux y esas cosas. Pero no me interesa. Una vez ejecutada la anterior orden, en este directorio se podrá ver que se han creado dos directorios, sdist y $nombreDelPrograma.egg-info. En el segundo directorio hay algo de información que se crea para poder meterla en el archivo comprimido, y en el primero es dónde se crean todos los archivos comprimidos. Si se usa git, es recomendable meter en el .gitignore ambos directorios y no borrar el directorio cada vez que se envíe versión nueva, ya que no va mal tener información de todas las versiones que se han ido publicando.

Ahora se tiene que registrar en la web, mediante el formulario que tienen. Se puede hacer usando el setup-py, pero este no usa SSL por lo que es peligroso. Una vez hecho, se crear el archivo ~/.pypirc, que contendrá información para loguearse de manera automática al subir paquetes. Seria tal que así:

[distutils]
index-servers =
  pypi
  pypi-test

[pypi]
repository=https://pypi.python.org/pypi
username:drymer
password:lalalalalal

[pypi-test]
repository=https://testpypi.python.org/pypi
username:drymer
password:lolololo

Se puede ver que hay dos secciones, PyPi y PyPi-test. El segundo irá bien para probar, es muy recomendable usarlo hasta que el proceso entero de empaquetado esté dominado. Para usar esa web, hay que volver a registrarse, ya que ambas webs no comparten base de datos de usuarios.

Creado el ultimo archivo, se instalará twine. Es una utilidad para subir paquetería de manera segura, por lo que comenté antes del SSL. Aún no se usará, por eso. Dado que se va a usar primero la web de PyPi-test, no pasa nada por que se puedan ver las credenciales en plano. Para subir el comprimido a la web: python setup.py upload sdist -r pypi-test. Con esto ya se puede ir testeando lo que haga falta. Hay que aprovechar que está esta web, y sólo cuando se tenga claro subir a la web oficial. Se puede usar el último comando si se es idiota, quitando el -r, o se puede usar twine instalándola (pip install twine) y luego ejecutando, desde la raíz, twine upload dist/$paquete-$version.tar.gz. Y con eso ya se será un nene mayor que empaqueta y programa.

Quejas

Ahora, empecemos con el primer problema. El fichero README sólo puede tener un formato, y este es el rst. La verdad, sabía que este lenguaje de marcado existía, pero nunca lo había usado, y al echarle un ojo por encima vi que es horrible y que nunca lo usaré. En la mayor parte de sitios relacionados con el hosting de paquetes, siempre, repito, siempre se usa markdown, un lenguaje de marcado con mucho más sentido. Pero bueno, yo escribo todo en org-mode y este permite exportar a muchísimos formatos, el rst incluido. Entonces, veamos que es lo que yo tengo en mi caso. Un fichero llamado README.org, otro llamado README.md para el git y ahora un tercero llamado README en rst. Al mirar el archivo escrito en markdown me di cuenta de que tendría problemas. Por defecto org-mode exporta la tabla de contenidos en HTML, ya que el de markdown queda muy bonito pero no funciona en HTML. Bueno, no está todo perdido, pensé. Lo exporto directamente desde el archivo escrito en org y fuera. Lo hice, fui a mirar la tabla y vi .. contents:: dónde debería ir esta. Lo busqué y vi que era el formato correcto. Bibah! Lo subí a PyPi y el propio setup.py me dio algún warning, aunque lo subió. Peeero, no lo procesó correctamente. Raro. Me puse a buscar y resulta que el procesador de rst de PyPi está anticuado y no procesa eso. Así que hay que hacerlo de manera manual. Seguiría así un buen rato, implicando incluso pandoc. Pero el resultado era que no funcionaba, así que acabé escribiendo lo siguiente.

#!/usr/bin/env python3

from pypandoc import convert
from os import system
from re import search
from sys import argv,exit

if len(argv) == 1:
  print('Usage: python3 createStupidToc.py README.md README.rst')
  exit()

README = argv[1]
TMPFILE1 = '/tmp/stupidshit1'
TMPFILE2 = '/tmp/stupidshit2'

with open(README) as shit:
  shit = shit.read()

shit = shit[shit.find('# '):]
supershit = ''
shitty = ''

with open(TMPFILE1, 'w') as tmp:
  for minishit in shit.splitlines():
      result = search('^#', minishit)
      if result != None:
          tmp.write(minishit.split('<')[0] + '\n')
      else:
          tmp.write(minishit + '\n')
# generate the stupid toc to export to rst
system("~/Scripts/createStupidToc.sh /tmp/stupidshit1 /tmp/stupidshit2")

with open('/tmp/stupidshit2') as stupidshit:
  stupidshit = stupidshit.read()
for shit in stupidshit.splitlines():
  if shit.startswith('<!-- '):
      pass
  else:
      shitty += shit + '\n'

with open(argv[2], 'w') as rst:
  rst.write(convert(shitty, 'rst', format='md'))

Muy salchichero, pero funciona. En el fichero ~/Scripts/createStupidToc.sh hay lo siguiente:

#!/bin/bash

echo "Creating the toc..."
emacs24 -Q $1 --batch -l ~/.emacs.d/init.el -f markdown-toc-generate-toc --eval='(write-file '\"$2\"')' 2>/dev/null

Y ahora explico lo que hace y lo que hace falta para usarlo. Lo que hace es coger un fichero en markdown con una tabla de contenidos en HTML, quitar esta tabla de contenidos, decirle a emacs que use el paquete markdown-toc y lo inserte en el buffer que contiene el markdown sin la tabla en HTML y coger este markdown con la tabla de contenidos en markdown y crear un README con la tabla funcional. Para usarlo hace falta instalar pypandoc (pip install pypandoc) y tener emacs con ese paquete. Por defecto llama la versión 24 de emacs, pero si se tiene otra es cuestión de cambiar la última línea de createStupidToc.sh. Entonces sólo hay que ejecutar este script cada vez que se vaya a crear el paquete para PyPi.

La segunda queja es que no permiten hostear los archivos comprimidos de manera externa. Hay una opción de la web de PyPi, cuando vas a la sección url de tu paquete, en la que te pregunta si quieres hostear el paquete comprimido en PyPi o en un servidor externo. La cosa es que esto ya no se permite (las opciones salen marcadas cómo deprecated) pero siguen saliendo. Y buscando se encuentra un este pep dónde se explica la motivación de la decisión de quitar el hosting externo. Aunque entiendo su motivación, a mi me parece una mierda, pero bueno, la vida. Mi queja es que la información que hay no es nada concluyente, ni siquiera el pep.

Al escribir me doy cuenta de que realmente al saber todo esto, no resulta tan problemático. Pero claro, he tenido que dedicar bastantes horas a encontrar todo esto. Ahora que lo sé, probablemente el siguiente paquete sea menos problemático. Aún así no se echaría en falta mejor documentación.

Cualquier duda o comentario, me puedes contactar en los canales descritos en la página principal