[format] Add ruff formating for apidoc/autosummary (#12447)

This commit is contained in:
Chris Sewell 2024-06-20 09:37:49 +02:00 committed by GitHub
parent 13b16c9b04
commit ee92847a0a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 501 additions and 251 deletions

View File

@ -448,7 +448,32 @@ exclude = [
"sphinx/directives/*", "sphinx/directives/*",
"sphinx/domains/*", "sphinx/domains/*",
"sphinx/environment/*", "sphinx/environment/*",
"sphinx/ext/*", "sphinx/ext/autodoc/__init__.py",
"sphinx/ext/autodoc/directive.py",
"sphinx/ext/autodoc/importer.py",
"sphinx/ext/autodoc/mock.py",
"sphinx/ext/autodoc/preserve_defaults.py",
"sphinx/ext/autodoc/type_comment.py",
"sphinx/ext/autodoc/typehints.py",
"sphinx/ext/autosectionlabel.py",
"sphinx/ext/autosummary/__init__.py",
"sphinx/ext/coverage.py",
"sphinx/ext/doctest.py",
"sphinx/ext/duration.py",
"sphinx/ext/extlinks.py",
"sphinx/ext/githubpages.py",
"sphinx/ext/graphviz.py",
"sphinx/ext/ifconfig.py",
"sphinx/ext/imgconverter.py",
"sphinx/ext/imgmath.py",
"sphinx/ext/inheritance_diagram.py",
"sphinx/ext/intersphinx/*",
"sphinx/ext/linkcode.py",
"sphinx/ext/mathjax.py",
"sphinx/ext/napoleon/__init__.py",
"sphinx/ext/napoleon/docstring.py",
"sphinx/ext/todo.py",
"sphinx/ext/viewcode.py",
"sphinx/pycode/*", "sphinx/pycode/*",
"sphinx/pygments_styles.py", "sphinx/pygments_styles.py",
"sphinx/registry.py", "sphinx/registry.py",

View File

@ -95,8 +95,9 @@ def write_file(name: str, text: str, opts: Any) -> None:
f.write(text) f.write(text)
def create_module_file(package: str | None, basename: str, opts: Any, def create_module_file(
user_template_dir: str | None = None) -> None: package: str | None, basename: str, opts: Any, user_template_dir: str | None = None
) -> None:
"""Build the text of the file and write the file.""" """Build the text of the file and write the file."""
options = copy(OPTIONS) options = copy(OPTIONS)
if opts.includeprivate and 'private-members' not in options: if opts.includeprivate and 'private-members' not in options:
@ -117,24 +118,32 @@ def create_module_file(package: str | None, basename: str, opts: Any,
write_file(qualname, text, opts) write_file(qualname, text, opts)
def create_package_file(root: str, master_package: str | None, subroot: str, def create_package_file(
py_files: list[str], root: str,
opts: Any, subs: list[str], is_namespace: bool, master_package: str | None,
excludes: Sequence[re.Pattern[str]] = (), subroot: str,
user_template_dir: str | None = None, py_files: list[str],
) -> None: opts: Any,
subs: list[str],
is_namespace: bool,
excludes: Sequence[re.Pattern[str]] = (),
user_template_dir: str | None = None,
) -> None:
"""Build the text of the file and write the file.""" """Build the text of the file and write the file."""
# build a list of sub packages (directories containing an __init__ file) # build a list of sub packages (directories containing an __init__ file)
subpackages = [module_join(master_package, subroot, pkgname) subpackages = [
for pkgname in subs module_join(master_package, subroot, pkgname)
if not is_skipped_package(path.join(root, pkgname), opts, excludes)] for pkgname in subs
if not is_skipped_package(path.join(root, pkgname), opts, excludes)
]
# build a list of sub modules # build a list of sub modules
submodules = [sub.split('.')[0] for sub in py_files submodules = [
if not is_skipped_module(path.join(root, sub), opts, excludes) and sub.split('.')[0]
not is_initpy(sub)] for sub in py_files
if not is_skipped_module(path.join(root, sub), opts, excludes) and not is_initpy(sub)
]
submodules = sorted(set(submodules)) submodules = sorted(set(submodules))
submodules = [module_join(master_package, subroot, modname) submodules = [module_join(master_package, subroot, modname) for modname in submodules]
for modname in submodules]
options = copy(OPTIONS) options = copy(OPTIONS)
if opts.includeprivate and 'private-members' not in options: if opts.includeprivate and 'private-members' not in options:
options.append('private-members') options.append('private-members')
@ -163,8 +172,9 @@ def create_package_file(root: str, master_package: str | None, subroot: str,
create_module_file(None, submodule, opts, user_template_dir) create_module_file(None, submodule, opts, user_template_dir)
def create_modules_toc_file(modules: list[str], opts: Any, name: str = 'modules', def create_modules_toc_file(
user_template_dir: str | None = None) -> None: modules: list[str], opts: Any, name: str = 'modules', user_template_dir: str | None = None
) -> None:
"""Create the module's index.""" """Create the module's index."""
modules.sort() modules.sort()
prev_module = '' prev_module = ''
@ -188,8 +198,9 @@ def create_modules_toc_file(modules: list[str], opts: Any, name: str = 'modules'
write_file(name, text, opts) write_file(name, text, opts)
def is_skipped_package(dirname: str, opts: Any, def is_skipped_package(
excludes: Sequence[re.Pattern[str]] = ()) -> bool: dirname: str, opts: Any, excludes: Sequence[re.Pattern[str]] = ()
) -> bool:
"""Check if we want to skip this module.""" """Check if we want to skip this module."""
if not path.isdir(dirname): if not path.isdir(dirname):
return False return False
@ -213,17 +224,22 @@ def is_skipped_module(filename: str, opts: Any, _excludes: Sequence[re.Pattern[s
return path.basename(filename).startswith('_') and not opts.includeprivate return path.basename(filename).startswith('_') and not opts.includeprivate
def walk(rootpath: str, excludes: Sequence[re.Pattern[str]], opts: Any, def walk(
) -> Iterator[tuple[str, list[str], list[str]]]: rootpath: str,
excludes: Sequence[re.Pattern[str]],
opts: Any,
) -> Iterator[tuple[str, list[str], list[str]]]:
"""Walk through the directory and list files and subdirectories up.""" """Walk through the directory and list files and subdirectories up."""
followlinks = getattr(opts, 'followlinks', False) followlinks = getattr(opts, 'followlinks', False)
includeprivate = getattr(opts, 'includeprivate', False) includeprivate = getattr(opts, 'includeprivate', False)
for root, subs, files in os.walk(rootpath, followlinks=followlinks): for root, subs, files in os.walk(rootpath, followlinks=followlinks):
# document only Python module files (that aren't excluded) # document only Python module files (that aren't excluded)
files = sorted(f for f in files files = sorted(
if f.endswith(PY_SUFFIXES) and f
not is_excluded(path.join(root, f), excludes)) for f in files
if f.endswith(PY_SUFFIXES) and not is_excluded(path.join(root, f), excludes)
)
# remove hidden ('.') and private ('_') directories, as well as # remove hidden ('.') and private ('_') directories, as well as
# excluded dirs # excluded dirs
@ -232,22 +248,27 @@ def walk(rootpath: str, excludes: Sequence[re.Pattern[str]], opts: Any,
else: else:
exclude_prefixes = ('.', '_') exclude_prefixes = ('.', '_')
subs[:] = sorted(sub for sub in subs if not sub.startswith(exclude_prefixes) and subs[:] = sorted(
not is_excluded(path.join(root, sub), excludes)) sub
for sub in subs
if not sub.startswith(exclude_prefixes)
and not is_excluded(path.join(root, sub), excludes)
)
yield root, subs, files yield root, subs, files
def has_child_module(rootpath: str, excludes: Sequence[re.Pattern[str]], opts: Any) -> bool: def has_child_module(rootpath: str, excludes: Sequence[re.Pattern[str]], opts: Any) -> bool:
"""Check the given directory contains child module/s (at least one).""" """Check the given directory contains child module/s (at least one)."""
return any( return any(files for _root, _subs, files in walk(rootpath, excludes, opts))
files
for _root, _subs, files in walk(rootpath, excludes, opts)
)
def recurse_tree(rootpath: str, excludes: Sequence[re.Pattern[str]], opts: Any, def recurse_tree(
user_template_dir: str | None = None) -> list[str]: rootpath: str,
excludes: Sequence[re.Pattern[str]],
opts: Any,
user_template_dir: str | None = None,
) -> list[str]:
""" """
Look for every file in the directory tree and create the corresponding Look for every file in the directory tree and create the corresponding
ReST files. ReST files.
@ -279,14 +300,21 @@ def recurse_tree(rootpath: str, excludes: Sequence[re.Pattern[str]], opts: Any,
if is_pkg or is_namespace: if is_pkg or is_namespace:
# we are in a package with something to document # we are in a package with something to document
if subs or len(files) > 1 or not is_skipped_package(root, opts): if subs or len(files) > 1 or not is_skipped_package(root, opts):
subpackage = root[len(rootpath):].lstrip(path.sep).\ subpackage = root[len(rootpath) :].lstrip(path.sep).replace(path.sep, '.')
replace(path.sep, '.')
# if this is not a namespace or # if this is not a namespace or
# a namespace and there is something there to document # a namespace and there is something there to document
if not is_namespace or has_child_module(root, excludes, opts): if not is_namespace or has_child_module(root, excludes, opts):
create_package_file(root, root_package, subpackage, create_package_file(
files, opts, subs, is_namespace, excludes, root,
user_template_dir) root_package,
subpackage,
files,
opts,
subs,
is_namespace,
excludes,
user_template_dir,
)
toplevels.append(module_join(root_package, subpackage)) toplevels.append(module_join(root_package, subpackage))
else: else:
# if we are at the root level, we don't require it to be a package # if we are at the root level, we don't require it to be a package
@ -312,8 +340,7 @@ def is_excluded(root: str, excludes: Sequence[re.Pattern[str]]) -> bool:
def get_parser() -> argparse.ArgumentParser: def get_parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser( parser = argparse.ArgumentParser(
usage='%(prog)s [OPTIONS] -o <OUTPUT_PATH> <MODULE_PATH> ' usage='%(prog)s [OPTIONS] -o <OUTPUT_PATH> <MODULE_PATH> ' '[EXCLUDE_PATTERN, ...]',
'[EXCLUDE_PATTERN, ...]',
epilog=__('For more information, visit <https://www.sphinx-doc.org/>.'), epilog=__('For more information, visit <https://www.sphinx-doc.org/>.'),
description=__(""" description=__("""
Look recursively in <MODULE_PATH> for Python modules and packages and create Look recursively in <MODULE_PATH> for Python modules and packages and create
@ -322,87 +349,196 @@ one reST file with automodule directives per package in the <OUTPUT_PATH>.
The <EXCLUDE_PATTERN>s can be file and/or directory patterns that will be The <EXCLUDE_PATTERN>s can be file and/or directory patterns that will be
excluded from generation. excluded from generation.
Note: By default this script will not overwrite already created files.""")) Note: By default this script will not overwrite already created files."""),
)
parser.add_argument('--version', action='version', dest='show_version', parser.add_argument(
version='%%(prog)s %s' % __display_version__) '--version',
action='version',
dest='show_version',
version='%%(prog)s %s' % __display_version__,
)
parser.add_argument('module_path', parser.add_argument('module_path', help=__('path to module to document'))
help=__('path to module to document')) parser.add_argument(
parser.add_argument('exclude_pattern', nargs='*', 'exclude_pattern',
help=__('fnmatch-style file and/or directory patterns ' nargs='*',
'to exclude from generation')) help=__('fnmatch-style file and/or directory patterns ' 'to exclude from generation'),
)
parser.add_argument('-o', '--output-dir', action='store', dest='destdir', parser.add_argument(
required=True, '-o',
help=__('directory to place all output')) '--output-dir',
parser.add_argument('-q', action='store_true', dest='quiet', action='store',
help=__('no output on stdout, just warnings on stderr')) dest='destdir',
parser.add_argument('-d', '--maxdepth', action='store', dest='maxdepth', required=True,
type=int, default=4, help=__('directory to place all output'),
help=__('maximum depth of submodules to show in the TOC ' )
'(default: 4)')) parser.add_argument(
parser.add_argument('-f', '--force', action='store_true', dest='force', '-q',
help=__('overwrite existing files')) action='store_true',
parser.add_argument('-l', '--follow-links', action='store_true', dest='quiet',
dest='followlinks', default=False, help=__('no output on stdout, just warnings on stderr'),
help=__('follow symbolic links. Powerful when combined ' )
'with collective.recipe.omelette.')) parser.add_argument(
parser.add_argument('-n', '--dry-run', action='store_true', dest='dryrun', '-d',
help=__('run the script without creating files')) '--maxdepth',
parser.add_argument('-e', '--separate', action='store_true', action='store',
dest='separatemodules', dest='maxdepth',
help=__('put documentation for each module on its own page')) type=int,
parser.add_argument('-P', '--private', action='store_true', default=4,
dest='includeprivate', help=__('maximum depth of submodules to show in the TOC ' '(default: 4)'),
help=__('include "_private" modules')) )
parser.add_argument('--tocfile', action='store', dest='tocfile', default='modules', parser.add_argument(
help=__("filename of table of contents (default: modules)")) '-f', '--force', action='store_true', dest='force', help=__('overwrite existing files')
parser.add_argument('-T', '--no-toc', action='store_false', dest='tocfile', )
help=__("don't create a table of contents file")) parser.add_argument(
parser.add_argument('-E', '--no-headings', action='store_true', '-l',
dest='noheadings', '--follow-links',
help=__("don't create headings for the module/package " action='store_true',
"packages (e.g. when the docstrings already " dest='followlinks',
"contain them)")) default=False,
parser.add_argument('-M', '--module-first', action='store_true', help=__(
dest='modulefirst', 'follow symbolic links. Powerful when combined ' 'with collective.recipe.omelette.'
help=__('put module documentation before submodule ' ),
'documentation')) )
parser.add_argument('--implicit-namespaces', action='store_true', parser.add_argument(
dest='implicit_namespaces', '-n',
help=__('interpret module paths according to PEP-0420 ' '--dry-run',
'implicit namespaces specification')) action='store_true',
parser.add_argument('-s', '--suffix', action='store', dest='suffix', dest='dryrun',
default='rst', help=__('run the script without creating files'),
help=__('file suffix (default: rst)')) )
parser.add_argument('-F', '--full', action='store_true', dest='full', parser.add_argument(
help=__('generate a full project with sphinx-quickstart')) '-e',
parser.add_argument('-a', '--append-syspath', action='store_true', '--separate',
dest='append_syspath', action='store_true',
help=__('append module_path to sys.path, used when --full is given')) dest='separatemodules',
parser.add_argument('-H', '--doc-project', action='store', dest='header', help=__('put documentation for each module on its own page'),
help=__('project name (default: root module name)')) )
parser.add_argument('-A', '--doc-author', action='store', dest='author', parser.add_argument(
help=__('project author(s), used when --full is given')) '-P',
parser.add_argument('-V', '--doc-version', action='store', dest='version', '--private',
help=__('project version, used when --full is given')) action='store_true',
parser.add_argument('-R', '--doc-release', action='store', dest='release', dest='includeprivate',
help=__('project release, used when --full is given, ' help=__('include "_private" modules'),
'defaults to --doc-version')) )
parser.add_argument(
'--tocfile',
action='store',
dest='tocfile',
default='modules',
help=__('filename of table of contents (default: modules)'),
)
parser.add_argument(
'-T',
'--no-toc',
action='store_false',
dest='tocfile',
help=__("don't create a table of contents file"),
)
parser.add_argument(
'-E',
'--no-headings',
action='store_true',
dest='noheadings',
help=__(
"don't create headings for the module/package "
'packages (e.g. when the docstrings already '
'contain them)'
),
)
parser.add_argument(
'-M',
'--module-first',
action='store_true',
dest='modulefirst',
help=__('put module documentation before submodule ' 'documentation'),
)
parser.add_argument(
'--implicit-namespaces',
action='store_true',
dest='implicit_namespaces',
help=__(
'interpret module paths according to PEP-0420 ' 'implicit namespaces specification'
),
)
parser.add_argument(
'-s',
'--suffix',
action='store',
dest='suffix',
default='rst',
help=__('file suffix (default: rst)'),
)
parser.add_argument(
'-F',
'--full',
action='store_true',
dest='full',
help=__('generate a full project with sphinx-quickstart'),
)
parser.add_argument(
'-a',
'--append-syspath',
action='store_true',
dest='append_syspath',
help=__('append module_path to sys.path, used when --full is given'),
)
parser.add_argument(
'-H',
'--doc-project',
action='store',
dest='header',
help=__('project name (default: root module name)'),
)
parser.add_argument(
'-A',
'--doc-author',
action='store',
dest='author',
help=__('project author(s), used when --full is given'),
)
parser.add_argument(
'-V',
'--doc-version',
action='store',
dest='version',
help=__('project version, used when --full is given'),
)
parser.add_argument(
'-R',
'--doc-release',
action='store',
dest='release',
help=__('project release, used when --full is given, ' 'defaults to --doc-version'),
)
group = parser.add_argument_group(__('extension options')) group = parser.add_argument_group(__('extension options'))
group.add_argument('--extensions', metavar='EXTENSIONS', dest='extensions', group.add_argument(
action='append', help=__('enable arbitrary extensions')) '--extensions',
metavar='EXTENSIONS',
dest='extensions',
action='append',
help=__('enable arbitrary extensions'),
)
for ext in EXTENSIONS: for ext in EXTENSIONS:
group.add_argument('--ext-%s' % ext, action='append_const', group.add_argument(
const='sphinx.ext.%s' % ext, dest='extensions', '--ext-%s' % ext,
help=__('enable %s extension') % ext) action='append_const',
const='sphinx.ext.%s' % ext,
dest='extensions',
help=__('enable %s extension') % ext,
)
group = parser.add_argument_group(__('Project templating')) group = parser.add_argument_group(__('Project templating'))
group.add_argument('-t', '--templatedir', metavar='TEMPLATEDIR', group.add_argument(
dest='templatedir', '-t',
help=__('template directory for template files')) '--templatedir',
metavar='TEMPLATEDIR',
dest='templatedir',
help=__('template directory for template files'),
)
return parser return parser
@ -436,6 +572,7 @@ def main(argv: Sequence[str] = (), /) -> int:
if args.full: if args.full:
from sphinx.cmd import quickstart as qs from sphinx.cmd import quickstart as qs
modules.sort() modules.sort()
prev_module = '' prev_module = ''
text = '' text = ''
@ -455,8 +592,7 @@ def main(argv: Sequence[str] = (), /) -> int:
'suffix': '.' + args.suffix, 'suffix': '.' + args.suffix,
'master': 'index', 'master': 'index',
'epub': True, 'epub': True,
'extensions': ['sphinx.ext.autodoc', 'sphinx.ext.viewcode', 'extensions': ['sphinx.ext.autodoc', 'sphinx.ext.viewcode', 'sphinx.ext.todo'],
'sphinx.ext.todo'],
'makefile': True, 'makefile': True,
'batchfile': True, 'batchfile': True,
'make_mode': True, 'make_mode': True,
@ -477,8 +613,7 @@ def main(argv: Sequence[str] = (), /) -> int:
d['extensions'].extend(ext.split(',')) d['extensions'].extend(ext.split(','))
if not args.dryrun: if not args.dryrun:
qs.generate(d, silent=True, overwrite=args.force, qs.generate(d, silent=True, overwrite=args.force, templatedir=args.templatedir)
templatedir=args.templatedir)
elif args.tocfile: elif args.tocfile:
create_modules_toc_file(modules, args, args.tocfile, args.templatedir) create_modules_toc_file(modules, args, args.tocfile, args.templatedir)
@ -486,5 +621,5 @@ def main(argv: Sequence[str] = (), /) -> int:
# So program can be started with "python -m sphinx.apidoc ..." # So program can be started with "python -m sphinx.apidoc ..."
if __name__ == "__main__": if __name__ == '__main__':
raise SystemExit(main(sys.argv[1:])) raise SystemExit(main(sys.argv[1:]))

View File

@ -65,7 +65,7 @@ class DummyApplication:
self.config = Config() self.config = Config()
self.registry = SphinxComponentRegistry() self.registry = SphinxComponentRegistry()
self.messagelog: list[str] = [] self.messagelog: list[str] = []
self.srcdir = "/" self.srcdir = '/'
self.translator = translator self.translator = translator
self.verbosity = 0 self.verbosity = 0
self._warncount = 0 self._warncount = 0
@ -98,10 +98,17 @@ def setup_documenters(app: Any) -> None:
ModuleDocumenter, ModuleDocumenter,
PropertyDocumenter, PropertyDocumenter,
) )
documenters: list[type[Documenter]] = [ documenters: list[type[Documenter]] = [
ModuleDocumenter, ClassDocumenter, ExceptionDocumenter, DataDocumenter, ModuleDocumenter,
FunctionDocumenter, MethodDocumenter, ClassDocumenter,
AttributeDocumenter, DecoratorDocumenter, PropertyDocumenter, ExceptionDocumenter,
DataDocumenter,
FunctionDocumenter,
MethodDocumenter,
AttributeDocumenter,
DecoratorDocumenter,
PropertyDocumenter,
] ]
for documenter in documenters: for documenter in documenters:
app.registry.add_documenter(documenter.objtype, documenter) app.registry.add_documenter(documenter.objtype, documenter)
@ -123,8 +130,9 @@ class AutosummaryRenderer:
raise ValueError(msg) raise ValueError(msg)
system_templates_path = [os.path.join(package_dir, 'ext', 'autosummary', 'templates')] system_templates_path = [os.path.join(package_dir, 'ext', 'autosummary', 'templates')]
loader = SphinxTemplateLoader(app.srcdir, app.config.templates_path, loader = SphinxTemplateLoader(
system_templates_path) app.srcdir, app.config.templates_path, system_templates_path
)
self.env = SandboxedEnvironment(loader=loader) self.env = SandboxedEnvironment(loader=loader)
self.env.filters['escape'] = rst.escape self.env.filters['escape'] = rst.escape
@ -132,7 +140,7 @@ class AutosummaryRenderer:
self.env.filters['underline'] = _underline self.env.filters['underline'] = _underline
if app.translator: if app.translator:
self.env.add_extension("jinja2.ext.i18n") self.env.add_extension('jinja2.ext.i18n')
# ``install_gettext_translations`` is injected by the ``jinja2.ext.i18n`` extension # ``install_gettext_translations`` is injected by the ``jinja2.ext.i18n`` extension
self.env.install_gettext_translations(app.translator) # type: ignore[attr-defined] self.env.install_gettext_translations(app.translator) # type: ignore[attr-defined]
@ -168,17 +176,17 @@ def _split_full_qualified_name(name: str) -> tuple[str | None, str]:
parts = name.split('.') parts = name.split('.')
for i, _part in enumerate(parts, 1): for i, _part in enumerate(parts, 1):
try: try:
modname = ".".join(parts[:i]) modname = '.'.join(parts[:i])
importlib.import_module(modname) importlib.import_module(modname)
except ImportError: except ImportError:
if parts[:i - 1]: if parts[: i - 1]:
return ".".join(parts[:i - 1]), ".".join(parts[i - 1:]) return '.'.join(parts[: i - 1]), '.'.join(parts[i - 1 :])
else: else:
return None, ".".join(parts) return None, '.'.join(parts)
except IndexError: except IndexError:
pass pass
return name, "" return name, ''
# -- Generating output --------------------------------------------------------- # -- Generating output ---------------------------------------------------------
@ -194,12 +202,19 @@ class ModuleScanner:
def is_skipped(self, name: str, value: Any, objtype: str) -> bool: def is_skipped(self, name: str, value: Any, objtype: str) -> bool:
try: try:
return self.app.emit_firstresult('autodoc-skip-member', objtype, return self.app.emit_firstresult(
name, value, False, {}) 'autodoc-skip-member', objtype, name, value, False, {}
)
except Exception as exc: except Exception as exc:
logger.warning(__('autosummary: failed to determine %r to be documented, ' logger.warning(
'the following exception was raised:\n%s'), __(
name, exc, type='autosummary') 'autosummary: failed to determine %r to be documented, '
'the following exception was raised:\n%s'
),
name,
exc,
type='autosummary',
)
return False return False
def scan(self, imported_members: bool) -> list[str]: def scan(self, imported_members: bool) -> list[str]:
@ -257,12 +272,19 @@ def members_of(obj: Any, conf: Config) -> Sequence[str]:
return getall(obj) or dir(obj) return getall(obj) or dir(obj)
def generate_autosummary_content(name: str, obj: Any, parent: Any, def generate_autosummary_content(
template: AutosummaryRenderer, template_name: str, name: str,
imported_members: bool, app: Any, obj: Any,
recursive: bool, context: dict, parent: Any,
modname: str | None = None, template: AutosummaryRenderer,
qualname: str | None = None) -> str: 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) doc = get_documenter(app, obj, parent)
ns: dict[str, Any] = {} ns: dict[str, Any] = {}
@ -275,23 +297,25 @@ def generate_autosummary_content(name: str, obj: Any, parent: Any,
respect_module_all = not app.config.autosummary_ignore_module_all respect_module_all = not app.config.autosummary_ignore_module_all
imported_members = imported_members or ('__all__' in dir(obj) and respect_module_all) imported_members = imported_members or ('__all__' in dir(obj) and respect_module_all)
ns['functions'], ns['all_functions'] = \ ns['functions'], ns['all_functions'] = _get_members(
_get_members(doc, app, obj, {'function'}, imported=imported_members) doc, app, obj, {'function'}, imported=imported_members
ns['classes'], ns['all_classes'] = \ )
_get_members(doc, app, obj, {'class'}, imported=imported_members) ns['classes'], ns['all_classes'] = _get_members(
ns['exceptions'], ns['all_exceptions'] = \ doc, app, obj, {'class'}, imported=imported_members
_get_members(doc, app, obj, {'exception'}, imported=imported_members) )
ns['attributes'], ns['all_attributes'] = \ ns['exceptions'], ns['all_exceptions'] = _get_members(
_get_module_attrs(name, ns['members']) doc, app, obj, {'exception'}, imported=imported_members
)
ns['attributes'], ns['all_attributes'] = _get_module_attrs(name, ns['members'])
ispackage = hasattr(obj, '__path__') ispackage = hasattr(obj, '__path__')
if ispackage and recursive: if ispackage and recursive:
# Use members that are not modules as skip list, because it would then mean # Use members that are not modules as skip list, because it would then mean
# that module was overwritten in the package namespace # that module was overwritten in the package namespace
skip = ( skip = (
ns["all_functions"] ns['all_functions']
+ ns["all_classes"] + ns['all_classes']
+ ns["all_exceptions"] + ns['all_exceptions']
+ ns["all_attributes"] + ns['all_attributes']
) )
# If respect_module_all and module has a __all__ attribute, first get # If respect_module_all and module has a __all__ attribute, first get
@ -301,40 +325,44 @@ def generate_autosummary_content(name: str, obj: Any, parent: Any,
# #
# Otherwise, use get_modules method normally # Otherwise, use get_modules method normally
if respect_module_all and '__all__' in dir(obj): if respect_module_all and '__all__' in dir(obj):
imported_modules, all_imported_modules = \ imported_modules, all_imported_modules = _get_members(
_get_members(doc, app, obj, {'module'}, imported=True) doc, app, obj, {'module'}, imported=True
)
skip += all_imported_modules skip += all_imported_modules
imported_modules = [name + '.' + modname for modname in imported_modules] imported_modules = [name + '.' + modname for modname in imported_modules]
all_imported_modules = \ all_imported_modules = [
[name + '.' + modname for modname in all_imported_modules] name + '.' + modname for modname in all_imported_modules
]
public_members = getall(obj) public_members = getall(obj)
else: else:
imported_modules, all_imported_modules = [], [] imported_modules, all_imported_modules = [], []
public_members = None public_members = None
modules, all_modules = _get_modules(obj, skip=skip, name=name, modules, all_modules = _get_modules(
public_members=public_members) obj, skip=skip, name=name, public_members=public_members
)
ns['modules'] = imported_modules + modules ns['modules'] = imported_modules + modules
ns["all_modules"] = all_imported_modules + all_modules ns['all_modules'] = all_imported_modules + all_modules
elif doc.objtype == 'class': elif doc.objtype == 'class':
ns['members'] = dir(obj) ns['members'] = dir(obj)
ns['inherited_members'] = \ ns['inherited_members'] = set(dir(obj)) - set(obj.__dict__.keys())
set(dir(obj)) - set(obj.__dict__.keys()) ns['methods'], ns['all_methods'] = _get_members(
ns['methods'], ns['all_methods'] = \ doc, app, obj, {'method'}, include_public={'__init__'}
_get_members(doc, app, obj, {'method'}, include_public={'__init__'}) )
ns['attributes'], ns['all_attributes'] = \ ns['attributes'], ns['all_attributes'] = _get_members(
_get_members(doc, app, obj, {'attribute', 'property'}) doc, app, obj, {'attribute', 'property'}
)
if modname is None or qualname is None: if modname is None or qualname is None:
modname, qualname = _split_full_qualified_name(name) modname, qualname = _split_full_qualified_name(name)
if doc.objtype in ('method', 'attribute', 'property'): if doc.objtype in ('method', 'attribute', 'property'):
ns['class'] = qualname.rsplit(".", 1)[0] ns['class'] = qualname.rsplit('.', 1)[0]
if doc.objtype == 'class': if doc.objtype == 'class':
shortname = qualname shortname = qualname
else: else:
shortname = qualname.rsplit(".", 1)[-1] shortname = qualname.rsplit('.', 1)[-1]
ns['fullname'] = name ns['fullname'] = name
ns['module'] = modname ns['module'] = modname
@ -352,12 +380,17 @@ def generate_autosummary_content(name: str, obj: Any, parent: Any,
def _skip_member(app: Sphinx, obj: Any, name: str, objtype: str) -> bool: def _skip_member(app: Sphinx, obj: Any, name: str, objtype: str) -> bool:
try: try:
return app.emit_firstresult('autodoc-skip-member', objtype, name, return app.emit_firstresult('autodoc-skip-member', objtype, name, obj, False, {})
obj, False, {})
except Exception as exc: except Exception as exc:
logger.warning(__('autosummary: failed to determine %r to be documented, ' logger.warning(
'the following exception was raised:\n%s'), __(
name, exc, type='autosummary') 'autosummary: failed to determine %r to be documented, '
'the following exception was raised:\n%s'
),
name,
exc,
type='autosummary',
)
return False return False
@ -384,9 +417,15 @@ def _get_all_members(doc: type[Documenter], app: Sphinx, obj: Any) -> dict[str,
return {} return {}
def _get_members(doc: type[Documenter], app: Sphinx, obj: Any, types: set[str], *, def _get_members(
include_public: Set[str] = frozenset(), doc: type[Documenter],
imported: bool = True) -> tuple[list[str], list[str]]: app: Sphinx,
obj: Any,
types: set[str],
*,
include_public: Set[str] = frozenset(),
imported: bool = True,
) -> tuple[list[str], list[str]]:
items: list[str] = [] items: list[str] = []
public: list[str] = [] public: list[str] = []
@ -423,20 +462,16 @@ def _get_module_attrs(name: str, members: Any) -> tuple[list[str], list[str]]:
if not attr_name.startswith('_'): if not attr_name.startswith('_'):
public.append(attr_name) public.append(attr_name)
except PycodeError: except PycodeError:
pass # give up if ModuleAnalyzer fails to parse code pass # give up if ModuleAnalyzer fails to parse code
return public, attrs return public, attrs
def _get_modules( def _get_modules(
obj: Any, obj: Any, *, skip: Sequence[str], name: str, public_members: Sequence[str] | None = None
*, ) -> tuple[list[str], list[str]]:
skip: Sequence[str],
name: str,
public_members: Sequence[str] | None = None) -> tuple[list[str], list[str]]:
items: list[str] = [] items: list[str] = []
public: list[str] = [] public: list[str] = []
for _, modname, _ispkg in pkgutil.iter_modules(obj.__path__): for _, modname, _ispkg in pkgutil.iter_modules(obj.__path__):
if modname in skip: if modname in skip:
# module was overwritten in __init__.py, so not accessible # module was overwritten in __init__.py, so not accessible
continue continue
@ -458,17 +493,20 @@ def _get_modules(
return public, items return public, items
def generate_autosummary_docs(sources: list[str], def generate_autosummary_docs(
output_dir: str | os.PathLike[str] | None = None, sources: list[str],
suffix: str = '.rst', output_dir: str | os.PathLike[str] | None = None,
base_path: str | os.PathLike[str] | None = None, suffix: str = '.rst',
imported_members: bool = False, app: Any = None, base_path: str | os.PathLike[str] | None = None,
overwrite: bool = True, encoding: str = 'utf-8') -> None: imported_members: bool = False,
app: Any = None,
overwrite: bool = True,
encoding: str = 'utf-8',
) -> None:
showed_sources = sorted(sources) showed_sources = sorted(sources)
if len(showed_sources) > 20: if len(showed_sources) > 20:
showed_sources = showed_sources[:10] + ['...'] + showed_sources[-10:] showed_sources = showed_sources[:10] + ['...'] + showed_sources[-10:]
logger.info(__('[autosummary] generating autosummary for: %s'), logger.info(__('[autosummary] generating autosummary for: %s'), ', '.join(showed_sources))
', '.join(showed_sources))
if output_dir: if output_dir:
logger.info(__('[autosummary] writing to %s'), output_dir) logger.info(__('[autosummary] writing to %s'), output_dir)
@ -501,30 +539,43 @@ def generate_autosummary_docs(sources: list[str],
try: try:
name, obj, parent, modname = import_by_name(entry.name) name, obj, parent, modname = import_by_name(entry.name)
qualname = name.replace(modname + ".", "") qualname = name.replace(modname + '.', '')
except ImportExceptionGroup as exc: except ImportExceptionGroup as exc:
try: try:
# try to import as an instance attribute # try to import as an instance attribute
name, obj, parent, modname = import_ivar_by_name(entry.name) name, obj, parent, modname = import_ivar_by_name(entry.name)
qualname = name.replace(modname + ".", "") qualname = name.replace(modname + '.', '')
except ImportError as exc2: except ImportError as exc2:
if exc2.__cause__: if exc2.__cause__:
exceptions: list[BaseException] = [*exc.exceptions, exc2.__cause__] exceptions: list[BaseException] = [*exc.exceptions, exc2.__cause__]
else: else:
exceptions = [*exc.exceptions, exc2] exceptions = [*exc.exceptions, exc2]
errors = list({f"* {type(e).__name__}: {e}" for e in exceptions}) errors = list({f'* {type(e).__name__}: {e}' for e in exceptions})
logger.warning(__('[autosummary] failed to import %s.\nPossible hints:\n%s'), logger.warning(
entry.name, '\n'.join(errors)) __('[autosummary] failed to import %s.\nPossible hints:\n%s'),
entry.name,
'\n'.join(errors),
)
continue continue
context: dict[str, Any] = {} context: dict[str, Any] = {}
if app: if app:
context.update(app.config.autosummary_context) context.update(app.config.autosummary_context)
content = generate_autosummary_content(name, obj, parent, template, entry.template, content = generate_autosummary_content(
imported_members, app, entry.recursive, context, name,
modname, qualname) obj,
parent,
template,
entry.template,
imported_members,
app,
entry.recursive,
context,
modname,
qualname,
)
filename = os.path.join(path, filename_map.get(name, name) + suffix) filename = os.path.join(path, filename_map.get(name, name) + suffix)
if os.path.isfile(filename): if os.path.isfile(filename):
@ -544,14 +595,20 @@ def generate_autosummary_docs(sources: list[str],
# descend recursively to new files # descend recursively to new files
if new_files: if new_files:
generate_autosummary_docs(new_files, output_dir=output_dir, generate_autosummary_docs(
suffix=suffix, base_path=base_path, new_files,
imported_members=imported_members, app=app, output_dir=output_dir,
overwrite=overwrite) suffix=suffix,
base_path=base_path,
imported_members=imported_members,
app=app,
overwrite=overwrite,
)
# -- Finding documented entries in files --------------------------------------- # -- Finding documented entries in files ---------------------------------------
def find_autosummary_in_files(filenames: list[str]) -> list[AutosummaryEntry]: def find_autosummary_in_files(filenames: list[str]) -> list[AutosummaryEntry]:
"""Find out what items are documented in source/*.rst. """Find out what items are documented in source/*.rst.
@ -566,7 +623,8 @@ def find_autosummary_in_files(filenames: list[str]) -> list[AutosummaryEntry]:
def find_autosummary_in_docstring( def find_autosummary_in_docstring(
name: str, filename: str | None = None, name: str,
filename: str | None = None,
) -> list[AutosummaryEntry]: ) -> list[AutosummaryEntry]:
"""Find out what items are documented in the given object's docstring. """Find out what items are documented in the given object's docstring.
@ -579,16 +637,21 @@ def find_autosummary_in_docstring(
except AttributeError: except AttributeError:
pass pass
except ImportExceptionGroup as exc: except ImportExceptionGroup as exc:
errors = '\n'.join({f"* {type(e).__name__}: {e}" for e in exc.exceptions}) 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 logger.warning(f'Failed to import {name}.\nPossible hints:\n{errors}') # NoQA: G004
except SystemExit: except SystemExit:
logger.warning("Failed to import '%s'; the module executes module level " logger.warning(
'statement and it might call sys.exit().', name) "Failed to import '%s'; the module executes module level "
'statement and it might call sys.exit().',
name,
)
return [] return []
def find_autosummary_in_lines( def find_autosummary_in_lines(
lines: list[str], module: str | None = None, filename: str | None = None, lines: list[str],
module: str | None = None,
filename: str | None = None,
) -> list[AutosummaryEntry]: ) -> list[AutosummaryEntry]:
"""Find out what items appear in autosummary:: directives in the """Find out what items appear in autosummary:: directives in the
given lines. given lines.
@ -601,10 +664,8 @@ def find_autosummary_in_lines(
corresponding options set. corresponding options set.
""" """
autosummary_re = re.compile(r'^(\s*)\.\.\s+autosummary::\s*') autosummary_re = re.compile(r'^(\s*)\.\.\s+autosummary::\s*')
automodule_re = re.compile( automodule_re = re.compile(r'^\s*\.\.\s+automodule::\s*([A-Za-z0-9_.]+)\s*$')
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*$')
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*.*?') autosummary_item_re = re.compile(r'^\s+(~?[_a-zA-Z][a-zA-Z0-9_.]*)\s*.*?')
recursive_arg_re = re.compile(r'^\s+:recursive:\s*$') recursive_arg_re = re.compile(r'^\s+:recursive:\s*$')
toctree_arg_re = re.compile(r'^\s+:toctree:\s*(.*?)\s*$') toctree_arg_re = re.compile(r'^\s+:toctree:\s*(.*?)\s*$')
@ -617,7 +678,7 @@ def find_autosummary_in_lines(
template = '' template = ''
current_module = module current_module = module
in_autosummary = False in_autosummary = False
base_indent = "" base_indent = ''
for line in lines: for line in lines:
if in_autosummary: if in_autosummary:
@ -630,8 +691,7 @@ def find_autosummary_in_lines(
if m: if m:
toctree = m.group(1) toctree = m.group(1)
if filename: if filename:
toctree = os.path.join(os.path.dirname(filename), toctree = os.path.join(os.path.dirname(filename), toctree)
toctree)
continue continue
m = template_arg_re.match(line) m = template_arg_re.match(line)
@ -647,13 +707,12 @@ def find_autosummary_in_lines(
name = m.group(1).strip() name = m.group(1).strip()
if name.startswith('~'): if name.startswith('~'):
name = name[1:] name = name[1:]
if current_module and \ if current_module and not name.startswith(current_module + '.'):
not name.startswith(current_module + '.'): name = f'{current_module}.{name}'
name = f"{current_module}.{name}"
documented.append(AutosummaryEntry(name, toctree, template, recursive)) documented.append(AutosummaryEntry(name, toctree, template, recursive))
continue continue
if not line.strip() or line.startswith(base_indent + " "): if not line.strip() or line.startswith(base_indent + ' '):
continue continue
in_autosummary = False in_autosummary = False
@ -671,8 +730,7 @@ def find_autosummary_in_lines(
if m: if m:
current_module = m.group(1).strip() current_module = m.group(1).strip()
# recurse into the automodule docstring # recurse into the automodule docstring
documented.extend(find_autosummary_in_docstring( documented.extend(find_autosummary_in_docstring(current_module, filename=filename))
current_module, filename=filename))
continue continue
m = module_re.match(line) m = module_re.match(line)
@ -698,33 +756,62 @@ The format of the autosummary directive is documented in the
``sphinx.ext.autosummary`` Python module and can be read using:: ``sphinx.ext.autosummary`` Python module and can be read using::
pydoc sphinx.ext.autosummary pydoc sphinx.ext.autosummary
""")) """),
)
parser.add_argument('--version', action='version', dest='show_version', parser.add_argument(
version='%%(prog)s %s' % __display_version__) '--version',
action='version',
dest='show_version',
version='%%(prog)s %s' % __display_version__,
)
parser.add_argument('source_file', nargs='+', parser.add_argument(
help=__('source files to generate rST files for')) 'source_file', nargs='+', help=__('source files to generate rST files for')
)
parser.add_argument('-o', '--output-dir', action='store', parser.add_argument(
dest='output_dir', '-o',
help=__('directory to place all output in')) '--output-dir',
parser.add_argument('-s', '--suffix', action='store', dest='suffix', action='store',
default='rst', dest='output_dir',
help=__('default suffix for files (default: ' help=__('directory to place all output in'),
'%(default)s)')) )
parser.add_argument('-t', '--templates', action='store', dest='templates', parser.add_argument(
default=None, '-s',
help=__('custom template directory (default: ' '--suffix',
'%(default)s)')) action='store',
parser.add_argument('-i', '--imported-members', action='store_true', dest='suffix',
dest='imported_members', default=False, default='rst',
help=__('document imported members (default: ' help=__('default suffix for files (default: ' '%(default)s)'),
'%(default)s)')) )
parser.add_argument('-a', '--respect-module-all', action='store_true', parser.add_argument(
dest='respect_module_all', default=False, '-t',
help=__('document exactly the members in module __all__ attribute. ' '--templates',
'(default: %(default)s)')) 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 return parser
@ -740,12 +827,15 @@ def main(argv: Sequence[str] = (), /) -> None:
if args.templates: if args.templates:
app.config.templates_path.append(path.abspath(args.templates)) app.config.templates_path.append(path.abspath(args.templates))
app.config.autosummary_ignore_module_all = (not args.respect_module_all) app.config.autosummary_ignore_module_all = not args.respect_module_all
generate_autosummary_docs(args.source_file, args.output_dir, generate_autosummary_docs(
'.' + args.suffix, args.source_file,
imported_members=args.imported_members, args.output_dir,
app=app) '.' + args.suffix,
imported_members=args.imported_members,
app=app,
)
if __name__ == '__main__': if __name__ == '__main__':