Usage¶
Using exhale can be simple or involved, depending on how much you want to change and how familiar you are with things like Sphinx, Breathe, Doxygen, and Jinja. At the top level, what you need is:
- Your C++ code you want to document, with “proper” Doxygen documentation. Please read the Doxygen Documentation Specifics for common documentation pitfalls, as well as features previously unavailable in standard Doxygen.
- Generating the API using Sphinx, Doxygen, Breathe already working.
Quickstart¶
In your conf.py
# setup is called auto-magically for you by Sphinx
def setup(app):
# create the dictionary to send to exhale
exhaleArgs = {
"doxygenIndexXMLPath" : "./doxyoutput/xml/index.xml",
"containmentFolder" : "./generated_api",
"rootFileName" : "library_root.rst",
"rootFileTitle" : "Library API",
"doxygenStripFromPath" : ".."
}
# import the exhale module from the current directory and generate the api
sys.path.insert(0, os.path.abspath('.')) # exhale.py is in this directory
from exhale import generate
generate(exhaleArgs)
In your index.rst
, you might have something like
.. toctree::
:maxdepth: 2
about
generated_api/library_root
Note
The above assumes that your Doxygen xml tree has already been created. The Fully Automated Building section provides additional steps to do this all at once.
Lastly, you will likely want to add these two lines somewhere in conf.py
as well:
# Tell sphinx what the primary language being documented is.
primary_domain = 'cpp'
# Tell sphinx what the pygments highlight language should be.
highlight_language = 'cpp'
The full documentation for the only (official) entry point is: exhale.generate()
.
Additional Usage and Customization¶
The main library page that you will link to from your documentation is laid out as follows:
1 {{ Library API Title }} Heading 2 {{ after title description }} Section 1 3 Class Hierarchy Section 2 4 File Hierarchy Section 3 5 Full API Listing Section 4 6 {{ after body description }} Section 5
- The dictionary key
rootFileTitle
passed toexhale.generate()
function is what will be the Heading title. - Section 1 can optionally be provided by the dictionary key
afterTitleDescription
in the argument toexhale.generate()
. - The class view hierarchy (including namespaces with class-like children).
- The file view hierarchy (including folders).
- An ordered enumeration of every Breathe compound found, except for groups.
- Section 5 can optionally be provided by the dictionary key
afterBodySummary
in the argument toexhale.generate()
.
Clickable Hierarchies¶
While I would love to automate this for you, it is not possible to do so very easily. If you would like to have a more interactive hierarchy view (instead of just bulleted lists), you will need to add some extra files for it to work. There are a lot of different options available, but I rather enjoy Stephen Morley’s collapsibleLists: it’s effective, easily customizable if you know front-end, and has a generous license.
You will need
- The javascript library.
- The css stylesheet and its associated images.
- A sphinx template override.
I have taken the liberty of adding these files to the exhale repository, just clone exhale and move the files to where you need them to go. Specifically, the exhale repository looks like this:
exhale/
│ README.md
│ exhale.py # put next to conf.py
└───treeView/
├───_static/
│ └───collapse/
│ CollapsibleLists.compressed.js # (1) library
│ tree_view.css # (2) stylesheet
│ button-closed.png # v associated images
│ button-open.png
│ button.png
│ list-item-contents.png
│ list-item-last-open.png
│ list-item-last.png
│ list-item-open.png
│ list-item-root.png
│ list-item.png
└───_templates/
layout.html # (3) MUST be layout.html
You then just need to to move the folder collapse
to your _static
directory, and
move layout.html
to your _templates
directory. So your docs
folder might
look something like:
docs/
│ conf.py # created by sphinx-quickstart
│ exhale.py # placed here by you
│ index.rst # created by sphinx-quickstart
│ about.rst # created by you
│ Makefile # created by sphinx-quickstart
├───_static/
│ └───collapse/
│ ... everything from above ...
└───_templates/
layout.html # copied from above
Sphinx will make everything else fall into place in the end. If you already have your
own layout.html
, you know what you are doing — just look at mine and add the
relevant lines to yours.
You can now add the key value pair createTreeView = True
to the dictionary you are
passing to exhale.generate()
.
Warning
If you are hosting on Read the Docs, you will need to make sure you are tracking all of those files with git!
Linking to a Generated File¶
Every file created by exhale is given a reStructuredText label that you can use to link to the API page. It is easiest to just show how the labels are created.
def initializeNodeFilenameAndLink(self, node):
html_safe_name = node.name.replace(":", "_").replace("/", "_")
node.link_name = "{}_{}".format(qualifyKind(node.kind).lower(), html_safe_name)
The parameter node
is an exhale.ExhaleNode
object. So if the node being
represented is a struct some_thing
in namespace arbitrary
, then
node.name := "arbitrary::some_thing"
node.link_name := "struct_arbitrary__some_thing"
Noting that there are two underscores between arbitrary
and some
. Refer to
the full documentation of exhale.qualifyKind()
for the possible return values.
If this is not working, simply generate the API once and look at the top of the file
generated for the thing you are trying to link to. Copy the link (ignoring the leading
underscore) and use that.
These are reStructuredText links, so in the above example you would write
I am linking to :ref:`struct_arbitrary__some_thing`.
Alternatively, you can link to a class with :class:`namespace::ClassName`
, as well
as link to a method within that class using :func:`namespace::ClassName::method`
.
Customizing Breathe Output¶
Breathe provides you with many excellent configurations for the various reStructuredText
directives it provides. Your preferences will likely be different than mine for what
you do / do not want to show up. The default behavior of exhale is to use all default
values for all Breathe directives except for classes and structs. Classes and structs
will request documentation for :members:
, :protected-members:
, and
:undoc-members:
.
To change the behavior of any of the breathe directives, you will need to implement your
own function and specify that as the customSpecificationFunction
for
exhale.generate()
. Please make sure you read the documentation for
exhale.specificationsForKind()
before implementing, the requirements are very
specific. An example custom implementation could be included in conf.py
as follows:
def customSpecificationsForKind(kind):
if kind == "class" or kind == "struct":
return " :members:\n :protected-members:\n :no-link:\n"
elif kind == "enum":
return " :outline:\n"
return ""
and you would then change the declaration of the dictionary you are passing to
exhale.generate()
to be:
exhaleArgs = {
"doxygenIndexXMLPath" : "./doxyoutput/xml/index.xml",
"containmentFolder" : "./generated_api",
"rootFileName" : "library_root.rst",
"rootFileTitle" : "Library API",
"customSpecificationFunction" : customSpecificationsForKind
}
Note
The value of the key customSpecificationFunction
is not a string, just the
name of the function. These are first class objects in Python, which makes the above exceptionally convenient :)
Customizing File
Pages¶
File pages are structured something like
File {{ filename of exhale node }} Heading 1 Definition ( {{ path to file with folders }} ) Section 1 2
- Program Listing for file (hyperlink)
... other common information ... 3 {{ appendBreatheFileDirective }}
- Heading:
- Uses the file name without a path to it. If the path was
include/File.h
, then the line would beFile File.h
. - Section 1:
- The following Doxygen variables control what this section looks like, as well as whether or not it is included at all.
Set the Doxygen variable
STRIP_FROM_PATH
to change the output inside of parentheses.If the file path is
../include/arbitrary/File.h
andSTRIP_FROM_PATH = ..
, the parentheses line will beDefinition ( include/arbitrary/File.h )
. If you changeSTRIP_FROM_PATH
to../include
, then line 1 will beDefinition ( arbitrary/File.h )
.The appearance of this line will also be affected by whether or not you are using the Doxygen variable
FULL_PATH_NAMES
. In addition to leaving its defaultYES
value, I have had best success with setting theSTRIP_FROM_PATH
variable.If you set
XML_PROGRAMLISTING = YES
, then the code of the program (as Doxygen would display it) will be included as a bulleted hyperlink. It is the full file including whitespace, with documentation strings removed. Programming comments remain in the file.Unlike Doxygen, I do not link to anything in the code. Maybe sometime in the future?
If the value of
"appendBreatheFileDirective" = True
in the arguments passed toexhale.generate()
, then the following section will be appended to the bottom of the file being generated:Full File Listing ---------------------------------------------------------------------------------- .. doxygenfile:: {{ exhale_node.location }}
This will hopefully be a temporary workaround until I can figure out how to robustly parse the xml for this, or figure out how to manipulate Breathe to give me this information (since it clearly exists...). This workaround is unideal in that any errors you have in any of the documentation of the items in the file will be duplicated by the build, as well as a large number of DUPLICATE id’s will be flagged. The generated links inside of the produced output by Breathe will now also link to items on this page first. AKA this is a buggy feature that I hope to fix soon, but if you really need the file documentation in your project, this is currently the only way to include it.
Note
If you set XML_PROGRAMLISTING = NO
, then the file in which an
enum
, class
, variable
, etc is declared may not be recovered. To my
experience, the missing items not recovered are only declared in the programlisting.
See the exhale.ExhaleRoot.fileRefDiscovery()
part of the parsing process.
Fully Automated Building¶
It is preferable to have everything generated at once, e.g. if you wish to host your
documentation on Read the Docs. I make the assumption that you already have a
Makefile
created by sphinx-quickstart
. Instead of a Doxyfile, though, we’re
going to take it one step further. Your specific arguments to Doxygen may be more
involved than this, but the below should get you started in the right direction.
In conf.py
we now define at the bottom
def generateDoxygenXML(stripPath):
'''
Generates the doxygen xml files used by breathe and exhale.
Approach modified from:
- https://github.com/fmtlib/fmt/blob/master/doc/build.py
:param stripPath:
The value you are sending to exhale.generate via the
key 'doxygenStripFromPath'. Usually, should be '..'.
'''
from subprocess import PIPE, Popen
try:
doxygen_cmd = ["doxygen", "-"]# "-" tells Doxygen to read configs from stdin
doxygen_proc = Popen(doxygen_cmd, stdin=PIPE)
doxygen_input = r'''
# Make this the same as what you tell exhale.
OUTPUT_DIRECTORY = doxyoutput
# If you need this to be YES, exhale will probably break.
CREATE_SUBDIRS = NO
# So that only include/ and subdirectories appear.
FULL_PATH_NAMES = YES
STRIP_FROM_PATH = "%s/"
# Tell Doxygen where the source code is (yours may be different).
INPUT = ../include
# Nested folders will be ignored without this. You may not need it.
RECURSIVE = YES
# Set to YES if you are debugging or want to compare.
GENERATE_HTML = NO
# Unless you want it?
GENERATE_LATEX = NO
# Both breathe and exhale need the xml.
GENERATE_XML = YES
# Set to NO if you do not want the Doxygen program listing included.
XML_PROGRAMLISTING = YES
# Allow for rst directives and advanced functions (e.g. grid tables)
ALIASES = "rst=\verbatim embed:rst:leading-asterisk"
ALIASES += "endrst=\endverbatim"
''' % stripPath)
# In python 3 strings and bytes are no longer interchangeable
if sys.version[0] == "3":
doxygen_input = bytes(doxygen_input, 'ASCII')
doxygen_proc.communicate(input=doxygen_input)
doxygen_proc.stdin.close()
if doxygen_proc.wait() != 0:
raise RuntimeError("Non-zero return code from 'doxygen'...")
except Exception as e:
raise Exception("Unable to execute 'doxygen': {}".format(e))
Note
The above code should work for Python 2 and 3, but be careful not to modify the somewhat delicate treatment of strings:
doxygen_input = r'''...
: ther
is required to prevent the verbatim rst directives to expand into control sequences (\v
)- In Python 3 you need to explicitly construct the bytes for communicating with the process.
Now that you have defined this at the bottom of conf.py
, we’ll add a modified
setup(app)
method:
# setup is called auto-magically for you by Sphinx
def setup(app):
stripPath = ".."
generateDoxygenXML(stripPath)
# create the dictionary to send to exhale
exhaleArgs = {
"doxygenIndexXMLPath" : "./doxyoutput/xml/index.xml",
"containmentFolder" : "./generated_api",
"rootFileName" : "library_root.rst",
"rootFileTitle" : "Library API",
"doxygenStripFromPath" : stripPath
}
# import the exhale module from the current directory and generate the api
sys.path.insert(0, os.path.abspath('.')) # exhale.py is in this directory
from exhale import generate
generate(exhaleArgs)
Now you can build the docs with make html
and it will re-parse using Doxygen,
generate all relevant files, and give you an updated website. While some may argue that
this is wasteful, exhale
is not smart enough and never will be smart enough to
provide incremental updates. The full api is regenerated. Every time. So you may as
well run Doxygen each time ;)
Note
Where Doxygen is concerned, you will likely need to give special attention to macros
and preprocessor definitions. Refer to the linked fmt
docs in the above code
snippet. Of particular concern would be the following Doxygen config variables:
ENABLE_PREPROCESSING
MACRO_EXPANSION
EXPAND_ONLY_PREDEF
PREDEFINED
(very useful if the Doxygen preprocessor is choking on your macros)SKIP_FUNCTION_MACROS
Doxygen Documentation Specifics¶
If you have not used Doxygen before, the below may be helpful in getting things started.
To make sure you have Doxygen working, first try just using Doxygen and viewing the html
output by setting GENERATE_HTML = YES
. This is the default value of the variable,
when you get Sphinx / Breathe / exhale going, just set this variable to NO
to avoid
creating unnecessary files.
There is a lot to make sure you do in terms of the documentation you write in a C++ file
to make Doxygen work. To get started, though, execute doxygen -g
from your terminal
in a directory where there is no Doxyfile
present and it will give you a large file
called Doxyfile
with documentation on what all of the variables do. You can leave
a large number of them to their default values. To execute doxygen now, just enter
doxygen
in the same directory as the Doxyfile
and it will generate the html
output for you so you can verify it is working. Doxygen builds similarly to make
.
Later, you can just use conf.py
and won’t need to keep your Doxyfile
, but you
could also just keep the Doxyfile
you have working for you and execute doxygen
with no parameters in conf.py
before calling exhale.generate()
.
Files you want documented must have
\file
somewhere. From the Doxygen documentation reiteration:Let’s repeat that, because it is often overlooked: to document global objects (functions, typedefs, enum, macros, etc), you must document the file in which they are defined.
Classes, structs, and unions need additional care in order for them to appear in the hierarchy correctly. If you have a file in a directory, the Doxygen FAQ explains that you need to specify this location:
You can also document your class as follows:
/*! \class MyClassName include.h path/include.h * * Docs for MyClassName */
So a minimal working example of the file directory/file.h
defining struct thing
might look like:
/** \file */
#ifndef _DIRECTORY_THING_H
#define _DIRECTORY_THING_H
/**
* \struct thing file.h directory/file.h
*
* \brief The documentation about the thing.
*/
struct thing {
/// The thing that makes the thing.
thing() {}
};
#endif // _DIRECTORY_THING_H
Deviations from the norm. The cool thing about using Sphinx in this context is that you have some flexibility inherent in the fact that we are using reStructuredText. For example, instead of using
\ref
, you can just link to another documented item with`item`
. This works across files as well, so you could link to class A in a different file from class B with`A`
in the documentation string. You could make a statement bold in your documentation with just**bold**
!I believe this includes the full range of reStructuredText syntax, but would not be surprised if there were directives or notation that break something.
Note
I do not support groups
with Doxygen, as I assume if you have gone through the
effort to group everything then you have a desire to manually control the output.
Breathe already has an excellent doxygengroup
directive, and you should use that.
Start to finish for Read the Docs¶
Assuming you already had the code that you are generating the API for documented,
navigate to the top-level folder of your repository. Read the Docs (RTD) will be
looking for a folder named either doc
or docs
at the root of your repository
by default:
$ cd ~/my_repo/
$ mkdir docs
Now we are ready to begin.
Generate your sphinx code by using the
sphinx-quickstart
utility. It may look something like the following:$ ~/my_repo/docs> sphinx-quickstart Welcome to the Sphinx 1.3.1 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 [.]: 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 [_]: ... and a whole lot more ...
Warning
The default value for
> Create Makefile? (y/n) [y]:
must be yes to work on RTD. They are giving you a unix virtual environment.
This will create the files
conf.py
,index.rst
,Makefile
, andmake.bat
if you are supporting Windows. It will also create the directories_static
and_templates
for customizing the sphinx output.Create a
requirements.txt
file with the linebreathe
so RTD will install it:$ ~/my_repo/docs> echo 'breathe' > requirements.txt
Alternatively, you can have RTD install via Git Tags. At the time of writing this, the latest tag for
breathe
is4.3.1
, so in yourrequirements.txt
you would havegit+git://github.com/michaeljones/breathe@v4.3.1#egg=breathe
Clone exhale and steal all of the files you will need:
$ ~/my_repo/docs> git clone https://github.com/svenevs/exhale.git $ ~/my_repo/docs> mv exhale/exhale.py . $ ~/my_repo/docs> mv exhale/treeView/_static/collapse/ ./_static/ $ ~/my_repo/docs> mv exhale/treeView/_templates/layout.html _templates/ $ ~/my_repo/docs> rm -rf exhale/
Uncomment the line
sys.path.insert(0, os.path.abspath('.'))
at the top of the generatedconf.py
so that Sphinx will know where to look forexhale.py
.Two options below (5) in
conf.py
, add'breathe'
to theextensions
list so that the directives from Breathe can be used.Just below the
extensions
list, configure breathe. Adding the following should be sufficient:breathe_projects = { "yourProjectName": "./doxyoutput/xml" } breathe_default_project = "yourProjectName"
Edit
conf.py
to use the RTD Theme. You are of course able to use a different Sphinx theme, but the RTD Theme is what this will enable. Replace thehtml_theme
andhtml_theme_path
lines (or comment them out) with:# on_rtd is whether we are on readthedocs.org, this line of code grabbed from docs.readthedocs.org on_rtd = os.environ.get('READTHEDOCS', None) == 'True' if not on_rtd: # only import and set the theme if we're building docs locally import sphinx_rtd_theme html_theme = 'sphinx_rtd_theme' html_theme_path = [sphinx_rtd_theme.get_html_theme_path()]
Edit
conf.py
to include thegenerateDoxygenXML
andsetup
methods provided in Fully Automated Building at the bottom of the file.Add
createTreeView = True
to the dictionary arguments sent toexhale.generate()
.Go to the admin page of your RTD website and select the Advanced Settings tab. Make sure the Install your project inside a virtualenv using
setup.py install
button is checked. In the Requirements file box below, enterdocs/requirements.txt
assuming you followed the steps above.I personally prefer to keep the
requirements.txt
hidden in thedocs
folder so that it is implicit that those are only requirements for building the docs, and not the actual project itself.
And you are done. Make sure you git add
all of the files in your new docs
directory, RTD will clone your repository / update when you push commits. You can
build it locally using make html
in the current directory, but make sure you do not
add the _build
directory to your git repository.
I hope that the above is successful for you, it looks like a lot but it’s not too bad... right?