Software Development

Documenting Python Programs With Doxygen

Doxygen Graphic
Written by John Woolsey

Skill Level: Intermediate

Table Of Contents

Introduction

This tutorial will teach you how to use the Doxygen utility to generate program documentation for your Python based project. A basic understanding of Python programming is expected.

The resources created for this tutorial are available on GitHub for your reference.

What Is Needed

  • Linux, macOS, Or Windows Based Computer

Background Information

Doxygen is a utility that generates program documentation from source code. It parses programs written in various programming languages that are annotated with certain commenting styles. The generated documentation will include summary descriptions for almost all of the elements and members defined in your program. It can also include additional information based on special annotations used within the comments. Doxygen can generate documentation in a variety of formats, e.g. HTML, LaTex, XML, RTF, etc. which makes it appealing to a wide audience. I will be focusing on HTML in this tutorial.

I am using a macOS based computer. If you are using a Linux or Windows computer, the vast majority of this tutorial should still apply, however, some minor changes may be necessary.

If you need assistance with your particular setup, post a question in the comments section below and I, or someone else, can try to help you.

Installing Doxygen

Please see the Doxygen downloads page for general installation instructions for your computer platform. The program binaries for the latest release are about half way down the page.

Since I am using a Mac and do not plan to use the GUI front end, I chose an alternative approach and installed Doxygen from the command line via the Homebrew package manager using the following command.

$ brew install doxygen

This installed the doxygen executable into the /usr/local/bin directory on my Mac.

Once the program is installed, either make sure that it can be found within your executable path or prepend the full directory path upon execution. Test that it is installed correctly by executing the following command within a terminal or command window that will simply print its version number.

$ doxygen -v

My version shows the following.

1.8.18

Creating A Sample Python Program

In order to generate source code based documentation using Doxygen, we first need to have source code for it to use. We will create a main program, named doxygen_example.py, and a module, named sensors.py, that will be used by the program. This program, along with the associated module, are not meant to actually do anything useful. They merely provide an example of how to comment your source code so that it can be properly parsed by the Doxygen utility. It contains various types of elements (e.g. constants, variables, functions, classes, modules, etc.) that are common in Python programs.

Create a project directory named MyDoxygenExample and go into that directory. Create a src directory under the project directory and go into that directory as well. This is where we will place our source code. Create and save a Python program named doxygen_example.py within this src directory with the code shown below.

#!/usr/bin/env python3
"""! @brief Example Python program with Doxygen style comments."""


##
# @mainpage Doxygen Example Project
#
# @section description_main Description
# An example Python program demonstrating how to use Doxygen style comments for
# generating source code documentation with Doxygen.
#
# @section notes_main Notes
# - Add special project notes here that you want to communicate to the user.
#
# Copyright (c) 2020 Woolsey Workshop.  All rights reserved.


##
# @file doxygen_example.py
#
# @brief Example Python program with Doxygen style comments.
#
# @section description_doxygen_example Description
# Example Python program with Doxygen style comments.
#
# @section libraries_main Libraries/Modules
# - time standard library (https://docs.python.org/3/library/time.html)
#   - Access to sleep function.
# - sensors module (local)
#   - Access to Sensor and TempSensor classes.
#
# @section notes_doxygen_example Notes
# - Comments are Doxygen compatible.
#
# @section todo_doxygen_example TODO
# - None.
#
# @section author_doxygen_example Author(s)
# - Created by John Woolsey on 05/27/2020.
# - Modified by John Woolsey on 06/11/2020.
#
# Copyright (c) 2020 Woolsey Workshop.  All rights reserved.


# Imports
from time import sleep
import sensors


# Global Constants
## The mode of operation; 0 = normal, 1 = debug.
DEBUG = 1
## The minimum number to map.
MIN_BASE = 1
## The maximum number to map.
MAX_BASE = 10
## The minimum mapped value.
MIN_MAPPED = 0
## The maximum mapped value.
MAX_MAPPED = 255


# Functions
def init():
    """! Initializes the program."""

    if DEBUG:
        print("Initializing program.")


def map_range(number, in_min, in_max, out_min, out_max):
    """! Maps a number from one range to another.

    @param number   The input number to map.
    @param in_min   The minimum value of an input number.
    @param in_max   The maximum value of an input number.
    @param out_min  The minimum value of an output number.
    @param out_max  The maximum value of an output number.

    @return  The mapped number.
    """

    mapped = (number - in_min) * (out_max - out_min) / (in_max - in_min) + out_min
    if out_min <= out_max:
        return max(min(mapped, out_max), out_min)
    return min(max(mapped, out_max), out_min)


def main():
    """! Main program entry."""

    init()  # program initialization

    # Map numbers
    for i in range(MIN_BASE, MAX_BASE + 1):
        print(
            f"Base: {i:2d}, Mapped: "
            f"{map_range(i, MIN_BASE, MAX_BASE, MIN_MAPPED, MAX_MAPPED):5.1f}"
        )
        sleep(0.25)  # wait 250 milliseconds

    # Sensors
    sensor = sensors.Sensor("MySensor")
    print(sensor)
    temp_in = sensors.TempSensor("Inside")
    print(temp_in)
    temp_out = sensors.TempSensor("Outside", "C")
    print(temp_out)
    temp_out.set_unit("K")
    print(temp_out)


if __name__ == "__main__":
    main()

Likewise, create and save a Python module named sensors.py with its code shown below.

"""! @brief Defines the sensor classes."""

##
# @file sensors.py
#
# @brief Defines the sensor classes.
#
# @section description_sensors Description
# Defines the base and end user classes for various sensors.
# - Sensor (base class)
# - TempSensor
#
# @section libraries_sensors Libraries/Modules
# - random standard library (https://docs.python.org/3/library/random.html)
#   - Access to randint function.
#
# @section notes_sensors Notes
# - Comments are Doxygen compatible.
#
# @section todo_sensors TODO
# - None.
#
# @section author_sensors Author(s)
# - Created by John Woolsey on 05/27/2020.
# - Modified by John Woolsey on 06/11/2020.
#
# Copyright (c) 2020 Woolsey Workshop.  All rights reserved.


import random


class Sensor:
    """! The sensor base class.

    Defines the base class utilized by all sensors.
    """

    def __init__(self, name):
        """! The Sensor base class initializer.

        @param name  The name of the sensor.

        @return  An instance of the Sensor class initialized with the specified name.
        """

        ## The name of the sensor.
        self.name = name
        ## The value of the sensor.
        self.value = random.randint(0, 50)

    def __str__(self):
        """! Retrieves the sensor's description.

        @return  A description of the sensor.
        """

        return f"The {self.name} sensor has a value of {self.value}."


class TempSensor(Sensor):
    """! The temperature sensor class.

    Provides access to the connected temperature sensor.

    Supported units are "F" (Fahrenheit), "C" (Celsius), and "K" (Kelvin)
    with "F" being the default unit.
    """

    def __init__(self, name, unit="F"):
        """! The TempSensor class initializer.

        @param name  The name of the temperature sensor.
        @param unit  The unit of the temperature sensor, defaults to "F".

        @return  An instance of the TempSensor class initialized with the specified name and unit.
        """

        super().__init__(name)
        ## The temperature unit.
        self.unit = unit

    def __str__(self):
        """! Retrieves the temperature sensor's description.

        @return  A description of the temperature sensor.
        """

        return (
            f"The {self.name} temperature sensor has a value of "
            f"{self.value} degrees {self.unit}."
        )

    def set_unit(self, unit):
        """! Sets the temperature unit.

        @param unit  The temperature unit ("F", "C", or "K"),
            defaults to "F" if a valid unit is not provided.
        """

        if unit in ("C", "K"):
            self.unit = unit
        else:
            self.unit = "F"

In my example code, the exclamation point (!) at the beginning of a docstring or the double number sign (##) at the beginning of a comment block tell Doxygen to parse this area. The at sign (@) represents a command, e.g. @param or @section, that provides the Doxygen parser with further instruction.

Along with generating documentation for the general programming elements, Doxygen also provides a facility for including additional documentation that will be shown on the main page of the documentation or on the pages for the individual files. These are enabled with the @mainpage and @file commands respectively. We will see how this works when we view the generated documentation later.

A variety of comment formatting styles are supported by Doxygen. I chose what I believe to be the most clear and concise styles for this example that are also compatible with the standard Python docstring conventions. Please feel free to view and play around with other supported styles shown within the Doxygen manual. However, you might want to wait until the end of this tutorial before making any changes.

Now run the program to make sure we did not accidentally introduce any errors. The first line of the main program, doxygen_example.py, contains a shebang (#!) statement allowing us to run the program as a command line script. To do so, make the program an executable with

$ chmod a+x doxygen_example.py

and then execute it.

$ ./doxygen_example.py

Alternatively, you could just run it with the Python interpreter.

$ python3 doxygen_example.py

The program output should look like the following.

Initializing program.
Base:  1, Mapped:   0.0
Base:  2, Mapped:  28.3
Base:  3, Mapped:  56.7
Base:  4, Mapped:  85.0
Base:  5, Mapped: 113.3
Base:  6, Mapped: 141.7
Base:  7, Mapped: 170.0
Base:  8, Mapped: 198.3
Base:  9, Mapped: 226.7
Base: 10, Mapped: 255.0
The MySensor sensor has a value of 25.
The Inside temperature sensor has a value of 29 degrees F.
The Outside temperature sensor has a value of 48 degrees C.
The Outside temperature sensor has a value of 48 degrees K.

Again, this is just an example program. Don’t pay too much attention to what it is actually doing, just how the comments are formatted for the various types of programming elements or pages.

Creating The Doxygen Configuration File

Now let’s create a documentation directory where our Doxygen based configuration and generated documentation will be located. Create a doxygen directory within the project directory parallel to the src directory. Alternatively, you could name the documentation directory as docs, as many people prefer, but I choose to name it based on the documentation generator in case I choose to use additional generators as well.

In order to effectively parse the source code to generate our project documentation, Doxygen requires the use of a configuration file. This file, named Doxyfile by default, is where we set up our project specific information and tell Doxygen how to parse the Python code.

Within a terminal or command window, go into the documentation directory and create a default Doxygen configuration file by running the following command.

$ doxygen -g

This will create a Doxyfile configuration file within the current directory. If you look at the file, each configurable setting is prepended by a comment letting us know what the setting does, how changing it will affect your generated documentation, and the default value. This is a nice touch by the Doxygen team. These settings are also listed in the Doxygen manual.

Now we need to edit the Doxyfile file to enter our project specifics. The first change we want to make is to tell Doxygen the name of our project. This is accomplished by changing the following line, around line 35, from

PROJECT_NAME           = "My Project"

to

PROJECT_NAME           = "My Doxygen Example Project"

This will be the title of the project within our generated documentation. This is where you change the title to something that makes sense for your project.

The next one enables Doxygen to utilize Javadoc style briefs which allows us to write comments such as

##
# Brief summary description of the particular element.
#
# Detailed description of the particular element
# that includes much more information.

for describing code elements. Although this particular style is not explicitly being used in my example, it is a useful option to have enabled. To do so, change the line, around line 198, from

JAVADOC_AUTOBRIEF      = NO

to

JAVADOC_AUTOBRIEF      = YES

To optimize the generated documentation for Java and Python based source code, change the line around 280 from

OPTIMIZE_OUTPUT_JAVA   = NO

to

OPTIMIZE_OUTPUT_JAVA   = YES

Next, tell Doxygen to generally extract all elements found in the source code. Change the line around 464 from

EXTRACT_ALL            = NO

to

EXTRACT_ALL            = YES

Note, the EXTRACT_PRIVATE and EXTRACT_STATIC settings can also be set to YES if you want to include private class members and static file members in the generated documentation as well.

Next up is around line 566. Change

HIDE_SCOPE_NAMES       = NO

to

HIDE_SCOPE_NAMES       = YES

This setting will hide the scope name that is typically prepended to element names contained within that scope. This is being done to make the documentation for some pages look a bit cleaner.

To sort the elements in alphabetical order instead of when they are declared, change the line, around 613, from

SORT_BRIEF_DOCS        = NO

to

SORT_BRIEF_DOCS        = YES

Now, we need to tell Doxygen where to find the Python source code by making the following change, around line 826.

INPUT                  =

to

INPUT                  = ../src

Finally, since we are not planning to generate LaTex based documentation, we will suppress it by changing the line, around 1716, from

GENERATE_LATEX         = YES

to

GENERATE_LATEX         = NO

These are my preferred options. OPTIMIZE_OUTPUT_JAVA and INPUT are really the only settings required for generating Python based documentation with Doxygen. The others provide enhancements to the extraction and readability of the generated documentation in my opinion.

Save your updated Doxyfile configuration file when complete.

Running Doxygen

Now that the configuration file is updated, run Doxygen to generate the HTML based documentation for our Python based project. In the same directory as the Doxyfile, run the Doxygen executable.

$ doxygen

Doxygen will print to the screen the various tasks it is performing while running. It will also print any warnings or errors that occurred during execution. Once complete, you should see that it created an html directory that contains all of your HTML based documentation that it generated for your project.

Viewing The Generated Documentation

Load the index.html file located within the html directory into your browser. This is the main project page and displays all of the information, separated by sections, that we specified within the comment block containing the @mainpage command in our doxygen_example.py program. It also shows the project title we specified in the configuration file.

Across the top of the page, under the project title, you will see the Main Page, Packages, Classes, and Files tabs with their accompanying pull down menus. These tabs and menus will contain all of the documentation generated for your project.

Main Page Of Doxygen Generated Documentation
Main Page Of Doxygen Generated Documentation

The Packages tab will provide a list, with brief descriptions and associated links, for the various packages (files/modules) that Doxygen found in your project. The detailed information for each package, seen by clicking the associated link, e.g. doxygen_example, is very similar to the information found under the Files tab. One major difference though is that the latter will also show the extra documentation that was added in the comment blocks containing the explicit @file command.

File Page (doxygen_example.py) Of Doxygen Generated Documentation
File Page (doxygen_example.py) Of Doxygen Generated Documentation
File Page (sensors.py) Of Doxygen Generated Documentation
File Page (sensors.py) Of Doxygen Generated Documentation

The Classes tab provides a list of the classes included in our project. The Sensor and TempSensor classes are both shown here under the sensors module. Clicking on TempSensor will show us the detailed information of the TempSensor class along with the members defined in that class.

Class Page (TempSensor) Of Doxygen Generated Documentation
Class Page (TempSensor) Of Doxygen Generated Documentation

Take a peak at the other menu options to get an idea of how the documentation is structured. This would also be a good time to look at some of the other options available in the configuration file. Play around with changing some of those settings and rerunning Doxygen to see how they change the resulting documentation. You could also play around with trying some of the other supported commenting styles mentioned earlier. You might find something you prefer more than my general setup.

Summary

In this tutorial, we learned how to generate project documentation from source code using the Doxygen utility for a Python based project. Generously commenting your code and generating the source code documentation is a great way to provide both a high level architectural overview and the low level implementation details of a project. Not only does it provide others the means to more easily understand your code, it can also help the original programmer who hasn’t worked on that code in a while.

We barely touched the surface of all the things you can do with Doxygen. If you are interested in learning more, please see the Doxygen manual along with reviewing the multitude of settings comments found within the Doxygen configuration file, Doxyfile, itself.

The Python project and Doxygen configuration file used for this tutorial are available on GitHub.

Thank you for joining me in this journey and I hope you enjoyed the experience. Please feel free to share your thoughts in the comments section below.

About the author

John Woolsey

John is an electrical engineer who loves science, math, and technology and teaching it to others even more.
 
He knew he wanted to work with electronics from an early age, building his first robot when he was in 8th grade. His first computer was a Timex/Sinclair 2068 followed by the Tandy 1000 TL (aka really old stuff).
 
He put himself through college (The University of Texas at Austin) by working at Motorola where he worked for many years afterward in the Semiconductor Products Sector in Research and Development.
 
John started developing mobile app software in 2010 for himself and for other companies. He has also taught programming to kids for summer school and enjoyed years of judging kids science projects at the Austin Energy Regional Science Festival.
 
Electronics, software, and teaching all culminate in his new venture to learn, make, and teach others via the Woolsey Workshop website.

Leave a Comment

This site uses Akismet to reduce spam. Learn how your comment data is processed.