Lei Mao bio photo

Lei Mao

Machine Learning, Artificial Intelligence, Computer Science.

Twitter Facebook LinkedIn GitHub   G. Scholar E-Mail RSS

Introduction

Documentation is undoubtedly a key component to a successful project, in addition to the actual useful code. I have been knowing Sphinx for Python documentation for a while. However, since I don’t have any knowledge in reStructuredText which Sphinx heavily relies on and there is no beginner’s tutorial that is really close to a real Python project, my attempt to learn Sphinx had not been very successful. Instead, I had been using Markdown for documenting projects. It turns out that Markdown is OK for a simple README or some simple usages of the project. When the project becomes heavy and huge, such as when developing a library, systematic and automatic documentation becomes necessary.


There are quite a few tutorial blogs or videos on Sphinx that could be found using Google. Usually the blogs and the videos are very short and the authors claimed that the readers or watchers should be able to use Sphinx in their own projects after reading or watching. This is not entirely true in practice. In those tutorials, there are missing logical connections between the kind of documentation we wanted to create and how we are going to create it. The concrete examples in those tutorials are nowhere close to a real Python library project. So those tutorials did not really helped me in studying Sphinx.


Recently, I watched Brandon Rhodes’s Sphinx tutorial session at PyCon 2013 by chance on YouTube. Although Brandon talked too much and it took almost three hours for the tutorial session, I found the concrete example he provided, the introduction to reStructuredText, the motivations and logical connections, and the hands-on Sphinx experiences are somethings that I was looking for. After watching the tutorial session and doing some offline practice, I am now able to create good documentation using Sphinx for my Python projects.


In this blog post, I am going to make some notes or updates on Brandon’s tutorial. Note that the Sphinx beginners should always watch Brandon’s long tutorial in the first place, and this blog post could by no means replace Brandon’s tutorial.

Triangle Python Library

Brandon has created a simple trianglelib library specifically for his Sphinx tutorial. The library and the accompanying Sphinx tutorial were created using Python 2 and Sphinx 1.x. To make the library and Sphinx tutorial compatible for Python 3 and the latest Sphinx 3.x, I modified Brandon’s code a little bit. In addition, I have also moved the API documentations entirely to the Python scripts, restructured the project directory layout, and taken better advantage of the latest autodoc extension in Sphinx.


The upgraded trianglelib could be found in Sphinx Python TriangleLib on my GitHub. The documentation corresponding to this project could be found on Read the Docs.

Notes

Sphinx Cultivates the Best Practice in Documentation

Previously, during the project development, I usually only put some brief comments in the Python functions or methods. However, since Python is not a strictly typing programming language, and those comments would not usually contain the typing information, I would hardly remember how those variables could be used.


In addition, creating a documentation after the entire project is done is an approach that I know it is wrong but I have been doing. It is low-efficiency and often error-prone. The correct approach should be documenting while programming. Since Sphinx could automatically generate documentations for all the modules, classes, and functions that follow its supported docstring format, we are encouraged to document the Python program more comprehensively. Tools, such as Python Docstring Generator for VS Code could automatically generate the template for docstring to save our time in documentation.


A well-documented Python program that is Sphinx-compatible looks like this.

"""
Use the triangle class to represent triangles.
"""

from math import sqrt

class Triangle(object):
    """
    A :class:`~trianglelib.shape.Triangle` object is a three-sided polygon.

    You instantiate a :class:`~trianglelib.shape.Triangle` by providing exactly three lengths ``a``, ``b``, and ``c``.

    They can either be intergers or floating-point numbers, and should be listed clockwise around the triangle.

    If the three lengths *cannot* make a valid triangle, then ``ValueError`` will be raised instead.

    >>> from trianglelib.shape import Triangle
    >>> t = Triangle(3, 4, 5)
    >>> print(t.is_equilateral())
    False
    >>> print(t.area())
    6.0

    Triangles support the following attributes, operators, and methods.

    .. attribute:: a
                   b
                   c

        The three side lengths provided during instantiation.

    .. index:: pair: equality; triangle
    .. method:: triangle1 == triangle2

        Returns true if the two triangles have sides of the same lengths,
        in the same order.
        Note that it is okay if the two triangles
        happen to start their list of sides at a different corner;
        ``3,4,5`` is the same triangle as ``4,5,3``
        but neither of these are the same triangle
        as their mirror image ``5,4,3``.
    """

    def __init__(self, a, b, c):
        """
        Create a :class:`~trianglelib.shape.Triangle` object with sides of lengths `a`, `b`, and `c`.

        Raises `ValueError` if the three length values provided cannot
        actually form a triangle.

        :param a: side length one
        :type a: :class:`float`
        :param b: side length two
        :type b: :class:`float`
        :param c: side length three
        :type c: :class:`float`
        :raises ValueError: side lengths must all be positive
        :raises ValueError: one side is too long to make a triangle
        """
        self.a, self.b, self.c = float(a), float(b), float(c)
        if any( s <= 0 for s in (a, b, c) ):
            raise ValueError('side lengths must all be positive')
        if any( a >= b + c for a, b, c in self._rotations() ):
            raise ValueError('one side is too long to make a triangle')

    def _rotations(self):
        """
        Return each of the three ways of rotating our sides.

        :return: three tuples of the side lengths of possible rotations
        :rtype: ((:class:`float`, :class:`float`, :class:`float`), (:class:`float`, :class:`float`, :class:`float`), (:class:`float`, :class:`float`, :class:`float`))
        """
        return ((self.a, self.b, self.c),
                (self.c, self.a, self.b),
                (self.b, self.c, self.a))

    def __eq__(self, other):
        """
        Return whether this :class:`~trianglelib.shape.Triangle` object equals another triangle.

        :param other: another :class:`~trianglelib.shape.Triangle` object
        :type other: :class:`~trianglelib.shape.Triangle`
        :return: whether the two :class:`~trianglelib.shape.Triangle` objects are equivalent
        :rtype: :class:`bool`
        """
        sides = (self.a, self.b, self.c)
        return any( sides == rotation for rotation in other._rotations() )

    def is_equivalent(self, triangle):
        """
        Return whether this triangle equals another triangle.

        :param triangle: another :class:`~trianglelib.shape.Triangle` object
        :type triangle: :class:`~trianglelib.shape.Triangle`
        :return: whether the two :class:`~trianglelib.shape.Triangle` objects are equivalent
        :rtype: :class:`bool`
        """
        return self == triangle

    def is_similar(self, triangle):
        """
        Return whether this :class:`~trianglelib.shape.Triangle` object is similar to another triangle.

        :param triangle: another :class:`~trianglelib.shape.Triangle` object
        :type triangle: :class:`~trianglelib.shape.Triangle`
        :return: whether the two :class:`~trianglelib.shape.Triangle` objects are similar
        :rtype: :class:`bool`
        """
        return any( (self.a / a == self.b / b == self.c / c)
                    for a, b, c in triangle._rotations() )

Make Good Use of Doctest

Sphinx allows running some simple tests on the programs we created via doctest. So sometimes writing some test code snippets in the reStructuredText not only helps documentation but also helps program validation.


The test code could be something like the followings.

>>> from trianglelib.shape import Triangle
>>> t1 = Triangle(3, 4, 5)
>>> t2 = Triangle(4, 5, 3)
>>> t3 = Triangle(3, 4, 6)
>>> print(t1 == t2)
True
>>> print(t1 == t3)
False
>>> print(t1.area())
6.0
>>> print(t1.scale(2.0).area())
24.0
.. testcode::

    from trianglelib.shape import Triangle
    t = Triangle(5, 5, 5)
    print('Equilateral?', t.is_equilateral())
    print('Isosceles?', t.is_isosceles())

.. testoutput::

    Equilateral? True
    Isosceles? True

Version Control

We could publish the documentations to our projects on Read the Docs for free. Read the Docs also allows us to do version control for our projects and documentations by creating tag or branch to our GitHub projects. It is extremely useful for the users who are not using the latest version of the software.

Final Remarks

Learning Sphinx is not easy and I think there is no 15-minute shortcut for beginners. Instead of randomly Googling and reading superficial blogs and videos, spending two to three hours on studying Brandon Rhodes’s Sphinx tutorial session at PyCon 2013 actually saves time.

References