"""
This module contains the functionality used to define the :rst:dir:`automodsumm`
directive and its :ref:`supporting configuration values <automodsumm-confvals>`.
.. contents:: Content
:local:
`automodsumm` Directive
-----------------------
.. rst:directive:: automodsumm
The :rst:dir:`automodsumm` directive is a wrapper on Sphinx's
:rst:dir:`autosummary` directive and, as such, all the options for
:rst:dir:`autosummary` still work. The difference, where :rst:dir:`autosummary`
requires a list of all the objects to document, :rst:dir:`automodsumm`
only requires the module name and then it will inspect the module to find
all the objects to be documented according to the listed options.
The module inspection will respect the ``__all__`` dunder if defined; otherwise,
it will inspect all objects of the module. The inspection will only gather
direct sub-modules and ignore any 3rd party objects, unless listed in
``__all__``.
The behavior of :rst:dir:`automodsumm` can additionally be set with the
:ref:`configuration values described below <automodsumm-confvals>`.
.. 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, **all** groups
will be included in the generated table.
Using the `plasmapy_sphinx.automodsumm.core` module as an example, the
:ref:`module's API <automodsumm-api>` shows it is made of classes
and functions. So the following yields,
.. code-block:: rst
.. automodsumm:: plasmapy_sphinx.automodsumm.core
or
.. automodsumm:: plasmapy_sphinx.automodsumm.core
:groups: all
.. automodsumm:: plasmapy_sphinx.automodsumm.core
However, if you only want to collect classes then one could do
.. code-block:: rst
.. automodsumm:: plasmapy_sphinx.automodsumm.core
:groups: classes
.. automodsumm:: plasmapy_sphinx.automodsumm.core
:groups: classes
If you want to include multiple groups, then specify all groups as a
comma separated list.
.. code-block:: rst
.. automodsumm:: plasmapy_sphinx.automodsumm.core
:groups: classes, functions
.. rst:directive:option:: exclude-groups
This option behaves just like :rst:dir:`automodsumm:groups` except
you are selectively excluding groups for the generated table. Using the
same example as before, a table of just **classes** can be generated by
doing
.. code-block:: rst
.. automodsumm:: plasmapy_sphinx.automodsumm.core
:exclude-groups: functions
.. automodsumm:: plasmapy_sphinx.automodsumm.core
:exclude-groups: functions
.. rst:directive:option:: skip
This option allows you to skip (exclude) selected objects from the
generated table. The argument is given as a comma separated list of
the object's short name. Continuing with the example from above, lets
skip `~plasmapy_sphinx.automodsumm.core.AutomodsummOptions` and
`~plasmapy_sphinx.automodsumm.core.setup` from the table.
.. code-block:: rst
.. automodsumm:: plasmapy_sphinx.automodsumm.core
:skip: AutomodsummOptions, setup
.. automodsumm:: plasmapy_sphinx.automodsumm.core
:skip: AutomodsummOptions, setup
.. rst:directive:option:: toctree
If you want the :rst:dir:`automodsumm` table to serve as a :rst:dir:`toctree`,
then specify this option with a directory path ``DIRNAME`` with respect to
the location of your ``conf.py`` file.
.. code-block:: rst
.. automodsumm:: plasmapy_sphinx.autodoc.automodapi
:toctree: DIRNAME
This will signal `sphinx-autogen
<https://www.sphinx-doc.org/en/master/man/sphinx-autogen.html>`_
to generate stub files for the objects in the table and place them in
the directory named by ``DIRNAME``. This behavior respects the
configuration value :confval:`autosummary_generate`. Additionally,
:rst:dir:`automodsumm` will not generate stub files for entry that
falls into the **modules** group (see the :rst:dir:`automodsumm:groups`
option below), unless :confval:`automodapi_generate_module_stub_files`
is set ``True``.
.. _automodsumm-confvals:
`automodsumm` Configuration Values
----------------------------------
A configuration value is a variable that con 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:`automodsumm` directive.
.. confval:: automodapi_custom_groups
Configuration value used to define custom groups which are used by
:rst:dir:`automodapi` and :rst:dir:`automodsumm` when sorting the discovered
objects of an inspected module. An example custom group definition looks like
.. code-block:: python
automodapi_custom_group = {
"aliases": {
"title": "Aliases",
"description": "Aliases are ...",
"dunder": "__aliases__",
}
}
where the top-level key (``"aliases"``) is the group name used in the
:rst:dir:`automodsumm:groups` option, ``"title"`` defines the title
text of the group heading used by :rst:dir:`automodapi`, ``"description"``
defines a brief description that will be placed after the title (item is
optional and can be omitted) and ``"dunder"`` defines the name of a dunder
variable (similar ``__all__``) in the module. This dunder variable is
defined at the top of the module being documented and defines a list of
object names just like ``__all__``. The :rst:dir:`automodapi` and
:rst:dir:`automodsumm` directives will used this defined dunder to identify
the objects associated with the custom group. Using
`plasmapy.formulary.speeds` as an example, the **aliases** group can
now be collected and displayed like
.. code-block:: rst
.. automodsumm:: plasmapy.formulary.speeds
:groups: aliases
.. automodsumm:: plasmapy.formulary.speeds
:groups: aliases
.. confval:: automodapi_generate_module_stub_files
(Default `False`) By default :rst:dir:`automodsumm` will not generated stub files
for the **modules** group, even when the `sphinx` configuration value
`autosummary_generate
<https://www.sphinx-doc.org/en/master/usage/extensions/autosummary.html?
highlight=autosummary_generate#confval-autosummary_generate>`_
is set `True`. Setting this configure variable to `True` will cause stub
files to be generated for the **modules** group.
.. confval:: autosummary_generate
Same as the :rst:dir:`autosummary` configuration value `autosummary_generate
<https://www.sphinx-doc.org/en/master/usage/extensions/autosummary.html?
highlight=autosummary_generate#confval-autosummary_generate>`_.
"""
__all__ = [
"Automodsumm",
"AutomodsummOptions",
"option_str_list",
"setup",
]
import os
from collections.abc import Callable
from importlib import import_module
from sphinx.ext.autosummary import Autosummary
from sphinx.util import logging
from typing import Any, Dict, List, Tuple, Union
from ..automodsumm.generate import GenDocsFromAutomodsumm
from ..utils import default_grouping_info, find_mod_objs, get_custom_grouping_info
if False:
# noqa
# for annotation, does not need real import
from docutils.nodes import Node
from docutils.statemachine import StringList
from sphinx.application import Sphinx
from sphinx.config import Config
from sphinx.environment import BuildEnvironment
logger = logging.getLogger(__name__)
[docs]
def option_str_list(argument):
"""
An option validator for parsing a comma-separated option argument. Similar to
the validators found in `docutils.parsers.rst.directives`.
"""
if argument is None:
raise ValueError("argument required but none supplied")
else:
return [s.strip() for s in argument.split(",")]
[docs]
class AutomodsummOptions:
"""
Class for advanced conditioning and manipulation of option arguments for
`plasmapy_sphinx.automodsumm.core.Automodsumm`.
"""
option_spec = {
**Autosummary.option_spec,
"groups": option_str_list,
"exclude-groups": option_str_list,
"skip": option_str_list,
}
"""
Mapping of option names to validator functions. (see
:attr:`docutils.parsers.rst.Directive.option_spec`)
"""
_default_grouping_info = default_grouping_info.copy()
logger = logger
"""
Instance of the `~sphinx.util.logging.SphinxLoggerAdapter` for report during
builds.
"""
def __init__(
self,
app: "Sphinx",
modname: str,
options: Dict[str, Any],
docname: str = None,
_warn: Callable = None,
):
"""
Parameters
----------
app : `~sphinx.application.Sphinx`
Instance of the sphinx application.
modname : `str`
Name of the module given in the :rst:dir:`automodsumm` directive. This
is the module to be inspected and have it's objects grouped.
options : Dict[str, Any]
Dictionary of options given for the :rst:dir:`automodsumm` directive
declaration.
docname : str
Name of the document/file where the :rst:dir:`automodsumm` direction
was declared.
_warn : Callable
Instance of a `sphinx.util.logging.SphinxLoggerAdapter.warning` for
reporting warning level messages during a build.
"""
self._app = app
self._modname = modname
self._options = options.copy()
self._docname = docname
self._warn = _warn if _warn is not None else self.logger.warning
self.toctree = {
"original": None,
"rel_to_doc": None,
"abspath": None,
} # type: Dict[str, Union[str, None]]
self.condition_options()
@property
def app(self) -> "Sphinx":
"""Instance of the sphinx application."""
return self._app
@property
def modname(self) -> str:
"""Name of the module given to :rst:dir:`automodsumm`."""
return self._modname
@property
def options(self) -> Dict[str, Any]:
"""Copy of the options given to :rst:dir:`automodsumm`."""
return self._options
@property
def docname(self) -> str:
"""Name of the document where :rst:dir:`automodsumm` was declared."""
return self._docname
@property
def warn(self) -> Callable:
"""
Instance of a `sphinx.util.logging.SphinxLoggerAdapter.warning` for
reporting warning level messages during a build.
"""
return self._warn
@property
def pkg_or_module(self) -> str:
"""
Is module specified by :attr:`modname` a package or module (i.e. `.py` file).
Return ``"pkg"`` for a package and ``"module"`` for a `.py` file.
"""
mod = import_module(self.modname)
if mod.__package__ == mod.__name__:
return "pkg"
else:
return "module"
[docs]
def condition_options(self):
"""
Method for doing any additional conditioning of option arguments.
Called during class instantiation."""
self.condition_toctree_option()
self.condition_group_options()
[docs]
def condition_toctree_option(self):
"""
Additional conditioning of the option argument ``toctree``. (See
:rst:dir:`automodsumm:toctree` for additional details.)
"""
if "toctree" not in self.options:
return
if self.docname is None:
doc_path = self.app.confdir
else:
doc_path = os.path.dirname(os.path.join(self.app.srcdir, self.docname))
self.toctree["original"] = self.options["toctree"]
self.toctree["abspath"] = os.path.abspath(
os.path.join(self.app.confdir, self.options["toctree"]),
)
self.toctree["rel_to_doc"] = os.path.relpath(
self.toctree["abspath"], doc_path
).replace(os.sep, "/")
self.options["toctree"] = self.toctree["rel_to_doc"]
[docs]
def condition_group_options(self):
"""
Additional conditioning of the option arguments ``groups`` and
``exclude-groups``. (See :rst:dir:`automodsumm:groups` and
:rst:dir:`automodsumm:exclude-groups` for additional details.)
"""
allowed_args = self.groupings | {"all"}
do_groups = self.groupings.copy() # defaulting to all groups
# groups option
if "groups" in self.options:
opt_args = set(self.options["groups"])
unknown_args = opt_args - allowed_args
if len(unknown_args) > 0:
self.warn(
f"Option 'groups' has unrecognized arguments "
f"{unknown_args}. Ignoring."
)
opt_args = opt_args - unknown_args
if "all" not in opt_args:
do_groups = opt_args
# exclude groupings
if "exclude-groups" in self.options:
opt_args = set(self.options["exclude-groups"])
del self.options["exclude-groups"]
else:
opt_args = set()
unknown_args = opt_args - allowed_args
if len(unknown_args) > 0:
self.warn(
f"Option 'exclude-groups' has unrecognized arguments "
f"{unknown_args}. Ignoring."
)
opt_args = opt_args - unknown_args
elif "all" in opt_args:
self.warn(
f"Arguments of 'groups' and 'exclude-groups' results in no content."
)
self.options["groups"] = []
return
do_groups = do_groups - opt_args
self.options["groups"] = list(do_groups)
@property
def mod_objs(self) -> Dict[str, Dict[str, Any]]:
"""
Dictionary of the grouped objects found in the module named by :attr:`modname`.
See Also
--------
plasmapy_sphinx.utils.find_mod_objs
"""
return find_mod_objs(self.modname, app=self.app)
@property
def groupings(self) -> set:
"""Set of all the grouping names."""
return set(self.grouping_info)
@property
def default_grouping_info(self) -> Dict[str, Dict[str, str]]:
"""
Dictionary of the default group information.
See Also
--------
plasmapy_sphinx.utils.default_grouping_info
"""
return self._default_grouping_info.copy()
@property
def custom_grouping_info(self) -> Dict[str, Dict[str, str]]:
"""
Dictionary of the custom group info.
See Also
--------
plasmapy_sphinx.utils.get_custom_grouping_info
"""
return get_custom_grouping_info(self.app)
@property
def grouping_info(self) -> Dict[str, Dict[str, str]]:
"""
The combined grouping info of :attr:`default_grouping_info` and
:attr:`custom_grouping_info`
"""
grouping_info = self.default_grouping_info
grouping_info.update(self.custom_grouping_info)
return grouping_info
@property
def mod_objs_option_filtered(self) -> Dict[str, Dict[str, Any]]:
"""
A filtered version of :attr:`mod_objs` according to the specifications
given in :attr:`options` (i.e. those given to :rst:dir:`automodsumm`).
"""
try:
mod_objs = self.mod_objs
except ImportError:
mod_objs = {}
self.warn(f"Could not import module {self.modname}")
do_groups = set(self.options["groups"])
if len(do_groups) == 0:
return {}
# remove excluded groups
for group in list(mod_objs):
if group not in do_groups:
del mod_objs[group]
# objects to skip
skip_names = set()
if "skip" in self.options:
skip_names = set(self.options["skip"])
# filter out skipped objects
for group in list(mod_objs.keys()):
names = mod_objs[group]["names"]
qualnames = mod_objs[group]["qualnames"]
objs = mod_objs[group]["objs"]
names_filtered = []
qualnames_filtered = []
objs_filtered = []
for name, qualname, obj in zip(names, qualnames, objs):
if not (name in skip_names or qualname in skip_names):
names_filtered.append(name)
qualnames_filtered.append(qualname)
objs_filtered.append(obj)
if len(names_filtered) == 0:
del mod_objs[group]
continue
mod_objs[group] = {
"names": names_filtered,
"qualnames": qualnames_filtered,
"objs": objs_filtered,
}
return mod_objs
[docs]
def generate_obj_list(self, exclude_modules: bool = False) -> List[str]:
"""
Take :attr:`mod_objs_option_filtered` and generated a list of the fully
qualified object names. The list is sorted based on the casefolded
short names of the objects.
Parameters
----------
exclude_modules : bool
(Default `False`) Set `True` to exclude the qualified names related to
objects sorted in the **modules** group.
"""
mod_objs = self.mod_objs_option_filtered
if not bool(mod_objs):
return []
gather_groups = set(mod_objs.keys())
if exclude_modules:
gather_groups.discard("modules")
names = []
qualnames = []
for group in gather_groups:
names.extend(mod_objs[group]["names"])
qualnames.extend(mod_objs[group]["qualnames"])
content = [
qualname
for name, qualname in sorted(
zip(names, qualnames), key=lambda x: str.casefold(x[0])
)
]
return content
[docs]
class Automodsumm(Autosummary):
"""The class that defines the :rst:dir:`automodsumm` directive."""
required_arguments = 1
optional_arguments = 0
has_content = False
option_spec = AutomodsummOptions.option_spec.copy()
[docs]
def run(self):
"""
This method is called whenever the :rst:dir:`automodsumm` directive is found
in a document. It is used to do any further manipulation of the directive,
its options, and its content to get the desired rendered outcome.
"""
env = self.env
modname = self.arguments[0]
# for some reason, even though ``currentmodule`` is substituted in,
# sphinx doesn't necessarily recognize this fact. So we just force
# it internally, and that seems to fix things
env.temp_data["py:module"] = modname
env.ref_context["py:module"] = modname
nodelist = []
# update toctree with relative path to file (not confdir)
if "toctree" in self.options:
self.options["toctree"] = self.option_processor().options["toctree"]
# define additional content
content = self.option_processor().generate_obj_list()
for ii, modname in enumerate(content):
if not modname.startswith("~"):
content[ii] = "~" + modname
self.content = content
nodelist.extend(Autosummary.run(self))
return nodelist
[docs]
def option_processor(self):
"""
Instance of `~plasmapy_sphinx.automodsumm.core.Automodsumm` so further processing
(beyond :attr:`option_spec`) of directive options can be performed.
"""
processor = AutomodsummOptions(
app=self.env.app,
modname=self.arguments[0],
options=self.options,
docname=self.env.docname,
_warn=self.warn,
)
return processor
[docs]
def get_items(self, names):
try:
self.bridge.genopt["imported-members"] = True
except AttributeError: # Sphinx < 4.0
self.genopt["imported-members"] = True
return Autosummary.get_items(self, names)
@property
def genopt(self):
""".. deprecated:: Sphinx 2.0.0"""
return super().genopt
@property
def env(self) -> "BuildEnvironment":
"""Reference to the :class:`~sphinx.environment.BuildEnvironment` object."""
return super().env
@property
def config(self) -> "Config":
"""Reference to the :class:`~sphinx.config.Config` object."""
return super().config
@property
def result(self) -> "StringList":
"""
A `docutils.statemachine.StringList` representing the lines of the
directive.
"""
return super().result
@property
def warnings(self) -> List["Node"]:
""".. deprecated:: Sphinx 2.0.0"""
return super().warnings
[docs]
def debug(self, message):
"""``level=0`` :meth:`directive_error`"""
return super().debug(message)
[docs]
def info(self, message):
"""``level=1`` :meth:`directive_error`"""
return super().info(message)
[docs]
def warning(self, message):
"""``level=2`` :meth:`directive_error`"""
return super().warning(message)
[docs]
def error(self, message):
"""``level=3`` :meth:`directive_error`"""
return super().error(message)
[docs]
def severe(self, message):
"""``level=4`` :meth:`directive_error`"""
return super().severe(message)
[docs]
def warn(self, msg: str) -> None:
""".. deprecated:: Sphinx 2.0.0"""
super(Automodsumm, self).warn(msg)
[docs]
def import_by_name(
self, name: str, prefixes: List[str]
) -> Tuple[str, Any, Any, str]:
"""See :func:`sphinx.ext.autosummary.import_by_name`"""
return super(Automodsumm, self).import_by_name(name, prefixes)
[docs]
def setup(app: "Sphinx"):
"""Sphinx ``setup()`` function for the :rst:dir:`automodsumm` functionality."""
app.setup_extension("sphinx.ext.autosummary")
app.add_directive("automodsumm", Automodsumm)
gendocs_from_automodsumm = GenDocsFromAutomodsumm()
app.connect("builder-inited", gendocs_from_automodsumm)
app.connect(
"autodoc-skip-member",
gendocs_from_automodsumm.event_handler__autodoc_skip_member,
)
app.add_config_value("automodapi_custom_groups", dict(), True)
app.add_config_value("automodapi_generate_module_stub_files", False, True)
return {"parallel_read_safe": True, "parallel_write_safe": True}