"""
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