From 2bd70193b7903e9ba5545457adc96e4c921f8026 Mon Sep 17 00:00:00 2001 From: Adam Turner <9087854+aa-turner@users.noreply.github.com> Date: Tue, 7 Jan 2025 08:20:31 +0000 Subject: [PATCH] Split out ``sphinx.ext.apidoc._cli`` --- sphinx/ext/apidoc/__init__.py | 322 --------------------- sphinx/ext/apidoc/_cli.py | 349 +++++++++++++++++++++++ tests/test_extensions/test_ext_apidoc.py | 2 +- 3 files changed, 350 insertions(+), 323 deletions(-) create mode 100644 sphinx/ext/apidoc/_cli.py diff --git a/sphinx/ext/apidoc/__init__.py b/sphinx/ext/apidoc/__init__.py index 1bc0c8e3e..1b3efd606 100644 --- a/sphinx/ext/apidoc/__init__.py +++ b/sphinx/ext/apidoc/__init__.py @@ -367,237 +367,6 @@ def is_excluded(root: str | Path, excludes: Sequence[re.Pattern[str]]) -> bool: return any(exclude.match(root_str) for exclude in excludes) -def get_parser() -> argparse.ArgumentParser: - parser = argparse.ArgumentParser( - usage='%(prog)s [OPTIONS] -o [EXCLUDE_PATTERN, ...]', - epilog=__('For more information, visit .'), - description=__(""" -Look recursively in for Python modules and packages and create -one reST file with automodule directives per package in the . - -The s can be file and/or directory patterns that will be -excluded from generation. - -Note: By default this script will not overwrite already created files."""), - ) - - parser.add_argument( - '--version', - action='version', - dest='show_version', - version=f'%(prog)s {__display_version__}', - ) - - parser.add_argument('module_path', help=__('path to module to document')) - parser.add_argument( - 'exclude_pattern', - nargs='*', - help=__( - 'fnmatch-style file and/or directory patterns to exclude from generation' - ), - ) - - parser.add_argument( - '-o', - '--output-dir', - action='store', - dest='destdir', - required=True, - help=__('directory to place all output'), - ) - parser.add_argument( - '-q', - action='store_true', - dest='quiet', - help=__('no output on stdout, just warnings on stderr'), - ) - parser.add_argument( - '-d', - '--maxdepth', - action='store', - dest='maxdepth', - type=int, - default=4, - help=__('maximum depth of submodules to show in the TOC (default: 4)'), - ) - parser.add_argument( - '-f', - '--force', - action='store_true', - dest='force', - help=__('overwrite existing files'), - ) - parser.add_argument( - '-l', - '--follow-links', - action='store_true', - dest='followlinks', - default=False, - help=__( - 'follow symbolic links. Powerful when combined with collective.recipe.omelette.' - ), - ) - parser.add_argument( - '-n', - '--dry-run', - action='store_true', - dest='dryrun', - help=__('run the script without creating files'), - ) - parser.add_argument( - '-e', - '--separate', - action='store_true', - dest='separatemodules', - help=__('put documentation for each module on its own page'), - ) - parser.add_argument( - '-P', - '--private', - action='store_true', - dest='includeprivate', - help=__('include "_private" modules'), - ) - 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( - '--automodule-options', - dest='automodule_options', - default='', - help=__( - 'Comma-separated list of options to pass to automodule directive ' - '(or use SPHINX_APIDOC_OPTIONS).' - ), - ) - parser.add_argument( - '-s', - '--suffix', - action='store', - dest='suffix', - default='rst', - help=__('file suffix (default: rst)'), - ) - exclusive_group = parser.add_mutually_exclusive_group() - exclusive_group.add_argument( - '--remove-old', - action='store_true', - dest='remove_old', - help=__( - 'Remove existing files in the output directory that were not generated' - ), - ) - exclusive_group.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.add_argument( - '--extensions', - metavar='EXTENSIONS', - dest='extensions', - action='append', - help=__('enable arbitrary extensions, used when --full is given'), - ) - for ext in EXTENSIONS: - group.add_argument( - f'--ext-{ext}', - action='append_const', - const=f'sphinx.ext.{ext}', - dest='extensions', - help=__('enable %s extension, used when --full is given') % ext, - ) - - group = parser.add_argument_group(__('Project templating')) - group.add_argument( - '-t', - '--templatedir', - metavar='TEMPLATEDIR', - dest='templatedir', - help=__('template directory for template files'), - ) - - return parser - - class CliOptions(Protocol): """Arguments parsed from the command line.""" @@ -631,97 +400,6 @@ class CliOptions(Protocol): templatedir: str | None -def main(argv: Sequence[str] = (), /) -> int: - """Run the apidoc CLI.""" - locale.setlocale(locale.LC_ALL, '') - sphinx.locale.init_console() - - parser = get_parser() - args: CliOptions = parser.parse_args(argv or sys.argv[1:]) - - rootpath = os.path.abspath(args.module_path) - - # normalize opts - - if args.header is None: - args.header = rootpath.split(os.path.sep)[-1] - args.suffix = args.suffix.removeprefix('.') - if not Path(rootpath).is_dir(): - logger.error(__('%s is not a directory.'), rootpath) - raise SystemExit(1) - if not args.dryrun: - ensuredir(args.destdir) - excludes = tuple( - re.compile(fnmatch.translate(os.path.abspath(exclude))) - for exclude in dict.fromkeys(args.exclude_pattern) - ) - if not args.automodule_options: - args.automodule_options = set() - elif isinstance(args.automodule_options, str): - args.automodule_options = set(args.automodule_options.split(',')) - written_files, modules = recurse_tree(rootpath, excludes, args, args.templatedir) - - if args.full: - from sphinx.cmd import quickstart as qs - - modules.sort() - prev_module = '' - text = '' - for module in modules: - if module.startswith(prev_module + '.'): - continue - prev_module = module - text += f' {module}\n' - d: dict[str, Any] = { - 'path': args.destdir, - 'sep': False, - 'dot': '_', - 'project': args.header, - 'author': args.author or 'Author', - 'version': args.version or '', - 'release': args.release or args.version or '', - 'suffix': '.' + args.suffix, - 'master': 'index', - 'epub': True, - 'extensions': [ - 'sphinx.ext.autodoc', - 'sphinx.ext.viewcode', - 'sphinx.ext.todo', - ], - 'makefile': True, - 'batchfile': True, - 'make_mode': True, - 'mastertocmaxdepth': args.maxdepth, - 'mastertoctree': text, - 'language': 'en', - 'module_path': rootpath, - 'append_syspath': args.append_syspath, - } - if args.extensions: - d['extensions'].extend(args.extensions) - if args.quiet: - d['quiet'] = True - - for ext in d['extensions'][:]: - if ',' in ext: - d['extensions'].remove(ext) - d['extensions'].extend(ext.split(',')) - - if not args.dryrun: - qs.generate( - d, silent=True, overwrite=args.force, templatedir=args.templatedir - ) - elif args.tocfile: - written_files.append( - create_modules_toc_file(modules, args, args.tocfile, args.templatedir) - ) - - if args.remove_old and not args.dryrun: - _remove_old_files(written_files, Path(args.destdir), args.suffix) - - return 0 - - def _remove_old_files( written_files: Sequence[Path], destdir: Path, suffix: str ) -> None: diff --git a/sphinx/ext/apidoc/_cli.py b/sphinx/ext/apidoc/_cli.py new file mode 100644 index 000000000..3a4d59efb --- /dev/null +++ b/sphinx/ext/apidoc/_cli.py @@ -0,0 +1,349 @@ +from __future__ import annotations + +import argparse +import fnmatch +import locale +import os +import os.path +import re +import sys +from pathlib import Path +from typing import TYPE_CHECKING, Any + +import sphinx.locale +from sphinx import __display_version__ +from sphinx.cmd.quickstart import EXTENSIONS +from sphinx.ext.apidoc import ( + CliOptions, + _remove_old_files, + create_modules_toc_file, + logger, + recurse_tree, +) +from sphinx.locale import __ +from sphinx.util.osutil import ensuredir + +if TYPE_CHECKING: + from collections.abc import Sequence + + +def get_parser() -> argparse.ArgumentParser: + parser = argparse.ArgumentParser( + usage='%(prog)s [OPTIONS] -o [EXCLUDE_PATTERN, ...]', + epilog=__('For more information, visit .'), + description=__(""" +Look recursively in for Python modules and packages and create +one reST file with automodule directives per package in the . + +The s can be file and/or directory patterns that will be +excluded from generation. + +Note: By default this script will not overwrite already created files."""), + ) + + parser.add_argument( + '--version', + action='version', + dest='show_version', + version=f'%(prog)s {__display_version__}', + ) + + parser.add_argument('module_path', help=__('path to module to document')) + parser.add_argument( + 'exclude_pattern', + nargs='*', + help=__( + 'fnmatch-style file and/or directory patterns to exclude from generation' + ), + ) + + parser.add_argument( + '-o', + '--output-dir', + action='store', + dest='destdir', + required=True, + help=__('directory to place all output'), + ) + parser.add_argument( + '-q', + action='store_true', + dest='quiet', + help=__('no output on stdout, just warnings on stderr'), + ) + parser.add_argument( + '-d', + '--maxdepth', + action='store', + dest='maxdepth', + type=int, + default=4, + help=__('maximum depth of submodules to show in the TOC (default: 4)'), + ) + parser.add_argument( + '-f', + '--force', + action='store_true', + dest='force', + help=__('overwrite existing files'), + ) + parser.add_argument( + '-l', + '--follow-links', + action='store_true', + dest='followlinks', + default=False, + help=__( + 'follow symbolic links. Powerful when combined with collective.recipe.omelette.' + ), + ) + parser.add_argument( + '-n', + '--dry-run', + action='store_true', + dest='dryrun', + help=__('run the script without creating files'), + ) + parser.add_argument( + '-e', + '--separate', + action='store_true', + dest='separatemodules', + help=__('put documentation for each module on its own page'), + ) + parser.add_argument( + '-P', + '--private', + action='store_true', + dest='includeprivate', + help=__('include "_private" modules'), + ) + 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( + '--automodule-options', + dest='automodule_options', + default='', + help=__( + 'Comma-separated list of options to pass to automodule directive ' + '(or use SPHINX_APIDOC_OPTIONS).' + ), + ) + parser.add_argument( + '-s', + '--suffix', + action='store', + dest='suffix', + default='rst', + help=__('file suffix (default: rst)'), + ) + exclusive_group = parser.add_mutually_exclusive_group() + exclusive_group.add_argument( + '--remove-old', + action='store_true', + dest='remove_old', + help=__( + 'Remove existing files in the output directory that were not generated' + ), + ) + exclusive_group.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.add_argument( + '--extensions', + metavar='EXTENSIONS', + dest='extensions', + action='append', + help=__('enable arbitrary extensions, used when --full is given'), + ) + for ext in EXTENSIONS: + group.add_argument( + f'--ext-{ext}', + action='append_const', + const=f'sphinx.ext.{ext}', + dest='extensions', + help=__('enable %s extension, used when --full is given') % ext, + ) + + group = parser.add_argument_group(__('Project templating')) + group.add_argument( + '-t', + '--templatedir', + metavar='TEMPLATEDIR', + dest='templatedir', + help=__('template directory for template files'), + ) + + return parser + + +def main(argv: Sequence[str] = (), /) -> int: + """Run the apidoc CLI.""" + locale.setlocale(locale.LC_ALL, '') + sphinx.locale.init_console() + + parser = get_parser() + args: CliOptions = parser.parse_args(argv or sys.argv[1:]) + + rootpath = os.path.abspath(args.module_path) + + # normalize opts + + if args.header is None: + args.header = rootpath.split(os.path.sep)[-1] + args.suffix = args.suffix.removeprefix('.') + if not Path(rootpath).is_dir(): + logger.error(__('%s is not a directory.'), rootpath) + raise SystemExit(1) + if not args.dryrun: + ensuredir(args.destdir) + excludes = tuple( + re.compile(fnmatch.translate(os.path.abspath(exclude))) + for exclude in dict.fromkeys(args.exclude_pattern) + ) + if not args.automodule_options: + args.automodule_options = set() + elif isinstance(args.automodule_options, str): + args.automodule_options = set(args.automodule_options.split(',')) + written_files, modules = recurse_tree(rootpath, excludes, args, args.templatedir) + + if args.full: + from sphinx.cmd import quickstart as qs + + modules.sort() + prev_module = '' + text = '' + for module in modules: + if module.startswith(prev_module + '.'): + continue + prev_module = module + text += f' {module}\n' + d: dict[str, Any] = { + 'path': args.destdir, + 'sep': False, + 'dot': '_', + 'project': args.header, + 'author': args.author or 'Author', + 'version': args.version or '', + 'release': args.release or args.version or '', + 'suffix': '.' + args.suffix, + 'master': 'index', + 'epub': True, + 'extensions': [ + 'sphinx.ext.autodoc', + 'sphinx.ext.viewcode', + 'sphinx.ext.todo', + ], + 'makefile': True, + 'batchfile': True, + 'make_mode': True, + 'mastertocmaxdepth': args.maxdepth, + 'mastertoctree': text, + 'language': 'en', + 'module_path': rootpath, + 'append_syspath': args.append_syspath, + } + if args.extensions: + d['extensions'].extend(args.extensions) + if args.quiet: + d['quiet'] = True + + for ext in d['extensions'][:]: + if ',' in ext: + d['extensions'].remove(ext) + d['extensions'].extend(ext.split(',')) + + if not args.dryrun: + qs.generate( + d, silent=True, overwrite=args.force, templatedir=args.templatedir + ) + elif args.tocfile: + written_files.append( + create_modules_toc_file(modules, args, args.tocfile, args.templatedir) + ) + + if args.remove_old and not args.dryrun: + _remove_old_files(written_files, Path(args.destdir), args.suffix) + + return 0 diff --git a/tests/test_extensions/test_ext_apidoc.py b/tests/test_extensions/test_ext_apidoc.py index f809df562..90a49a184 100644 --- a/tests/test_extensions/test_ext_apidoc.py +++ b/tests/test_extensions/test_ext_apidoc.py @@ -8,7 +8,7 @@ from typing import TYPE_CHECKING import pytest import sphinx.ext.apidoc -from sphinx.ext.apidoc import main as apidoc_main +from sphinx.ext.apidoc._cli import main as apidoc_main if TYPE_CHECKING: from pathlib import Path