summaryrefslogtreecommitdiffstats
path: root/sphinx/ext/autodoc/directive.py
blob: 64cbc9b52cd7292ed1dbfeda0dd9aed45157d270 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
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