1""" 2 sphinx.ext.autosummary 3 ~~~~~~~~~~~~~~~~~~~~~~ 4 5 Sphinx extension that adds an autosummary:: directive, which can be 6 used to generate function/method/attribute/etc. summary lists, similar 7 to those output eg. by Epydoc and other API doc generation tools. 8 9 An :autolink: role is also provided. 10 11 autosummary directive 12 --------------------- 13 14 The autosummary directive has the form:: 15 16 .. autosummary:: 17 :nosignatures: 18 :toctree: generated/ 19 20 module.function_1 21 module.function_2 22 ... 23 24 and it generates an output table (containing signatures, optionally) 25 26 ======================== ============================================= 27 module.function_1(args) Summary line from the docstring of function_1 28 module.function_2(args) Summary line from the docstring 29 ... 30 ======================== ============================================= 31 32 If the :toctree: option is specified, files matching the function names 33 are inserted to the toctree with the given prefix: 34 35 generated/module.function_1 36 generated/module.function_2 37 ... 38 39 Note: The file names contain the module:: or currentmodule:: prefixes. 40 41 .. seealso:: autosummary_generate.py 42 43 44 autolink role 45 ------------- 46 47 The autolink role functions as ``:obj:`` when the name referred can be 48 resolved to a Python object, and otherwise it becomes simple emphasis. 49 This can be used as the default role to make links 'smart'. 50 51 :copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS. 52 :license: BSD, see LICENSE for details. 53""" 54 55import inspect 56import os 57import posixpath 58import re 59import sys 60import warnings 61from os import path 62from types import ModuleType 63from typing import Any, Dict, List, Tuple, cast 64 65from docutils import nodes 66from docutils.nodes import Element, Node, system_message 67from docutils.parsers.rst import directives 68from docutils.parsers.rst.states import Inliner, RSTStateMachine, Struct, state_classes 69from docutils.statemachine import StringList 70 71import sphinx 72from sphinx import addnodes 73from sphinx.application import Sphinx 74from sphinx.config import Config 75from sphinx.deprecation import RemovedInSphinx40Warning, RemovedInSphinx50Warning 76from sphinx.environment import BuildEnvironment 77from sphinx.environment.adapters.toctree import TocTree 78from sphinx.ext.autodoc import INSTANCEATTR, Documenter 79from sphinx.ext.autodoc.directive import DocumenterBridge, Options 80from sphinx.ext.autodoc.importer import import_module 81from sphinx.ext.autodoc.mock import mock 82from sphinx.locale import __ 83from sphinx.pycode import ModuleAnalyzer, PycodeError 84from sphinx.util import logging, rst 85from sphinx.util.docutils import (NullReporter, SphinxDirective, SphinxRole, new_document, 86 switch_source_input) 87from sphinx.util.matching import Matcher 88from sphinx.writers.html import HTMLTranslator 89 90if False: 91 # For type annotation 92 from typing import Type # for python3.5.1 93 94 95logger = logging.getLogger(__name__) 96 97 98periods_re = re.compile(r'\.(?:\s+)') 99literal_re = re.compile(r'::\s*$') 100 101WELL_KNOWN_ABBREVIATIONS = ('et al.', ' i.e.',) 102 103 104# -- autosummary_toc node ------------------------------------------------------ 105 106class autosummary_toc(nodes.comment): 107 pass 108 109 110def process_autosummary_toc(app: Sphinx, doctree: nodes.document) -> None: 111 """Insert items described in autosummary:: to the TOC tree, but do 112 not generate the toctree:: list. 113 """ 114 warnings.warn('process_autosummary_toc() is deprecated', 115 RemovedInSphinx50Warning, stacklevel=2) 116 env = app.builder.env 117 crawled = {} 118 119 def crawl_toc(node: Element, depth: int = 1) -> None: 120 crawled[node] = True 121 for j, subnode in enumerate(node): 122 try: 123 if (isinstance(subnode, autosummary_toc) and 124 isinstance(subnode[0], addnodes.toctree)): 125 TocTree(env).note(env.docname, subnode[0]) 126 continue 127 except IndexError: 128 continue 129 if not isinstance(subnode, nodes.section): 130 continue 131 if subnode not in crawled: 132 crawl_toc(subnode, depth + 1) 133 crawl_toc(doctree) 134 135 136def autosummary_toc_visit_html(self: nodes.NodeVisitor, node: autosummary_toc) -> None: 137 """Hide autosummary toctree list in HTML output.""" 138 raise nodes.SkipNode 139 140 141def autosummary_noop(self: nodes.NodeVisitor, node: Node) -> None: 142 pass 143 144 145# -- autosummary_table node ---------------------------------------------------- 146 147class autosummary_table(nodes.comment): 148 pass 149 150 151def autosummary_table_visit_html(self: HTMLTranslator, node: autosummary_table) -> None: 152 """Make the first column of the table non-breaking.""" 153 try: 154 table = cast(nodes.table, node[0]) 155 tgroup = cast(nodes.tgroup, table[0]) 156 tbody = cast(nodes.tbody, tgroup[-1]) 157 rows = cast(List[nodes.row], tbody) 158 for row in rows: 159 col1_entry = cast(nodes.entry, row[0]) 160 par = cast(nodes.paragraph, col1_entry[0]) 161 for j, subnode in enumerate(list(par)): 162 if isinstance(subnode, nodes.Text): 163 new_text = subnode.astext().replace(" ", "\u00a0") 164 par[j] = nodes.Text(new_text) 165 except IndexError: 166 pass 167 168 169# -- autodoc integration ------------------------------------------------------- 170 171# current application object (used in `get_documenter()`). 172_app = None # type: Sphinx 173 174 175class FakeDirective(DocumenterBridge): 176 def __init__(self) -> None: 177 settings = Struct(tab_width=8) 178 document = Struct(settings=settings) 179 env = BuildEnvironment() 180 env.config = Config() 181 state = Struct(document=document) 182 super().__init__(env, None, Options(), 0, state) 183 184 185def get_documenter(app: Sphinx, obj: Any, parent: Any) -> "Type[Documenter]": 186 """Get an autodoc.Documenter class suitable for documenting the given 187 object. 188 189 *obj* is the Python object to be documented, and *parent* is an 190 another Python object (e.g. a module or a class) to which *obj* 191 belongs to. 192 """ 193 from sphinx.ext.autodoc import DataDocumenter, ModuleDocumenter 194 195 if inspect.ismodule(obj): 196 # ModuleDocumenter.can_document_member always returns False 197 return ModuleDocumenter 198 199 # Construct a fake documenter for *parent* 200 if parent is not None: 201 parent_doc_cls = get_documenter(app, parent, None) 202 else: 203 parent_doc_cls = ModuleDocumenter 204 205 if hasattr(parent, '__name__'): 206 parent_doc = parent_doc_cls(FakeDirective(), parent.__name__) 207 else: 208 parent_doc = parent_doc_cls(FakeDirective(), "") 209 210 # Get the corrent documenter class for *obj* 211 classes = [cls for cls in app.registry.documenters.values() 212 if cls.can_document_member(obj, '', False, parent_doc)] 213 if classes: 214 classes.sort(key=lambda cls: cls.priority) 215 return classes[-1] 216 else: 217 return DataDocumenter 218 219 220# -- .. autosummary:: ---------------------------------------------------------- 221 222class Autosummary(SphinxDirective): 223 """ 224 Pretty table containing short signatures and summaries of functions etc. 225 226 autosummary can also optionally generate a hidden toctree:: node. 227 """ 228 229 required_arguments = 0 230 optional_arguments = 0 231 final_argument_whitespace = False 232 has_content = True 233 option_spec = { 234 'caption': directives.unchanged_required, 235 'toctree': directives.unchanged, 236 'nosignatures': directives.flag, 237 'recursive': directives.flag, 238 'template': directives.unchanged, 239 } 240 241 def run(self) -> List[Node]: 242 self.bridge = DocumenterBridge(self.env, self.state.document.reporter, 243 Options(), self.lineno, self.state) 244 245 names = [x.strip().split()[0] for x in self.content 246 if x.strip() and re.search(r'^[~a-zA-Z_]', x.strip()[0])] 247 items = self.get_items(names) 248 nodes = self.get_table(items) 249 250 if 'toctree' in self.options: 251 dirname = posixpath.dirname(self.env.docname) 252 253 tree_prefix = self.options['toctree'].strip() 254 docnames = [] 255 excluded = Matcher(self.config.exclude_patterns) 256 filename_map = self.config.autosummary_filename_map 257 for name, sig, summary, real_name in items: 258 real_name = filename_map.get(real_name, real_name) 259 docname = posixpath.join(tree_prefix, real_name) 260 docname = posixpath.normpath(posixpath.join(dirname, docname)) 261 if docname not in self.env.found_docs: 262 if excluded(self.env.doc2path(docname, None)): 263 msg = __('autosummary references excluded document %r. Ignored.') 264 else: 265 msg = __('autosummary: stub file not found %r. ' 266 'Check your autosummary_generate setting.') 267 268 logger.warning(msg, real_name, location=self.get_source_info()) 269 continue 270 271 docnames.append(docname) 272 273 if docnames: 274 tocnode = addnodes.toctree() 275 tocnode['includefiles'] = docnames 276 tocnode['entries'] = [(None, docn) for docn in docnames] 277 tocnode['maxdepth'] = -1 278 tocnode['glob'] = None 279 tocnode['caption'] = self.options.get('caption') 280 281 nodes.append(autosummary_toc('', '', tocnode)) 282 283 if 'toctree' not in self.options and 'caption' in self.options: 284 logger.warning(__('A captioned autosummary requires :toctree: option. ignored.'), 285 location=nodes[-1]) 286 287 return nodes 288 289 def import_by_name(self, name: str, prefixes: List[str]) -> Tuple[str, Any, Any, str]: 290 with mock(self.config.autosummary_mock_imports): 291 try: 292 return import_by_name(name, prefixes) 293 except ImportError as exc: 294 # check existence of instance attribute 295 try: 296 return import_ivar_by_name(name, prefixes) 297 except ImportError: 298 pass 299 300 raise exc # re-raise ImportError if instance attribute not found 301 302 def create_documenter(self, app: Sphinx, obj: Any, 303 parent: Any, full_name: str) -> "Documenter": 304 """Get an autodoc.Documenter class suitable for documenting the given 305 object. 306 307 Wraps get_documenter and is meant as a hook for extensions. 308 """ 309 doccls = get_documenter(app, obj, parent) 310 return doccls(self.bridge, full_name) 311 312 def get_items(self, names: List[str]) -> List[Tuple[str, str, str, str]]: 313 """Try to import the given names, and return a list of 314 ``[(name, signature, summary_string, real_name), ...]``. 315 """ 316 prefixes = get_import_prefixes_from_env(self.env) 317 318 items = [] # type: List[Tuple[str, str, str, str]] 319 320 max_item_chars = 50 321 322 for name in names: 323 display_name = name 324 if name.startswith('~'): 325 name = name[1:] 326 display_name = name.split('.')[-1] 327 328 try: 329 real_name, obj, parent, modname = self.import_by_name(name, prefixes=prefixes) 330 except ImportError: 331 logger.warning(__('autosummary: failed to import %s'), name, 332 location=self.get_source_info()) 333 continue 334 335 self.bridge.result = StringList() # initialize for each documenter 336 full_name = real_name 337 if not isinstance(obj, ModuleType): 338 # give explicitly separated module name, so that members 339 # of inner classes can be documented 340 full_name = modname + '::' + full_name[len(modname) + 1:] 341 # NB. using full_name here is important, since Documenters 342 # handle module prefixes slightly differently 343 documenter = self.create_documenter(self.env.app, obj, parent, full_name) 344 if not documenter.parse_name(): 345 logger.warning(__('failed to parse name %s'), real_name, 346 location=self.get_source_info()) 347 items.append((display_name, '', '', real_name)) 348 continue 349 if not documenter.import_object(): 350 logger.warning(__('failed to import object %s'), real_name, 351 location=self.get_source_info()) 352 items.append((display_name, '', '', real_name)) 353 continue 354 if documenter.options.members and not documenter.check_module(): 355 continue 356 357 # try to also get a source code analyzer for attribute docs 358 try: 359 documenter.analyzer = ModuleAnalyzer.for_module( 360 documenter.get_real_modname()) 361 # parse right now, to get PycodeErrors on parsing (results will 362 # be cached anyway) 363 documenter.analyzer.find_attr_docs() 364 except PycodeError as err: 365 logger.debug('[autodoc] module analyzer failed: %s', err) 366 # no source file -- e.g. for builtin and C modules 367 documenter.analyzer = None 368 369 # -- Grab the signature 370 371 try: 372 sig = documenter.format_signature(show_annotation=False) 373 except TypeError: 374 # the documenter does not support ``show_annotation`` option 375 sig = documenter.format_signature() 376 377 if not sig: 378 sig = '' 379 else: 380 max_chars = max(10, max_item_chars - len(display_name)) 381 sig = mangle_signature(sig, max_chars=max_chars) 382 383 # -- Grab the summary 384 385 documenter.add_content(None) 386 summary = extract_summary(self.bridge.result.data[:], self.state.document) 387 388 items.append((display_name, sig, summary, real_name)) 389 390 return items 391 392 def get_table(self, items: List[Tuple[str, str, str, str]]) -> List[Node]: 393 """Generate a proper list of table nodes for autosummary:: directive. 394 395 *items* is a list produced by :meth:`get_items`. 396 """ 397 table_spec = addnodes.tabular_col_spec() 398 table_spec['spec'] = r'\X{1}{2}\X{1}{2}' 399 400 table = autosummary_table('') 401 real_table = nodes.table('', classes=['longtable']) 402 table.append(real_table) 403 group = nodes.tgroup('', cols=2) 404 real_table.append(group) 405 group.append(nodes.colspec('', colwidth=10)) 406 group.append(nodes.colspec('', colwidth=90)) 407 body = nodes.tbody('') 408 group.append(body) 409 410 def append_row(*column_texts: str) -> None: 411 row = nodes.row('') 412 source, line = self.state_machine.get_source_and_line() 413 for text in column_texts: 414 node = nodes.paragraph('') 415 vl = StringList() 416 vl.append(text, '%s:%d:<autosummary>' % (source, line)) 417 with switch_source_input(self.state, vl): 418 self.state.nested_parse(vl, 0, node) 419 try: 420 if isinstance(node[0], nodes.paragraph): 421 node = node[0] 422 except IndexError: 423 pass 424 row.append(nodes.entry('', node)) 425 body.append(row) 426 427 for name, sig, summary, real_name in items: 428 qualifier = 'obj' 429 if 'nosignatures' not in self.options: 430 col1 = ':%s:`%s <%s>`\\ %s' % (qualifier, name, real_name, rst.escape(sig)) 431 else: 432 col1 = ':%s:`%s <%s>`' % (qualifier, name, real_name) 433 col2 = summary 434 append_row(col1, col2) 435 436 return [table_spec, table] 437 438 def warn(self, msg: str) -> None: 439 warnings.warn('Autosummary.warn() is deprecated', 440 RemovedInSphinx40Warning, stacklevel=2) 441 logger.warning(msg) 442 443 @property 444 def genopt(self) -> Options: 445 warnings.warn('Autosummary.genopt is deprecated', 446 RemovedInSphinx40Warning, stacklevel=2) 447 return self.bridge.genopt 448 449 @property 450 def warnings(self) -> List[Node]: 451 warnings.warn('Autosummary.warnings is deprecated', 452 RemovedInSphinx40Warning, stacklevel=2) 453 return [] 454 455 @property 456 def result(self) -> StringList: 457 warnings.warn('Autosummary.result is deprecated', 458 RemovedInSphinx40Warning, stacklevel=2) 459 return self.bridge.result 460 461 462def strip_arg_typehint(s: str) -> str: 463 """Strip a type hint from argument definition.""" 464 return s.split(':')[0].strip() 465 466 467def mangle_signature(sig: str, max_chars: int = 30) -> str: 468 """Reformat a function signature to a more compact form.""" 469 # Strip return type annotation 470 s = re.sub(r"\)\s*->\s.*$", ")", sig) 471 472 # Remove parenthesis 473 s = re.sub(r"^\((.*)\)$", r"\1", s).strip() 474 475 # Strip literals (which can contain things that confuse the code below) 476 s = re.sub(r"\\\\", "", s) # escaped backslash (maybe inside string) 477 s = re.sub(r"\\'", "", s) # escaped single quote 478 s = re.sub(r'\\"', "", s) # escaped double quote 479 s = re.sub(r"'[^']*'", "", s) # string literal (w/ single quote) 480 s = re.sub(r'"[^"]*"', "", s) # string literal (w/ double quote) 481 482 # Strip complex objects (maybe default value of arguments) 483 while re.search(r'\([^)]*\)', s): # contents of parenthesis (ex. NamedTuple(attr=...)) 484 s = re.sub(r'\([^)]*\)', '', s) 485 while re.search(r'<[^>]*>', s): # contents of angle brackets (ex. <object>) 486 s = re.sub(r'<[^>]*>', '', s) 487 while re.search(r'{[^}]*}', s): # contents of curly brackets (ex. dict) 488 s = re.sub(r'{[^}]*}', '', s) 489 490 # Parse the signature to arguments + options 491 args = [] # type: List[str] 492 opts = [] # type: List[str] 493 494 opt_re = re.compile(r"^(.*, |)([a-zA-Z0-9_*]+)\s*=\s*") 495 while s: 496 m = opt_re.search(s) 497 if not m: 498 # The rest are arguments 499 args = s.split(', ') 500 break 501 502 opts.insert(0, m.group(2)) 503 s = m.group(1)[:-2] 504 505 # Strip typehints 506 for i, arg in enumerate(args): 507 args[i] = strip_arg_typehint(arg) 508 509 for i, opt in enumerate(opts): 510 opts[i] = strip_arg_typehint(opt) 511 512 # Produce a more compact signature 513 sig = limited_join(", ", args, max_chars=max_chars - 2) 514 if opts: 515 if not sig: 516 sig = "[%s]" % limited_join(", ", opts, max_chars=max_chars - 4) 517 elif len(sig) < max_chars - 4 - 2 - 3: 518 sig += "[, %s]" % limited_join(", ", opts, 519 max_chars=max_chars - len(sig) - 4 - 2) 520 521 return "(%s)" % sig 522 523 524def extract_summary(doc: List[str], document: Any) -> str: 525 """Extract summary from docstring.""" 526 def parse(doc: List[str], settings: Any) -> nodes.document: 527 state_machine = RSTStateMachine(state_classes, 'Body') 528 node = new_document('', settings) 529 node.reporter = NullReporter() 530 state_machine.run(doc, node) 531 532 return node 533 534 # Skip a blank lines at the top 535 while doc and not doc[0].strip(): 536 doc.pop(0) 537 538 # If there's a blank line, then we can assume the first sentence / 539 # paragraph has ended, so anything after shouldn't be part of the 540 # summary 541 for i, piece in enumerate(doc): 542 if not piece.strip(): 543 doc = doc[:i] 544 break 545 546 if doc == []: 547 return '' 548 549 # parse the docstring 550 node = parse(doc, document.settings) 551 if not isinstance(node[0], nodes.paragraph): 552 # document starts with non-paragraph: pick up the first line 553 summary = doc[0].strip() 554 else: 555 # Try to find the "first sentence", which may span multiple lines 556 sentences = periods_re.split(" ".join(doc)) 557 if len(sentences) == 1: 558 summary = sentences[0].strip() 559 else: 560 summary = '' 561 for i in range(len(sentences)): 562 summary = ". ".join(sentences[:i + 1]).rstrip(".") + "." 563 node[:] = [] 564 node = parse(doc, document.settings) 565 if summary.endswith(WELL_KNOWN_ABBREVIATIONS): 566 pass 567 elif not node.traverse(nodes.system_message): 568 # considered as that splitting by period does not break inline markups 569 break 570 571 # strip literal notation mark ``::`` from tail of summary 572 summary = literal_re.sub('.', summary) 573 574 return summary 575 576 577def limited_join(sep: str, items: List[str], max_chars: int = 30, 578 overflow_marker: str = "...") -> str: 579 """Join a number of strings to one, limiting the length to *max_chars*. 580 581 If the string overflows this limit, replace the last fitting item by 582 *overflow_marker*. 583 584 Returns: joined_string 585 """ 586 full_str = sep.join(items) 587 if len(full_str) < max_chars: 588 return full_str 589 590 n_chars = 0 591 n_items = 0 592 for j, item in enumerate(items): 593 n_chars += len(item) + len(sep) 594 if n_chars < max_chars - len(overflow_marker): 595 n_items += 1 596 else: 597 break 598 599 return sep.join(list(items[:n_items]) + [overflow_marker]) 600 601 602# -- Importing items ----------------------------------------------------------- 603 604def get_import_prefixes_from_env(env: BuildEnvironment) -> List[str]: 605 """ 606 Obtain current Python import prefixes (for `import_by_name`) 607 from ``document.env`` 608 """ 609 prefixes = [None] # type: List[str] 610 611 currmodule = env.ref_context.get('py:module') 612 if currmodule: 613 prefixes.insert(0, currmodule) 614 615 currclass = env.ref_context.get('py:class') 616 if currclass: 617 if currmodule: 618 prefixes.insert(0, currmodule + "." + currclass) 619 else: 620 prefixes.insert(0, currclass) 621 622 return prefixes 623 624 625def import_by_name(name: str, prefixes: List[str] = [None]) -> Tuple[str, Any, Any, str]: 626 """Import a Python object that has the given *name*, under one of the 627 *prefixes*. The first name that succeeds is used. 628 """ 629 tried = [] 630 for prefix in prefixes: 631 try: 632 if prefix: 633 prefixed_name = '.'.join([prefix, name]) 634 else: 635 prefixed_name = name 636 obj, parent, modname = _import_by_name(prefixed_name) 637 return prefixed_name, obj, parent, modname 638 except ImportError: 639 tried.append(prefixed_name) 640 raise ImportError('no module named %s' % ' or '.join(tried)) 641 642 643def _import_by_name(name: str) -> Tuple[Any, Any, str]: 644 """Import a Python object given its full name.""" 645 try: 646 name_parts = name.split('.') 647 648 # try first interpret `name` as MODNAME.OBJ 649 modname = '.'.join(name_parts[:-1]) 650 if modname: 651 try: 652 mod = import_module(modname) 653 return getattr(mod, name_parts[-1]), mod, modname 654 except (ImportError, IndexError, AttributeError): 655 pass 656 657 # ... then as MODNAME, MODNAME.OBJ1, MODNAME.OBJ1.OBJ2, ... 658 last_j = 0 659 modname = None 660 for j in reversed(range(1, len(name_parts) + 1)): 661 last_j = j 662 modname = '.'.join(name_parts[:j]) 663 try: 664 import_module(modname) 665 except ImportError: 666 continue 667 668 if modname in sys.modules: 669 break 670 671 if last_j < len(name_parts): 672 parent = None 673 obj = sys.modules[modname] 674 for obj_name in name_parts[last_j:]: 675 parent = obj 676 obj = getattr(obj, obj_name) 677 return obj, parent, modname 678 else: 679 return sys.modules[modname], None, modname 680 except (ValueError, ImportError, AttributeError, KeyError) as e: 681 raise ImportError(*e.args) from e 682 683 684def import_ivar_by_name(name: str, prefixes: List[str] = [None]) -> Tuple[str, Any, Any, str]: 685 """Import an instance variable that has the given *name*, under one of the 686 *prefixes*. The first name that succeeds is used. 687 """ 688 try: 689 name, attr = name.rsplit(".", 1) 690 real_name, obj, parent, modname = import_by_name(name, prefixes) 691 qualname = real_name.replace(modname + ".", "") 692 analyzer = ModuleAnalyzer.for_module(modname) 693 if (qualname, attr) in analyzer.find_attr_docs(): 694 return real_name + "." + attr, INSTANCEATTR, obj, modname 695 except (ImportError, ValueError, PycodeError): 696 pass 697 698 raise ImportError 699 700 701# -- :autolink: (smart default role) ------------------------------------------- 702 703def autolink_role(typ: str, rawtext: str, etext: str, lineno: int, inliner: Inliner, 704 options: Dict = {}, content: List[str] = [] 705 ) -> Tuple[List[Node], List[system_message]]: 706 """Smart linking role. 707 708 Expands to ':obj:`text`' if `text` is an object that can be imported; 709 otherwise expands to '*text*'. 710 """ 711 warnings.warn('autolink_role() is deprecated.', RemovedInSphinx40Warning, stacklevel=2) 712 env = inliner.document.settings.env 713 pyobj_role = env.get_domain('py').role('obj') 714 objects, msg = pyobj_role('obj', rawtext, etext, lineno, inliner, options, content) 715 if msg != []: 716 return objects, msg 717 718 assert len(objects) == 1 719 pending_xref = cast(addnodes.pending_xref, objects[0]) 720 prefixes = get_import_prefixes_from_env(env) 721 try: 722 name, obj, parent, modname = import_by_name(pending_xref['reftarget'], prefixes) 723 except ImportError: 724 literal = cast(nodes.literal, pending_xref[0]) 725 objects[0] = nodes.emphasis(rawtext, literal.astext(), classes=literal['classes']) 726 727 return objects, msg 728 729 730class AutoLink(SphinxRole): 731 """Smart linking role. 732 733 Expands to ':obj:`text`' if `text` is an object that can be imported; 734 otherwise expands to '*text*'. 735 """ 736 def run(self) -> Tuple[List[Node], List[system_message]]: 737 pyobj_role = self.env.get_domain('py').role('obj') 738 objects, errors = pyobj_role('obj', self.rawtext, self.text, self.lineno, 739 self.inliner, self.options, self.content) 740 if errors: 741 return objects, errors 742 743 assert len(objects) == 1 744 pending_xref = cast(addnodes.pending_xref, objects[0]) 745 try: 746 # try to import object by name 747 prefixes = get_import_prefixes_from_env(self.env) 748 import_by_name(pending_xref['reftarget'], prefixes) 749 except ImportError: 750 literal = cast(nodes.literal, pending_xref[0]) 751 objects[0] = nodes.emphasis(self.rawtext, literal.astext(), 752 classes=literal['classes']) 753 754 return objects, errors 755 756 757def get_rst_suffix(app: Sphinx) -> str: 758 def get_supported_format(suffix: str) -> Tuple[str, ...]: 759 parser_class = app.registry.get_source_parsers().get(suffix) 760 if parser_class is None: 761 return ('restructuredtext',) 762 return parser_class.supported 763 764 suffix = None # type: str 765 for suffix in app.config.source_suffix: 766 if 'restructuredtext' in get_supported_format(suffix): 767 return suffix 768 769 return None 770 771 772def process_generate_options(app: Sphinx) -> None: 773 genfiles = app.config.autosummary_generate 774 775 if genfiles is True: 776 env = app.builder.env 777 genfiles = [env.doc2path(x, base=None) for x in env.found_docs 778 if os.path.isfile(env.doc2path(x))] 779 elif genfiles is False: 780 pass 781 else: 782 ext = list(app.config.source_suffix) 783 genfiles = [genfile + (ext[0] if not genfile.endswith(tuple(ext)) else '') 784 for genfile in genfiles] 785 786 for entry in genfiles[:]: 787 if not path.isfile(path.join(app.srcdir, entry)): 788 logger.warning(__('autosummary_generate: file not found: %s'), entry) 789 genfiles.remove(entry) 790 791 if not genfiles: 792 return 793 794 suffix = get_rst_suffix(app) 795 if suffix is None: 796 logger.warning(__('autosummary generats .rst files internally. ' 797 'But your source_suffix does not contain .rst. Skipped.')) 798 return 799 800 from sphinx.ext.autosummary.generate import generate_autosummary_docs 801 802 imported_members = app.config.autosummary_imported_members 803 with mock(app.config.autosummary_mock_imports): 804 generate_autosummary_docs(genfiles, suffix=suffix, base_path=app.srcdir, 805 app=app, imported_members=imported_members, 806 overwrite=app.config.autosummary_generate_overwrite, 807 encoding=app.config.source_encoding) 808 809 810def setup(app: Sphinx) -> Dict[str, Any]: 811 # I need autodoc 812 app.setup_extension('sphinx.ext.autodoc') 813 app.add_node(autosummary_toc, 814 html=(autosummary_toc_visit_html, autosummary_noop), 815 latex=(autosummary_noop, autosummary_noop), 816 text=(autosummary_noop, autosummary_noop), 817 man=(autosummary_noop, autosummary_noop), 818 texinfo=(autosummary_noop, autosummary_noop)) 819 app.add_node(autosummary_table, 820 html=(autosummary_table_visit_html, autosummary_noop), 821 latex=(autosummary_noop, autosummary_noop), 822 text=(autosummary_noop, autosummary_noop), 823 man=(autosummary_noop, autosummary_noop), 824 texinfo=(autosummary_noop, autosummary_noop)) 825 app.add_directive('autosummary', Autosummary) 826 app.add_role('autolink', AutoLink()) 827 app.connect('builder-inited', process_generate_options) 828 app.add_config_value('autosummary_context', {}, True) 829 app.add_config_value('autosummary_filename_map', {}, 'html') 830 app.add_config_value('autosummary_generate', [], True, [bool]) 831 app.add_config_value('autosummary_generate_overwrite', True, False) 832 app.add_config_value('autosummary_mock_imports', 833 lambda config: config.autodoc_mock_imports, 'env') 834 app.add_config_value('autosummary_imported_members', [], False, [bool]) 835 836 return {'version': sphinx.__display_version__, 'parallel_read_safe': True} 837