Source code for plasmapy_sphinx.autodoc.automodapi

"""
This module contains the functionality used to define the :rst:dir:`automodapi`
directive and its :ref:`supporting configuration values <automodapi-confvals>`.

.. contents:: Content
    :local:

`automodapi` Directive
----------------------

.. rst:directive:: automodapi

    The :rst:dir:`automodapi` directive is a wrapper on Sphinx's
    :rst:dir:`automodule` directive and registered as one of the
    `sphinx.ext.autodoc` directives.  As such, many of the :rst:dir:`automodule`
    options still work, except any of the *member* options and *ignore-module-all*.
    The registration of :rst:dir:`automodapi` with `~sphinx.ext.autodoc` means
    :rst:dir:`automodapi` is used by Sphinx when indexing the documented module.

    :rst:dir:`automodapi` is used to document modules (i.e. packages and ``.py``
    files.  The contents of the `plasmapy_sphinx.utils` page shows an example
    of how the following declaration is rendered.

    .. code-block:: rst

        `plasmapy_sphinx.utils`
        =======================

        .. automodapi:: plasmapy_sphinx.utils

    The module level docstring is first rendered and then the categorized groups
    are rendered in :rst:dir:`automodsumm` tables filtered for the associated group
    and placed under the appropriate object type heading.  The order in which the
    :rst:dir:`automodsumm` tables are displayed is controlled by the configuration
    value :confval:`automodapi_group_order`.

    .. rst:directive:option:: groups

        When a module is inspected all the identified objects are categorized into
        groups.  The built-in groups are:

        +----------------+-------------------------------------------------------------+
        | **modules**    | Direct sub-packages and modules.                            |
        +----------------+-------------------------------------------------------------+
        | **classes**    | Python classes. (excluding **exceptions** and **warnings**) |
        +----------------+-------------------------------------------------------------+
        | **exceptions** | Classes that inherit from `BaseException`. (excluding       |
        |                | **warnings**)                                               |
        +----------------+-------------------------------------------------------------+
        | **warnings**   | Classes that inherit from `Warning`.                        |
        +----------------+-------------------------------------------------------------+
        | **functions**  | Objects that satisfy :func:`inspect.isroutine`.             |
        +----------------+-------------------------------------------------------------+
        | **variables**  | All other objects.                                          |
        +----------------+-------------------------------------------------------------+

        In addition to the built-in groups, groups defined in
        :confval:`automodapi_custom_groups` will be categorized.  When objects are
        collected and grouped the **modules** will be done first, followed by any
        custom group, and, finally, the built-in groups.  By default, tables will
        be generated for **all** groups.

        The contents of the :ref:`API section <automodapi-api>` below shows
        an example of how the follow declaration is rendered.

        .. code-block:: rst

            .. automodapi:: plasmapy_sphinx.autodoc.automodapi
               :no-main-docstring:

            or

            .. automodapi:: plasmapy_sphinx.autodoc.automodapi
               :no-main-docstring:
               :groups: all

        The behavior of ``:no-main-docstring:`` is described below
        in the :rst:dir:`automodapi:no-main-docstring` section.  If you
        only want to display only **classes**, then the following can be done.

        .. code-block:: rst

            .. automodapi:: plasmapy_sphinx.autodoc.automodapi
               :no-main-docstring:
               :groups: classes

        If you want to include multiple groups, then specify all groups as a
        comma separated list.

        .. code-block:: rst

            .. automodapi:: plasmapy_sphinx.autodoc.automodapi
               :groups: classes, functions

    .. rst:directive:option:: exclude-groups

        This option behaves just like :rst:dir:`automodapi:groups` except
        you are selectively excluding groups for the generated tables.  Using the
        same example as before, a table of just **classes** can be generated by
        doing

        .. code-block:: rst

            .. automodapi:: plasmapy_sphinx.autodoc.automodapi
               :exclude-groups: functions

    .. rst:directive:option:: no-groups

        Specify if you want to exclude all :rst:dir:`automodsumm` tables from
        being generated.

        .. code-block:: rst

            .. automodapi:: plasmapy_sphinx.autodoc.automodapi
               :no-groups:

    .. rst:directive:option:: skip

        This option allows you to skip (exclude) selected objects from the
        generated tables.  The argument is given as a comma separated list of
        the object's short name.  Continuing with the example from above, let's
        skip `~plasmapy_sphinx.autodoc.automodapi.ModAPIDocumenter` and
        `~plasmapy_sphinx.autodoc.automodapi.setup` from the tables.

        .. code-block:: rst

            .. automodapi:: plasmapy_sphinx.autodoc.automodapi
               :no-main-docstrings:
               :skip: ModAPIDocumenter, setup

        .. automodapi:: plasmapy_sphinx.autodoc.automodapi
           :noindex:
           :no-toctree:
           :no-main-docstring:
           :skip: ModAPIDocumenter, setup

    .. rst:directive:option:: noindex

        Like the rest of Sphinx's `~sphinx.ext.autodoc` directives,
        :rst:dir:`automodapi` is used by Sphinx to index the documented module
        and make available cross-referencing.  To prevent multiple
        cross-references to the same documented module, ``:noindex:`` can be used
        to prevent the indexing.

        .. code-block:: rst

            .. automodapi:: plasmapy_sphinx.autodoc.automodapi
               :noindex:

    .. rst:directive:option:: include-heading

        This option will create a top level heading for the documented module.

        .. code-block:: rst

            .. automodapi:: foo.bar
               :include-heading:

        If ``bar`` is a package, then the heading will look like

        .. code-block:: rst

            foo.bar Package
            ---------------

        and if ``bar`` is a ``.py`` file, then the heading will look like

        .. code-block:: rst

            foo.bar Module
            --------------

    .. rst:directive:option:: heading-chars

        (Default ``-^``) A two character string specifying the underline characters
        used for the heading created by :rst:dir:`automodapi`.  The following code

        .. code-block:: rst

            .. automodapi:: foo.bar
               :include-heading:
               :heading-chars: ^~

        would generate reStructuredText equivalent to

        .. code-block:: rst

            foo.bar Package
            ^^^^^^^^^^^^^^^

            I am the main docstring

            Sub-Packages & Modules
            ~~~~~~~~~~~~~~~~~~~~~~

            .. automodsumm:: foo.bar
               :toctree: api
               :groups: modules

        If :rst:dir:`automodapi:include-heading` is not given, then the first
        character will be used for the :rst:dir:`automodsumm` table headings.

    .. rst:directive:option:: toctree

        If you want the tables generated by :rst:dir:`automodapi` to serve as
        :rst:dir:`toctree`'s, then specify this option with a directory path
        ``DIRNAME`` with respect to the location of your ``conf.py``.

        .. code-block:: rst

            .. automodapi:: plasmapy_sphinx.autodoc.automodapi
               :toctree: DIRNAME

        If ``:toctree:`` is not set, then ``DIRNAME`` will default to what is set
        by :confval:`automodapi_default_toctree_dir`.  If no :rst:dir:`toctree`
        is desired, then use option :rst:dir:`automodapi:no-toctree`.

    .. rst:directive:option:: no-toctree

        Use this option if you do not want the tables generated by
        :rst:dir:`automodapi` to serve as :rst:dir:`toctree`'s.

        .. code-block:: rst

            .. automodapi:: plasmapy_sphinx.autodoc.automodapi
               :no-toctree:

    .. rst:directive:option:: no-main-docstring

        By default the module level docstring will be rendered and displayed, but
        setting this option will omit the module level docstring and leave just
        the :rst:dir:`autosummary` tables for each collected group of objects.

        .. code-block:: rst

            .. automodapi:: plasmapy_sphinx.autodoc.automodapi
               :no-main-docstring:

    .. rst:directive:option:: inheritance-diagram

        Set this option to generate inheritance diagrams (using the directive
        :rst:dir:`inheritance-diagram`) for the groups listed in the configuration
        value :confval:`automodapi_groups_with_inheritance_diagrams`.  Default
        behavior is controlled by the configuration value
        :confval:`automodapi_include_inheritance_diagram`.  The option
        :rst:dir:`automodapi:no-inheritance-diagram` will supersede this option.

        .. code-block:: rst

            .. automodapi:: plasmapy_sphinx.autodoc.automodapi
               :inheritance-diagram:

    .. rst:directive:option:: no-inheritance-diagram

        Set this option to not generate inheritance diagrams.  This option will
        take precedence over the :rst:dir:`automodapi:inheritance-diagram` option.

        .. code-block:: rst

            .. automodapi:: plasmapy_sphinx.autodoc.automodapi
               :no-inheritance-diagram:

.. _automodapi-confvals:

`automodapi` Configuration Values
---------------------------------

A configuration value is a variable that can be defined in ``conf.py`` to configure
the default behavior of related `sphinx` directives.  The configuration values
below relate to the behavior of the :rst:dir:`automodapi` directive.

.. confval:: automodapi_default_toctree_dir

    (Default ``"api"``)  The default directory name, with respect to ``conf.py``,
    for :rst:dir:`automodapi` to write stub files to.  This is the default
    value for :rst:dir:`automodapi:toctree`.

.. confval:: automodapi_group_order

    (Default ``("modules", "classes", "exceptions", "warnings", "functions",
    "variables")``)  A tuple defining the order :rst:dir:`automodapi` should
    display the collected groups.  If :rst:dir:`automodapi` identifies groups
    that are not identified here (e.g. unlisted custom groups), then those
    groups will be displayed alphabetically after the groups defined in
    :confval:`automodapi_group_order`.  *This configuration value should be
    defined if custom groups are defined by* :confval:`automodapi_custom_groups` *.*

.. confval:: automodapi_groups_with_inheritance_diagrams

    (Default ``["classes", "exceptions", "warnings"]``) A list defining which
    groups should have inheritance diagrams associated with them when displayed
    by :rst:dir:`automodapi`.

.. confval:: automodapi_include_inheritance_diagram

    (Default `True`) Controls if :rst:dir:`automodapi` will by default generated
    inheritance diagrams (see directive :rst:dir:`inheritance-diagram`) for a
    given group.  The groups that should get inheritance diagrams are defined by
    the configuration value :confval:`automodapi_groups_with_inheritance_diagrams`.

"""
__all__ = ["AutomodapiOptions", "ModAPIDocumenter", "setup"]

import inspect
import re
import sys
import warnings

from collections import OrderedDict
from collections.abc import Callable
from docutils.parsers.rst import directives
from docutils.statemachine import StringList
from sphinx.application import Sphinx

try:
    from sphinx.deprecation import RemovedInSphinx50Warning
except ImportError:

    class RemovedInSphinx50Warning(PendingDeprecationWarning):
        pass


from sphinx.ext.autodoc import bool_option, ModuleDocumenter
from sphinx.locale import __
from sphinx.util import logging
from typing import Any, Dict, List, Optional, Union

from ..automodsumm.core import AutomodsummOptions, option_str_list
from ..utils import default_grouping_info

if sys.version_info >= (3, 0):
    text_type = str
else:
    text_type = unicode  # noqa

logger = logging.getLogger(__name__)


_option_spec = {
    **ModuleDocumenter.option_spec,
    "groups": option_str_list,
    "exclude-groups": option_str_list,
    "no-groups": bool_option,
    # "group-order": option_str_list,
    # "merge-groups": bool_option,
    "skip": option_str_list,
    "include-heading": bool_option,
    # "members": lambda x: None,
    "heading-chars": directives.unchanged,
    "toctree": directives.unchanged,
    "no-toctree": bool_option,
    "no-main-docstring": bool_option,
    "inheritance-diagram": bool_option,
    "no-inheritance-diagram": bool_option,
}  # type: Dict[str, Callable]
for option_name in list(_option_spec):
    if "member" in option_name:
        del _option_spec[option_name]
del _option_spec["ignore-module-all"]
del option_name


[docs] class AutomodapiOptions(AutomodsummOptions): """ Class for advanced conditioning and manipulation of option arguments of `plasmapy_sphinx.autodoc.automodapi.ModAPIDocumenter`. """ option_spec = _option_spec logger = logger
[docs] def condition_options(self): super().condition_options() self.condition_heading_chars_option() self.condition_include_heading_option() self.condition_inheritance_diagram_option()
[docs] def condition_toctree_option(self): """ Additional conditioning of the ``:toctree:`` option. (See options :rst:dir:`automodapi:toctree` and :rst:dir:`automodapi:no-toctree` for additional details.) """ if "no-toctree" in self.options and self.options["no-toctree"]: if "toctree" in self.options: del self.options["toctree"] elif "toctree" not in self.options: self.options["toctree"] = self.app.config.automodapi_default_toctree_dir super().condition_toctree_option()
[docs] def condition_heading_chars_option(self): """ Additional conditioning of the ``:heading-chars:`` option. (See option :rst:dir:`automodapi:heading-chars` for additional details.) """ non_alphanumerics = re.compile("[^0-9a-zA-Z]]+") heading_chars = self.options.get("heading-chars", None) if ( heading_chars is None or heading_chars == "" or len(heading_chars) < 2 or non_alphanumerics.fullmatch(heading_chars) is None ): self.options["heading-chars"] = "-^"
[docs] def condition_include_heading_option(self): """ Additional conditioning of the ``:include-heading:`` option. (See option :rst:dir:`automodapi:include-heading` for additional details.) """ if "include-heading" not in self.options: self.options["include-heading"] = False
[docs] def condition_inheritance_diagram_option(self): """ Additional conditioning of the ``:inheritance-diagram:`` option. (See options :rst:dir:`automodapi:inheritance-diagram` and :rst:dir:`automodapi:no-inheritance-diagram` for additional details.) """ if "no-inheritance-diagram" in self.options: self.options["inheritance-diagram"] = False del self.options["no-inheritance-diagram"] elif "inheritance-diagram" in self.options: self.options["inheritance-diagram"] = False else: self.options[ "inheritance-diagram" ] = self.app.config.automodapi_include_inheritance_diagram
[docs] def condition_group_options(self): """ Additional conditioning of the grouping options. (See options :rst:dir:`automodapi:groups`, :rst:dir:`automodapi:exclude-groups`, and :rst:dir:`automodapi:no-groups` for additional details.) """ if "no-groups" in self.options and self.options["no-groups"]: self.options["groups"] = [] if "exclude-groups" in self.options: del self.options["exclude-groups"] return super().condition_group_options()
@property def options_for_automodsumm(self) -> Dict[str, Any]: """ A dictionary of options suitable for :rst:dir:`automodsumm` based on the options given to :rst:dir:`automodapi`, and excluding the group options. """ options = {} asumm_opts = list(AutomodsummOptions.option_spec) for name in asumm_opts: if name in self.options and name not in ("groups", "exclude-groups"): val = self.options[name] if isinstance(val, list): val = ", ".join(val) if name == "toctree" and self.toctree["original"] is not None: # automodsumm requires path relative to the confdir but # Automodsumm.review_toctree_option generates the path relative # to the file location options[name] = self.toctree["original"] else: options[name] = val return options
[docs] class ModAPIDocumenter(ModuleDocumenter): """ The class that defines the `~sphinx.ext.autodoc` directive :rst:dir:`automodapi`. """ objtype = "modapi" """Defines the *auto* directive name. In this case ``automodapi``.""" directivetype = "module" """Defines the generated directive name. In this case ``.. :py:module::``.""" titles_allowed = True content_indent = "" logger = logger """ Instance of the `~sphinx.util.logging.SphinxLoggerAdapter` for report during builds. """ option_spec = _option_spec """Mapping of option names to validator functions.""" # Templates used for generated the additional content associated with the # directive (e.g. the automodsumm tables) _templates = { "mod-heading": "\n".join(["{modname} {pkg_or_mod}", "{underline}", ""]), "heading": "\n".join(["{title}", "{underline}"]), "automodsumm": "\n".join([".. automodsumm:: {modname}", " :groups: {group}"]), "options": " :{option}: {opt_args}", "inheritance-diagram": "\n".join( [ ".. inheritance-diagram:: {cls_list}", " :private-bases:", " :parts: 1", ], ), } @property def grouping_info(self) -> Dict[str, Dict[str, Union[str, None]]]: """ Dictionary of :rst:dir:`automodapi` and :rst:dir:`automodsumm` group information. The primary key is the group name, e.g. **modules**, **classes**, etc. The value associated with the primary key is a dictionary with the following keys: +-------------+------------------------------------------------------------+ | title | Title used to head the :rst:dir:`automodsumm` table. | +-------------+------------------------------------------------------------+ | description | Brief description to follow the title. | +-------------+------------------------------------------------------------+ | dunder | Name of the dunder variable used to define a custom group. | +-------------+------------------------------------------------------------+ """ group_order = tuple(self.env.app.config.automodapi_group_order) custom_group_info = self.env.app.config.automodapi_custom_groups group_names = set(default_grouping_info) | set(custom_group_info) remainder = list(group_names - set(group_order)) if len(remainder) > 0: group_order += tuple(sorted(remainder)) _grouping_info = OrderedDict() for name in group_order: if name in default_grouping_info: _info = default_grouping_info[name] else: _info = custom_group_info[name] _grouping_info.update( { name: { "title": _info.get("title", None), "description": _info.get("description", None), "dunder": _info.get("dunder", None), }, }, ) return _grouping_info
[docs] def generate_more_content(self, modname: str) -> List[str]: """ Generate the "more content" associate with the :rst:dir:`automodsumm` tables and inheritance diagrams. Parameters ---------- modname : str Name of the module being documented (i.e. that given to :rst:dir:`automodapi`). Returns ------- List[str] A list of strings to be added the to the directive's content. """ app = self.env.app inheritance_groups = app.config.automodapi_groups_with_inheritance_diagrams lines = [] option_processor = AutomodapiOptions( app, modname, self.options, _warn=self.logger.warning, docname=self.env.docname, ) asumm_options = option_processor.options_for_automodsumm mod_objs = option_processor.mod_objs_option_filtered heading_char = ( option_processor.options["heading-chars"][1] if option_processor.options["include-heading"] else option_processor.options["heading-chars"][0] ) include_inheritance_diagram = option_processor.options["inheritance-diagram"] # scan thru default groups first for group, info in self.grouping_info.items(): if group not in mod_objs: continue title = info["title"] description = info["description"] # type: str underline = heading_char * len(title) # sub-heading lines.extend( self._templates["heading"] .format(title=title, underline=underline) .splitlines() ) lines.append("") # add group description if description is not None: description = inspect.cleandoc(description) descr_list = description.split("\n") lines.extend(descr_list) lines.append("") # add automodsumm directive lines.extend( self._templates["automodsumm"] .format(modname=modname, group=group) .splitlines() ) # add options for automodsumm directive for name, val in asumm_options.items(): if name == "toctree" and group == "modules": continue lines.extend( self._templates["options"] .format(option=name, opt_args=val) .splitlines() ) lines.append("") # add inheritance-diagram if group in inheritance_groups and include_inheritance_diagram: cls_list = " ".join(mod_objs[group]["qualnames"]) lines.extend( self._templates["inheritance-diagram"] .format(cls_list=cls_list) .splitlines() ) lines.append("") return lines
[docs] def generate_heading(self, modname: str) -> None: """ Generate and place a heading at the top of the directive's content. If ``modname`` is a package then the title will be ``<modname> Package``; and if a module (``.py`` file) then the title will be ``<modname> Module``. Parameters ---------- modname : str Name of the module being documented (i.e. that given to :rst:dir:`automodapi`). """ app = self.env.app sourcename = self.get_sourcename() option_processor = AutomodapiOptions( app, modname, self.options, _warn=self.logger.warning, docname=self.env.docname, ) if not option_processor.options["include-heading"]: return modname = re.escape(modname) if option_processor.pkg_or_module == "pkg": pkg_or_mod = "Package" else: pkg_or_mod = "Module" heading_char = option_processor.options["heading-chars"][0] underline = heading_char * (len(modname) + 1 + len(pkg_or_mod)) heading_lines = ( self._templates["mod-heading"] .format(modname=modname, pkg_or_mod=pkg_or_mod, underline=underline) .splitlines() ) for line in heading_lines: self.add_line(line, source=sourcename)
[docs] def add_content( self, more_content: Optional[StringList], no_docstring: bool = False ) -> None: """Add content from docstrings, attribute documentation and user.""" if no_docstring: warnings.warn( "The 'no_docstring' argument to %s.add_content() is deprecated." % self.__class__.__name__, RemovedInSphinx50Warning, stacklevel=2, ) no_docstring = self.options.get("no-main-docstring", False) # set sourcename and add content from attribute documentation sourcename = self.get_sourcename() if self.analyzer: attr_docs = self.analyzer.find_attr_docs() if self.objpath: key = (".".join(self.objpath[:-1]), self.objpath[-1]) if key in attr_docs: no_docstring = True # make a copy of docstring for attributes to avoid cache # the change of autodoc-process-docstring event. docstrings = [list(attr_docs[key])] for i, line in enumerate(self.process_doc(docstrings)): self.add_line(line, sourcename, i) # add content from docstrings if not no_docstring: docstrings = self.get_doc() if docstrings is None: # Do not call autodoc-process-docstring on get_doc() returns None. pass else: if not docstrings: # append at least a dummy docstring, so that the event # autodoc-process-docstring is fired and can add some # content if desired docstrings.append([]) for i, line in enumerate(self.process_doc(docstrings)): self.add_line(line, sourcename, i) # add additional content (e.g. from document), if present if more_content: for line, src in zip(more_content.data, more_content.items): self.add_line(line, src[0], src[1])
[docs] def generate( self, more_content: Optional[StringList] = None, real_modname: str = None, check_module: bool = False, all_members: bool = False, ) -> None: """ Generate reST for the object given by *self.name*, and possibly for its members. If *more_content* is given, include that content. If *real_modname* is given, use that module name to find attribute docs. If *check_module* is True, only generate if the object is defined in the module name it is imported from. If *all_members* is True, document all members. """ docname = self.env.docname if not docname.endswith(".rst"): docname += ".rst" if not self.parse_name(): # need a module to import logger.warning( __( f"don't know which module to import for autodocumenting " f"{self.name} (try placing a 'module' or 'currentmodule' " f"directive in the document, or giving an explicit module name)" ), type="autodoc", ) return # now, import the module and get object to document if not self.import_object(): return # If there is no real module defined, figure out which to use. # The real module is used in the module analyzer to look up the module # where the attribute documentation would actually be found in. # This is used for situations where you have a module that collects the # functions and classes of internal submodules. real_modname = real_modname or self.get_real_modname() # type: str # Generate heading self.generate_heading(modname=real_modname) # Generate the 'more_content' (automodsumm and inheritance diagrams) more_content = StringList() more_lines = self.generate_more_content(modname=real_modname) for line in more_lines: more_content.append(line, source=docname) # generate super().generate( more_content=more_content, real_modname=real_modname, check_module=check_module, all_members=all_members, )
[docs] def setup(app: Sphinx): """Sphinx ``setup()`` function for the :rst:dir:`automodapi` functionality.""" from ..automodsumm.core import setup as setup_automodsumm rtn = setup_automodsumm(app) app.setup_extension("sphinx.ext.inheritance_diagram") app.add_autodocumenter(ModAPIDocumenter) app.add_config_value("automodapi_include_inheritance_diagram", True, True) app.add_config_value("automodapi_default_toctree_dir", "api", True) app.add_config_value( "automodapi_group_order", ("modules", "classes", "exceptions", "warnings", "functions", "variables"), True, ) app.add_config_value( "automodapi_groups_with_inheritance_diagrams", ["classes", "exceptions", "warnings"], True, ) return rtn