diff --git a/sphinx/ext/apidoc/_cli.py b/sphinx/ext/apidoc/_cli.py index 63b7bde6c8e..f653eea5ce7 100644 --- a/sphinx/ext/apidoc/_cli.py +++ b/sphinx/ext/apidoc/_cli.py @@ -263,18 +263,19 @@ def main(argv: Sequence[str] = (), /) -> int: sphinx.locale.init_console() parser = get_parser() - args: CliOptions = parser.parse_args(argv or sys.argv[1:]) + args = parser.parse_args(argv or sys.argv[1:]) - rootpath = os.path.abspath(args.module_path) + args.module_path = rootpath = Path(args.module_path).resolve() # normalize opts if args.header is None: - args.header = rootpath.split(os.path.sep)[-1] + args.header = str(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) + args.destdir = destdir = Path(args.destdir) if not args.dryrun: ensuredir(args.destdir) excludes = tuple( @@ -285,7 +286,9 @@ def main(argv: Sequence[str] = (), /) -> int: 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) + + opts = CliOptions(**args.__dict__) + written_files, modules = recurse_tree(rootpath, excludes, opts, args.templatedir) if args.full: from sphinx.cmd import quickstart as qs @@ -299,7 +302,7 @@ def main(argv: Sequence[str] = (), /) -> int: prev_module = module text += f' {module}\n' d: dict[str, Any] = { - 'path': args.destdir, + 'path': str(destdir), 'sep': False, 'dot': '_', 'project': args.header, @@ -320,7 +323,7 @@ def main(argv: Sequence[str] = (), /) -> int: 'mastertocmaxdepth': args.maxdepth, 'mastertoctree': text, 'language': 'en', - 'module_path': rootpath, + 'module_path': str(rootpath), 'append_syspath': args.append_syspath, } if args.extensions: @@ -339,10 +342,10 @@ def main(argv: Sequence[str] = (), /) -> int: ) elif args.tocfile: written_files.append( - create_modules_toc_file(modules, args, args.tocfile, args.templatedir) + create_modules_toc_file(modules, opts, args.tocfile, args.templatedir) ) if args.remove_old and not args.dryrun: - _remove_old_files(written_files, Path(args.destdir), args.suffix) + _remove_old_files(written_files, destdir, args.suffix) return 0 diff --git a/sphinx/ext/apidoc/_generate.py b/sphinx/ext/apidoc/_generate.py index 9d3fbe56277..708e7ca35d7 100644 --- a/sphinx/ext/apidoc/_generate.py +++ b/sphinx/ext/apidoc/_generate.py @@ -1,11 +1,12 @@ from __future__ import annotations +import dataclasses import glob import os import os.path from importlib.machinery import EXTENSION_SUFFIXES from pathlib import Path -from typing import TYPE_CHECKING, Protocol +from typing import TYPE_CHECKING from sphinx import package_dir from sphinx.ext.apidoc import logger @@ -267,7 +268,7 @@ def has_child_module( def recurse_tree( - rootpath: str, + rootpath: str | os.PathLike[str], excludes: Sequence[re.Pattern[str]], opts: CliOptions, user_template_dir: str | None = None, @@ -277,6 +278,7 @@ def recurse_tree( ReST files. """ # check if the base directory is a package and get its name + rootpath = os.fspath(rootpath) if is_packagedir(rootpath) or opts.implicit_namespaces: root_package = rootpath.split(os.path.sep)[-1] else: @@ -349,34 +351,35 @@ def is_excluded(root: str | Path, excludes: Sequence[re.Pattern[str]]) -> bool: return any(exclude.match(root_str) for exclude in excludes) -class CliOptions(Protocol): - """Arguments parsed from the command line.""" +@dataclasses.dataclass(frozen=True, kw_only=True, slots=True) +class CliOptions: + """Options for apidoc.""" - module_path: str + module_path: Path exclude_pattern: list[str] - destdir: str - quiet: bool - maxdepth: int - force: bool - followlinks: bool - dryrun: bool - separatemodules: bool - includeprivate: bool - tocfile: str - noheadings: bool - modulefirst: bool - implicit_namespaces: bool - automodule_options: set[str] - suffix: str - - remove_old: bool + destdir: Path + quiet: bool = False + maxdepth: int = 4 + force: bool = False + followlinks: bool = False + dryrun: bool = False + separatemodules: bool = False + includeprivate: bool = False + tocfile: str = 'modules' + noheadings: bool = False + modulefirst: bool = False + implicit_namespaces: bool = False + automodule_options: set[str] = dataclasses.field(default_factory=set) + suffix: str = 'rst' + + remove_old: bool = False # --full only - full: bool - append_syspath: bool - header: str - author: str | None - version: str | None - release: str | None - extensions: list[str] | None - templatedir: str | None + full: bool = False + append_syspath: bool = False + header: str = '' + author: str | None = None + version: str | None = None + release: str | None = None + extensions: list[str] | None = None + templatedir: str | None = None