""" sphinx.ext.autodoc.directive ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ :copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS. :license: BSD, see LICENSE for details. """ import warnings from typing import Any, Callable, Dict, List, Set from docutils import nodes from docutils.nodes import Element, Node from docutils.parsers.rst.states import RSTState, Struct from docutils.statemachine import StringList from docutils.utils import Reporter, assemble_option_dict from sphinx.config import Config from sphinx.deprecation import RemovedInSphinx40Warning from sphinx.environment import BuildEnvironment from sphinx.ext.autodoc import Documenter, Options from sphinx.util import logging from sphinx.util.docutils import SphinxDirective, switch_source_input from sphinx.util.nodes import nested_parse_with_titles if False: # For type annotation from typing import Type # for python3.5.1 logger = logging.getLogger(__name__) # common option names for autodoc directives AUTODOC_DEFAULT_OPTIONS = ['members', 'undoc-members', 'inherited-members', 'show-inheritance', 'private-members', 'special-members', 'ignore-module-all', 'exclude-members', 'member-order', 'imported-members'] class DummyOptionSpec(dict): """An option_spec allows any options.""" def __bool__(self) -> bool: """Behaves like some options are defined.""" return True def __getitem__(self, key: str) -> Callable[[str], str]: return lambda x: x class DocumenterBridge: """A parameters container for Documenters.""" def __init__(self, env: BuildEnvironment, reporter: Reporter, options: Options, lineno: int, state: Any = None) -> None: self.env = env self.reporter = reporter self.genopt = options self.lineno = lineno self.filename_set = set() # type: Set[str] self.result = StringList() if state: self.state = state else: # create fake object for self.state.document.settings.tab_width warnings.warn('DocumenterBridge requires a state object on instantiation.', RemovedInSphinx40Warning, stacklevel=2) settings = Struct(tab_width=8) document = Struct(settings=settings) self.state = Struct(document=document) def warn(self, msg: str) -> None: logger.warning(msg, location=(self.env.docname, self.lineno)) def process_documenter_options(documenter: "Type[Documenter]", config: Config, options: Dict ) -> Options: """Recognize options of Documenter from user input.""" for name in AUTODOC_DEFAULT_OPTIONS: if name not in documenter.option_spec: continue else: negated = options.pop('no-' + name, True) is None if name in config.autodoc_default_options and not negated: options[name] = config.autodoc_default_options[name] return Options(assemble_option_dict(options.items(), documenter.option_spec)) def parse_generated_content(state: RSTState, content: StringList, documenter: Documenter ) -> List[Node]: """Parse a generated content by Documenter.""" with switch_source_input(state, content): if documenter.titles_allowed: node = nodes.section() # type: Element # necessary so that the child nodes get the right source/line set node.document = state.document nested_parse_with_titles(state, content, node) else: node = nodes.paragraph() node.document = state.document state.nested_parse(content, 0, node) return node.children class AutodocDirective(SphinxDirective): """A directive class for all autodoc directives. It works as a dispatcher of Documenters. It invokes a Documenter on running. After the processing, it parses and returns the generated content by Documenter. """ option_spec = DummyOptionSpec() has_content = True required_arguments = 1 optional_arguments = 0 final_argument_whitespace = True def run(self) -> List[Node]: reporter = self.state.document.reporter try: source, lineno = reporter.get_source_and_line(self.lineno) # type: ignore except AttributeError: source, lineno = (None, None) logger.debug('[autodoc] %s:%s: input:\n%s', source, lineno, self.block_text) # look up target Documenter objtype = self.name[4:] # strip prefix (auto-). doccls = self.env.app.registry.documenters[objtype] # process the options with the selected documenter's option_spec try: documenter_options = process_documenter_options(doccls, self.config, self.options) except (KeyError, ValueError, TypeError) as exc: # an option is either unknown or has a wrong type logger.error('An option to %s is either unknown or has an invalid value: %s' % (self.name, exc), location=(self.env.docname, lineno)) return [] # generate the output params = DocumenterBridge(self.env, reporter, documenter_options, lineno, self.state) documenter = doccls(params, self.arguments[0]) documenter.generate(more_content=self.content) if not params.result: return [] logger.debug('[autodoc] output:\n%s', '\n'.join(params.result)) # record all filenames as dependencies -- this will at least # partially make automatic invalidation possible for fn in params.filename_set: self.state.document.settings.record_dependencies.add(fn) result = parse_generated_content(self.state, params.result, documenter) return result