sphinx-autoapi/autoapi/extension.py

331 lines
12 KiB
Python
Raw Normal View History

2015-03-27 19:50:56 +00:00
# -*- coding: utf-8 -*-
"""
Sphinx Auto-API Top-level Extension.
This extension allows you to automagically generate API documentation from your project.
2015-03-27 19:50:56 +00:00
"""
2019-08-25 22:53:58 +00:00
import io
import os
2015-04-07 20:35:50 +00:00
import shutil
import sys
import warnings
2015-03-27 19:50:56 +00:00
2017-02-08 14:53:19 +00:00
import sphinx
2015-06-10 18:35:54 +00:00
from sphinx.util.console import darkgreen, bold
from sphinx.addnodes import toctree
2015-08-03 20:55:33 +00:00
from sphinx.errors import ExtensionError
import sphinx.util.logging
from docutils.parsers.rst import directives
2015-06-10 18:35:54 +00:00
2018-08-01 23:04:45 +00:00
from . import documenters
2019-01-27 05:20:45 +00:00
from .backends import (
2019-10-05 22:11:23 +00:00
DEFAULT_FILE_PATTERNS,
DEFAULT_IGNORE_PATTERNS,
LANGUAGE_MAPPERS,
LANGUAGE_REQUIREMENTS,
2019-01-27 05:20:45 +00:00
)
2017-08-31 23:42:47 +00:00
from .directives import AutoapiSummary, NestedParse
from .inheritance_diagrams import AutoapiInheritanceDiagram
from .settings import API_ROOT
from .toctree import add_domain_to_toctree
2015-04-01 00:01:41 +00:00
LOGGER = sphinx.util.logging.getLogger(__name__)
_DEFAULT_OPTIONS = [
"members",
"undoc-members",
"private-members",
"show-inheritance",
"show-module-summary",
"special-members",
"imported-members",
]
2019-10-05 22:11:23 +00:00
_VIEWCODE_CACHE = {}
2018-05-11 00:49:32 +00:00
"""Caches a module's parse results for use in viewcode.
:type: dict(str, tuple)
"""
2015-04-01 00:01:41 +00:00
class RemovedInAutoAPI2Warning(DeprecationWarning):
"""Indicates something that will be removed in sphinx-autoapi v2."""
if "PYTHONWARNINGS" not in os.environ:
warnings.filterwarnings("default", category=RemovedInAutoAPI2Warning)
def _normalise_autoapi_dirs(autoapi_dirs, srcdir):
normalised_dirs = []
if isinstance(autoapi_dirs, str):
autoapi_dirs = [autoapi_dirs]
for path in autoapi_dirs:
if os.path.isabs(path):
normalised_dirs.append(path)
else:
normalised_dirs.append(os.path.normpath(os.path.join(srcdir, path)))
return normalised_dirs
2019-10-05 22:11:23 +00:00
def run_autoapi(app): # pylint: disable=too-many-branches
"""
Load AutoAPI data from the filesystem.
"""
2019-10-05 22:11:23 +00:00
if app.config.autoapi_type not in LANGUAGE_MAPPERS:
raise ExtensionError(
"Invalid autoapi_type setting, "
"following values is allowed: {}".format(
", ".join(
2019-10-05 23:09:26 +00:00
'"{}"'.format(api_type) for api_type in sorted(LANGUAGE_MAPPERS)
)
)
)
if not app.config.autoapi_dirs:
2019-01-27 05:20:45 +00:00
raise ExtensionError("You must configure an autoapi_dirs setting")
2015-06-10 18:35:54 +00:00
if app.config.autoapi_include_summaries is not None:
warnings.warn(
"autoapi_include_summaries has been replaced by "
"the show-module-summary AutoAPI option\n",
RemovedInAutoAPI2Warning,
)
if app.config.autoapi_include_summaries:
app.config.autoapi_options.append("show-module-summary")
# Make sure the paths are full
normalised_dirs = _normalise_autoapi_dirs(app.config.autoapi_dirs, app.srcdir)
for _dir in normalised_dirs:
if not os.path.exists(_dir):
raise ExtensionError(
2019-01-27 05:20:45 +00:00
"AutoAPI Directory `{dir}` not found. "
"Please check your `autoapi_dirs` setting.".format(dir=_dir)
)
2019-01-27 05:20:45 +00:00
normalized_root = os.path.normpath(
2021-04-06 22:46:00 +00:00
os.path.join(app.srcdir, app.config.autoapi_root)
2019-01-27 05:20:45 +00:00
)
url_root = os.path.join("/", app.config.autoapi_root)
if not all(
import_name in sys.modules
2019-10-05 22:11:23 +00:00
for _, import_name in LANGUAGE_REQUIREMENTS[app.config.autoapi_type]
):
raise ExtensionError(
"AutoAPI of type `{type}` requires following "
"packages to be installed and included in extensions list: "
"{packages}".format(
type=app.config.autoapi_type,
packages=", ".join(
'{import_name} (available as "{pkg_name}" on PyPI)'.format(
pkg_name=pkg_name, import_name=import_name
)
2019-10-05 22:11:23 +00:00
for pkg_name, import_name in LANGUAGE_REQUIREMENTS[
app.config.autoapi_type
]
),
)
)
2019-10-05 22:11:23 +00:00
sphinx_mapper = LANGUAGE_MAPPERS[app.config.autoapi_type]
template_dir = app.config.autoapi_template_dir
if template_dir and not os.path.isabs(template_dir):
if not os.path.isdir(template_dir):
template_dir = os.path.join(app.srcdir, app.config.autoapi_template_dir)
elif app.srcdir != os.getcwd():
warnings.warn(
"autoapi_template_dir will be expected to be "
"relative to the Sphinx source directory instead of "
"relative to where sphinx-build is run\n",
RemovedInAutoAPI2Warning,
)
sphinx_mapper_obj = sphinx_mapper(app, template_dir=template_dir, url_root=url_root)
2015-06-10 18:35:54 +00:00
2015-07-07 23:32:38 +00:00
if app.config.autoapi_file_patterns:
2015-09-22 15:02:34 +00:00
file_patterns = app.config.autoapi_file_patterns
else:
2019-10-05 22:11:23 +00:00
file_patterns = DEFAULT_FILE_PATTERNS.get(app.config.autoapi_type, [])
2015-07-20 18:37:31 +00:00
if app.config.autoapi_ignore:
ignore_patterns = app.config.autoapi_ignore
else:
2019-10-05 22:11:23 +00:00
ignore_patterns = DEFAULT_IGNORE_PATTERNS.get(app.config.autoapi_type, [])
2015-07-20 18:37:31 +00:00
2019-01-27 05:20:45 +00:00
if ".rst" in app.config.source_suffix:
out_suffix = ".rst"
elif ".txt" in app.config.source_suffix:
out_suffix = ".txt"
2016-08-25 17:58:06 +00:00
else:
# Fallback to first suffix listed
out_suffix = next(iter(app.config.source_suffix))
2016-08-25 17:58:06 +00:00
if sphinx_mapper_obj.load(
patterns=file_patterns, dirs=normalised_dirs, ignore=ignore_patterns
):
sphinx_mapper_obj.map(options=app.config.autoapi_options)
2015-06-10 18:35:54 +00:00
if app.config.autoapi_generate_api_docs:
sphinx_mapper_obj.output_rst(root=normalized_root, source_suffix=out_suffix)
2015-06-10 18:35:54 +00:00
2015-03-27 19:50:56 +00:00
2015-04-07 20:35:50 +00:00
def build_finished(app, exception):
2018-08-10 16:00:54 +00:00
if not app.config.autoapi_keep_files and app.config.autoapi_generate_api_docs:
2019-01-27 05:20:45 +00:00
normalized_root = os.path.normpath(
os.path.join(app.srcdir, app.config.autoapi_root)
2019-01-27 05:20:45 +00:00
)
2015-04-07 20:35:50 +00:00
if app.verbosity > 1:
2019-01-27 05:20:45 +00:00
LOGGER.info(bold("[AutoAPI] ") + darkgreen("Cleaning generated .rst files"))
shutil.rmtree(normalized_root)
2015-04-07 20:35:50 +00:00
2019-10-05 22:11:23 +00:00
sphinx_mapper = LANGUAGE_MAPPERS[app.config.autoapi_type]
2019-01-27 05:20:45 +00:00
if hasattr(sphinx_mapper, "build_finished"):
2016-11-04 20:42:15 +00:00
sphinx_mapper.build_finished(app, exception)
2015-04-07 20:35:50 +00:00
def source_read(app, docname, source): # pylint: disable=unused-argument
# temp_data is cleared after each source file has been processed,
# so populate the annotations at the beginning of every file read.
app.env.temp_data["annotations"] = getattr(app.env, "autoapi_annotations", {})
def doctree_read(app, doctree):
"""
Inject AutoAPI into the TOC Tree dynamically.
"""
if app.config.autoapi_add_objects_to_toctree:
add_domain_to_toctree(app, doctree, app.env.docname)
2019-01-27 05:20:45 +00:00
if app.env.docname == "index":
all_docs = set()
insert = True
2021-02-15 13:37:52 +00:00
nodes = list(doctree.traverse(toctree))
2019-01-27 05:20:45 +00:00
toc_entry = "%s/index" % app.config.autoapi_root
add_entry = (
nodes
and app.config.autoapi_generate_api_docs
and app.config.autoapi_add_toctree_entry
)
if not add_entry:
return
# Capture all existing toctree entries
for node in nodes:
2019-01-27 05:20:45 +00:00
for entry in node["entries"]:
all_docs.add(entry[1])
# Don't insert autoapi it's already present
for doc in all_docs:
2015-07-07 22:48:00 +00:00
if doc.find(app.config.autoapi_root) != -1:
insert = False
if insert and app.config.autoapi_add_toctree_entry:
# Insert AutoAPI index
2022-02-09 03:37:55 +00:00
nodes[-1]["entries"].append((None, "%s/index" % app.config.autoapi_root))
nodes[-1]["includefiles"].append("%s/index" % app.config.autoapi_root)
2019-01-27 05:20:45 +00:00
message_prefix = bold("[AutoAPI] ")
2018-08-07 21:33:42 +00:00
message = darkgreen(
2019-01-27 05:20:45 +00:00
"Adding AutoAPI TOCTree [{0}] to index.rst".format(toc_entry)
2018-08-07 21:33:42 +00:00
)
LOGGER.info(message_prefix + message)
2018-05-11 00:49:32 +00:00
def viewcode_find(app, modname):
objects = app.env.autoapi_objects
if modname not in objects:
2018-05-11 00:49:32 +00:00
return None
2019-10-05 22:11:23 +00:00
if modname in _VIEWCODE_CACHE:
return _VIEWCODE_CACHE[modname]
2018-05-11 00:49:32 +00:00
locations = {}
module = objects[modname]
2018-05-11 00:49:32 +00:00
for child in module.children:
2019-01-27 05:20:45 +00:00
stack = [("", child)]
2018-05-11 00:49:32 +00:00
while stack:
prefix, obj = stack.pop()
2019-01-27 05:20:45 +00:00
type_ = "other"
if obj.type == "class":
type_ = "class"
elif obj.type in ("function", "method"):
type_ = "def"
2018-05-11 00:49:32 +00:00
full_name = prefix + obj.name
2019-01-27 05:20:45 +00:00
if "from_line_no" in obj.obj:
locations[full_name] = (
2019-01-27 05:20:45 +00:00
type_,
obj.obj["from_line_no"],
obj.obj["to_line_no"],
)
2019-01-27 05:20:45 +00:00
children = getattr(obj, "children", ())
stack.extend((full_name + ".", gchild) for gchild in children)
2018-05-11 00:49:32 +00:00
2019-01-27 05:20:45 +00:00
if module.obj["encoding"]:
2019-08-25 22:53:58 +00:00
source = io.open(
2019-01-27 05:20:45 +00:00
module.obj["file_path"], encoding=module.obj["encoding"]
).read()
else:
2019-01-27 05:20:45 +00:00
source = open(module.obj["file_path"]).read()
result = (source, locations)
2019-10-05 22:11:23 +00:00
_VIEWCODE_CACHE[modname] = result
2018-05-11 00:49:32 +00:00
return result
def viewcode_follow_imported(app, modname, attribute):
2019-01-27 05:20:45 +00:00
fullname = "{}.{}".format(modname, attribute)
all_objects = app.env.autoapi_all_objects
if fullname not in all_objects:
return None
orig_path = all_objects[fullname].obj.get("original_path", "")
if orig_path.endswith(attribute):
2019-01-27 05:20:45 +00:00
return orig_path[: -len(attribute) - 1]
return modname
2015-03-27 19:50:56 +00:00
def setup(app):
2019-01-27 05:20:45 +00:00
app.connect("builder-inited", run_autoapi)
app.connect("source-read", source_read)
2019-01-27 05:20:45 +00:00
app.connect("doctree-read", doctree_read)
app.connect("build-finished", build_finished)
if "viewcode-find-source" in app.events.events:
app.connect("viewcode-find-source", viewcode_find)
if "viewcode-follow-imported" in app.events.events:
app.connect("viewcode-follow-imported", viewcode_follow_imported)
2019-01-27 05:20:45 +00:00
app.add_config_value("autoapi_type", "python", "html")
app.add_config_value("autoapi_root", API_ROOT, "html")
app.add_config_value("autoapi_ignore", [], "html")
2019-10-05 22:11:23 +00:00
app.add_config_value("autoapi_options", _DEFAULT_OPTIONS, "html")
app.add_config_value("autoapi_member_order", "bysource", "html")
2019-01-27 05:20:45 +00:00
app.add_config_value("autoapi_file_patterns", None, "html")
app.add_config_value("autoapi_dirs", [], "html")
app.add_config_value("autoapi_keep_files", False, "html")
app.add_config_value("autoapi_add_toctree_entry", True, "html")
app.add_config_value("autoapi_template_dir", None, "html")
app.add_config_value("autoapi_include_summaries", None, "html")
app.add_config_value("autoapi_python_use_implicit_namespaces", False, "html")
2019-01-27 05:20:45 +00:00
app.add_config_value("autoapi_python_class_content", "class", "html")
app.add_config_value("autoapi_generate_api_docs", True, "html")
app.add_config_value("autoapi_prepare_jinja_env", None, "html")
app.add_config_value("autoapi_add_objects_to_toctree", True, "html")
2018-08-01 23:04:45 +00:00
app.add_autodocumenter(documenters.AutoapiFunctionDocumenter)
app.add_autodocumenter(documenters.AutoapiPropertyDocumenter)
app.add_autodocumenter(documenters.AutoapiDecoratorDocumenter)
2018-08-01 23:04:45 +00:00
app.add_autodocumenter(documenters.AutoapiClassDocumenter)
app.add_autodocumenter(documenters.AutoapiMethodDocumenter)
app.add_autodocumenter(documenters.AutoapiDataDocumenter)
app.add_autodocumenter(documenters.AutoapiAttributeDocumenter)
app.add_autodocumenter(documenters.AutoapiModuleDocumenter)
app.add_autodocumenter(documenters.AutoapiExceptionDocumenter)
2019-01-27 05:20:45 +00:00
directives.register_directive("autoapi-nested-parse", NestedParse)
directives.register_directive("autoapisummary", AutoapiSummary)
app.setup_extension("sphinx.ext.autosummary")
2019-09-04 21:44:35 +00:00
app.add_event("autoapi-skip-member")
app.setup_extension("sphinx.ext.inheritance_diagram")
app.add_directive("autoapi-inheritance-diagram", AutoapiInheritanceDiagram)
return {
"parallel_read_safe": True,
"parallel_write_safe": True,
}