26 diciembre 2013

Documentando el código con Sphinx

A la hora de escribir código que vaya a ser usado por otros resulta crítico documentarlo adecuadamente. Esto ayudará a los usuarios de nuestro código a sacarle el máximo provecho y a comprender las opciones de diseño adoptadas.

Sin embargo, documentar código puede ser una tarea tediosa. Ya es difícil que los programadores comenten adecuadamente el código fuente como para encima exigirles que mantengan actualizado un documento aparte con las explicaciones de uso de funciones y objetos. Por eso, hay muchas herramientas que permiten generar el código a partir de los comentarios plasmados en el mismo código fuente. De esta manera los programadores sólo tienen que plasmar la información en un sitio (el código fuente) lo que facilita el mantenerla actualizada.

En el mundo Python, una de las herramientas más usadas para estos fines es Sphinx, la cual genera una documentación con un formato similar a la de la documentación de Python (no en vano esta también ha sido generada con Sphinx). Para generar la documentación, Sphinx inspecciona el código fuente del directorio que le digamos y detecta todos los paquetes, módulos, clases y funciones recogidos allí, y a cada uno de ellos le asocia sus correspondientes docstrings.

Un docstring es un comentario que se coloca en la primera línea del módulo, función o clase que comenta. Se encierra entre dos grupos de tres comillas dobles ("""...""") y a diferencia de los comentarios "de programador" (iniciados con una almohadilla #), que sencillamente son ignorados por el intérprete de Python, los docstrings se guardan en el atributo __doc__ de aquello que comentan (hay que recordar que en Python hasta las funciones son objetos susceptibles de tener sus propios atributos). Mientras que los comentarios de programador están pensados para ser leídos por aquellos que vayan a modificar directamente el código, los docstring están más orientados a aquellos que vayan a hacer uso del código. Por eso, mientras los comentarios de programador se enfocan a explicar por qué se ha programado el código de una determinada manera, los docstring se suelen utilizar para explicar cómo utilizar cada módulo, función o clase. La PEP-8 establece las buenas prácticas de escritura de código en Python y redirige a la PEP-257 para el caso concreto de los docstrings.

En realidad, no hay una única manera de escribir docstrings y aún siguiendo las buenas prácticas el tema está muy abierto. Google recomienda un formato para los docstrings que prima la claridad en su lectura directa, sin embargo si queremos generar documentación de manera automática mediante Sphinx deberemos seguir un formato que quizás no es tan claro a primera vista, pero que utiliza las etiquetas que el programa identificará para construir la documentación en base a los docstrings.

Pero empecemos por el principio instalando primero Sphinx. El código fuente del programa se puede obtener de su repositorio en Bitbucket. Desde allí se puede bajar el programa en un único paquete comprimido. En Ubuntu se encuentra en los repositorios oficiales con el nombre de "python-sphinx", por lo que bastará con un simple:

dante@Camelot:~$ sudo aptitude install python-sphinx


En Windows, lo mejor es usar pip o easy_install (incluido en setuptools), dado que Sphinx se encuentra en el repositorio oficial de Pypi.

Una vez instalado, Sphinx se usa con unos pocos comandos. Para empezar debemos generar la carpeta de trabajo de Sphinx. Supongamos que nuestros ficheros de código fuente están en la carpeta /pyalgorithm , allí he volcado una librería denominada PyAlgorithm que está desarrollando un amigo, de manera que el contenido de la carpeta es el siguiente:

dante@Camelot:~/pyalgorithm$ ls __init__.py  __init__.pyc  __pycache__  checkers  exceptions.py  exceptions.pyc  multiprocessing  stats  time

Todos los elementos, menos los que tienen extensión, son directorios con código fuente.

Desde allí lanzaremos el asistente para la configuración inicial de Sphinx para nuestro proyecto, sphinx-quickstart:

dante@Camelot:~/pyalgorithm$ sphinx-quickstart Welcome to the Sphinx 1.1.3 quickstart utility. Please enter values for the following settings (just press Enter to accept a default value, if one is given in brackets). Enter the root path for documentation. > Root path for the documentation [.]: docs                                                                                                                                                                                                                   You have two options for placing the build directory for Sphinx output.                                                                                                                                           Either, you use a directory "_build" within the root path, or you separate                                                                                                                                       "source" and "build" directories within the root path.                                                                                                                                                           > Separate source and build directories (y/N) [n]:                                                                                                                                                                                                                                                                                                                                                                                 Inside the root directory, two more directories will be created; "_templates"                                                                                                                                     for custom HTML templates and "_static" for custom stylesheets and other static                                                                                                                                   files. You can enter another prefix (such as ".") to replace the underscore.                                                                                                                                     > Name prefix for templates and static dir [_]:                                                                                                                                                                                                                                                                                                                                                                                     The project name will occur in several places in the built documentation.                                                                                                                                         > Project name: PyAlgorithm                                                                                                                                                                                   > Author name(s): Nobody                                                                                                                                                                                     Sphinx has the notion of a "version" and a "release" for the software. Each version can have multiple releases. For example, for Python the version is something like 2.5 or 3.0, while the release is something like 2.5.1 or 3.0a1.  If you don't need this dual structure, just set both to the same value. > Project version: 1 > Project release [1]: The file name suffix for source files. Commonly, this is either ".txt" or ".rst".  Only files with this suffix are considered documents. > Source file suffix [.rst]: One document is special in that it is considered the top node of the "contents tree", that is, it is the root of the hierarchical structure of the documents. Normally, this is "index", but if your "index" document is a custom template, you can also set this to another filename. > Name of your master document (without suffix) [index]: Sphinx can also add configuration for epub output: > Do you want to use the epub builder (y/N) [n]: Please indicate if you want to use one of the following Sphinx extensions: > autodoc: automatically insert docstrings from modules (y/N) [n]: y > doctest: automatically test code snippets in doctest blocks (y/N) [n]: > intersphinx: link between Sphinx documentation of different projects (y/N) [n]: > todo: write "todo" entries that can be shown or hidden on build (y/N) [n]: > coverage: checks for documentation coverage (y/N) [n]: > pngmath: include math, rendered as PNG images (y/N) [n]: > mathjax: include math, rendered in the browser by MathJax (y/N) [n]: > ifconfig: conditional inclusion of content based on config values (y/N) [n]: > viewcode: include links to the source code of documented Python objects (y/N) [n]: y A Makefile and a Windows command file can be generated for you so that you only have to run e.g. `make html' instead of invoking sphinx-build directly. > Create Makefile? (Y/n) [y]: > Create Windows command file? (Y/n) [y]: Creating file docs/conf.py. Creating file docs/index.rst. Creating file docs/Makefile. Creating file docs/make.bat. Finished: An initial directory structure has been created. You should now populate your master file docs/index.rst and create other documentation source files. Use the Makefile to build the docs, like so:    make builder where "builder" is one of the supported builders, e.g. html, latex or linkcheck. dante@Camelot:~/pyalgorithm$


Como se puede ver el asistente es muy sencillo y la mayor parte de las veces nos basta con la opción por defecto con un par de notables excepciones:

  1. Si no queremos que nuestros ficheros de documentación se mezclen con los del código fuente debemos responder a la pregunta "Root path for the documentation [.]:" con el directorio que queremos que se cree, dentro de /pyalgorithm, para ir colocando los ficheros de trabajo de Sphinx así como los de documentación generado.
  2. Por otro lado, hay que estar atento para responder "Yes" a la pregunta: "autodoc: automatically insert docstrings from modules (y/N) [n]:", ya que autodoc es precisamente la extensión de Sphinx que analiza los docstrings para generar la documentación.
Vemos que el asistente ha creado el directorio docs:

dante@Camelot:~/source$ ls __init__.py  __init__.pyc  __pycache__  checkers  docs  exceptions.py  exceptions.pyc  multiprocessing  stats  time

Así como 4 ficheros dentro de él:
  • conf.py: Es un fichero de Python donde se recoge la configuración de Sphinx. Si en algún momento quisieramos cambiar alguna de las opciones elegidas con sphinx-quickstart sería en este fichero donde la cambiaríamos. Por ejemplo, si quisiéramos cambiar los números de versión de la configuración lo haríamos cambiando las variables version y release de ese fichero. En realidad, lo único que es indispensable modificar en ese fichero es una línea que hay que descomentar al principio y que dice: "sys.path.insert(0, os.path.abspath('.'))". Esa línea sirve para que Sphinx sepa encontrar los elementos de nuestro código fuente. Tenemos dos opciones: poner la ruta a nuestro código fuente relativa al directorio de Sphinxs (en nuestro ejemplo docs/) o poner la ruta absoluta. En el primer caso y partiendo de que, en nuestro ejemplo, el archivo se encuentra en /pyalgorithm/docs/conf.py habría que poner: sys.path.insert(0, os.path.abspath('../../')). En caso de que queramos usar la ruta absoluta habría que quitar las parte del abspath() y poner directamente la ruta: sys.path.insert(0, "ruta hasta conf.py").
  • Makefile: A la hora de generar la documentación Sphinx usa make. Este fichero Makefile cuenta con lo necesario para renderizar la documentación en múltiples formatos: html, pdf, latex, epub, etc. De esta manera, para generar la documentación en formato html basta con hacer: make html. La documentación generada se deposita en el directorio build, dentro del de Sphinx.
  • make.bat: Ejecutable de make para los usuarios de Windows.
  • index.rst: Plantilla de la página de inicio de la documentación generada. Está estructurada en formato reStructuredText. Este formato permite crear plantillas que le sirven a Sphinx para estructurar la información extraída de los ficheros de código fuente.
Si llegados a este punto ejecutamos "make html" veremos que se generan ficheros html, pero si abrimos el de index veremos que está vacío. Esto es así porque Sphinx espera encontrar un fichero .rst por cada uno de los paquetes que queramos documentar. Podemos crearlos a mano, pero es más cómodo utilizar el comando sphinx-apidoc, el cual examina el directorio del código fuente y genera un fichero .rst por cada uno de los paquetes que detecte. Su uso es muy sencillo, en nuestro caso bastaría con hacer:

dante@Camelot:~/pyalgorithm$ sphinx-apidoc -o docs .

En la opción -o ponemos el directorio donde queremos que se creen los fichero .rst y en el siguiente parámetro el directorio desde el que queremos que sphinx-apidoc empiece a buscar. En nuestro ejemplo se generarían los siguientes ficheros:
dante@Camelot:~/pyalgorithm$ ls *.rst index.rst pyalgorithm.checkers.rst pyalgorithm.rst pyalgorithm.time.rst modules.rst  pyalgorithm.multiprocessing.rst  pyalgorithm.stats.rst
Con estos ficheros podríamos hacer "make html" y veríamos que se generarían bastantes ficheros html (en realidad uno por fichero .rst). Sin embargo, el fichero de index.html seguiría mostrando una página vacía. Eso se debe a que tenemos que configurarla para que incluya enlaces al resto de las páginas. En nuestro caso la documentación hace referencia a una librería cuya raiz es pyalgorithm, por eso en index.rst debemos hacer incluir la referencia a él:

Welcome to PyAlgorithm's documentation!
=======================================
Contents:
.. toctree::
   :maxdepth: 3
 
   pyalgorithm

Al encontrar esa referencia, Sphinx buscará el fichero pyalgorithm.rst y plasmará su contenido en index.html, así como las referencias que se encuentren a su vez en pyalgorithm.rst y luego recursivamente en este, así hasta alcanzar la "profundidad" máxima definida en el parámetro ":maxdepth". Siguiendo nuestro ejemplo, ya hemos visto el contenido de index.rst, si definimos un maxdepth de 3 en index.rst, siendo el contenido de pyalgorithm.rst:

pyalgorithm package
===================
Subpackages
-----------
.. toctree::
    pyalgorithm.checkers
    pyalgorithm.multiprocessing
    pyalgorithm.stats
    pyalgorithm.time
pyalgorithm.exceptions module
-----------------------------
.. automodule:: pyalgorithm.exceptions
    :members:
    :undoc-members:
    :show-inheritance:

el resultado sería:



Mientras que si lo fijásemos en 4 acudiría a examinar cada uno de los .rst que cuelgan de pyalgorithm.rst y el resultado sería:



Podemos fijar el nivel que queramos. Una vez alcanzado el nivel máximo hay que pinchar en cada enlace para ver los subniveles adicionales que haya agregados en él.

Los ficheros .rst que se refieren a los paquetes con módulos cuentan con una serie de tags de Sphinx a los que hay que prestar especial atención, veamos un ejemplo:

pyalgorithm.checkers package
============================
pyalgorithm.checkers.input_decorators module
--------------------------------------------
.. automodule:: pyalgorithm.checkers.input_decorators
    :members:
    :undoc-members:
    :show-inheritance:

Este fichero .rst, generado por sphinx-apidoc, hace referencia a un paquete ("pyalgorithm.checkers") en el que se han detectado módulos (en este caso "input_decorators"). El fichero .rst explica cómo generar la documentación asociada a dicho módulo. El tag automodule pertenece a las etiquetas que utiliza la extensión autodoc de Sphinx (junto con las etiquetas autoclass y autofunction) y le dice a Sphinx que documente el módulo cogiendo el docstring que haya en el fichero de código fuente del módulo a comienzo del todo. Las etiquetas autoclass y autofunction hacen lo mismo pero a nivel de clase y función respectivamente. La etiqueta members hace que autodoc incluya en la documentación a los miembros "publicos" del elemento, es decir aquellos atributos cuyo nombre no vaya precedido de un guión bajo ("_"), mientras que "undoc-members" hace que se incluya en la documentación aquellos elementos que carezcan de docstring poniendo al menos su declaración. Si quisiéramos mostrar los elementos privados (los que empiecen por "_" o "__") deberíamos utilizar la etiqueta "private-members". Si sólo queremos mostrar los miembros públicos nos encontraremos con que en las clases no se muestran los constructores, para que aparezcan en la documentación hay que incluir la etiqueta ":special-members: __init__" junto a la de "members". En cuanto a la etiqueta "show-inheritance" lo que hace es mostrar el listado de clases base de la que hereda la documentada situando dicho listado debajo de la declaración de la clase. Estas son las etiquetas que incluye sphinx-apidoc, pero hay muchas más. Somos libres de incluir más etiquetas y opciones de formato para hacer que nuestra documentación plasme la información que deseamos en el formato que más nos guste. También podemos cambiar el texto de los ficheros .rst dado que estos son meras plantillas, lo que pongamos en ellas se añadirá a lo que genere automáticamente Sphinx al hacer el "make".

Las etiquetas también se pueden incluir a los docstrings para ayudar a Sphinx a distinguir a qué hace referencia cada elemento del texto. Veamos cómo se documentaría una función:

 def apply_async(self, func, args):
        """Insert a function and its arguments in process pool.
     
        Input is inserted in queues using a round-robin fashion. Every job is
        identified by and index that is returned by function. Not all parameters
        of original multiprocessing.Pool.apply_aync are implemented so far.
     
        :param func: Function to process.
        :type func: Callable.
        :param args: Arguments for the function to process.
        :type args: Tuple.
        :returns: Assigned job id.
        :rtype: Int.
        """

Las etiquetas de Sphinx utilizadas en el ejemplo anterior son:
  • param : Identifica un argumento de la declaración de la función.
  • type : Tipo esperado del argumento.
  • returns: Lo que devuelve la función.
  • rtype: El tipo esperado de lo que devuelve la función.
El docstring anterior se renderizaría de la siguiente manera:




No quiero acabar sin comentar un problema con el que me he encontrado cuando se le pide a Sphinx que actúe sobre un directorio con carpetas que tengan scripts de Unittest. En esos casos he tenido que mover la carpeta fuera del ámbito que examina Sphinx para permitir que este funcionase sin problemas al hacer el "make html". Esto no es sino un workaround bastante tosco, si alguien encuentra una solución más elegante estaré encantado de escucharla.