Publicando artefactos Python


Hace un par de semanas que comencé un proyectillo Python que se ha transformado en mi primer paquete pypi serio. Bueno, realmente se ha transformado en dos paquetes, lo que me ha hecho darme cuenta de lo repetitivo de algunas tareas... Y cómo no, he decidido compartirlo aquí.

Así aprovecharé para contar algunas lecciones aprendidas, ahorrando así tiempo la próxima vez y ayudando a otros. De hecho, ya estoy preparando el siguiente XD

Comenzaré desde el principio del todo: la creación del repositorio.

Antes de publicar

Creando un repositorio

Lo primero es crear un repositorio. No me voy a liar aquí a contar cómo hay que hacerlo, porque ya conté en su día cómo crear un repositorio Git. Baste decir que recomiendo Git en GitHub, y más tarde veremos por qué.

README

Lo primero en el repositorio es crear el archivo README. En principio, tenemos varios formatos a elegir... Pero si queremos reutilizarlo para que se muestre lo mismo en pypi y en GitHub, es recomendable usar ReStructured Text, que es lo único que entiende pypi. Por lo tanto, debería llamarse README.rst.

Estructura

Lo segundo a tener en cuenta es la estructura de directorios. Recomiendo la siguiente:

.
├── .coveragerc
├── .travis.yml
├── .gitignore
├── Makefile
├── README.rst
├── requirements-dev.txt
├── setup.py
├── tests
│   ├── acceptance
│   │   └── __init__.py
│   ├── integration
│   │   └── __init__.py
│   └── unit
│       └── __init__.py
└── <library name>
    └── __init__.py

En este tutorial me ha quedado muy largo porque tengo que mostrar casi todos ellos, pero podéis ver dos ejemplos funcionales: python-readchar y python-inquirer. Recomiendo el primero por ser más sencillo.

Pues ya tenemos el repositorio y un archivo... ¡Comencemos!

Comenzando el proyecto

Lo mejor es comenzar por definir el proyecto, con el archivo setup.py. Para su concepción recomiendo echar un ojo a las setuptools. Como suelo hacerle algún hack, os muestro un ejemplo:

setup.py

# -*- coding: utf-8 -*-

from setuptools import setup, find_packages
from readchar import __version__, __description__


def read_full_description():
    with open('README.rst') as fd:
        return fd.read()


setup(name='<PACKAGE>',
      version=__version__,
      description=__description__,
      long_description=read_full_description(),
      classifiers=[
          'Development Status :: 4 - Beta',
          'Environment :: Console',
          'Intended Audience :: Developers',
          'License :: OSI Approved :: MIT License',
          'Programming Language :: Python :: 3.3',
          'Topic :: Software Development',
      ],
      keywords='stdin,command line',
      author='MagMax',
      author_email='magmax@example.org',
      url='https://example.org/repository',
      license='Whatever',
      packages=find_packages(exclude=['tests', 'venv']),
      include_package_data=True,
      zip_safe=False,
      install_requires=[
        ],
      )

Varias cosas aquí: como veis, importo la versión y la descripción del propio paquete Eso me facilita tocar un único punto (más o menos) a la hora de crear el paquete. Luego veremos este archivo. El nombre de variable __version__ es estándar, vamos, que se tiene que llamar así.

Además, la descripción larga la leo del propio archivo README.rst que comenté antes, de manera que se vea chulo en línea de órdenes y en pypi. Tened cuidado, porque cualquier error y pypi no lo renderizará como debe. GitHub es bastante más permisivo.

Finalmente, en el apartado install_requires podéis añadir todas las dependencias de la misma manera que en el requirements.txt. Intenté un hack para leerlo, pero durante la instalación no me lo encontraba... Así que opté por eliminar el archivo y gestionar los requisitos desde aquí. Es un tema sobre el que tengo que volver.

<paquete>/__init__.py

__version__ = '0.1'
__description__ = 'Loren Ipsum'

Es necesario algo así para que lo lea bien nuestro programita setup.py. Para la versión hay mogollón de opiniones... La mía es que con dos ó tres números es suficiente. Tres como mucho si usais la fecha con formato YYYYMMDD (mayor.fecha.minor).

Probando

Un paquetito que se precie debe estar acompañado de pruebas... Y a ser posible de porcentajes de cobertura. Pues podemos usar herramientas gratuitas para ello: Travis y Coveralls

Travis

Es un sistema de integración continua gratuito para proyectos libres y de pago para los que no lo son. Permite ejecutar los tests de todos los changesets. Se configura mediante el archivo .travis.yml, situado en el directorio principal y con formato YAML. Veamos un ejemplo:

language: python
python:
  - "2.6"
  - "2.7"
  - "3.2"
  - "3.3"

install:
  - pip install -r requirements-dev.txt
  - if [[ $TRAVIS_PYTHON_VERSION == 2* ]]; then pip install unittest2; fi
  - python setup.py install

script:
  - make pep8
  - if [[ $TRAVIS_PYTHON_VERSION != '2.6' ]]; then make flakes; fi
  - make test

after_success:
  - make coveralls

notifications:
  email:
    on_success: change
    on_failure: change

Básicamente crea un ejecutor para cada versión de Python y ejecuta los scripts del install y script, y si todo va bien, el after_success. Hay más pasos en el ciclo de vida, pero podéis verlos en la documentación de Travis.

Para que funcione, tenéis que:

  1. daros de alta en Travis,
  2. sincronizar vuestros proyectos GitHub (ahora entendéis por qué lo recomendaba, ¿eh?),
  3. dar permisos a Travis para instalar un hook en el repositorio,
  4. y activarlo para el repositorio de vuestro proyecto.

Los tres primeros se hacen sólo una vez y el último hay que repetirlo cada vez que comencemos un proyecto.

Como veis, el paso de after_success es llamar al plugin de Coveralls.

Coveralls

Requiere de un plugin para invocarlo. Yo suelo utilizar python-coveralls, pero también está disponible coveralls-python (no, no me estoy quedando con vosotros). Se configura con el archivo .coverage:

[run]
omit = */tests*

Además de instalar el paquete correspondiente, es necesario darse de alta en Coveralls. Veréis información sobre claves SSL en la documentación y tal... pero eso sólo es para los proyectos privados.

Es necesario generar el archivo .coverage para que Coveralls muestre los resultados correctamente. Si usais nose, como yo, necesitaréis también nosexcover.

Gestión

Veamos cómo orquesto toda esta maraña de archivos: mediante virtualenv. Siempre me creo el entorno venv y lo añado al archivo .gitignore:

*~
\#*
\.\#*
*.pyc
*.egg-info/
build/
dist/
venv/
.coverage
coverage.xml

Claro... He mencionado mogollón de historias, pero no mis herramientas, que están en el requirements-dev.txt:

pep8==1.5.1
flake8==2.1.0
nose==1.3.1
coverage==3.7.1
nosexcover==1.0.10
python-coveralls==2.4.2
doublex==1.8.1
pexpect==3.2

Me gusta indicar las versiones, por si algo se rompe por sorpresa, tenerlo controlado.

Además de éstas, suelo instalarme siempre ipython, pero no lo incluyo aquí porque es meter demasiada carga a Travis cuando no lo va a usar.

Y, finalmente, el director de la orquesta, el archivo Makefile:

MODULES=<PACKAGE NAME>

all: pep8 flakes test

test:: clear_coverage run_unit_tests run_acceptance_tests

unit_test:: run_unit_tests

acceptance_test:: run_acceptance_tests

analysis:: pep8 flakes

pep8:
    @echo Checking PEP8 style...
    @pep8 --statistics ${MODULES} tests

flakes:
    @echo Searching for static errors...
    @pyflakes ${MODULES}

coveralls::
    coveralls

run_unit_tests:
    @echo Running Tests...
    @nosetests -dv --exe --with-xcoverage --cover-package=${MODULES} --cover-tests tests/unit

run_acceptance_tests:
    @echo Running Tests...
    @nosetests -dv --exe tests/acceptance

clear_coverage:
    @echo Cleaning previous coverage...
    @coverage erase

Publicación

Ya sólo nos queda publicar el paquete en pypi. Para ello seguiremos los siguientes pasos:

  1. Registrarse en la web de pypi
  2. Registrar el paquete: python setup.py register
  3. Empaquetar y subir: python setup.py sdist upload

Como veis, yo distribuyo los fuentes, ya que los binarios me dieron problemas.

Afinando

Y sólo me queda contar cómo poner los iconos monos XD

Tanto Travis como pypi y Coveralls disponen de unos iconos accesibles con la misma URL. Como hemos dicho, vamos a usar el formato ReStructured Text, así que ésta es la manera más chula:

.. Uso:

|pip version|
|pip downloads|
|travis|
|coveralls|

.. Al final del documento:

.. |travis| image:: https://travis-ci.org/<USER>/<REPOSITORY>.png
  :target: `Travis`_
  :alt: Travis results

.. |coveralls| image:: https://coveralls.io/repos/<USER>/<REPOSITORY>/badge.png
  :target: `Coveralls`_
  :alt: Coveralls results_

.. |pip version| image:: https://pypip.in/v/<PIP_NAME>/badge.png
    :target: https://pypi.python.org/pypi/<PIP_NAME>
    :alt: Latest PyPI version

.. |pip downloads| image:: https://pypip.in/d/<PIP_NAME>/badge.png
    :target: https://pypi.python.org/pypi/<PIP_NAME>
    :alt: Number of PyPI downloads

.. _Travis: https://travis-ci.org/<USER>/<REPOSITORY>
.. _Coveralls: https://coveralls.io/r/<USER>/<REPOSITORY>

Como véis, hay que cambiar <USER>, <REPOSITORY> y <PIP_NAME>, y vale para cualquier proyecto.

Más información

Hay muchas cosas en el tintero aún, como subir la documentación a read the docs, pero este artículo ya tiene suficiente caña XD

Recomiendo indagar un poquito en cada herramienta si queréis ampliar funcionalidad.


Comentarios

Comments powered by Disqus