1# -*- coding: utf-8 -*-
2"""
3Sphinx Auto-API Top-level Extension.
4
5This extension allows you to automagically generate API documentation from your project.
6"""
7import io
8import os
9import shutil
10import sys
11import warnings
12
13import sphinx
14from sphinx.util.console import darkgreen, bold
15from sphinx.addnodes import toctree
16from sphinx.errors import ExtensionError
17import sphinx.util.logging
18from docutils.parsers.rst import directives
19
20from . import documenters
21from .backends import (
22    DEFAULT_FILE_PATTERNS,
23    DEFAULT_IGNORE_PATTERNS,
24    LANGUAGE_MAPPERS,
25    LANGUAGE_REQUIREMENTS,
26)
27from .directives import AutoapiSummary, NestedParse
28from .inheritance_diagrams import AutoapiInheritanceDiagram
29from .settings import API_ROOT
30from .toctree import add_domain_to_toctree
31
32LOGGER = sphinx.util.logging.getLogger(__name__)
33
34_DEFAULT_OPTIONS = [
35    "members",
36    "undoc-members",
37    "private-members",
38    "show-inheritance",
39    "show-module-summary",
40    "special-members",
41    "imported-members",
42]
43_VIEWCODE_CACHE = {}
44"""Caches a module's parse results for use in viewcode.
45
46:type: dict(str, tuple)
47"""
48
49
50class RemovedInAutoAPI2Warning(DeprecationWarning):
51    """Indicates something that will be removed in sphinx-autoapi v2."""
52
53
54if "PYTHONWARNINGS" not in os.environ:
55    warnings.filterwarnings("default", category=RemovedInAutoAPI2Warning)
56
57
58def _normalise_autoapi_dirs(autoapi_dirs, srcdir):
59    normalised_dirs = []
60
61    if isinstance(autoapi_dirs, str):
62        autoapi_dirs = [autoapi_dirs]
63    for path in autoapi_dirs:
64        if os.path.isabs(path):
65            normalised_dirs.append(path)
66        else:
67            normalised_dirs.append(os.path.normpath(os.path.join(srcdir, path)))
68
69    return normalised_dirs
70
71
72def run_autoapi(app):  # pylint: disable=too-many-branches
73    """
74    Load AutoAPI data from the filesystem.
75    """
76    if app.config.autoapi_type not in LANGUAGE_MAPPERS:
77        raise ExtensionError(
78            "Invalid autoapi_type setting, "
79            "following values is allowed: {}".format(
80                ", ".join(
81                    '"{}"'.format(api_type) for api_type in sorted(LANGUAGE_MAPPERS)
82                )
83            )
84        )
85
86    if not app.config.autoapi_dirs:
87        raise ExtensionError("You must configure an autoapi_dirs setting")
88
89    if app.config.autoapi_include_summaries is not None:
90        warnings.warn(
91            "autoapi_include_summaries has been replaced by "
92            "the show-module-summary AutoAPI option\n",
93            RemovedInAutoAPI2Warning,
94        )
95        if app.config.autoapi_include_summaries:
96            app.config.autoapi_options.append("show-module-summary")
97
98    # Make sure the paths are full
99    normalised_dirs = _normalise_autoapi_dirs(app.config.autoapi_dirs, app.srcdir)
100    for _dir in normalised_dirs:
101        if not os.path.exists(_dir):
102            raise ExtensionError(
103                "AutoAPI Directory `{dir}` not found. "
104                "Please check your `autoapi_dirs` setting.".format(dir=_dir)
105            )
106
107    normalized_root = os.path.normpath(
108        os.path.join(app.srcdir, app.config.autoapi_root)
109    )
110    url_root = os.path.join("/", app.config.autoapi_root)
111
112    if not all(
113        import_name in sys.modules
114        for _, import_name in LANGUAGE_REQUIREMENTS[app.config.autoapi_type]
115    ):
116        raise ExtensionError(
117            "AutoAPI of type `{type}` requires following "
118            "packages to be installed and included in extensions list: "
119            "{packages}".format(
120                type=app.config.autoapi_type,
121                packages=", ".join(
122                    '{import_name} (available as "{pkg_name}" on PyPI)'.format(
123                        pkg_name=pkg_name, import_name=import_name
124                    )
125                    for pkg_name, import_name in LANGUAGE_REQUIREMENTS[
126                        app.config.autoapi_type
127                    ]
128                ),
129            )
130        )
131
132    sphinx_mapper = LANGUAGE_MAPPERS[app.config.autoapi_type]
133    template_dir = app.config.autoapi_template_dir
134    if template_dir and not os.path.isabs(template_dir):
135        if not os.path.isdir(template_dir):
136            template_dir = os.path.join(app.srcdir, app.config.autoapi_template_dir)
137        elif app.srcdir != os.getcwd():
138            warnings.warn(
139                "autoapi_template_dir will be expected to be "
140                "relative to the Sphinx source directory instead of "
141                "relative to where sphinx-build is run\n",
142                RemovedInAutoAPI2Warning,
143            )
144    sphinx_mapper_obj = sphinx_mapper(app, template_dir=template_dir, url_root=url_root)
145
146    if app.config.autoapi_file_patterns:
147        file_patterns = app.config.autoapi_file_patterns
148    else:
149        file_patterns = DEFAULT_FILE_PATTERNS.get(app.config.autoapi_type, [])
150
151    if app.config.autoapi_ignore:
152        ignore_patterns = app.config.autoapi_ignore
153    else:
154        ignore_patterns = DEFAULT_IGNORE_PATTERNS.get(app.config.autoapi_type, [])
155
156    if ".rst" in app.config.source_suffix:
157        out_suffix = ".rst"
158    elif ".txt" in app.config.source_suffix:
159        out_suffix = ".txt"
160    else:
161        # Fallback to first suffix listed
162        out_suffix = app.config.source_suffix[0]
163
164    if sphinx_mapper_obj.load(
165        patterns=file_patterns, dirs=normalised_dirs, ignore=ignore_patterns
166    ):
167        sphinx_mapper_obj.map(options=app.config.autoapi_options)
168
169        if app.config.autoapi_generate_api_docs:
170            sphinx_mapper_obj.output_rst(root=normalized_root, source_suffix=out_suffix)
171
172
173def build_finished(app, exception):
174    if not app.config.autoapi_keep_files and app.config.autoapi_generate_api_docs:
175        normalized_root = os.path.normpath(
176            os.path.join(app.srcdir, app.config.autoapi_root)
177        )
178        if app.verbosity > 1:
179            LOGGER.info(bold("[AutoAPI] ") + darkgreen("Cleaning generated .rst files"))
180        shutil.rmtree(normalized_root)
181
182        sphinx_mapper = LANGUAGE_MAPPERS[app.config.autoapi_type]
183        if hasattr(sphinx_mapper, "build_finished"):
184            sphinx_mapper.build_finished(app, exception)
185
186
187def source_read(app, docname, source):  # pylint: disable=unused-argument
188    # temp_data is cleared after each source file has been processed,
189    # so populate the annotations at the beginning of every file read.
190    app.env.temp_data["annotations"] = getattr(app.env, "autoapi_annotations", {})
191
192
193def doctree_read(app, doctree):
194    """
195    Inject AutoAPI into the TOC Tree dynamically.
196    """
197
198    add_domain_to_toctree(app, doctree, app.env.docname)
199
200    if app.env.docname == "index":
201        all_docs = set()
202        insert = True
203        nodes = list(doctree.traverse(toctree))
204        toc_entry = "%s/index" % app.config.autoapi_root
205        add_entry = (
206            nodes
207            and app.config.autoapi_generate_api_docs
208            and app.config.autoapi_add_toctree_entry
209        )
210        if not add_entry:
211            return
212        # Capture all existing toctree entries
213        for node in nodes:
214            for entry in node["entries"]:
215                all_docs.add(entry[1])
216        # Don't insert autoapi it's already present
217        for doc in all_docs:
218            if doc.find(app.config.autoapi_root) != -1:
219                insert = False
220        if insert and app.config.autoapi_add_toctree_entry:
221            # Insert AutoAPI index
222            nodes[-1]["entries"].append((None, u"%s/index" % app.config.autoapi_root))
223            nodes[-1]["includefiles"].append(u"%s/index" % app.config.autoapi_root)
224            message_prefix = bold("[AutoAPI] ")
225            message = darkgreen(
226                "Adding AutoAPI TOCTree [{0}] to index.rst".format(toc_entry)
227            )
228            LOGGER.info(message_prefix + message)
229
230
231def viewcode_find(app, modname):
232    objects = app.env.autoapi_objects
233    if modname not in objects:
234        return None
235
236    if modname in _VIEWCODE_CACHE:
237        return _VIEWCODE_CACHE[modname]
238
239    locations = {}
240    module = objects[modname]
241    for child in module.children:
242        stack = [("", child)]
243        while stack:
244            prefix, obj = stack.pop()
245            type_ = "other"
246            if obj.type == "class":
247                type_ = "class"
248            elif obj.type in ("function", "method"):
249                type_ = "def"
250            full_name = prefix + obj.name
251            if "from_line_no" in obj.obj:
252                locations[full_name] = (
253                    type_,
254                    obj.obj["from_line_no"],
255                    obj.obj["to_line_no"],
256                )
257            children = getattr(obj, "children", ())
258            stack.extend((full_name + ".", gchild) for gchild in children)
259
260    if module.obj["encoding"]:
261        source = io.open(
262            module.obj["file_path"], encoding=module.obj["encoding"]
263        ).read()
264    else:
265        source = open(module.obj["file_path"]).read()
266
267    result = (source, locations)
268    _VIEWCODE_CACHE[modname] = result
269    return result
270
271
272def viewcode_follow_imported(app, modname, attribute):
273    fullname = "{}.{}".format(modname, attribute)
274    all_objects = app.env.autoapi_all_objects
275    if fullname not in all_objects:
276        return None
277
278    orig_path = all_objects[fullname].obj.get("original_path", "")
279    if orig_path.endswith(attribute):
280        return orig_path[: -len(attribute) - 1]
281
282    return modname
283
284
285def setup(app):
286    app.connect("builder-inited", run_autoapi)
287    app.connect("source-read", source_read)
288    app.connect("doctree-read", doctree_read)
289    app.connect("build-finished", build_finished)
290    if "viewcode-find-source" in app.events.events:
291        app.connect("viewcode-find-source", viewcode_find)
292    if "viewcode-follow-imported" in app.events.events:
293        app.connect("viewcode-follow-imported", viewcode_follow_imported)
294    app.add_config_value("autoapi_type", "python", "html")
295    app.add_config_value("autoapi_root", API_ROOT, "html")
296    app.add_config_value("autoapi_ignore", [], "html")
297    app.add_config_value("autoapi_options", _DEFAULT_OPTIONS, "html")
298    app.add_config_value("autoapi_member_order", "bysource", "html")
299    app.add_config_value("autoapi_file_patterns", None, "html")
300    app.add_config_value("autoapi_dirs", [], "html")
301    app.add_config_value("autoapi_keep_files", False, "html")
302    app.add_config_value("autoapi_add_toctree_entry", True, "html")
303    app.add_config_value("autoapi_template_dir", None, "html")
304    app.add_config_value("autoapi_include_summaries", None, "html")
305    app.add_config_value("autoapi_python_use_implicit_namespaces", False, "html")
306    app.add_config_value("autoapi_python_class_content", "class", "html")
307    app.add_config_value("autoapi_generate_api_docs", True, "html")
308    app.add_config_value("autoapi_prepare_jinja_env", None, "html")
309    app.add_autodocumenter(documenters.AutoapiFunctionDocumenter)
310    app.add_autodocumenter(documenters.AutoapiPropertyDocumenter)
311    app.add_autodocumenter(documenters.AutoapiDecoratorDocumenter)
312    app.add_autodocumenter(documenters.AutoapiClassDocumenter)
313    app.add_autodocumenter(documenters.AutoapiMethodDocumenter)
314    app.add_autodocumenter(documenters.AutoapiDataDocumenter)
315    app.add_autodocumenter(documenters.AutoapiAttributeDocumenter)
316    app.add_autodocumenter(documenters.AutoapiModuleDocumenter)
317    app.add_autodocumenter(documenters.AutoapiExceptionDocumenter)
318    directives.register_directive("autoapi-nested-parse", NestedParse)
319    directives.register_directive("autoapisummary", AutoapiSummary)
320    app.setup_extension("sphinx.ext.autosummary")
321    app.add_event("autoapi-skip-member")
322    app.setup_extension("sphinx.ext.inheritance_diagram")
323    app.add_directive("autoapi-inheritance-diagram", AutoapiInheritanceDiagram)
324
325    return {
326        "parallel_read_safe": False,
327        "parallel_write_safe": True,
328    }
329