diff options
Diffstat (limited to 'sphinx/ext/autosummary')
-rw-r--r-- | sphinx/ext/autosummary/__init__.py | 847 | ||||
-rw-r--r-- | sphinx/ext/autosummary/generate.py | 754 | ||||
-rw-r--r-- | sphinx/ext/autosummary/templates/autosummary/base.rst | 5 | ||||
-rw-r--r-- | sphinx/ext/autosummary/templates/autosummary/class.rst | 29 | ||||
-rw-r--r-- | sphinx/ext/autosummary/templates/autosummary/module.rst | 60 |
5 files changed, 1695 insertions, 0 deletions
diff --git a/sphinx/ext/autosummary/__init__.py b/sphinx/ext/autosummary/__init__.py new file mode 100644 index 0000000..edb8f0d --- /dev/null +++ b/sphinx/ext/autosummary/__init__.py @@ -0,0 +1,847 @@ +"""Extension that adds an autosummary:: directive. + +The directive can be used to generate function/method/attribute/etc. summary +lists, similar to those output eg. by Epydoc and other API doc generation tools. + +An :autolink: role is also provided. + +autosummary directive +--------------------- + +The autosummary directive has the form:: + + .. autosummary:: + :nosignatures: + :toctree: generated/ + + module.function_1 + module.function_2 + ... + +and it generates an output table (containing signatures, optionally) + + ======================== ============================================= + module.function_1(args) Summary line from the docstring of function_1 + module.function_2(args) Summary line from the docstring + ... + ======================== ============================================= + +If the :toctree: option is specified, files matching the function names +are inserted to the toctree with the given prefix: + + generated/module.function_1 + generated/module.function_2 + ... + +Note: The file names contain the module:: or currentmodule:: prefixes. + +.. seealso:: autosummary_generate.py + + +autolink role +------------- + +The autolink role functions as ``:obj:`` when the name referred can be +resolved to a Python object, and otherwise it becomes simple emphasis. +This can be used as the default role to make links 'smart'. +""" + +from __future__ import annotations + +import inspect +import os +import posixpath +import re +import sys +from inspect import Parameter +from os import path +from types import ModuleType +from typing import TYPE_CHECKING, Any, cast + +from docutils import nodes +from docutils.parsers.rst import directives +from docutils.parsers.rst.states import RSTStateMachine, Struct, state_classes +from docutils.statemachine import StringList + +import sphinx +from sphinx import addnodes +from sphinx.config import Config +from sphinx.environment import BuildEnvironment +from sphinx.ext.autodoc import INSTANCEATTR, Documenter +from sphinx.ext.autodoc.directive import DocumenterBridge, Options +from sphinx.ext.autodoc.importer import import_module +from sphinx.ext.autodoc.mock import mock +from sphinx.locale import __ +from sphinx.project import Project +from sphinx.pycode import ModuleAnalyzer, PycodeError +from sphinx.registry import SphinxComponentRegistry +from sphinx.util import logging, rst +from sphinx.util.docutils import ( + NullReporter, + SphinxDirective, + SphinxRole, + new_document, + switch_source_input, +) +from sphinx.util.inspect import getmro, signature_from_str +from sphinx.util.matching import Matcher + +if TYPE_CHECKING: + from collections.abc import Sequence + + from docutils.nodes import Node, system_message + + from sphinx.application import Sphinx + from sphinx.extension import Extension + from sphinx.util.typing import OptionSpec + from sphinx.writers.html import HTML5Translator + +logger = logging.getLogger(__name__) + + +periods_re = re.compile(r'\.(?:\s+)') +literal_re = re.compile(r'::\s*$') + +WELL_KNOWN_ABBREVIATIONS = ('et al.', 'e.g.', 'i.e.') + + +# -- autosummary_toc node ------------------------------------------------------ + +class autosummary_toc(nodes.comment): + pass + + +def autosummary_toc_visit_html(self: nodes.NodeVisitor, node: autosummary_toc) -> None: + """Hide autosummary toctree list in HTML output.""" + raise nodes.SkipNode + + +def autosummary_noop(self: nodes.NodeVisitor, node: Node) -> None: + pass + + +# -- autosummary_table node ---------------------------------------------------- + +class autosummary_table(nodes.comment): + pass + + +def autosummary_table_visit_html(self: HTML5Translator, node: autosummary_table) -> None: + """Make the first column of the table non-breaking.""" + try: + table = cast(nodes.table, node[0]) + tgroup = cast(nodes.tgroup, table[0]) + tbody = cast(nodes.tbody, tgroup[-1]) + rows = cast(list[nodes.row], tbody) + for row in rows: + col1_entry = cast(nodes.entry, row[0]) + par = cast(nodes.paragraph, col1_entry[0]) + for j, subnode in enumerate(list(par)): + if isinstance(subnode, nodes.Text): + new_text = subnode.astext().replace(" ", "\u00a0") + par[j] = nodes.Text(new_text) + except IndexError: + pass + + +# -- autodoc integration ------------------------------------------------------- + +class FakeApplication: + def __init__(self) -> None: + self.doctreedir = None + self.events = None + self.extensions: dict[str, Extension] = {} + self.srcdir = None + self.config = Config() + self.project = Project('', {}) + self.registry = SphinxComponentRegistry() + + +class FakeDirective(DocumenterBridge): + def __init__(self) -> None: + settings = Struct(tab_width=8) + document = Struct(settings=settings) + app = FakeApplication() + app.config.add('autodoc_class_signature', 'mixed', True, None) + env = BuildEnvironment(app) # type: ignore[arg-type] + state = Struct(document=document) + super().__init__(env, None, Options(), 0, state) + + +def get_documenter(app: Sphinx, obj: Any, parent: Any) -> type[Documenter]: + """Get an autodoc.Documenter class suitable for documenting the given + object. + + *obj* is the Python object to be documented, and *parent* is an + another Python object (e.g. a module or a class) to which *obj* + belongs to. + """ + from sphinx.ext.autodoc import DataDocumenter, ModuleDocumenter + + if inspect.ismodule(obj): + # ModuleDocumenter.can_document_member always returns False + return ModuleDocumenter + + # Construct a fake documenter for *parent* + if parent is not None: + parent_doc_cls = get_documenter(app, parent, None) + else: + parent_doc_cls = ModuleDocumenter + + if hasattr(parent, '__name__'): + parent_doc = parent_doc_cls(FakeDirective(), parent.__name__) + else: + parent_doc = parent_doc_cls(FakeDirective(), "") + + # Get the correct documenter class for *obj* + classes = [cls for cls in app.registry.documenters.values() + if cls.can_document_member(obj, '', False, parent_doc)] + if classes: + classes.sort(key=lambda cls: cls.priority) + return classes[-1] + else: + return DataDocumenter + + +# -- .. autosummary:: ---------------------------------------------------------- + +class Autosummary(SphinxDirective): + """ + Pretty table containing short signatures and summaries of functions etc. + + autosummary can also optionally generate a hidden toctree:: node. + """ + + required_arguments = 0 + optional_arguments = 0 + final_argument_whitespace = False + has_content = True + option_spec: OptionSpec = { + 'caption': directives.unchanged_required, + 'toctree': directives.unchanged, + 'nosignatures': directives.flag, + 'recursive': directives.flag, + 'template': directives.unchanged, + } + + def run(self) -> list[Node]: + self.bridge = DocumenterBridge(self.env, self.state.document.reporter, + Options(), self.lineno, self.state) + + names = [x.strip().split()[0] for x in self.content + if x.strip() and re.search(r'^[~a-zA-Z_]', x.strip()[0])] + items = self.get_items(names) + nodes = self.get_table(items) + + if 'toctree' in self.options: + dirname = posixpath.dirname(self.env.docname) + + tree_prefix = self.options['toctree'].strip() + docnames = [] + excluded = Matcher(self.config.exclude_patterns) + filename_map = self.config.autosummary_filename_map + for _name, _sig, _summary, real_name in items: + real_name = filename_map.get(real_name, real_name) + docname = posixpath.join(tree_prefix, real_name) + docname = posixpath.normpath(posixpath.join(dirname, docname)) + if docname not in self.env.found_docs: + if excluded(self.env.doc2path(docname, False)): + msg = __('autosummary references excluded document %r. Ignored.') + else: + msg = __('autosummary: stub file not found %r. ' + 'Check your autosummary_generate setting.') + + logger.warning(msg, real_name, location=self.get_location()) + continue + + docnames.append(docname) + + if docnames: + tocnode = addnodes.toctree() + tocnode['includefiles'] = docnames + tocnode['entries'] = [(None, docn) for docn in docnames] + tocnode['maxdepth'] = -1 + tocnode['glob'] = None + tocnode['caption'] = self.options.get('caption') + + nodes.append(autosummary_toc('', '', tocnode)) + + if 'toctree' not in self.options and 'caption' in self.options: + logger.warning(__('A captioned autosummary requires :toctree: option. ignored.'), + location=nodes[-1]) + + return nodes + + def import_by_name( + self, name: str, prefixes: list[str | None], + ) -> tuple[str, Any, Any, str]: + with mock(self.config.autosummary_mock_imports): + try: + return import_by_name(name, prefixes) + except ImportExceptionGroup as exc: + # check existence of instance attribute + try: + return import_ivar_by_name(name, prefixes) + except ImportError as exc2: + if exc2.__cause__: + errors: list[BaseException] = exc.exceptions + [exc2.__cause__] + else: + errors = exc.exceptions + [exc2] + + raise ImportExceptionGroup(exc.args[0], errors) from None + + def create_documenter(self, app: Sphinx, obj: Any, + parent: Any, full_name: str) -> Documenter: + """Get an autodoc.Documenter class suitable for documenting the given + object. + + Wraps get_documenter and is meant as a hook for extensions. + """ + doccls = get_documenter(app, obj, parent) + return doccls(self.bridge, full_name) + + def get_items(self, names: list[str]) -> list[tuple[str, str, str, str]]: + """Try to import the given names, and return a list of + ``[(name, signature, summary_string, real_name), ...]``. + """ + prefixes = get_import_prefixes_from_env(self.env) + + items: list[tuple[str, str, str, str]] = [] + + max_item_chars = 50 + + for name in names: + display_name = name + if name.startswith('~'): + name = name[1:] + display_name = name.split('.')[-1] + + try: + real_name, obj, parent, modname = self.import_by_name(name, prefixes=prefixes) + except ImportExceptionGroup as exc: + errors = list({f"* {type(e).__name__}: {e}" for e in exc.exceptions}) + logger.warning(__('autosummary: failed to import %s.\nPossible hints:\n%s'), + name, '\n'.join(errors), location=self.get_location()) + continue + + self.bridge.result = StringList() # initialize for each documenter + full_name = real_name + if not isinstance(obj, ModuleType): + # give explicitly separated module name, so that members + # of inner classes can be documented + full_name = modname + '::' + full_name[len(modname) + 1:] + # NB. using full_name here is important, since Documenters + # handle module prefixes slightly differently + documenter = self.create_documenter(self.env.app, obj, parent, full_name) + if not documenter.parse_name(): + logger.warning(__('failed to parse name %s'), real_name, + location=self.get_location()) + items.append((display_name, '', '', real_name)) + continue + if not documenter.import_object(): + logger.warning(__('failed to import object %s'), real_name, + location=self.get_location()) + items.append((display_name, '', '', real_name)) + continue + + # try to also get a source code analyzer for attribute docs + try: + documenter.analyzer = ModuleAnalyzer.for_module( + documenter.get_real_modname()) + # parse right now, to get PycodeErrors on parsing (results will + # be cached anyway) + documenter.analyzer.find_attr_docs() + except PycodeError as err: + logger.debug('[autodoc] module analyzer failed: %s', err) + # no source file -- e.g. for builtin and C modules + documenter.analyzer = None + + # -- Grab the signature + + try: + sig = documenter.format_signature(show_annotation=False) + except TypeError: + # the documenter does not support ``show_annotation`` option + sig = documenter.format_signature() + + if not sig: + sig = '' + else: + max_chars = max(10, max_item_chars - len(display_name)) + sig = mangle_signature(sig, max_chars=max_chars) + + # -- Grab the summary + + # bodge for ModuleDocumenter + documenter._extra_indent = '' # type: ignore[attr-defined] + + documenter.add_content(None) + summary = extract_summary(self.bridge.result.data[:], self.state.document) + + items.append((display_name, sig, summary, real_name)) + + return items + + def get_table(self, items: list[tuple[str, str, str, str]]) -> list[Node]: + """Generate a proper list of table nodes for autosummary:: directive. + + *items* is a list produced by :meth:`get_items`. + """ + table_spec = addnodes.tabular_col_spec() + table_spec['spec'] = r'\X{1}{2}\X{1}{2}' + + table = autosummary_table('') + real_table = nodes.table('', classes=['autosummary longtable']) + table.append(real_table) + group = nodes.tgroup('', cols=2) + real_table.append(group) + group.append(nodes.colspec('', colwidth=10)) + group.append(nodes.colspec('', colwidth=90)) + body = nodes.tbody('') + group.append(body) + + def append_row(*column_texts: str) -> None: + row = nodes.row('') + source, line = self.state_machine.get_source_and_line() + for text in column_texts: + node = nodes.paragraph('') + vl = StringList() + vl.append(text, '%s:%d:<autosummary>' % (source, line)) + with switch_source_input(self.state, vl): + self.state.nested_parse(vl, 0, node) + try: + if isinstance(node[0], nodes.paragraph): + node = node[0] + except IndexError: + pass + row.append(nodes.entry('', node)) + body.append(row) + + for name, sig, summary, real_name in items: + qualifier = 'obj' + if 'nosignatures' not in self.options: + col1 = f':py:{qualifier}:`{name} <{real_name}>`\\ {rst.escape(sig)}' + else: + col1 = f':py:{qualifier}:`{name} <{real_name}>`' + col2 = summary + append_row(col1, col2) + + return [table_spec, table] + + +def strip_arg_typehint(s: str) -> str: + """Strip a type hint from argument definition.""" + return s.split(':')[0].strip() + + +def _cleanup_signature(s: str) -> str: + """Clean up signature using inspect.signautre() for mangle_signature()""" + try: + sig = signature_from_str(s) + parameters = list(sig.parameters.values()) + for i, param in enumerate(parameters): + if param.annotation is not Parameter.empty: + # Remove typehints + param = param.replace(annotation=Parameter.empty) + if param.default is not Parameter.empty: + # Replace default value by "None" + param = param.replace(default=None) + parameters[i] = param + sig = sig.replace(parameters=parameters, return_annotation=Parameter.empty) + return str(sig) + except Exception: + # Return the original signature string if failed to clean (ex. parsing error) + return s + + +def mangle_signature(sig: str, max_chars: int = 30) -> str: + """Reformat a function signature to a more compact form.""" + s = _cleanup_signature(sig) + + # Strip return type annotation + s = re.sub(r"\)\s*->\s.*$", ")", s) + + # Remove parenthesis + s = re.sub(r"^\((.*)\)$", r"\1", s).strip() + + # Strip literals (which can contain things that confuse the code below) + s = re.sub(r"\\\\", "", s) # escaped backslash (maybe inside string) + s = re.sub(r"\\'", "", s) # escaped single quote + s = re.sub(r'\\"', "", s) # escaped double quote + s = re.sub(r"'[^']*'", "", s) # string literal (w/ single quote) + s = re.sub(r'"[^"]*"', "", s) # string literal (w/ double quote) + + # Strip complex objects (maybe default value of arguments) + while re.search(r'\([^)]*\)', s): # contents of parenthesis (ex. NamedTuple(attr=...)) + s = re.sub(r'\([^)]*\)', '', s) + while re.search(r'<[^>]*>', s): # contents of angle brackets (ex. <object>) + s = re.sub(r'<[^>]*>', '', s) + while re.search(r'{[^}]*}', s): # contents of curly brackets (ex. dict) + s = re.sub(r'{[^}]*}', '', s) + + # Parse the signature to arguments + options + args: list[str] = [] + opts: list[str] = [] + + opt_re = re.compile(r"^(.*, |)([a-zA-Z0-9_*]+)\s*=\s*") + while s: + m = opt_re.search(s) + if not m: + # The rest are arguments + args = s.split(', ') + break + + opts.insert(0, m.group(2)) + s = m.group(1)[:-2] + + # Strip typehints + for i, arg in enumerate(args): + args[i] = strip_arg_typehint(arg) + + for i, opt in enumerate(opts): + opts[i] = strip_arg_typehint(opt) + + # Produce a more compact signature + sig = limited_join(", ", args, max_chars=max_chars - 2) + if opts: + if not sig: + sig = "[%s]" % limited_join(", ", opts, max_chars=max_chars - 4) + elif len(sig) < max_chars - 4 - 2 - 3: + sig += "[, %s]" % limited_join(", ", opts, + max_chars=max_chars - len(sig) - 4 - 2) + + return "(%s)" % sig + + +def extract_summary(doc: list[str], document: Any) -> str: + """Extract summary from docstring.""" + def parse(doc: list[str], settings: Any) -> nodes.document: + state_machine = RSTStateMachine(state_classes, 'Body') + node = new_document('', settings) + node.reporter = NullReporter() + state_machine.run(doc, node) + + return node + + # Skip a blank lines at the top + while doc and not doc[0].strip(): + doc.pop(0) + + # If there's a blank line, then we can assume the first sentence / + # paragraph has ended, so anything after shouldn't be part of the + # summary + for i, piece in enumerate(doc): + if not piece.strip(): + doc = doc[:i] + break + + if doc == []: + return '' + + # parse the docstring + node = parse(doc, document.settings) + if isinstance(node[0], nodes.section): + # document starts with a section heading, so use that. + summary = node[0].astext().strip() + elif not isinstance(node[0], nodes.paragraph): + # document starts with non-paragraph: pick up the first line + summary = doc[0].strip() + else: + # Try to find the "first sentence", which may span multiple lines + sentences = periods_re.split(" ".join(doc)) + if len(sentences) == 1: + summary = sentences[0].strip() + else: + summary = '' + for i in range(len(sentences)): + summary = ". ".join(sentences[:i + 1]).rstrip(".") + "." + node[:] = [] + node = parse(doc, document.settings) + if summary.endswith(WELL_KNOWN_ABBREVIATIONS): + pass + elif not any(node.findall(nodes.system_message)): + # considered as that splitting by period does not break inline markups + break + + # strip literal notation mark ``::`` from tail of summary + summary = literal_re.sub('.', summary) + + return summary + + +def limited_join(sep: str, items: list[str], max_chars: int = 30, + overflow_marker: str = "...") -> str: + """Join a number of strings into one, limiting the length to *max_chars*. + + If the string overflows this limit, replace the last fitting item by + *overflow_marker*. + + Returns: joined_string + """ + full_str = sep.join(items) + if len(full_str) < max_chars: + return full_str + + n_chars = 0 + n_items = 0 + for item in items: + n_chars += len(item) + len(sep) + if n_chars < max_chars - len(overflow_marker): + n_items += 1 + else: + break + + return sep.join(list(items[:n_items]) + [overflow_marker]) + + +# -- Importing items ----------------------------------------------------------- + + +class ImportExceptionGroup(Exception): + """Exceptions raised during importing the target objects. + + It contains an error messages and a list of exceptions as its arguments. + """ + + def __init__(self, message: str | None, exceptions: Sequence[BaseException]): + super().__init__(message) + self.exceptions = list(exceptions) + + +def get_import_prefixes_from_env(env: BuildEnvironment) -> list[str | None]: + """ + Obtain current Python import prefixes (for `import_by_name`) + from ``document.env`` + """ + prefixes: list[str | None] = [None] + + currmodule = env.ref_context.get('py:module') + if currmodule: + prefixes.insert(0, currmodule) + + currclass = env.ref_context.get('py:class') + if currclass: + if currmodule: + prefixes.insert(0, currmodule + "." + currclass) + else: + prefixes.insert(0, currclass) + + return prefixes + + +def import_by_name( + name: str, prefixes: Sequence[str | None] = (None,), +) -> tuple[str, Any, Any, str]: + """Import a Python object that has the given *name*, under one of the + *prefixes*. The first name that succeeds is used. + """ + tried = [] + errors: list[ImportExceptionGroup] = [] + for prefix in prefixes: + try: + if prefix: + prefixed_name = '.'.join([prefix, name]) + else: + prefixed_name = name + obj, parent, modname = _import_by_name(prefixed_name, grouped_exception=True) + return prefixed_name, obj, parent, modname + except ImportError: + tried.append(prefixed_name) + except ImportExceptionGroup as exc: + tried.append(prefixed_name) + errors.append(exc) + + exceptions: list[BaseException] = sum((e.exceptions for e in errors), []) + raise ImportExceptionGroup('no module named %s' % ' or '.join(tried), exceptions) + + +def _import_by_name(name: str, grouped_exception: bool = True) -> tuple[Any, Any, str]: + """Import a Python object given its full name.""" + errors: list[BaseException] = [] + + try: + name_parts = name.split('.') + + # try first interpret `name` as MODNAME.OBJ + modname = '.'.join(name_parts[:-1]) + if modname: + try: + mod = import_module(modname) + return getattr(mod, name_parts[-1]), mod, modname + except (ImportError, IndexError, AttributeError) as exc: + errors.append(exc.__cause__ or exc) + + # ... then as MODNAME, MODNAME.OBJ1, MODNAME.OBJ1.OBJ2, ... + last_j = 0 + modname = '' + for j in reversed(range(1, len(name_parts) + 1)): + last_j = j + modname = '.'.join(name_parts[:j]) + try: + import_module(modname) + except ImportError as exc: + errors.append(exc.__cause__ or exc) + + if modname in sys.modules: + break + + if last_j < len(name_parts): + parent = None + obj = sys.modules[modname] + for obj_name in name_parts[last_j:]: + parent = obj + obj = getattr(obj, obj_name) + return obj, parent, modname + else: + return sys.modules[modname], None, modname + except (ValueError, ImportError, AttributeError, KeyError) as exc: + errors.append(exc) + if grouped_exception: + raise ImportExceptionGroup('', errors) from None # NoQA: EM101 + else: + raise ImportError(*exc.args) from exc + + +def import_ivar_by_name(name: str, prefixes: Sequence[str | None] = (None,), + grouped_exception: bool = True) -> tuple[str, Any, Any, str]: + """Import an instance variable that has the given *name*, under one of the + *prefixes*. The first name that succeeds is used. + """ + try: + name, attr = name.rsplit(".", 1) + real_name, obj, parent, modname = import_by_name(name, prefixes) + + # Get ancestors of the object (class.__mro__ includes the class itself as + # the first entry) + candidate_objects = getmro(obj) + if len(candidate_objects) == 0: + candidate_objects = (obj,) + + for candidate_obj in candidate_objects: + analyzer = ModuleAnalyzer.for_module(getattr(candidate_obj, '__module__', modname)) + analyzer.analyze() + # check for presence in `annotations` to include dataclass attributes + found_attrs = set() + found_attrs |= {attr for (qualname, attr) in analyzer.attr_docs} + found_attrs |= {attr for (qualname, attr) in analyzer.annotations} + if attr in found_attrs: + return real_name + "." + attr, INSTANCEATTR, obj, modname + except (ImportError, ValueError, PycodeError) as exc: + raise ImportError from exc + except ImportExceptionGroup: + raise # pass through it as is + + raise ImportError + + +# -- :autolink: (smart default role) ------------------------------------------- + +class AutoLink(SphinxRole): + """Smart linking role. + + Expands to ':obj:`text`' if `text` is an object that can be imported; + otherwise expands to '*text*'. + """ + def run(self) -> tuple[list[Node], list[system_message]]: + pyobj_role = self.env.get_domain('py').role('obj') + assert pyobj_role is not None + objects, errors = pyobj_role('obj', self.rawtext, self.text, self.lineno, + self.inliner, self.options, self.content) + if errors: + return objects, errors + + assert len(objects) == 1 + pending_xref = cast(addnodes.pending_xref, objects[0]) + try: + # try to import object by name + prefixes = get_import_prefixes_from_env(self.env) + import_by_name(pending_xref['reftarget'], prefixes) + except ImportExceptionGroup: + literal = cast(nodes.literal, pending_xref[0]) + objects[0] = nodes.emphasis(self.rawtext, literal.astext(), + classes=literal['classes']) + + return objects, errors + + +def get_rst_suffix(app: Sphinx) -> str | None: + def get_supported_format(suffix: str) -> tuple[str, ...]: + parser_class = app.registry.get_source_parsers().get(suffix) + if parser_class is None: + return ('restructuredtext',) + return parser_class.supported + + suffix = None + for suffix in app.config.source_suffix: + if 'restructuredtext' in get_supported_format(suffix): + return suffix + + return None + + +def process_generate_options(app: Sphinx) -> None: + genfiles = app.config.autosummary_generate + + if genfiles is True: + env = app.builder.env + genfiles = [env.doc2path(x, base=False) for x in env.found_docs + if os.path.isfile(env.doc2path(x))] + elif genfiles is False: + pass + else: + ext = list(app.config.source_suffix) + genfiles = [genfile + (ext[0] if not genfile.endswith(tuple(ext)) else '') + for genfile in genfiles] + + for entry in genfiles[:]: + if not path.isfile(path.join(app.srcdir, entry)): + logger.warning(__('autosummary_generate: file not found: %s'), entry) + genfiles.remove(entry) + + if not genfiles: + return + + suffix = get_rst_suffix(app) + if suffix is None: + logger.warning(__('autosummary generats .rst files internally. ' + 'But your source_suffix does not contain .rst. Skipped.')) + return + + from sphinx.ext.autosummary.generate import generate_autosummary_docs + + imported_members = app.config.autosummary_imported_members + with mock(app.config.autosummary_mock_imports): + generate_autosummary_docs(genfiles, suffix=suffix, base_path=app.srcdir, + app=app, imported_members=imported_members, + overwrite=app.config.autosummary_generate_overwrite, + encoding=app.config.source_encoding) + + +def setup(app: Sphinx) -> dict[str, Any]: + # I need autodoc + app.setup_extension('sphinx.ext.autodoc') + app.add_node(autosummary_toc, + html=(autosummary_toc_visit_html, autosummary_noop), + latex=(autosummary_noop, autosummary_noop), + text=(autosummary_noop, autosummary_noop), + man=(autosummary_noop, autosummary_noop), + texinfo=(autosummary_noop, autosummary_noop)) + app.add_node(autosummary_table, + html=(autosummary_table_visit_html, autosummary_noop), + latex=(autosummary_noop, autosummary_noop), + text=(autosummary_noop, autosummary_noop), + man=(autosummary_noop, autosummary_noop), + texinfo=(autosummary_noop, autosummary_noop)) + app.add_directive('autosummary', Autosummary) + app.add_role('autolink', AutoLink()) + app.connect('builder-inited', process_generate_options) + app.add_config_value('autosummary_context', {}, True) + app.add_config_value('autosummary_filename_map', {}, 'html') + app.add_config_value('autosummary_generate', True, True, [bool, list]) + app.add_config_value('autosummary_generate_overwrite', True, False) + app.add_config_value('autosummary_mock_imports', + lambda config: config.autodoc_mock_imports, 'env') + app.add_config_value('autosummary_imported_members', [], False, [bool]) + app.add_config_value('autosummary_ignore_module_all', True, 'env', bool) + + return {'version': sphinx.__display_version__, 'parallel_read_safe': True} diff --git a/sphinx/ext/autosummary/generate.py b/sphinx/ext/autosummary/generate.py new file mode 100644 index 0000000..06814f9 --- /dev/null +++ b/sphinx/ext/autosummary/generate.py @@ -0,0 +1,754 @@ +"""Generates reST source files for autosummary. + +Usable as a library or script to generate automatic RST source files for +items referred to in autosummary:: directives. + +Each generated RST file contains a single auto*:: directive which +extracts the docstring of the referred item. + +Example Makefile rule:: + + generate: + sphinx-autogen -o source/generated source/*.rst +""" + +from __future__ import annotations + +import argparse +import importlib +import inspect +import locale +import os +import pkgutil +import pydoc +import re +import sys +from os import path +from typing import TYPE_CHECKING, Any, NamedTuple + +from jinja2 import TemplateNotFound +from jinja2.sandbox import SandboxedEnvironment + +import sphinx.locale +from sphinx import __display_version__, package_dir +from sphinx.builders import Builder +from sphinx.config import Config +from sphinx.ext.autodoc.importer import import_module +from sphinx.ext.autosummary import ( + ImportExceptionGroup, + get_documenter, + import_by_name, + import_ivar_by_name, +) +from sphinx.locale import __ +from sphinx.pycode import ModuleAnalyzer, PycodeError +from sphinx.registry import SphinxComponentRegistry +from sphinx.util import logging, rst +from sphinx.util.inspect import getall, safe_getattr +from sphinx.util.osutil import ensuredir +from sphinx.util.template import SphinxTemplateLoader + +if TYPE_CHECKING: + from collections.abc import Sequence, Set + from gettext import NullTranslations + + from sphinx.application import Sphinx + from sphinx.ext.autodoc import Documenter + +logger = logging.getLogger(__name__) + + +class DummyApplication: + """Dummy Application class for sphinx-autogen command.""" + + def __init__(self, translator: NullTranslations) -> None: + self.config = Config() + self.registry = SphinxComponentRegistry() + self.messagelog: list[str] = [] + self.srcdir = "/" + self.translator = translator + self.verbosity = 0 + self._warncount = 0 + self.warningiserror = False + + self.config.add('autosummary_context', {}, True, None) + self.config.add('autosummary_filename_map', {}, True, None) + self.config.add('autosummary_ignore_module_all', True, 'env', bool) + self.config.init_values() + + def emit_firstresult(self, *args: Any) -> None: + pass + + +class AutosummaryEntry(NamedTuple): + name: str + path: str | None + template: str + recursive: bool + + +def setup_documenters(app: Any) -> None: + from sphinx.ext.autodoc import ( + AttributeDocumenter, + ClassDocumenter, + DataDocumenter, + DecoratorDocumenter, + ExceptionDocumenter, + FunctionDocumenter, + MethodDocumenter, + ModuleDocumenter, + PropertyDocumenter, + ) + documenters: list[type[Documenter]] = [ + ModuleDocumenter, ClassDocumenter, ExceptionDocumenter, DataDocumenter, + FunctionDocumenter, MethodDocumenter, + AttributeDocumenter, DecoratorDocumenter, PropertyDocumenter, + ] + for documenter in documenters: + app.registry.add_documenter(documenter.objtype, documenter) + + +def _underline(title: str, line: str = '=') -> str: + if '\n' in title: + msg = 'Can only underline single lines' + raise ValueError(msg) + return title + '\n' + line * len(title) + + +class AutosummaryRenderer: + """A helper class for rendering.""" + + def __init__(self, app: Sphinx) -> None: + if isinstance(app, Builder): + msg = 'Expected a Sphinx application object!' + raise ValueError(msg) + + system_templates_path = [os.path.join(package_dir, 'ext', 'autosummary', 'templates')] + loader = SphinxTemplateLoader(app.srcdir, app.config.templates_path, + system_templates_path) + + self.env = SandboxedEnvironment(loader=loader) + self.env.filters['escape'] = rst.escape + self.env.filters['e'] = rst.escape + self.env.filters['underline'] = _underline + + if app.translator: + self.env.add_extension("jinja2.ext.i18n") + self.env.install_gettext_translations(app.translator) + + def render(self, template_name: str, context: dict) -> str: + """Render a template file.""" + try: + template = self.env.get_template(template_name) + except TemplateNotFound: + try: + # objtype is given as template_name + template = self.env.get_template('autosummary/%s.rst' % template_name) + except TemplateNotFound: + # fallback to base.rst + template = self.env.get_template('autosummary/base.rst') + + return template.render(context) + + +def _split_full_qualified_name(name: str) -> tuple[str | None, str]: + """Split full qualified name to a pair of modname and qualname. + + A qualname is an abbreviation for "Qualified name" introduced at PEP-3155 + (https://peps.python.org/pep-3155/). It is a dotted path name + from the module top-level. + + A "full" qualified name means a string containing both module name and + qualified name. + + .. note:: This function actually imports the module to check its existence. + Therefore you need to mock 3rd party modules if needed before + calling this function. + """ + parts = name.split('.') + for i, _part in enumerate(parts, 1): + try: + modname = ".".join(parts[:i]) + importlib.import_module(modname) + except ImportError: + if parts[:i - 1]: + return ".".join(parts[:i - 1]), ".".join(parts[i - 1:]) + else: + return None, ".".join(parts) + except IndexError: + pass + + return name, "" + + +# -- Generating output --------------------------------------------------------- + + +class ModuleScanner: + def __init__(self, app: Any, obj: Any) -> None: + self.app = app + self.object = obj + + def get_object_type(self, name: str, value: Any) -> str: + return get_documenter(self.app, value, self.object).objtype + + def is_skipped(self, name: str, value: Any, objtype: str) -> bool: + try: + return self.app.emit_firstresult('autodoc-skip-member', objtype, + name, value, False, {}) + except Exception as exc: + logger.warning(__('autosummary: failed to determine %r to be documented, ' + 'the following exception was raised:\n%s'), + name, exc, type='autosummary') + return False + + def scan(self, imported_members: bool) -> list[str]: + members = [] + try: + analyzer = ModuleAnalyzer.for_module(self.object.__name__) + attr_docs = analyzer.find_attr_docs() + except PycodeError: + attr_docs = {} + + for name in members_of(self.object, self.app.config): + try: + value = safe_getattr(self.object, name) + except AttributeError: + value = None + + objtype = self.get_object_type(name, value) + if self.is_skipped(name, value, objtype): + continue + + try: + if ('', name) in attr_docs: + imported = False + elif inspect.ismodule(value): # NoQA: SIM114 + imported = True + elif safe_getattr(value, '__module__') != self.object.__name__: + imported = True + else: + imported = False + except AttributeError: + imported = False + + respect_module_all = not self.app.config.autosummary_ignore_module_all + if ( + # list all members up + imported_members + # list not-imported members + or imported is False + # list members that have __all__ set + or (respect_module_all and '__all__' in dir(self.object)) + ): + members.append(name) + + return members + + +def members_of(obj: Any, conf: Config) -> Sequence[str]: + """Get the members of ``obj``, possibly ignoring the ``__all__`` module attribute + + Follows the ``conf.autosummary_ignore_module_all`` setting.""" + + if conf.autosummary_ignore_module_all: + return dir(obj) + else: + return getall(obj) or dir(obj) + + +def generate_autosummary_content(name: str, obj: Any, parent: Any, + template: AutosummaryRenderer, template_name: str, + imported_members: bool, app: Any, + recursive: bool, context: dict, + modname: str | None = None, + qualname: str | None = None) -> str: + doc = get_documenter(app, obj, parent) + + ns: dict[str, Any] = {} + ns.update(context) + + if doc.objtype == 'module': + scanner = ModuleScanner(app, obj) + ns['members'] = scanner.scan(imported_members) + + respect_module_all = not app.config.autosummary_ignore_module_all + imported_members = imported_members or ('__all__' in dir(obj) and respect_module_all) + + ns['functions'], ns['all_functions'] = \ + _get_members(doc, app, obj, {'function'}, imported=imported_members) + ns['classes'], ns['all_classes'] = \ + _get_members(doc, app, obj, {'class'}, imported=imported_members) + ns['exceptions'], ns['all_exceptions'] = \ + _get_members(doc, app, obj, {'exception'}, imported=imported_members) + ns['attributes'], ns['all_attributes'] = \ + _get_module_attrs(name, ns['members']) + ispackage = hasattr(obj, '__path__') + if ispackage and recursive: + # Use members that are not modules as skip list, because it would then mean + # that module was overwritten in the package namespace + skip = ( + ns["all_functions"] + + ns["all_classes"] + + ns["all_exceptions"] + + ns["all_attributes"] + ) + + # If respect_module_all and module has a __all__ attribute, first get + # modules that were explicitly imported. Next, find the rest with the + # get_modules method, but only put in "public" modules that are in the + # __all__ list + # + # Otherwise, use get_modules method normally + if respect_module_all and '__all__' in dir(obj): + imported_modules, all_imported_modules = \ + _get_members(doc, app, obj, {'module'}, imported=True) + skip += all_imported_modules + imported_modules = [name + '.' + modname for modname in imported_modules] + all_imported_modules = \ + [name + '.' + modname for modname in all_imported_modules] + public_members = getall(obj) + else: + imported_modules, all_imported_modules = [], [] + public_members = None + + modules, all_modules = _get_modules(obj, skip=skip, name=name, + public_members=public_members) + ns['modules'] = imported_modules + modules + ns["all_modules"] = all_imported_modules + all_modules + elif doc.objtype == 'class': + ns['members'] = dir(obj) + ns['inherited_members'] = \ + set(dir(obj)) - set(obj.__dict__.keys()) + ns['methods'], ns['all_methods'] = \ + _get_members(doc, app, obj, {'method'}, include_public={'__init__'}) + ns['attributes'], ns['all_attributes'] = \ + _get_members(doc, app, obj, {'attribute', 'property'}) + + if modname is None or qualname is None: + modname, qualname = _split_full_qualified_name(name) + + if doc.objtype in ('method', 'attribute', 'property'): + ns['class'] = qualname.rsplit(".", 1)[0] + + if doc.objtype in ('class',): + shortname = qualname + else: + shortname = qualname.rsplit(".", 1)[-1] + + ns['fullname'] = name + ns['module'] = modname + ns['objname'] = qualname + ns['name'] = shortname + + ns['objtype'] = doc.objtype + ns['underline'] = len(name) * '=' + + if template_name: + return template.render(template_name, ns) + else: + return template.render(doc.objtype, ns) + + +def _skip_member(app: Sphinx, obj: Any, name: str, objtype: str) -> bool: + try: + return app.emit_firstresult('autodoc-skip-member', objtype, name, + obj, False, {}) + except Exception as exc: + logger.warning(__('autosummary: failed to determine %r to be documented, ' + 'the following exception was raised:\n%s'), + name, exc, type='autosummary') + return False + + +def _get_class_members(obj: Any) -> dict[str, Any]: + members = sphinx.ext.autodoc.get_class_members(obj, None, safe_getattr) + return {name: member.object for name, member in members.items()} + + +def _get_module_members(app: Sphinx, obj: Any) -> dict[str, Any]: + members = {} + for name in members_of(obj, app.config): + try: + members[name] = safe_getattr(obj, name) + except AttributeError: + continue + return members + + +def _get_all_members(doc: type[Documenter], app: Sphinx, obj: Any) -> dict[str, Any]: + if doc.objtype == 'module': + return _get_module_members(app, obj) + elif doc.objtype == 'class': + return _get_class_members(obj) + return {} + + +def _get_members(doc: type[Documenter], app: Sphinx, obj: Any, types: set[str], *, + include_public: Set[str] = frozenset(), + imported: bool = True) -> tuple[list[str], list[str]]: + items: list[str] = [] + public: list[str] = [] + + all_members = _get_all_members(doc, app, obj) + for name, value in all_members.items(): + documenter = get_documenter(app, value, obj) + if documenter.objtype in types: + # skip imported members if expected + if imported or getattr(value, '__module__', None) == obj.__name__: + skipped = _skip_member(app, value, name, documenter.objtype) + if skipped is True: + pass + elif skipped is False: + # show the member forcedly + items.append(name) + public.append(name) + else: + items.append(name) + if name in include_public or not name.startswith('_'): + # considers member as public + public.append(name) + return public, items + + +def _get_module_attrs(name: str, members: Any) -> tuple[list[str], list[str]]: + """Find module attributes with docstrings.""" + attrs, public = [], [] + try: + analyzer = ModuleAnalyzer.for_module(name) + attr_docs = analyzer.find_attr_docs() + for namespace, attr_name in attr_docs: + if namespace == '' and attr_name in members: + attrs.append(attr_name) + if not attr_name.startswith('_'): + public.append(attr_name) + except PycodeError: + pass # give up if ModuleAnalyzer fails to parse code + return public, attrs + + +def _get_modules( + obj: Any, + *, + skip: Sequence[str], + name: str, + public_members: Sequence[str] | None = None) -> tuple[list[str], list[str]]: + items: list[str] = [] + public: list[str] = [] + for _, modname, _ispkg in pkgutil.iter_modules(obj.__path__): + + if modname in skip: + # module was overwritten in __init__.py, so not accessible + continue + fullname = name + '.' + modname + try: + module = import_module(fullname) + if module and hasattr(module, '__sphinx_mock__'): + continue + except ImportError: + pass + + items.append(fullname) + if public_members is not None: + if modname in public_members: + public.append(fullname) + else: + if not modname.startswith('_'): + public.append(fullname) + return public, items + + +def generate_autosummary_docs(sources: list[str], + output_dir: str | os.PathLike[str] | None = None, + suffix: str = '.rst', + base_path: str | os.PathLike[str] | None = None, + imported_members: bool = False, app: Any = None, + overwrite: bool = True, encoding: str = 'utf-8') -> None: + showed_sources = sorted(sources) + if len(showed_sources) > 20: + showed_sources = showed_sources[:10] + ['...'] + showed_sources[-10:] + logger.info(__('[autosummary] generating autosummary for: %s') % + ', '.join(showed_sources)) + + if output_dir: + logger.info(__('[autosummary] writing to %s') % output_dir) + + if base_path is not None: + sources = [os.path.join(base_path, filename) for filename in sources] + + template = AutosummaryRenderer(app) + + # read + items = find_autosummary_in_files(sources) + + # keep track of new files + new_files = [] + + if app: + filename_map = app.config.autosummary_filename_map + else: + filename_map = {} + + # write + for entry in sorted(set(items), key=str): + if entry.path is None: + # The corresponding autosummary:: directive did not have + # a :toctree: option + continue + + path = output_dir or os.path.abspath(entry.path) + ensuredir(path) + + try: + name, obj, parent, modname = import_by_name(entry.name) + qualname = name.replace(modname + ".", "") + except ImportExceptionGroup as exc: + try: + # try to import as an instance attribute + name, obj, parent, modname = import_ivar_by_name(entry.name) + qualname = name.replace(modname + ".", "") + except ImportError as exc2: + if exc2.__cause__: + exceptions: list[BaseException] = exc.exceptions + [exc2.__cause__] + else: + exceptions = exc.exceptions + [exc2] + + errors = list({f"* {type(e).__name__}: {e}" for e in exceptions}) + logger.warning(__('[autosummary] failed to import %s.\nPossible hints:\n%s'), + entry.name, '\n'.join(errors)) + continue + + context: dict[str, Any] = {} + if app: + context.update(app.config.autosummary_context) + + content = generate_autosummary_content(name, obj, parent, template, entry.template, + imported_members, app, entry.recursive, context, + modname, qualname) + + filename = os.path.join(path, filename_map.get(name, name) + suffix) + if os.path.isfile(filename): + with open(filename, encoding=encoding) as f: + old_content = f.read() + + if content == old_content: + continue + if overwrite: # content has changed + with open(filename, 'w', encoding=encoding) as f: + f.write(content) + new_files.append(filename) + else: + with open(filename, 'w', encoding=encoding) as f: + f.write(content) + new_files.append(filename) + + # descend recursively to new files + if new_files: + generate_autosummary_docs(new_files, output_dir=output_dir, + suffix=suffix, base_path=base_path, + imported_members=imported_members, app=app, + overwrite=overwrite) + + +# -- Finding documented entries in files --------------------------------------- + +def find_autosummary_in_files(filenames: list[str]) -> list[AutosummaryEntry]: + """Find out what items are documented in source/*.rst. + + See `find_autosummary_in_lines`. + """ + documented: list[AutosummaryEntry] = [] + for filename in filenames: + with open(filename, encoding='utf-8', errors='ignore') as f: + lines = f.read().splitlines() + documented.extend(find_autosummary_in_lines(lines, filename=filename)) + return documented + + +def find_autosummary_in_docstring( + name: str, filename: str | None = None, +) -> list[AutosummaryEntry]: + """Find out what items are documented in the given object's docstring. + + See `find_autosummary_in_lines`. + """ + try: + real_name, obj, parent, modname = import_by_name(name) + lines = pydoc.getdoc(obj).splitlines() + return find_autosummary_in_lines(lines, module=name, filename=filename) + except AttributeError: + pass + except ImportExceptionGroup as exc: + errors = '\n'.join({f"* {type(e).__name__}: {e}" for e in exc.exceptions}) + logger.warning(f'Failed to import {name}.\nPossible hints:\n{errors}') # NoQA: G004 + except SystemExit: + logger.warning("Failed to import '%s'; the module executes module level " + 'statement and it might call sys.exit().', name) + return [] + + +def find_autosummary_in_lines( + lines: list[str], module: str | None = None, filename: str | None = None, +) -> list[AutosummaryEntry]: + """Find out what items appear in autosummary:: directives in the + given lines. + + Returns a list of (name, toctree, template) where *name* is a name + of an object and *toctree* the :toctree: path of the corresponding + autosummary directive (relative to the root of the file name), and + *template* the value of the :template: option. *toctree* and + *template* ``None`` if the directive does not have the + corresponding options set. + """ + autosummary_re = re.compile(r'^(\s*)\.\.\s+autosummary::\s*') + automodule_re = re.compile( + r'^\s*\.\.\s+automodule::\s*([A-Za-z0-9_.]+)\s*$') + module_re = re.compile( + r'^\s*\.\.\s+(current)?module::\s*([a-zA-Z0-9_.]+)\s*$') + autosummary_item_re = re.compile(r'^\s+(~?[_a-zA-Z][a-zA-Z0-9_.]*)\s*.*?') + recursive_arg_re = re.compile(r'^\s+:recursive:\s*$') + toctree_arg_re = re.compile(r'^\s+:toctree:\s*(.*?)\s*$') + template_arg_re = re.compile(r'^\s+:template:\s*(.*?)\s*$') + + documented: list[AutosummaryEntry] = [] + + recursive = False + toctree: str | None = None + template = '' + current_module = module + in_autosummary = False + base_indent = "" + + for line in lines: + if in_autosummary: + m = recursive_arg_re.match(line) + if m: + recursive = True + continue + + m = toctree_arg_re.match(line) + if m: + toctree = m.group(1) + if filename: + toctree = os.path.join(os.path.dirname(filename), + toctree) + continue + + m = template_arg_re.match(line) + if m: + template = m.group(1).strip() + continue + + if line.strip().startswith(':'): + continue # skip options + + m = autosummary_item_re.match(line) + if m: + name = m.group(1).strip() + if name.startswith('~'): + name = name[1:] + if current_module and \ + not name.startswith(current_module + '.'): + name = f"{current_module}.{name}" + documented.append(AutosummaryEntry(name, toctree, template, recursive)) + continue + + if not line.strip() or line.startswith(base_indent + " "): + continue + + in_autosummary = False + + m = autosummary_re.match(line) + if m: + in_autosummary = True + base_indent = m.group(1) + recursive = False + toctree = None + template = '' + continue + + m = automodule_re.search(line) + if m: + current_module = m.group(1).strip() + # recurse into the automodule docstring + documented.extend(find_autosummary_in_docstring( + current_module, filename=filename)) + continue + + m = module_re.match(line) + if m: + current_module = m.group(2) + continue + + return documented + + +def get_parser() -> argparse.ArgumentParser: + parser = argparse.ArgumentParser( + usage='%(prog)s [OPTIONS] <SOURCE_FILE>...', + epilog=__('For more information, visit <https://www.sphinx-doc.org/>.'), + description=__(""" +Generate ReStructuredText using autosummary directives. + +sphinx-autogen is a frontend to sphinx.ext.autosummary.generate. It generates +the reStructuredText files from the autosummary directives contained in the +given input files. + +The format of the autosummary directive is documented in the +``sphinx.ext.autosummary`` Python module and can be read using:: + + pydoc sphinx.ext.autosummary +""")) + + parser.add_argument('--version', action='version', dest='show_version', + version='%%(prog)s %s' % __display_version__) + + parser.add_argument('source_file', nargs='+', + help=__('source files to generate rST files for')) + + parser.add_argument('-o', '--output-dir', action='store', + dest='output_dir', + help=__('directory to place all output in')) + parser.add_argument('-s', '--suffix', action='store', dest='suffix', + default='rst', + help=__('default suffix for files (default: ' + '%(default)s)')) + parser.add_argument('-t', '--templates', action='store', dest='templates', + default=None, + help=__('custom template directory (default: ' + '%(default)s)')) + parser.add_argument('-i', '--imported-members', action='store_true', + dest='imported_members', default=False, + help=__('document imported members (default: ' + '%(default)s)')) + parser.add_argument('-a', '--respect-module-all', action='store_true', + dest='respect_module_all', default=False, + help=__('document exactly the members in module __all__ attribute. ' + '(default: %(default)s)')) + + return parser + + +def main(argv: Sequence[str] = (), /) -> None: + locale.setlocale(locale.LC_ALL, '') + sphinx.locale.init_console() + + app = DummyApplication(sphinx.locale.get_translator()) + logging.setup(app, sys.stdout, sys.stderr) # type: ignore[arg-type] + setup_documenters(app) + args = get_parser().parse_args(argv or sys.argv[1:]) + + if args.templates: + app.config.templates_path.append(path.abspath(args.templates)) + app.config.autosummary_ignore_module_all = ( # type: ignore[attr-defined] + not args.respect_module_all + ) + + generate_autosummary_docs(args.source_file, args.output_dir, + '.' + args.suffix, + imported_members=args.imported_members, + app=app) + + +if __name__ == '__main__': + main(sys.argv[1:]) diff --git a/sphinx/ext/autosummary/templates/autosummary/base.rst b/sphinx/ext/autosummary/templates/autosummary/base.rst new file mode 100644 index 0000000..b7556eb --- /dev/null +++ b/sphinx/ext/autosummary/templates/autosummary/base.rst @@ -0,0 +1,5 @@ +{{ fullname | escape | underline}} + +.. currentmodule:: {{ module }} + +.. auto{{ objtype }}:: {{ objname }} diff --git a/sphinx/ext/autosummary/templates/autosummary/class.rst b/sphinx/ext/autosummary/templates/autosummary/class.rst new file mode 100644 index 0000000..0f7d6f3 --- /dev/null +++ b/sphinx/ext/autosummary/templates/autosummary/class.rst @@ -0,0 +1,29 @@ +{{ fullname | escape | underline}} + +.. currentmodule:: {{ module }} + +.. autoclass:: {{ objname }} + + {% block methods %} + .. automethod:: __init__ + + {% if methods %} + .. rubric:: {{ _('Methods') }} + + .. autosummary:: + {% for item in methods %} + ~{{ name }}.{{ item }} + {%- endfor %} + {% endif %} + {% endblock %} + + {% block attributes %} + {% if attributes %} + .. rubric:: {{ _('Attributes') }} + + .. autosummary:: + {% for item in attributes %} + ~{{ name }}.{{ item }} + {%- endfor %} + {% endif %} + {% endblock %} diff --git a/sphinx/ext/autosummary/templates/autosummary/module.rst b/sphinx/ext/autosummary/templates/autosummary/module.rst new file mode 100644 index 0000000..e74c012 --- /dev/null +++ b/sphinx/ext/autosummary/templates/autosummary/module.rst @@ -0,0 +1,60 @@ +{{ fullname | escape | underline}} + +.. automodule:: {{ fullname }} + + {% block attributes %} + {% if attributes %} + .. rubric:: {{ _('Module Attributes') }} + + .. autosummary:: + {% for item in attributes %} + {{ item }} + {%- endfor %} + {% endif %} + {% endblock %} + + {% block functions %} + {% if functions %} + .. rubric:: {{ _('Functions') }} + + .. autosummary:: + {% for item in functions %} + {{ item }} + {%- endfor %} + {% endif %} + {% endblock %} + + {% block classes %} + {% if classes %} + .. rubric:: {{ _('Classes') }} + + .. autosummary:: + {% for item in classes %} + {{ item }} + {%- endfor %} + {% endif %} + {% endblock %} + + {% block exceptions %} + {% if exceptions %} + .. rubric:: {{ _('Exceptions') }} + + .. autosummary:: + {% for item in exceptions %} + {{ item }} + {%- endfor %} + {% endif %} + {% endblock %} + +{% block modules %} +{% if modules %} +.. rubric:: Modules + +.. autosummary:: + :toctree: + :recursive: +{% for item in modules %} + {{ item }} +{%- endfor %} +{% endif %} +{% endblock %} |