# -*- coding: utf8 -*-
########################################################################################
# This file is part of exhale. Copyright (c) 2017-2019, Stephen McDowell. #
# Full BSD 3-Clause license available here: #
# #
# https://github.com/svenevs/exhale/blob/master/LICENSE #
########################################################################################
'''
The ``configs`` module exists to contain the Sphinx Application configurations specific
to this extension. Almost every ``global`` variable defined in this file can be
modified using the ``exhale_args`` in ``conf.py``. The convention for this file is as
follows:
1. Things that are **not** supposed to change, because their value is expected to be
constant, are declared in ``ALL_CAPS``. See
- :data:`~exhale.configs.SECTION_HEADING_CHAR`
- :data:`~exhale.configs.SUB_SECTION_HEADING_CHAR`
- :data:`~exhale.configs.SUB_SUB_SECTION_HEADING_CHAR`
- :data:`~exhale.configs.DEFAULT_DOXYGEN_STDIN_BASE`
2. Internal / private variables that are **not** supposed to changed except for by this
extension are declared as ``_lower_case_with_single_leading_underscore`` as is common
in Python ;).
3. Every other variable is declared as ``camelCase``, indicating that it can be
configured **indirectly** by using it as a key in the arguments to ``exhale_args``
present in your ``conf.py``. For example, one of the *required* arguments for this
extension is :data:`~exhale.configs.containmentFolder`. This means that the key
``"containmentFolder"`` is *expected* to be present in ``exhale_args``.
.. code-block:: py
exhale_args = {
"containmentFolder": "./api",
# ...
}
Read the documentation for the various configs present to see what the various
options are to modify the behavior of Exhale.
'''
from __future__ import unicode_literals
import os
import six
import textwrap
from sphinx.errors import ConfigError, ExtensionError
from sphinx.util import logging
from types import FunctionType, ModuleType
try:
# Python 2 StringIO
from cStringIO import StringIO
except ImportError:
# Python 3 StringIO
from io import StringIO
logger = logging.getLogger(__name__)
"""
The |SphinxLoggerAdapter| for communicating with the sphinx build process.
.. |SphinxLoggerAdapter| replace:: :class:`sphinx:sphinx.util.SphinxLoggerAdapter`
"""
########################################################################################
## #
## Required configurations, these get set indirectly via the dictionary argument #
## given to exhale in your conf.py. #
## #
########################################################################################
containmentFolder = None
'''
**Required**
The location where Exhale is going to generate all of the reStructuredText documents.
**Value in** ``exhale_args`` (str)
The value of key ``"containmentFolder"`` should be a string representing the
(relative or absolute) path to the location where Exhale will be creating all of the
files. **Relative paths are relative to the Sphinx application source directory**,
which is almost always wherever the file ``conf.py`` is.
.. note::
To better help you the user know what Exhale is generating (and therefore safe
to delete), it is a **hard requirement** that ``containmentFolder`` is a
**subdirectory** of the Sphinx Source Directory. AKA the path ``"."`` will be
rejected, but the path ``"./api"`` will be accepted.
The suggested value for ``"containmentFolder"`` is ``"./api"``, or
``"./source/api"`` if you have separate source and build directories with Sphinx.
When the html is eventually generated, this will make for a more human friendly
url being generated.
.. warning::
The verbiage subdirectory means **direct** subdirectory. So the path
``"./library/api"`` will be rejected. This is because I make the assumption that
``containmentFolder`` is "owned" by Exhale / is safe to delete.
'''
rootFileName = None
'''
**Required**
The name of the file that **you** will be linking to from your reStructuredText
documents. Do **not** include the ``containmentFolder`` path in this file name,
Exhale will create the file ``"{contaimentFolder}/{rootFileName}"`` for you.
**Value in** ``exhale_args`` (str)
The value of key ``"rootFileName"`` should be a string representing the name of
the file you will be including in your top-level ``toctree`` directive. In order
for Sphinx to be happy, you should include a ``.rst`` suffix. All of the generated
API uses reStructuredText, and that will not ever change.
For example, if you specify
- ``"containmentFolder" = "./api"``, and
- ``"rootFileName" = "library_root.rst"``
Then exhale will generate the file ``./api/library_root.rst``. You would then
include this file in a ``toctree`` directive (say in ``index.rst``) with:
.. raw:: html
<div class="highlight-rest">
<div class="highlight">
<pre>
.. toctree::
:maxdepth: 2
about
<b>api/library_root</b></pre>
</div>
</div>
'''
rootFileTitle = None
'''
**Required**
The title to be written at the top of ``rootFileName``, which will appear in your
file including it in the ``toctree`` directive.
**Value in** ``exhale_args`` (str)
The value of the key ``"rootFileTitle"`` should be a string that has the title of
the main library root document folder Exhale will be generating. The user is
required to supply this value because its value directly affects the overall
presentation of your documentation. For example, if you are including the Exhale
generated library root file in your ``index.rst`` top-level ``toctree`` directive,
the title you supply here will show up on both your main page, as well as in the
navigation menus.
An example value could be ``"Library API"``.
'''
doxygenStripFromPath = None
'''
**Required**
When building on Read the Docs, there seem to be issues regarding the Doxygen
variable ``STRIP_FROM_PATH`` when built remotely. That is, it isn't stripped at
all. This value enables Exhale to manually strip the path.
**Value in** ``exhale_args`` (str)
The value of the key ``"doxygenStripFromPath"`` should be a string representing the
(relative or absolute) path to be stripped from the final documentation. As with
:data:`~exhale.configs.containmentFolder`, relative paths are relative to the Sphinx
source directory (where ``conf.py`` is). Consider the following directory structure::
my_project/
├───docs/
│ conf.py
│
└───include/
└───my_project/
common.hpp
In this scenario, if you supplied ``"doxygenStripFromPath" = ".."``, then the file
page for ``common.hpp`` would list its declaration as
``include/my_project/common.hpp``. If you instead set it to be ``"../include"``,
then the file page for ``common.hpp`` would list its declaration as just
``my_project/common.hpp``.
As a consequence, modification of this variable directly affects what shows up in
the file view hierarchy. In the previous example, the difference would really just
be whether or not all files are nestled underneath a global ``include`` folder or
not.
.. warning::
It is **your** responsibility to ensure that the value you provide for this
configuration is valid. The file view hierarchy will almost certainly break if
you give nonsense.
.. note::
Depending on your project layout, some links may be broken in the above example
if you use ``"../include"`` that work when you use ``".."``. To get your docs
working, revert to ``".."``. If you're feeling nice, raise an issue on GitHub
and let me know --- I haven't been able to track this one down yet :/
Particularly, this seems to happen with projects that have duplicate filenames
in different folders, e.g.::
include/
└───my_project/
│ common.hpp
│
└───viewing/
common.hpp
'''
########################################################################################
## #
## Additional configurations available to further customize the output of exhale. #
## #
########################################################################################
# Build Process Logging, Colors, and Debugging #
########################################################################################
verboseBuild = False
'''
**Optional**
If you are having a hard time getting documentation to build, or say hierarchies are
not appearing as they should be, set this to ``True``.
**Value in** ``exhale_args`` (bool)
Set the boolean value to be ``True`` to include colorized printing at various stages
of the build process.
.. warning::
There is only one level of verbosity: excessively verbose. **All logging is
written to** ``sys.stderr``. See :data:`~exhale.configs.alwaysColorize`.
.. tip::
Looking at the actual code of Exhale trying to figure out what is going on? All
logging sections have a comment ``# << verboseBuild`` just before the logging
section. So you can ``grep -r '# << verboseBuild' exhale/`` if you're working
with the code locally.
'''
alwaysColorize = True
'''
**Optional**
Exhale prints various messages throughout the build process to both ``sys.stdout``
and ``sys.stderr``. The default behavior is to colorize output always, regardless
of if the output is being directed to a file. This is because you can simply use
``cat`` or ``less -R``. By setting this to ``False``, when redirecting output to
a file the color will not be included.
**Value in** ``exhale_args`` (bool)
The default is ``True`` because I find color to be something developers should
embrace. Simply use ``less -R`` to view colorized output conveniently. While I
have a love of all things color, I understand you may not. So just set this to
``False``.
.. note::
There is not and will never be a way to remove the colorized logging from the
console. This only controls when ``sys.stdout`` and ``sys.stderr`` are being
redirected to a file.
'''
generateBreatheFileDirectives = False
'''
**Optional**
Append the ``.. doxygenfile::`` directive from Breathe for *every* file page
generated in the API.
**Value in** ``exhale_args`` (bool)
If True, then the breathe directive (``doxygenfile``) will be incorporated at the
bottom of the file.
.. danger::
**This feature is not intended for production release of pages, only debugging.**
This feature is "deprecated" in lieu of minimal parsing of the input Doxygen xml
for a given documented file. This feature can be used to help determine if
Exhale has made a mistake in parsing the file level documentation, but usage of
this feature will create **many** duplicate id's and the Sphinx build process
will be littered with complaints.
**Usage of this feature will completely dismantle the links coordinated in all
parts of Exhale**. Because duplicate id's are generated, Sphinx chooses where
to link to. It seems to reliably choose the links generated by the Breathe File
directive, meaning the majority of the navigational setup of Exhale is pretty
much invalidated.
'''
########################################################################################
# Root API Document Customization and Treeview #
########################################################################################
afterTitleDescription = None
'''
**Optional**
Provide a description to appear just after :data:`~exhale.configs.rootFileTitle`.
**Value in** ``exhale_args`` (str)
If you want to provide a brief summary of say the layout of the API, or call
attention to specific classes, functions, etc, use this. For example, if you had
Python bindings but no explicit documentation for the Python side of the API, you
could use something like
.. code-block:: py
exhale_args = {
# ... other required arguments...
"rootFileTitle": "Library API",
"afterTitleDescription": textwrap.dedent(\'\'\'
.. note::
The following documentation presents the C++ API. The Python API
generally mirrors the C++ API, but some methods may not be available in
Python or may perform different actions.
\'\'\')
}
'''
afterHierarchyDescription = None
'''
**Optional**
Provide a description that appears after the Class and File hierarchies, but before
the full (and usually very long) API listing.
**Value in** ``exhale_args`` (str)
Similar to :data:`~exhale.configs.afterTitleDescription`, only it is included in the
middle of the document.
'''
fullApiSubSectionTitle = "Full API"
'''
**Optional**
The title for the subsection that comes after the Class and File hierarchies, just
before the enumeration of the full API.
**Value in** ``exhale_args`` (str)
The default value is simply ``"Full API"``. Change this to be something else if you
so desire.
'''
afterBodySummary = None
'''
**Optional**
Provide a summary to be included at the bottom of the root library file.
**Value in** ``exhale_args`` (str)
Similar to :data:`~exhale.configs.afterTitleDescription`, only it is included at the
bottom of the document.
.. note::
The root library document generated can be quite long, depending on your
framework. Important notes to developers should be included at the top of the
file using :data:`~exhale.configs.afterTitleDescription`, or after the hierarchies
using :data:`~exhale.configs.afterHierarchyDescription`.
'''
fullToctreeMaxDepth = 5
'''
**Optional**
The generated library root document performs ``.. include:: unabridged_api.rst`` at
the bottom, after the Class and File hierarchies. Inside ``unabridged_api.rst``,
every generated file is included using a ``toctree`` directive to prevent Sphinx
from getting upset about documents not being included. This value controls the
``:maxdepth:`` for all of these ``toctree`` directives.
**Value in** ``exhale_args`` (int)
The default value is ``5``, but you may want to give a smaller value depending on
the framework being documented.
.. warning::
This value must be greater than or equal to ``1``. You are advised not to use
a value greater than ``5``.
'''
listingExclude = []
'''
**Optional**
A list of regular expressions to exclude from both the class hierarchy and namespace
page enumerations. This can be useful when you want to keep the listings for the
hierarchy / namespace pages more concise, but **do** ultimately want the excluded
items documented somewhere.
Nodes whose ``name`` (fully qualified, e.g., ``namespace::ClassName``) matches any
regular expression supplied here will:
1. Exclude this item from the class view hierarchy listing.
2. Exclude this item from the defining namespace's listing (where applicable).
3. The "excluded" item will still have it's own documentation **and** be linked in
the "full API listing", as well as from the file page that defined the compound
(if recovered). Otherwise Sphinx will explode with warnings about documents not
being included in any ``toctree`` directives.
This configuration variable is **one size fits all**. It was created as a band-aid
fix for PIMPL frameworks.
.. todo::
More fine-grained control will be available in the pickleable writer API
sometime in Exhale 1.x.
.. note::
If you want to skip documentation of a compound in your framework *entirely*,
this configuration variable is **not** where you do it. See
:ref:`Doxygen PREDEFINED <doxygen_predefined>` for information on excluding
compounds entirely using the doxygen preprocessor.
**Value in** ``exhale_args`` (list)
The list can be of variable types, but each item will be compiled into an internal
list using :func:`python:re.compile`. The arguments for
``re.compile(pattern, flags=0)`` should be specified in order, but for convenience
if no ``flags`` are needed for your use case you can just specify a string. For
example:
.. code-block:: py
exhale_args = {
# These two patterns should be equitable for excluding PIMPL
# objects in a framework that uses the ``XxxImpl`` naming scheme.
"listingExclude": [r".*Impl$", (r".*impl$", re.IGNORECASE)]
}
Each item in ``listingExclude`` may either be a string (the regular expression
pattern), or it may be a length two iterable ``(string pattern, int flags)``.
'''
# Compiled regular expressions from listingExclude
# TODO: moves into config object
_compiled_listing_exclude = []
unabridgedOrphanKinds = {"dir", "file"}
"""
**Optional**
The list of node kinds to **exclude** from the unabridged API listing beneath the
class and file hierarchies.
**Value in** ``exhale_args`` (list or set of strings)
The list of kinds (see :data:`~exhale.utils.AVAILABLE_KINDS`) that will **not** be
included in the unabridged API listing. The default is to exclude directories and
files (which are already in the file hierarchy). Note that if this variable is
provided, it will overwrite the default ``{"dir", "file"}``, meaning if you want
to exclude something in addition you need to include ``"dir"`` and ``"file"``:
.. code-block:: py
# In conf.py
exhale_args = {
# Case 1: _only_ exclude union
"unabridgedOrphanKinds": {"union"}
# Case 2: exclude union in addition to dir / file.
"unabridgedOrphanKinds": {"dir", "file", "union"}
}
.. tip::
See :data:`~exhale.configs.fullToctreeMaxDepth`, users seeking to reduce the
length of the unabridged API should set this value to ``1``.
.. warning::
If **either** ``"class"`` **or** ``"struct"`` appear in
``unabridgedOrphanKinds`` then **both** will be excluded. The unabridged API
will present classes and structs together.
"""
########################################################################################
# Clickable Hierarchies <3 #
########################################################################################
createTreeView = False
'''
**Optional**
When set to ``True``, clickable hierarchies for the Class and File views will be
generated. **Set this variable to** ``True`` **if you are generating html** output
for much more attractive websites!
**Value in** ``exhale_args`` (bool)
When set to ``False``, the Class and File hierarches are just reStructuredText
bullet lists. This is rather unattractive, but the default of ``False`` is to
hopefully enable non-html writers to still be able to use ``exhale``.
.. tip::
Using ``html_theme = "bootstrap"`` (the `Sphinx Bootstrap Theme`__)? Make sure
you set :data:`~exhale.configs.treeViewIsBootstrap` to ``True``!
__ https://ryan-roemer.github.io/sphinx-bootstrap-theme/
'''
minifyTreeView = True
'''
**Optional**
When set to ``True``, the generated html and/or json for the class and file
hierarchy trees will be minified.
**Value in** ``exhale_args`` (bool)
The default value is ``True``, which should help page load times for larger APIs.
Setting to ``False`` should only really be necessary if there is a problem -- the
minified version will be hard to parse as a human.
'''
treeViewIsBootstrap = False
'''
**Optional**
If the generated html website is using ``bootstrap``, make sure to set this to
``True``. The `Bootstrap Treeview`__ library will be used.
__ http://jonmiles.github.io/bootstrap-treeview/
**Value in** ``exhale_args`` (bool)
When set to ``True``, the clickable hierarchies will be generated using a Bootstrap
friendly library.
'''
treeViewBootstrapTextSpanClass = "text-muted"
'''
**Optional**
What **span** class to use for the *qualifying* text after the icon, but before the
hyperlink to the actual documentation page. For example, ``Struct Foo`` in the
hierarchy would have ``Struct`` as the *qualifying* text (controlled by this
variable), and ``Foo`` will be a hyperlink to ``Foo``'s actual documentation.
**Value in** ``exhale_args`` (str)
A valid class to apply to a ``span``. The actual HTML being generated is something
like:
.. code-block:: html
<span class="{span_cls}">{qualifier}</span> {hyperlink text}
So if the value of this input was ``"text-muted"``, and it was the hierarchy element
for ``Struct Foo``, it would be
.. code-block:: html
<span class="text-muted">Struct</span> Foo
The ``Foo`` portion will receive the hyperlink styling elsewhere.
.. tip::
Easy choices to consider are the `contextual classes`__ provided by your
bootstrap theme. Alternatively, add your own custom stylesheet to Sphinx
directly and create a class with the color you want there.
__ https://getbootstrap.com/docs/3.3/css/#helper-classes-colors
.. danger::
No validity checks are performed. If you supply a class that cannot be used,
there is no telling what will happen.
'''
treeViewBootstrapIconMimicColor = "text-muted"
'''
**Optional**
The **paragraph** CSS class to *mimic* for the icon color in the tree view.
**Value in** ``exhale_args`` (str)
This value must be a valid CSS class for a **paragraph**. The way that it is used
is in JavaScript, on page-load, a "fake paragraph" is inserted with the class
specified by this variable. The color is extracted, and then a force-override is
applied to the page's stylesheet. This was necessary to override some aspects of
what the ``bootstrap-treeview`` library does. It's full usage looks like this:
.. code-block:: js
/* Inspired by very informative answer to get color of links:
https://stackoverflow.com/a/2707837/3814202 */
/* vvvvvvvvvv what you give */
var $fake_p = $('<p class="icon_mimic"></p>').hide().appendTo("body");
/* ^^^^^^^^^^ */
var iconColor = $fake_p.css("color");
$fake_p.remove();
/* later on */
// Part 2: override the style of the glyphicons by injecting some CSS
$('<style type="text/css" id="exhaleTreeviewOverride">' +
' .treeview span[class~=icon] { ' +
' color: ' + iconColor + ' ! important;' +
' }' +
'</style>').appendTo('head');
.. tip::
Easy choices to consider are the `contextual classes`__ provided by your
bootstrap theme. Alternatively, add your own custom stylesheet to Sphinx
directly and create a class with the color you want there.
__ https://getbootstrap.com/docs/3.3/css/#helper-classes-colors
.. danger::
No validity checks are performed. If you supply a class that cannot be used,
there is no telling what will happen.
'''
treeViewBootstrapOnhoverColor = "#F5F5F5"
'''
**Optional**
The hover color for elements in the hierarchy trees. Default color is a light-grey,
as specified by default value of ``bootstrap-treeview``'s `onhoverColor`_.
*Value in** ``exhale_args`` (str)
Any valid color. See `onhoverColor`_ for information.
.. _onhoverColor: https://github.com/jonmiles/bootstrap-treeview#onhovercolor
'''
treeViewBootstrapUseBadgeTags = True
'''
**Optional**
When set to ``True`` (default), a Badge indicating the number of nested children
will be included **when 1 or more children are present**.
When enabled, each node in the json data generated has it's `tags`_ set, and the
global `showTags`_ option is set to ``true``.
.. _tags: https://github.com/jonmiles/bootstrap-treeview#tags
.. _showTags: https://github.com/jonmiles/bootstrap-treeview#showtags
**Value in** ``exhale_args`` (bool)
Set to ``False`` to exclude the badges. Search for ``Tags as Badges`` on the
`example bootstrap treeview page`__, noting that if a given node does not have any
children, no badge will be added. This is simply because a ``0`` badge is likely
more confusing than helpful.
__ http://jonmiles.github.io/bootstrap-treeview/
'''
treeViewBootstrapExpandIcon = "glyphicon glyphicon-plus"
'''
**Optional**
Global setting for what the "expand" icon is for the bootstrap treeview. The
default value here is the default of the ``bootstrap-treeview`` library.
**Value in** ``exhale_args`` (str)
See the `expandIcon`_ description of ``bootstrap-treeview`` for more information.
.. _expandIcon: https://github.com/jonmiles/bootstrap-treeview#expandicon
.. note::
Exhale handles wrapping this in quotes, you just need to specify the class
(making sure that it has spaces where it should). Exhale does **not** perform
any validity checks on the value of this variable. For example, you could use
something like:
.. code-block:: py
exhale_args = {
# ... required / other optional args ...
# you can set one, both, or neither. just showing both in same example
# set the icon to show it can be expanded
"treeViewBootstrapExpandIcon": "glyphicon glyphicon-chevron-right",
# set the icon to show it can be collapsed
"treeViewBootstrapCollapseIcon": "glyphicon glyphicon-chevron-down"
}
'''
treeViewBootstrapCollapseIcon = "glyphicon glyphicon-minus"
'''
**Optional**
Global setting for what the "collapse" icon is for the bootstrap treeview. The
default value here is the default of the ``bootstrap-treeview`` library.
**Value in** ``exhale_args`` (str)
See the `collapseIcon`_ description of ``bootstrap-treeview`` for more information.
See :data:`~exhale.configs.treeViewBootstrapExpandIcon` for how to specify this
CSS class value.
.. _collapseIcon: https://github.com/jonmiles/bootstrap-treeview#collapseicon
'''
treeViewBootstrapLevels = 1
'''
**Optional**
The default number of levels to expand on page load. Note that the
``bootstrap-treeview`` default `levels`_ value is ``2``. ``1`` seems like a safer
default for Exhale since the value you choose here largely depends on how you have
structured your code.
.. _levels: https://github.com/jonmiles/bootstrap-treeview#levels
**Value in** ``exhale_args`` (int)
An integer representing the number of levels to expand for **both** the Class and
File hierarchies. **This value should be greater than or equal to** ``1``, but
**no validity checks are performed** on your input. Buyer beware.
'''
_class_hierarchy_id = "class-treeView"
'''
The ``id`` attribute of the HTML element associated with the **Class** Hierarchy when
:data:`~exhale.configs.createTreeView` is ``True``.
1. When :data:`~exhale.configs.treeViewIsBootstrap` is ``False``, this ``id`` is attached
to the outer-most ``ul``.
2. For bootstrap, an empty ``div`` is inserted with this ``id``, which will be the
anchor point for the ``bootstrap-treeview`` library.
'''
_file_hierarchy_id = "file-treeView"
'''
The ``id`` attribute of the HTML element associated with the **Class** Hierarchy when
:data:`~exhale.configs.createTreeView` is ``True``.
1. When :data:`~exhale.configs.treeViewIsBootstrap` is ``False``, this ``id`` is attached
to the outer-most ``ul``.
2. For bootstrap, an empty ``div`` is inserted with this ``id``, which will be the
anchor point for the ``bootstrap-treeview`` library.
'''
_bstrap_class_hierarchy_fn_data_name = "getClassHierarchyTree"
'''
The name of the JavaScript function that returns the ``json`` data associated with the
**Class** Hierarchy when :data:`~exhale.configs.createTreeView` is ``True`` **and**
:data:`~exhale.configs.treeViewIsBootstrap` is ``True``.
'''
_bstrap_file_hierarchy_fn_data_name = "getFileHierarchyTree"
'''
The name of the JavaScript function that returns the ``json`` data associated with the
**File** Hierarchy when :data:`~exhale.configs.createTreeView` is ``True`` **and**
:data:`~exhale.configs.treeViewIsBootstrap` is ``True``.
'''
########################################################################################
# Page Level Customization #
########################################################################################
includeTemplateParamOrderList = False
'''
**Optional**
For Classes and Structs (only), Exhale can provide a numbered list enumeration
displaying the template parameters in the order they should be specified.
**Value in** ``exhale_args`` (bool)
This feature can be useful when you have template classes that have **many**
template parameters. The Breathe directives **will** include the parameters in the
order they should be given. However, if you have a template class with more than
say 5 parameters, it can become a little hard to read.
.. note::
This configuration is all or nothing, and applies to every template Class /
Struct. Additionally, **no** ``tparam`` documentation is displayed with this
listing. Just the types / names they are declared as (and default values if
provided).
This feature really only exists as a historical accident.
.. warning::
As a consequence of the (hacky) implementation, if you use this feature you commit
to HTML output only. Where applicable, template parameters that generate links to
other items being documented **only** work in HTML.
'''
pageLevelConfigMeta = None
'''
**Optional**
reStructuredText allows you to employ page-level configurations. These are included
at the top of the page, before the title.
**Value in** ``exhale_args`` (str)
An example of one such feature would be ``":tocdepth: 5"``. To be honest, I'm not
sure why you would need this feature. But it's easy to implement, you just need to
make sure that you provide valid reStructuredText or *every* page will produce
errors.
See the `Field Lists`__ guide for more information.
__ http://www.sphinx-doc.org/en/latest/usage/restructuredtext/field-lists.html
'''
repoRedirectURL = None
'''
.. todo::
**This feature is NOT implemented yet**! Hopefully soon. It definitely gets under
my skin. It's mostly documented just to show up in the ``todolist`` for me ;)
**Optional**
When using the Sphinx RTD theme, there is a button placed in the top-right saying
something like "Edit this on GitHub". Since the documents are all being generated
dynamically (and not supposed to be tracked by ``git``), the links all go nowhere.
Set this so Exhale can try and fix this.
**Value in** ``exhale_args`` (str)
The url of the repository your documentation is being generated from.
.. warning::
Seriously this isn't implemented. I may not even need this from you. The harder
part is figuring out how to map a given nodes "``def_in_file``" to the correct
URL. I should be able to get the URL from ``git remote`` and construct the
URL from that and ``git branch``. Probably just some path hacking with
``git rev-parse --show-toplevel`` and comparing that to
:data:`~exhale.configs.doxygenStripFromPath`?
Please feel free to `add your input here`__.
__ https://github.com/svenevs/exhale/issues/2
'''
# Using Contents Directives ############################################################
contentsDirectives = True
'''
**Optional**
Include a ``.. contents::`` directive beneath the title on pages that have potential
to link to a decent number of documents.
**Value in** ``exhale_args`` (bool)
By default, Exhale will include a ``.. contents::`` directive on the individual
generated pages for the types specified by
:data:`~exhale.configs.kindsWithContentsDirectives`. Set this to ``False`` to
disable globally.
See the :ref:`using_contents_directives` section for all pieces of the puzzle.
'''
contentsTitle = "Contents"
'''
**Optional**
The title of the ``.. contents::`` directive for an individual file page, when it's
``kind`` is in the list specified by
:data:`~exhale.configs.kindsWithContentsDirectives` **and**
:data:`~exhale.configs.contentsDirectives` is ``True``.
**Value in** ``exhale_args`` (str)
The default (for both Exhale and reStructuredText) is to label this as ``Contents``.
You can choose whatever value you like. If you prefer to have **no title** for the
``.. contents::`` directives, **specify the empty string**.
.. note::
Specifying the empty string only removes the title **when** ``":local:"`` **is
present in** :data:`~exhale.configs.contentsSpecifiers`. See the
:ref:`using_contents_directives` section for more information.
'''
contentsSpecifiers = [":local:", ":backlinks: none"]
'''
**Optional**
The specifications to apply to ``.. contents::`` directives for the individual file
pages when it's ``kind`` is in the list specified by
:data:`~exhale.configs.kindsWithContentsDirectives` **and**
:data:`~exhale.configs.contentsDirectives` is ``True``.
**Value in** ``exhale_args`` (list)
A (one-dimensional) list of strings that will be applied to any ``.. contents::``
directives generated. Provide the **empty list** if you wish to have no specifiers
added to these directives. See the :ref:`using_contents_directives` section for
more information.
'''
kindsWithContentsDirectives = ["file", "namespace"]
'''
**Optional**
The kinds of compounds that will include a ``.. contents::`` directive on their
individual library page. The default is to generate one for Files and Namespaces.
Only takes meaning when :data:`~exhale.configs.contentsDirectives` is ``True``.
**Value in** ``exhale_args`` (list)
Provide a (one-dimensional) ``list`` or ``tuple`` of strings of the kinds of
compounds that should include a ``.. contents::`` directive. Each kind given
must one of the entries in :data:`~exhale.utils.AVAILABLE_KINDS`.
For example, if you wanted to enable Structs and Classes as well you would do
something like:
.. code-block:: py
# in conf.py
exhale_args = {
# ... required / optional args ...
"kindsWithContentsDirectives": ["file", "namespace", "class", "struct"]
}
.. note::
This is a "full override". So if you want to still keep the defaults of
``"file"`` and ``"namespace"``, **you** must include them yourself.
'''
########################################################################################
# Breathe Customization #
########################################################################################
customSpecificationsMapping = None
'''
**Optional**
See the :ref:`usage_customizing_breathe_output` section for how to use this.
**Value in** ``exhale_args`` (dict)
The dictionary produced by calling
:func:`~exhale.utils.makeCustomSpecificationsMapping` with your custom function.
'''
_closure_map_sanity_check = "blargh_BLARGH_blargh"
'''
See :func:`~exhale.utils.makeCustomSpecificationsMapping` implementation, this is
inserted to help enforce that Exhale made the dictionary going into
:data:`~exhale.configs.customSpecificationsMapping`.
'''
########################################################################################
# Doxygen Execution and Customization #
########################################################################################
_doxygen_xml_output_directory = None
'''
The absolute path the the root level of the doxygen xml output. If the path to the
``index.xml`` file created by doxygen was ``./doxyoutput/xml/index.xml``, then this
would simply be ``./doxyoutput/xml``.
.. note::
This is the exact same path as ``breathe_projects[breathe_default_project]``, only it
is an absolute path.
'''
exhaleExecutesDoxygen = False
'''
**Optional**
Have Exhale launch Doxygen when you execute ``make html``.
**Value in** ``exhale_args`` (bool)
Set to ``True`` to enable launching Doxygen. You must set either
:data:`~exhale.configs.exhaleUseDoxyfile` or :data:`~exhale.configs.exhaleDoxygenStdin`.
'''
exhaleUseDoxyfile = False
'''
**Optional**
If :data:`~exhale.configs.exhaleExecutesDoxygen` is ``True``, this tells Exhale to
use your own ``Doxyfile``. The encouraged approach is to use
:data:`~exhale.configs.exhaleDoxygenStdin`.
**Value in** ``exhale_args`` (bool)
Set to ``True`` to have Exhale use your ``Doxyfile``.
.. note::
The ``Doxyfile`` must be in the **same** directory as ``conf.py``. Exhale will
change directories to here before launching Doxygen when you have separate source
and build directories for Sphinx configured.
.. warning::
No sanity checks on the ``Doxyfile`` are performed. If you are using this option
you need to verify two parameters in particular:
1. ``OUTPUT_DIRECTORY`` is configured so that
``breathe_projects[breathe_default_project]`` agrees. See the
:ref:`Mapping of Project Names to Doxygen XML Output Paths <breathe_project>`
section.
2. ``STRIP_FROM_PATH`` is configured to be identical to what is specified with
:data:`~exhale.configs.doxygenStripFromPath`.
I have no idea what happens when these conflict, but it likely will never result
in valid documentation.
'''
exhaleDoxygenStdin = None
'''
**Optional**
If :data:`~exhale.configs.exhaleExecutesDoxygen` is ``True``, this tells Exhale to
use the (multiline string) value specified in this argument *in addition to* the
:data:`~exhale.configs.DEFAULT_DOXYGEN_STDIN_BASE`.
**Value in** ``exhale_args`` (str)
This string describes your project's specific Doxygen configurations. At the very
least, it must provide ``INPUT``. See the :ref:`usage_exhale_executes_doxygen`
section for how to use this in conjunction with the default configurations, as well
as how to override them.
'''
DEFAULT_DOXYGEN_STDIN_BASE = textwrap.dedent(r'''
# If you need this to be YES, exhale will probably break.
CREATE_SUBDIRS = NO
# So that only Doxygen does not trim paths, which affects the File hierarchy
FULL_PATH_NAMES = YES
# 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"
# Enable preprocessing and related preprocessor necessities
ENABLE_PREPROCESSING = YES
MACRO_EXPANSION = YES
EXPAND_ONLY_PREDEF = NO
SKIP_FUNCTION_MACROS = NO
# extra defs for to help with building the _right_ version of the docs
PREDEFINED = DOXYGEN_DOCUMENTATION_BUILD
PREDEFINED += DOXYGEN_SHOULD_SKIP_THIS
''')
'''
These are the default values sent to Doxygen along stdin when
:data:`~exhale.configs.exhaleExecutesDoxygen` is ``True``. This is sent to Doxygen
immediately **before** the :data:`~exhale.configs.exhaleDoxygenStdin` provided to
``exhale_args`` in your ``conf.py``. In this way, you can override any of the specific
defaults shown here.
.. tip::
See the documentation for :data:`~exhale.configs.exhaleDoxygenStdin`, as well as
:data:`~exhale.configs.exhaleUseDoxyfile`. Only **one** may be provided to the
``exhale_args`` in your ``conf.py``.
.. include:: ../DEFAULT_DOXYGEN_STDIN_BASE_value.rst
'''
exhaleSilentDoxygen = False
'''
**Optional**
When set to ``True``, the Doxygen output is omitted from the build.
**Value in** ``exhale_args`` (bool)
Documentation generation can be quite verbose, especially when running both Sphinx
and Doxygen in the same process. Use this to silence Doxygen.
.. danger::
You are **heavily** discouraged from setting this to ``True``. Many problems
that may arise through either Exhale or Breathe are because the Doxygen
documentation itself has errors. It will be much more difficult to find these
when you squelch the Doxygen output.
The reason you would do this is for actual limitations on your specific
``stdout`` (e.g. you are getting a buffer maxed out). The likelihood of this
being a problem for you is exceptionally small.
'''
########################################################################################
# Programlisting Customization #
########################################################################################
lexerMapping = {}
'''
**Optional**
When specified, and ``XML_PROGRAMLISTING`` is set to ``YES`` in Doxygen (either via
your ``Doxyfile`` or :data:`exhaleDoxygenStdin <exhale.configs.exhaleDoxygenStdin>`),
this mapping can be used to customize / correct the Pygments lexer used for the
program listing page generated for files. Most projects will **not** need to use
this setting.
**Value in** ``exhale_args`` (dict)
The keys and values are both strings. Each key is a regular expression that will be
used to check with :func:`python:re.match`, noting that the primary difference
between :func:`python:re.match` and :func:`python:re.search` that you should be
aware of is that ``match`` searches from the **beginning** of the string. Each
value should be a **valid** `Pygments lexer <http://pygments.org/docs/lexers/>`_.
Example usage:
.. code-block:: py
exhale_args {
# ...
"lexerMapping": {
r".*\.cuh": "cuda",
r"path/to/exact_filename\.ext": "c"
}
}
.. note::
The pattern is used to search the full path of a file, **as represented in
Doxygen**. This is so that duplicate file names in separate folders can be
distinguished if needed. The file path as represented in Doxygen is defined
by the path to the file, with some prefix stripped out. The prefix stripped out
depends entirely on what you provided to
:data:`doxygenStripFromPath <exhale.configs.doxygenStripFromPath>`.
.. tip::
This mapping is used in
:func:`utils.doxygenLanguageToPygmentsLexer <exhale.utils.doxygenLanguageToPygmentsLexer>`,
when provided it is queried first. If you are trying to get program listings for
a file that is otherwise not supported directly by Doxygen, you typically want to
tell Doxygen to interpret the file as a different language. Take the CUDA case.
In my input to :data:`exhaleDoxygenStdin <exhale.configs.exhaleDoxygenStdin>`, I
will want to set both ``FILE_PATTERNS`` and append to ``EXTENSION_MAPPING``:
.. code-block:: make
FILE_PATTERNS = *.hpp *.cuh
EXTENSION_MAPPING += cuh=c++
By setting ``FILE_PATTERNS``, Doxygen will now try and process ``*.cuh`` files.
By *appending* to ``EXTENSION_MAPPING``, it will treat ``*.cuh`` as C++ files.
For CUDA, this is a reasonable choice because Doxygen is generally able to parse
the file as C++ and get everything right in terms of member definitions,
docstrings, etc. **However**, now the XML generated by doxygen looks like this:
.. code-block:: xml
<!-- >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> vvv -->
<compounddef id="bilateral__filter_8cuh" kind="file" language="C++">
So Exhale would be default put the program listing in a ``.. code-block:: cpp``.
By setting this variable in ``exhale_args``, you can bypass this and get the
desired lexer of your choice.
Some important notes for those not particularly comfortable or familiar with regular
expressions in python:
1. Note that each key defines a *raw* string (prefix with ``r``): ``r"pattern"``.
This is not entirely necessary for this case, but using raw strings makes it so
that you do not have to escape as many things. It's a good practice to adopt,
but for these purposes should not matter all that much.
2. Note the escaped ``.`` character. This means find the literal ``.``, rather than
the regular expression wildcard for *any character*. Observe the difference
with and without:
.. code-block:: pycon
>>> import re
>>> if re.match(r".*.cuh", "some_filecuh.hpp"): print("Oops!")
...
Oops!
>>> if re.match(r".*\.cuh", "some_filecuh.hpp"): print("Oops!")
...
>>>
Without ``\.``, the ``.cuh`` matches ``ecuh`` since ``.`` is a wildcard for *any*
character. You may also want to use ``$`` at the end of the expression if there
are multiple file extensions involved: ``r".*\.cuh$"``. The ``$`` states
"end-of-pattern", which in the usage of Exhale means end of line (the compiled
regular expressions are not compiled with :data:`python:re.MULTILINE`).
3. Take special care at the beginning of your regular expression. The pattern
``r"*\.cuh"`` does **not** compile! You need to use ``r".*\.cuh"``, with the
leading ``.`` being required.
'''
_compiled_lexer_mapping = {}
'''
Internal mapping of compiled regular expression objects to Pygments lexer strings. This
dictionary is created by compiling every key in
:data:`lexerMapping <exhale.configs.lexerMapping>`. See implementation of
:func:`utils.doxygenLanguageToPygmentsLexer <exhale.utils.doxygenLanguageToPygmentsLexer>`
for usage.
'''
########################################################################################
## #
## Utility variables. #
## #
########################################################################################
SECTION_HEADING_CHAR = "="
''' The restructured text H1 heading character used to underline sections. '''
SUB_SECTION_HEADING_CHAR = "-"
''' The restructured text H2 heading character used to underline subsections. '''
SUB_SUB_SECTION_HEADING_CHAR = "*"
''' The restructured text H3 heading character used to underline sub-subsections. '''
MAXIMUM_FILENAME_LENGTH = 255
'''
When a potential filename is longer than ``255``, a sha1 sum is used to shorten. Note
that there is no ubiquitous and reliable way to query this information, as it depends
on both the operating system, filesystem, **and** even the location (directory path) the
file would be generated to (depending on the filesystem). As such, a conservative value
of ``255`` should guarantee that the desired filename can always be created.
'''
MAXIMUM_WINDOWS_PATH_LENGTH = 260
r'''
The file path length on Windows cannot be greater than or equal to ``260`` characters.
Since Windows' pathetically antiquated filesystem cannot handle this, they have enabled
a "magic" prefix they call an *extended-length path*. This is achieved by inserting
the prefix ``\\?\`` which allows you to go up to a maximum path of ``32,767`` characters
**but you may only do this for absolute paths**. See `Maximum Path Length Limitation`__
for more information.
Dear Windows, did you know it is the 21st century?
__ https://docs.microsoft.com/en-us/windows/desktop/fileio/naming-a-file#maximum-path-length-limitation
'''
_the_app = None
''' The Sphinx ``app`` object. Currently unused, saved for availability in future. '''
_app_src_dir = None
'''
**Do not modify**. The location of ``app.srcdir`` of the Sphinx application, once the
build process has begun to execute. Saved to be able to run a few different sanity
checks in different places.
'''
_on_rtd = os.environ.get('READTHEDOCS', None) == 'True'
'''
**Do not modify**. Signals whether or not the build is taking place on ReadTheDocs. If
it is, then colorization of output is disabled, as well as the Doxygen output (where
applicable) is directed to ``/dev/null`` as capturing it can cause the ``subprocess``
buffers to overflow.
'''
########################################################################################
## #
## Secondary Sphinx Entry Point #
## Called from exhale/__init__.py:environment_ready during the sphinx build process. #
## #
########################################################################################
[docs]def apply_sphinx_configurations(app):
'''
This method applies the various configurations users place in their ``conf.py``, in
the dictionary ``exhale_args``. The error checking seems to be robust, and
borderline obsessive, but there may very well be some glaring flaws.
When the user requests for the ``treeView`` to be created, this method is also
responsible for adding the various CSS / JavaScript to the Sphinx Application
to support the hierarchical views.
.. danger::
This method is **not** supposed to be called directly. See
``exhale/__init__.py`` for how this function is called indirectly via the Sphinx
API.
**Parameters**
``app`` (:class:`sphinx.application.Sphinx`)
The Sphinx Application running the documentation build.
'''
# Import local to function to prevent circular imports elsewhere in the framework.
from . import deploy
from . import utils
####################################################################################
# Make sure they have the `breathe` configs setup in a way that we can use them. #
####################################################################################
# Breathe allows users to have multiple projects to configure in one `conf.py`
# A dictionary of keys := project names, values := path to Doxygen xml output dir
breathe_projects = app.config.breathe_projects
if not breathe_projects:
raise ConfigError("You must set the `breathe_projects` in `conf.py`.")
elif type(breathe_projects) is not dict:
raise ConfigError("The type of `breathe_projects` in `conf.py` must be a dictionary.")
# The breathe_default_project is required by `exhale` to determine where to look for
# the doxygen xml.
#
# TODO: figure out how to allow multiple breathe projects?
breathe_default_project = app.config.breathe_default_project
if not breathe_default_project:
raise ConfigError("You must set the `breathe_default_project` in `conf.py`.")
elif not isinstance(breathe_default_project, six.string_types):
raise ConfigError("The type of `breathe_default_project` must be a string.")
if breathe_default_project not in breathe_projects:
raise ConfigError(
"The given breathe_default_project='{0}' was not a valid key in `breathe_projects`:\n{1}".format(
breathe_default_project, breathe_projects
)
)
# Grab where the Doxygen xml output is supposed to go, make sure it is a string,
# defer validation of existence until after potentially running Doxygen based on
# the configs given to exhale
doxy_xml_dir = breathe_projects[breathe_default_project]
if not isinstance(doxy_xml_dir, six.string_types):
raise ConfigError(
"The type of `breathe_projects[breathe_default_project]` from `conf.py` was not a string."
)
# Make doxy_xml_dir relative to confdir (where conf.py is)
if not os.path.isabs(doxy_xml_dir):
doxy_xml_dir = os.path.abspath(os.path.join(app.confdir, doxy_xml_dir))
####################################################################################
# Initial sanity-check that we have the arguments needed. #
####################################################################################
exhale_args = app.config.exhale_args
if not exhale_args:
raise ConfigError("You must set the `exhale_args` dictionary in `conf.py`.")
elif type(exhale_args) is not dict:
raise ConfigError("The type of `exhale_args` in `conf.py` must be a dictionary.")
####################################################################################
# In order to be able to loop through things below, we want to grab the globals #
# dictionary (rather than needing to do `global containmentFolder` etc for every #
# setting that is being changed). #
####################################################################################
configs_globals = globals()
# Used for internal verification of available keys
keys_available = []
# At the end of input processing, fail out if unrecognized keys were found.
keys_processed = []
####################################################################################
# Gather the mandatory input for exhale. #
####################################################################################
key_error = "Did not find required key `{key}` in `exhale_args`."
val_error = "The type of the value for key `{key}` must be `{exp}`, but was `{got}`."
req_kv = [
("containmentFolder", six.string_types, True),
("rootFileName", six.string_types, False),
("rootFileTitle", six.string_types, False),
("doxygenStripFromPath", six.string_types, True)
]
for key, expected_type, make_absolute in req_kv:
# Used in error checking later
keys_available.append(key)
# Make sure we have the key
if key not in exhale_args:
raise ConfigError(key_error.format(key=key))
# Make sure the value is at the very least the correct type
val = exhale_args[key]
if not isinstance(val, expected_type):
val_t = type(val)
raise ConfigError(val_error.format(key=key, exp=expected_type, got=val_t))
# Make sure that a value was provided (e.g. no empty strings)
if not val:
raise ConfigError("Non-empty value for key [{0}] required.".format(key))
# If the string represents a path, make it absolute
if make_absolute:
# Directories are made absolute relative to app.confdir (where conf.py is)
if not os.path.isabs(val):
val = os.path.abspath(os.path.join(os.path.abspath(app.confdir), val))
# Set the config for use later
try:
configs_globals[key] = val
keys_processed.append(key)
except Exception as e:
raise ExtensionError(
"Critical error: unable to set `global {0}` to `{1}` in exhale.configs:\n{2}".format(
key, val, e
)
)
####################################################################################
# Validate what can be checked from the required arguments at this time. #
####################################################################################
global _the_app
_the_app = app
# Make sure they know this is a bad idea. The order of these checks is important.
# This assumes the path given was not the empty string (3 will break if it is).
#
# 1. If containmentFolder and app.srcdir are the same, problem.
# 2. If app.srcdir is not at the beginning of containmentFolder, problem.
# 3. If the first two checks have not raised a problem, the final check is to make
# sure that a subdirectory was actually used, as opposed to something that just
# starts with the same path.
#
# Note for the third check lazy evaluation is the only thing that makes checking
# _parts[1] acceptable ;)
_one = containmentFolder == app.srcdir
_two = not containmentFolder.startswith(app.srcdir)
_parts = containmentFolder.split(app.srcdir)
_three = _parts[0] != "" or len(_parts[1].split(os.path.sep)) > 2 or \
os.path.join(app.srcdir, _parts[1].replace(os.path.sep, "", 1)) != containmentFolder # noqa
# If they are equal, containmentFolder points somewhere entirely differently, or the
# relative path (made absolute again) does not have the srcdir
if _one or _two or _three:
raise ConfigError(
"The given `containmentFolder` [{0}] must be a *SUBDIRECTORY* of [{1}].".format(
containmentFolder, app.srcdir
)
)
global _app_src_dir
_app_src_dir = os.path.abspath(app.srcdir)
# We *ONLY* generate reStructuredText, make sure Sphinx is expecting this as well as
# the to-be-generated library root file is correctly suffixed.
if not rootFileName.endswith(".rst"):
raise ConfigError(
"The given `rootFileName` ({0}) did not end with '.rst'; Exhale is reStructuredText only.".format(
rootFileName
)
)
if ".rst" not in app.config.source_suffix:
raise ConfigError(
"Exhale is reStructuredText only, but '.rst' was not found in `source_suffix` list of `conf.py`."
)
# Make sure the doxygen strip path is an exclude-able path
if not os.path.exists(doxygenStripFromPath):
raise ConfigError(
"The path given as `doxygenStripFromPath` ({0}) does not exist!".format(doxygenStripFromPath)
)
####################################################################################
# Gather the optional input for exhale. #
####################################################################################
# TODO: `list` -> `(list, tuple)`, update docs too.
opt_kv = [
# Build Process Logging, Colors, and Debugging
("verboseBuild", bool),
("alwaysColorize", bool),
("generateBreatheFileDirectives", bool),
# Root API Document Customization and Treeview
("afterTitleDescription", six.string_types),
("afterHierarchyDescription", six.string_types),
("fullApiSubSectionTitle", six.string_types),
("afterBodySummary", six.string_types),
("fullToctreeMaxDepth", int),
("listingExclude", list),
("unabridgedOrphanKinds", (list, set)),
# Clickable Hierarchies <3
("createTreeView", bool),
("minifyTreeView", bool),
("treeViewIsBootstrap", bool),
("treeViewBootstrapTextSpanClass", six.string_types),
("treeViewBootstrapIconMimicColor", six.string_types),
("treeViewBootstrapOnhoverColor", six.string_types),
("treeViewBootstrapUseBadgeTags", bool),
("treeViewBootstrapExpandIcon", six.string_types),
("treeViewBootstrapCollapseIcon", six.string_types),
("treeViewBootstrapLevels", int),
# Page Level Customization
("includeTemplateParamOrderList", bool),
("pageLevelConfigMeta", six.string_types),
("repoRedirectURL", six.string_types),
("contentsDirectives", bool),
("contentsTitle", six.string_types),
("contentsSpecifiers", list),
("kindsWithContentsDirectives", list),
# Breathe Customization
("customSpecificationsMapping", dict),
# Doxygen Execution and Customization
("exhaleExecutesDoxygen", bool),
("exhaleUseDoxyfile", bool),
("exhaleDoxygenStdin", six.string_types),
("exhaleSilentDoxygen", bool),
# Programlisting Customization
("lexerMapping", dict)
]
for key, expected_type in opt_kv:
# Used in error checking later
keys_available.append(key)
# Override the default settings if the key was provided
if key in exhale_args:
# Make sure the value is at the very least the correct type
val = exhale_args[key]
if not isinstance(val, expected_type):
val_t = type(val)
raise ConfigError(val_error.format(key=key, exp=expected_type, got=val_t))
# Set the config for use later
try:
configs_globals[key] = val
keys_processed.append(key)
except Exception as e:
raise ExtensionError(
"Critical error: unable to set `global {0}` to `{1}` in exhale.configs:\n{2}".format(
key, val, e
)
)
# These two need to be lists of strings, check to make sure
def _list_of_strings(lst, title):
for spec in lst:
if not isinstance(spec, six.string_types):
raise ConfigError(
"`{title}` must be a list of strings. `{spec}` was of type `{spec_t}`".format(
title=title,
spec=spec,
spec_t=type(spec)
)
)
_list_of_strings( contentsSpecifiers, "contentsSpecifiers")
_list_of_strings(kindsWithContentsDirectives, "kindsWithContentsDirectives")
_list_of_strings( unabridgedOrphanKinds, "unabridgedOrphanKinds")
# Make sure the kinds they specified are valid
unknown = "Unknown kind `{kind}` given in `{config}`. See utils.AVAILABLE_KINDS."
for kind in kindsWithContentsDirectives:
if kind not in utils.AVAILABLE_KINDS:
raise ConfigError(
unknown.format(kind=kind, config="kindsWithContentsDirectives")
)
for kind in unabridgedOrphanKinds:
if kind not in utils.AVAILABLE_KINDS:
raise ConfigError(
unknown.format(kind=kind, config="unabridgedOrphanKinds")
)
# Make sure the listingExlcude is usable
if "listingExclude" in exhale_args:
import re
# TODO: remove this once config objects are in. Reset needed for testing suite.
configs_globals["_compiled_listing_exclude"] = []
# used for error printing, tries to create string out of item otherwise
# returns 'at index {idx}'
def item_or_index(item, idx):
try:
return "`{item}`".format(item=item)
except:
return "at index {idx}".format(idx=idx)
exclusions = exhale_args["listingExclude"]
for idx in range(len(exclusions)):
# Gather the `pattern` and `flags` parameters for `re.compile`
item = exclusions[idx]
if isinstance(item, six.string_types):
pattern = item
flags = 0
else:
try:
pattern, flags = item
except Exception as e:
raise ConfigError(
"listingExclude item {0} cannot be unpacked as `pattern, flags = item`:\n{1}".format(
item_or_index(item, idx), e
)
)
# Compile the regular expression object.
try:
regex = re.compile(pattern, flags)
except Exception as e:
raise ConfigError(
"Unable to compile specified listingExclude {0}:\n{1}".format(
item_or_index(item, idx), e
)
)
configs_globals["_compiled_listing_exclude"].append(regex)
# Make sure the lexerMapping is usable
if "lexerMapping" in exhale_args:
from pygments import lexers
import re
# TODO: remove this once config objects are in. Reset needed for testing suite.
configs_globals["_compiled_lexer_mapping"] = {}
lexer_mapping = exhale_args["lexerMapping"]
for key in lexer_mapping:
val = lexer_mapping[key]
# Make sure both are strings
if not isinstance(key, six.string_types) or not isinstance(val, six.string_types):
raise ConfigError("All keys and values in `lexerMapping` must be strings.")
# Make sure the key is a valid regular expression
try:
regex = re.compile(key)
except Exception as e:
raise ConfigError(
"The `lexerMapping` key [{0}] is not a valid regular expression: {1}".format(key, e)
)
# Make sure the provided lexer is available
try:
lex = lexers.find_lexer_class_by_name(val)
except Exception as e:
raise ConfigError(
"The `lexerMapping` value of [{0}] for key [{1}] is not a valid Pygments lexer.".format(
val, key
)
)
# Everything works, stash for later processing
configs_globals["_compiled_lexer_mapping"][regex] = val
####################################################################################
# Internal consistency check to make sure available keys are accurate. #
####################################################################################
# See naming conventions described at top of file for why this is ok!
keys_expected = []
for key in configs_globals.keys():
val = configs_globals[key]
# Ignore modules and functions
if not isinstance(val, FunctionType) and not isinstance(val, ModuleType):
if key != "logger": # band-aid for logging api with Sphinx prior to config objects
# Ignore specials like __name__ and internal variables like _the_app
if "_" not in key and len(key) > 0: # don't think there can be zero length ones...
first = key[0]
if first.isalpha() and first.islower():
keys_expected.append(key)
keys_expected = set(keys_expected)
keys_available = set(keys_available)
if keys_expected != keys_available:
err = StringIO()
err.write(textwrap.dedent('''
CRITICAL: Exhale encountered an internal error, please raise an Issue on GitHub:
https://github.com/svenevs/exhale/issues
Please paste the following in the issue report:
Expected keys:
'''))
for key in keys_expected:
err.write("- {0}\n".format(key))
err.write(textwrap.dedent('''
Available keys:
'''))
for key in keys_available:
err.write("- {0}\n".format(key))
err.write(textwrap.dedent('''
The Mismatch(es):
'''))
for key in (keys_available ^ keys_expected):
err.write("- {0}\n".format(key))
err_msg = err.getvalue()
err.close()
raise ExtensionError(err_msg)
####################################################################################
# See if unexpected keys were presented. #
####################################################################################
all_keys = set(exhale_args.keys())
keys_processed = set(keys_processed)
if all_keys != keys_processed:
# Much love: https://stackoverflow.com/a/17388505/3814202
from difflib import SequenceMatcher
def similar(a, b):
return SequenceMatcher(None, a, b).ratio() * 100.0
# If there are keys left over after taking the differences of keys_processed
# (which is all keys Exhale expects to see), inform the user of keys they might
# have been trying to provide.
#
# Convert everything to lower case for better matching success
potential_keys = keys_available - keys_processed
potential_keys_lower = {key.lower(): key for key in potential_keys}
extras = all_keys - keys_processed
extra_error = StringIO()
extra_error.write("Exhale found unexpected keys in `exhale_args`:\n")
for key in extras:
extra_error.write(" - Extra key: {0}\n".format(key))
potentials = []
for mate in potential_keys_lower:
similarity = similar(key, mate)
if similarity > 50.0:
# Output results with the non-lower version they should put in exhale_args
potentials.append((similarity, potential_keys_lower[mate]))
if potentials:
potentials = reversed(sorted(potentials))
for rank, mate in potentials:
extra_error.write(" - {0:2.2f}% match with: {1}\n".format(rank, mate))
extra_error_str = extra_error.getvalue()
extra_error.close()
raise ConfigError(extra_error_str)
####################################################################################
# Verify some potentially inconsistent or ignored settings. #
####################################################################################
# treeViewIsBootstrap only takes meaning when createTreeView is True
if not createTreeView and treeViewIsBootstrap:
logger.warning("Exhale: `treeViewIsBootstrap=True` ignored since `createTreeView=False`")
# fullToctreeMaxDepth > 5 may produce other sphinx issues unrelated to exhale
if fullToctreeMaxDepth > 5:
logger.warning(
"Exhale: `fullToctreeMaxDepth={0}` is greater than 5 and may build errors for non-html.".format(
fullToctreeMaxDepth
)
)
# Make sure that we received a valid mapping created by utils.makeCustomSpecificationsMapping
sanity = _closure_map_sanity_check
insane = "`customSpecificationsMapping` *MUST* be made using exhale.utils.makeCustomSpecificationsMapping"
if customSpecificationsMapping:
# Sanity check to make sure exhale made this mapping
if sanity not in customSpecificationsMapping:
raise ConfigError(insane)
elif customSpecificationsMapping[sanity] != sanity: # LOL
raise ConfigError(insane)
# Sanity check #2: enforce no new additions were made
expected_keys = set([sanity]) | set(utils.AVAILABLE_KINDS)
provided_keys = set(customSpecificationsMapping.keys())
diff = provided_keys - expected_keys
if diff:
raise ConfigError("Found extra keys in `customSpecificationsMapping`: {0}".format(diff))
# Sanity check #3: make sure the return values are all strings
for key in customSpecificationsMapping:
val_t = type(customSpecificationsMapping[key])
if not isinstance(key, six.string_types):
raise ConfigError(
"`customSpecificationsMapping` key `{key}` gave value type `{val_t}` (need `str`).".format(
key=key, val_t=val_t
)
)
# Specify where the doxygen output should be going
global _doxygen_xml_output_directory
_doxygen_xml_output_directory = doxy_xml_dir
# If requested, the time is nigh for executing doxygen. The strategy:
# 1. Execute doxygen if requested
# 2. Verify that the expected doxy_xml_dir (specified to `breathe`) was created
# 3. Assuming everything went to plan, let exhale take over and create all of the .rst docs
if exhaleExecutesDoxygen:
# Cannot use both, only one or the other
if exhaleUseDoxyfile and (exhaleDoxygenStdin is not None):
raise ConfigError("You must choose one of `exhaleUseDoxyfile` or `exhaleDoxygenStdin`, not both.")
# The Doxyfile *must* be at the same level as conf.py
# This is done so that when separate source / build directories are being used,
# we can guarantee where the Doxyfile is.
if exhaleUseDoxyfile:
doxyfile_path = os.path.abspath(os.path.join(app.confdir, "Doxyfile"))
if not os.path.exists(doxyfile_path):
raise ConfigError("The file [{0}] does not exist".format(doxyfile_path))
here = os.path.abspath(os.curdir)
if here == app.confdir:
returnPath = None
else:
returnPath = here
# All necessary information ready, go to where the Doxyfile is, run Doxygen
# and then return back (where applicable) so sphinx can continue
start = utils.get_time()
if returnPath:
logger.info(utils.info(
"Exhale: changing directories to [{0}] to execute Doxygen.".format(app.confdir)
))
os.chdir(app.confdir)
logger.info(utils.info("Exhale: executing doxygen."))
status = deploy.generateDoxygenXML()
# Being overly-careful to put sphinx back where it was before potentially erroring out
if returnPath:
logger.info(utils.info(
"Exhale: changing directories back to [{0}] after Doxygen.".format(returnPath)
))
os.chdir(returnPath)
if status:
raise ExtensionError(status)
else:
end = utils.get_time()
logger.info(utils.progress(
"Exhale: doxygen ran successfully in {0}.".format(utils.time_string(start, end))
))
else:
if exhaleUseDoxyfile:
logger.warning("Exhale: `exhaleUseDoxyfile` ignored since `exhaleExecutesDoxygen=False`")
if exhaleDoxygenStdin is not None:
logger.warning("Exhale: `exhaleDoxygenStdin` ignored since `exhaleExecutesDoxygen=False`")
if exhaleSilentDoxygen:
logger.warning("Exhale: `exhaleSilentDoxygen=True` ignored since `exhaleExecutesDoxygen=False`")
# Either Doxygen was run prior to this being called, or we just finished running it.
# Make sure that the files we need are actually there.
if not os.path.isdir(doxy_xml_dir):
raise ConfigError(
"Exhale: the specified folder [{0}] does not exist. Has Doxygen been run?".format(doxy_xml_dir)
)
index = os.path.join(doxy_xml_dir, "index.xml")
if not os.path.isfile(index):
raise ConfigError("Exhale: the file [{0}] does not exist. Has Doxygen been run?".format(index))
# Legacy / debugging feature, warn of its purpose
if generateBreatheFileDirectives:
logger.warning("Exhale: `generateBreatheFileDirectives` is a debugging feature not intended for production.")
####################################################################################
# If using a fancy treeView, add the necessary frontend files. #
####################################################################################
if createTreeView:
if treeViewIsBootstrap:
tree_data_static_base = "treeView-bootstrap"
tree_data_css = [os.path.join("bootstrap-treeview", "bootstrap-treeview.min.css")]
tree_data_js = [
os.path.join("bootstrap-treeview", "bootstrap-treeview.min.js"),
# os.path.join("bootstrap-treeview", "apply-bootstrap-treview.js")
]
tree_data_ext = []
else:
tree_data_static_base = "treeView"
tree_data_css = [os.path.join("collapsible-lists", "css", "tree_view.css")]
tree_data_js = [
os.path.join("collapsible-lists", "js", "CollapsibleLists.compressed.js"),
os.path.join("collapsible-lists", "js", "apply-collapsible-lists.js")
]
# The tree_view.css file uses these
tree_data_ext = [
os.path.join("collapsible-lists", "css", "button-closed.png"),
os.path.join("collapsible-lists", "css", "button-open.png"),
os.path.join("collapsible-lists", "css", "button.png"),
os.path.join("collapsible-lists", "css", "list-item-contents.png"),
os.path.join("collapsible-lists", "css", "list-item-last-open.png"),
os.path.join("collapsible-lists", "css", "list-item-last.png"),
os.path.join("collapsible-lists", "css", "list-item-open.png"),
os.path.join("collapsible-lists", "css", "list-item.png"),
os.path.join("collapsible-lists", "css", "list-item-root.png"),
]
# Make sure we have everything we need
collapse_data = os.path.join(os.path.abspath(os.path.dirname(__file__)), "data", tree_data_static_base)
if not os.path.isdir(collapse_data):
raise ExtensionError(
"Exhale: the path to [{0}] was not found, possible installation error.".format(collapse_data)
)
else:
all_files = tree_data_css + tree_data_js + tree_data_ext
missing = []
for file in all_files:
path = os.path.join(collapse_data, file)
if not os.path.isfile(path):
missing.append(path)
if missing:
raise ExtensionError(
"Exhale: the path(s) {0} were not found, possible installation error.".format(missing)
)
# We have all the files we need, the extra files will be copied automatically by
# sphinx to the correct _static/ location, but stylesheets and javascript need
# to be added explicitly
logger.info(utils.info("Exhale: adding tree view css / javascript."))
app.config.html_static_path.append(collapse_data)
# In Sphinx 1.8+ these have been renamed.
# - app.add_stylesheet -> app.add_css_file
# - app.add_javascript -> app.add_js_file
#
# RemovedInSphinx40Warning:
# - The app.add_stylesheet() is deprecated. Please use app.add_css_file() instead.
# - The app.add_javascript() is deprecated. Please use app.add_js_file() instead.
#
# So we'll need to keep this funky `getattr` chain for a little while ;)
# Or else pin min sphinx version to 1.8 or higher. Probably when 2.0 is out?
add_css_file = getattr(app, "add_css_file", getattr(app, "add_stylesheet", None))
add_js_file = getattr(app, "add_js_file", getattr(app, "add_javascript", None))
# Add the stylesheets
for css in tree_data_css:
add_css_file(css)
# Add the javascript
for js in tree_data_js:
add_js_file(js)
logger.info(utils.progress("Exhale: added tree view css / javascript."))