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