from __future__ import annotations from typing import TYPE_CHECKING, Any, Callable from docutils import nodes from docutils.statemachine import StringList from docutils.utils import Reporter, assemble_option_dict 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 TYPE_CHECKING: from docutils.nodes import Element, Node from docutils.parsers.rst.states import RSTState from sphinx.config import Config from sphinx.environment import BuildEnvironment 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-doc-from', 'no-value'] AUTODOC_EXTENDABLE_OPTIONS = ['members', 'private-members', 'special-members', 'exclude-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 | None, options: Options, lineno: int, state: Any) -> None: self.env = env self._reporter = reporter self.genopt = options self.lineno = lineno self.record_dependencies: set[str] = set() self.result = StringList() self.state = state 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 negated = options.pop('no-' + name, True) is None if name in config.autodoc_default_options and not negated: if name in options and isinstance(config.autodoc_default_options[name], str): # take value from options if present or extend it # with autodoc_default_options if necessary if name in AUTODOC_EXTENDABLE_OPTIONS: if options[name] is not None and options[name].startswith('+'): options[name] = ','.join([config.autodoc_default_options[name], options[name][1:]]) else: options[name] = config.autodoc_default_options[name] elif options.get(name) is not None: # remove '+' from option argument if there's nothing to merge it with options[name] = options[name].lstrip('+') return Options(assemble_option_dict(options.items(), documenter.option_spec)) def parse_generated_content(state: RSTState, content: StringList, documenter: Documenter, ) -> list[Node]: """Parse an item of content generated by Documenter.""" with switch_source_input(state, content): if documenter.titles_allowed: node: Element = nodes.section() # 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 upon running. After the processing, it parses and returns the content generated 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( # type: ignore[attr-defined] self.lineno) 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.record_dependencies: self.state.document.settings.record_dependencies.add(fn) result = parse_generated_content(self.state, params.result, documenter) return result