Source code for exhale.configs

# -*- 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."))