755 lines
27 KiB
Python
755 lines
27 KiB
Python
"""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:])
|