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