Source code for plasmapy_sphinx.automodsumm.core

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