Source code for plasmapy_sphinx.automodsumm.generate

"""
This module contains functionality for auto-generating the stub files related to
the :rst:dir:`automodapi` and :rst:dir:`automodsumm` directives.
"""
__all__ = ["AutomodsummEntry", "AutomodsummRenderer", "GenDocsFromAutomodsumm"]

import os
import re

from jinja2 import TemplateNotFound
from sphinx.ext.autodoc.mock import mock
from sphinx.ext.autosummary import get_rst_suffix, import_by_name, import_ivar_by_name
from sphinx.ext.autosummary.generate import (
    AutosummaryEntry,
    AutosummaryRenderer,
    generate_autosummary_content,
)
from sphinx.locale import __
from sphinx.util import logging
from sphinx.util.osutil import ensuredir
from typing import Any, Dict, List, Union

from ..utils import templates_dir

if False:
    # noqa
    # for annotation, does not need real import
    from sphinx.application import Sphinx
    from sphinx.builders import Builder


logger = logging.getLogger(__name__)


[docs] class AutomodsummEntry(AutosummaryEntry): """ A typed version of `~collections.namedtuple` representing an stub file entry for :rst:dir:`automodsumm`. Parameters ---------- name : `str` The objects fully qualified name of the object for which the stub file will be generated. path : `str` Absolute file path to the toctree directory. This is where the stub file will be placed. recursive : `bool` Specifies if stub file for modules and and sub-packages should be generated. template : `str` Name of the template file to be used in generating the stub file. """
[docs] class AutomodsummRenderer(AutosummaryRenderer): """ A helper class for retrieving and rendering :rst:dir:`automodsumm` templates when writing stub files. Parameters ---------- app : `sphinx.application.Sphinx` Instance of the `sphinx` application. """ def __init__(self, app: "Sphinx") -> None: # add plasmapy_sphinx templates directory to the overall templates path asumm_path = templates_dir relpath = os.path.relpath(asumm_path, start=app.srcdir) app.config.templates_path.append(relpath) super().__init__(app)
[docs] def render(self, template_name: str, context: Dict) -> str: """ Render a template file. The render will first search for the template in the path specified by the sphinx configuration value :confval:`templates_path`, then the `~plasmapy_sphinx.utils.templates_dir`, and finally the :rst:dir:`autosummary` templates directory. Upon finding the template, the values from the ``context`` dictionary will inserted into the template and returned. Parameters ---------- template_name : str Name of the template file. context: dict Dictionary of values to be rendered (inserted) into the template. """ if not template_name.endswith(".rst"): # if does not have '.rst' then objtype likely given for template_name template_name += ".rst" template = None for name in [template_name, "base.rst"]: for _path in ["", "automodsumm/", "autosummary/"]: try: template = self.env.get_template(_path + name) return template.render(context) except TemplateNotFound: pass if template is None: raise TemplateNotFound
[docs] class GenDocsFromAutomodsumm: """ Class used for stub file generation from :rst:dir:`automodapi` and :rst:dir:`automodsumm`. An instance of the class is connected to the Sphinx event :event:`builder-inited`, which is emitted when the builder object is created. """ _re = { "automodsumm": re.compile(r"^\n?(\s*)\.\.\s+automodsumm::\s*(\S+)\s*(?:\n|$)"), "automodapi": re.compile(r"^\n?(\s*)\.\.\s+automodapi::\s*(\S+)\s*(?:\n|$)"), "option": re.compile(r"^\n?(\s+):(\S*):\s*(\S.*|)\s*(?:\n|$)"), "currentmodule": re.compile( r"^\s*\.\.\s+(|\S+:)(current)?module::\s*([a-zA-Z0-9_.]+)\s*$" ), } """ Dictionary of regular expressions used for string matching a read document and identify key directives. """ app = None # type: "Sphinx" """Instance of the Sphinx application.""" logger = logger """ Instance of the `~sphinx.util.logging.SphinxLoggerAdapter` for report during builds. """
[docs] def __call__(self, app: "Sphinx"): """ Scan through source files, check for the :rst:dir:`automodsumm` and :rst:dir:`automodapi` directives, and auto generate any associated stub files. Parameters ---------- app : `~sphinx.application.Sphinx` Instance of the Sphinx application. .. note:: Adapted from :func:`sphinx.ext.autosummary.process_generate_options`. """ self.app = app genfiles = app.config.autosummary_generate if genfiles is True: env = app.builder.env genfiles = [ env.doc2path(x, base=None) for x in env.found_docs if os.path.isfile(env.doc2path(x)) ] elif genfiles is False: pass else: ext = list(app.config.source_suffix) genfiles = [ genfile + (ext[0] if not genfile.endswith(tuple(ext)) else "") for genfile in genfiles ] for entry in genfiles[:]: if not os.path.isfile(os.path.join(app.srcdir, entry)): self.logger.warning( __(f"automodsumm_generate: file not found: {entry}") ) genfiles.remove(entry) if not genfiles: return suffix = get_rst_suffix(app) if suffix is None: self.logger.warning( __( "automodsumm generates .rst files internally. " "But your source_suffix does not contain .rst. Skipped." ) ) return imported_members = app.config.autosummary_imported_members with mock(app.config.autosummary_mock_imports): self.generate_docs( genfiles, suffix=suffix, base_path=app.srcdir, imported_members=imported_members, overwrite=app.config.autosummary_generate_overwrite, encoding=app.config.source_encoding, )
[docs] def generate_docs( self, source_filenames: List[str], output_dir: str = None, suffix: str = ".rst", base_path: str = None, imported_members: bool = False, overwrite: bool = True, encoding: str = "utf-8", ) -> None: """ Generate and write stub files for objects defined in the :rst:dir:`automodapi` and :rst:dir:`automodsumm` directives. Parameters ---------- source_filenames : List[str] A list of all filenames for with the :rst:dir:`automodapi` and :rst:dir:`automodsumm` directives will be searched for. output_dir : `str` Directory for which the stub files will be written to. suffix : `str` (Default ``".rst"``) Suffix given to the written stub files. base_path : `str` The common base path for the filenames listed in ``source_filenames``. This is typically the source directory of the Sphinx application. imported_members : `bool` (Default `False`) Set `True` to include imported members in the stub file documentation for *module* object types. overwrite : `bool` (Default `True`) Will cause existing stub files to be overwritten. encoding : `str` (Default: ``"utf-8"``) Encoding for the written stub files. .. note:: Adapted from :func:`sphinx.ext.autosummary.generate.generate_autosummary_docs`. """ app = self.app _info = self.logger.info _warn = self.logger.warning showed_sources = list(sorted(source_filenames)) _info( __(f"[automodsumm] generating stub files for {len(showed_sources)} sources") ) if output_dir: _info(__(f"[automodsumm] writing to {output_dir}")) if base_path is not None: source_filenames = [ os.path.join(base_path, filename) for filename in source_filenames ] template = AutomodsummRenderer(app) # read items = self.find_in_files(source_filenames) # keep track of new files new_files = [] if app: filename_map = app.config.autosummary_filename_map else: filename_map = {} # write for entry in sorted(set(items), key=str): if entry.path is None: # The corresponding automodsumm:: directive did not have # a :toctree: option continue path = output_dir or os.path.abspath(entry.path) ensuredir(path) try: name, obj, parent, modname = import_by_name(entry.name) qualname = name.replace(modname + ".", "") except ImportError as e: try: # try to import as an instance attribute name, obj, parent, modname = import_ivar_by_name(entry.name) qualname = name.replace(modname + ".", "") except ImportError: _warn(__(f"[automodsumm] failed to import {entry.name}: {e}")) continue context = {} if app: context.update(app.config.autosummary_context) content = generate_autosummary_content( name, obj, parent, template, entry.template, imported_members, app, entry.recursive, context, modname, qualname, ) filename = os.path.join(path, filename_map.get(name, name) + suffix) if os.path.isfile(filename): with open(filename, encoding=encoding) as f: old_content = f.read() if content == old_content: continue elif overwrite: # content has changed with open(filename, "w", encoding=encoding) as f: f.write(content) new_files.append(filename) else: with open(filename, "w", encoding=encoding) as f: f.write(content) new_files.append(filename) # descend recursively to new files if new_files: self.generate_docs( new_files, output_dir=output_dir, suffix=suffix, base_path=base_path, imported_members=imported_members, overwrite=overwrite, )
[docs] def find_in_files(self, filenames: List[str]) -> List[AutomodsummEntry]: """ Search files for the :rst:dir:`automodapi` and :rst:dir:`automodsumm` directives and generate a list of `~plasmapy_sphinx.automodsumm.generate.AutomodsummEntry`'s indicating which stub files need to be generated. Parameters ---------- filenames : List[str] List of filenames to be searched. .. note:: Adapted from :func:`sphinx.ext.autosummary.generate.find_autosummary_in_files`. """ documented = [] # type: List[AutomodsummEntry] for filename in filenames: with open(filename, encoding="utf-8", errors="ignore") as f: lines = f.read().splitlines() documented.extend(self.find_in_lines(lines, filename=filename)) return documented
[docs] def find_in_lines( self, lines: List[str], filename: str = None, ) -> List[AutomodsummEntry]: """ Search a list of strings for the :rst:dir:`automodapi` and :rst:dir:`automodsumm` directives and generate a list of `~plasmapy_sphinx.automodsumm.generate.AutomodsummEntry`'s indicating which stub files need to be generated. Parameters ---------- lines : List[str] List of strings to be searched. filename : str The file from which ``lines`` came from. .. note:: Adapted from :func:`sphinx.ext.autosummary.generate.find_autosummary_in_lines`. """ from ..autodoc.automodapi import AutomodapiOptions from .core import AutomodsummOptions documented = [] # type: List[AutomodsummEntry] current_module = None modname = "" options = {} # type: Dict[str, Any] _option_cls = None in_automodapi_directive = False gather_objs = False last_line = False nlines = len(lines) for ii, line in enumerate(lines): if ii == nlines - 1: last_line = True # looking for option ` :option: option_args` if in_automodapi_directive: match = self._re["option"].search(line) if match is not None: option_name = match.group(2) option_args = match.group(3) try: option_args = _option_cls.option_spec[option_name](option_args) options[option_name] = option_args except (KeyError, TypeError): pass else: # done reading options in_automodapi_directive = False gather_objs = True if last_line: # end of lines reached in_automodapi_directive = False gather_objs = True if in_automodapi_directive: continue # looking for `.. automodsumm:: <modname>` match = self._re["automodsumm"].search(line) if match is not None: in_automodapi_directive = True # base_indent = match.group(1) modname = match.group(2) if current_module is None or modname == current_module: pass elif not modname.startswith(f"{current_module}."): modname = f"{current_module}.{modname}" _option_cls = AutomodsummOptions self.logger.info(f"[automodsumm] {modname}") if last_line: # end of lines reached in_automodapi_directive = False gather_objs = True else: continue # looking for `.. automodapi:: <modname>` match = self._re["automodapi"].search(line) if match is not None: in_automodapi_directive = True # base_indent = match.group(1) modname = match.group(2) if current_module is None or modname == current_module: pass elif not modname.startswith(f"{current_module}."): modname = f"{current_module}.{modname}" _option_cls = AutomodapiOptions if last_line: # end of lines reached in_automodapi_directive = False gather_objs = True else: continue # looking for `.. py:currentmodule:: <current_module>` match = self._re["currentmodule"].search(line) if match is not None: current_module = match.group(3) continue # gather objects and update documented list if gather_objs: process_options = _option_cls( self.app, modname, options, docname=filename, _warn=self.logger.warning, ) options = { "toctree": process_options.toctree["abspath"], "template": process_options.options.get("template", None), "recursive": process_options.options.get("recursive", False), } exclude_modules = ( not self.app.config.automodapi_generate_module_stub_files ) obj_list = process_options.generate_obj_list( exclude_modules=exclude_modules ) for name in obj_list: documented.append( AutomodsummEntry( name=name, path=options["toctree"], template=options["template"], recursive=options["recursive"], ) ) self.logger.info( f"[automodsumm stub file gen] collected {len(obj_list):4d} " f"object(s) in '{modname}'" ) # reset for next search options = {} gather_objs = False _option_cls = None return documented
[docs] @staticmethod def event_handler__autodoc_skip_member( app: "Sphinx", what: str, name: str, obj: Any, skip: bool, options: dict ): # noqa """ Event handler for the Sphinx event :event:`autodoc-skip-member`. This handler ensures the ``__call__`` method is documented if defined by the associated class. """ if what != "method": return if name == "__call__": return False return