sphinx_argparse_cli-1.21.3/src/sphinx_argparse_cli/__init__.py0000644000000000000000000000206613615410400021472 0ustar00"""Sphinx generator for argparse.""" from __future__ import annotations from typing import TYPE_CHECKING, Any from .version import __version__ if TYPE_CHECKING: from sphinx.application import Sphinx def setup(app: Sphinx) -> dict[str, Any]: from ._logic import SphinxArgparseCli # noqa: PLC0415 app.add_directive(SphinxArgparseCli.name, SphinxArgparseCli) app.add_config_value("sphinx_argparse_cli_prefix_document", False, "env") # noqa: FBT003 app.add_css_file("sphinx_argparse_cli.css") app.connect("build-finished", _write_css) return {"parallel_read_safe": True} def _write_css(app: Sphinx, exception: Exception | None) -> None: if exception or not app.builder or app.builder.format != "html": return from pathlib import Path # noqa: PLC0415 static = Path(app.outdir) / "_static" static.mkdir(parents=True, exist_ok=True) (static / "sphinx_argparse_cli.css").write_text( ".sphinx-argparse-cli-wrap pre { white-space: pre-wrap; word-wrap: break-word; }\n" ) __all__ = [ "__version__", ] sphinx_argparse_cli-1.21.3/src/sphinx_argparse_cli/_logic.py0000644000000000000000000004522413615410400021172 0ustar00from __future__ import annotations import os import re import sys from argparse import ( SUPPRESS, Action, ArgumentParser, HelpFormatter, RawDescriptionHelpFormatter, _ArgumentGroup, _StoreFalseAction, _StoreTrueAction, _SubParsersAction, ) from collections import defaultdict from contextlib import contextmanager from pathlib import Path from typing import TYPE_CHECKING, Any, ClassVar, NamedTuple, cast from unittest.mock import patch from docutils.nodes import ( Element, Node, Text, bullet_list, container, fully_normalize_name, list_item, literal, literal_block, paragraph, reference, section, strong, title, whitespace_normalize_name, ) from docutils.parsers.rst.directives import flag, positive_int, unchanged, unchanged_required from docutils.statemachine import StringList from sphinx.locale import __ from sphinx.util.docutils import SphinxDirective from sphinx.util.logging import getLogger if TYPE_CHECKING: from collections.abc import Iterator from docutils.parsers.rst.states import RSTState, RSTStateMachine from sphinx.domains.std import StandardDomain class TextAsDefault(NamedTuple): text: str def make_id(key: str) -> str: return "-".join(key.split()).rstrip("-") def make_id_lower(key: str) -> str: # replace all capital letters "X" with "_lower(X)" return re.sub("[A-Z]", lambda m: "_" + m.group(0).lower(), make_id(key)) logger = getLogger(__name__) class SphinxArgparseCli(SphinxDirective): name = "sphinx_argparse_cli" has_content = True option_spec: ClassVar[dict[str, Any]] = { "module": unchanged_required, "func": unchanged_required, "hook": flag, "prog": unchanged, "title": unchanged, "description": unchanged, "epilog": unchanged, "usage_width": positive_int, "usage_first": flag, "group_title_prefix": unchanged, "group_sub_title_prefix": unchanged, "no_default_values": unchanged, # :ref: only supports lower-case. If this is set, any # would-be-upper-case chars will be prefixed with _. Since # this is backwards incompatible for URL's, this is opt-in. "force_refs_lower": flag, } def __init__( # noqa: PLR0913 self, name: str, arguments: list[str], options: dict[str, str | None], content: StringList, lineno: int, content_offset: int, block_text: str, state: RSTState, state_machine: RSTStateMachine, ) -> None: options.setdefault("group_title_prefix", None) options.setdefault("group_sub_title_prefix", None) super().__init__(name, arguments, options, content, lineno, content_offset, block_text, state, state_machine) self._parser: ArgumentParser | None = None self._std_domain: StandardDomain = cast("StandardDomain", self.env.get_domain("std")) self._raw_format: bool = False self.make_id = make_id_lower if "force_refs_lower" in self.options else make_id @property def parser(self) -> ArgumentParser: if self._parser is None: module_name, attr_name = self.options["module"], self.options["func"] try: module = __import__(module_name, fromlist=[attr_name]) except ImportError: msg = f"Failed to import module {module_name!r}" raise self.error(msg) # noqa: B904 try: parser_creator = getattr(module, attr_name) except AttributeError: del sys.modules[module_name] msg = f"Module {module_name!r} has no attribute {attr_name!r}" raise self.error(msg) # noqa: B904 if "hook" in self.options: original_parse_known_args = ArgumentParser.parse_known_args ArgumentParser.parse_known_args = _parse_known_args_hook # type: ignore[method-assign,assignment] try: parser_creator() except HookError as hooked: self._parser = hooked.parser finally: ArgumentParser.parse_known_args = original_parse_known_args else: self._parser = parser_creator() del sys.modules[module_name] if self._parser is None: msg = "Failed to hook argparse to get ArgumentParser" raise self.error(msg) if "prog" in self.options: old_prog, new_prog = self._parser.prog, self.options["prog"] self._parser.prog = new_prog _update_sub_parser_prog(self._parser, old_prog, new_prog) formatter = self._parser.formatter_class self._raw_format = isinstance(formatter, type) and issubclass(formatter, RawDescriptionHelpFormatter) return self._parser def _load_sub_parsers( self, sub_parser: _SubParsersAction[ArgumentParser] ) -> Iterator[tuple[list[str], str, ArgumentParser]]: parser_to_args: dict[int, list[str]] = defaultdict(list) str_to_parser: dict[str, ArgumentParser] = {} for key, parser in sub_parser._name_parser_map.items(): # noqa: SLF001 parser_to_args[id(parser)].append(key) str_to_parser[key] = parser done_parser: set[int] = set() for name, parser in sub_parser.choices.items(): parser_id = id(parser) if parser_id in done_parser: continue done_parser.add(parser_id) aliases = parser_to_args[id(parser)] aliases.remove(name) # help is stored in a pseudo action help_msg = next((a.help for a in sub_parser._choices_actions if a.dest == name), None) or "" # noqa: SLF001 yield aliases, help_msg, parser # If this parser has a subparser, recurse into it if parser._subparsers: # noqa: SLF001 sub_sub_parser: _SubParsersAction[ArgumentParser] = parser._subparsers._group_actions[0] # type: ignore[assignment] # noqa: SLF001 yield from self._load_sub_parsers(sub_sub_parser) def load_sub_parsers(self) -> Iterator[tuple[list[str], str, ArgumentParser]]: top_sub_parser = self.parser._subparsers # noqa: SLF001 if not top_sub_parser: return sub_parser: _SubParsersAction[ArgumentParser] sub_parser = top_sub_parser._group_actions[0] # type: ignore[assignment] # noqa: SLF001 yield from self._load_sub_parsers(sub_parser) def run(self) -> list[Node]: # construct headers self.env.note_reread() # this document needs to be always updated title_text = self.options.get("title", f"{self.parser.prog} - CLI interface").strip() if not title_text: home_section: Element = container("") else: home_section = section("", title("", Text(title_text)), ids=[self.make_id(title_text)], names=[title_text]) if "usage_first" in self.options: home_section += self._mk_usage(self.parser) if description := self._pre_format(self.options.get("description", self.parser.description)): home_section += description if "usage_first" not in self.options: home_section += self._mk_usage(self.parser) # construct groups excluding sub-parsers for group in self.parser._action_groups: # noqa: SLF001 if not group._group_actions or group is self.parser._subparsers: # noqa: SLF001 continue home_section += self._mk_option_group( group, prefix=self.parser.prog.split("/")[-1], prog=self.parser.prog.split("/")[-1] ) # construct sub-parser for aliases, help_msg, parser in self.load_sub_parsers(): home_section += self._mk_sub_command(aliases, help_msg, parser) if epilog := self._pre_format(self.options.get("epilog", self.parser.epilog)): home_section += epilog if self.content: self.state.nested_parse(self.content, self.content_offset, home_section) return [home_section] def _pre_format(self, block: None | str) -> None | paragraph | literal_block: if block is None: return None if self._raw_format and "\n" in block: lit = literal_block("", Text(block), classes=["sphinx-argparse-cli-wrap"]) lit["language"] = "none" return lit return paragraph("", Text(block)) def _mk_option_group(self, group: _ArgumentGroup, prefix: str, prog: str) -> section: sub_title_prefix: str = self.options["group_sub_title_prefix"] title_prefix = self.options["group_title_prefix"] title_text = self._build_opt_grp_title(group, prefix, prog, sub_title_prefix, title_prefix) title_ref: str = f"{prefix}{' ' if prefix else ''}{group.title}" ref_id = self.make_id(title_ref) # the text sadly needs to be prefixed, because otherwise the autosectionlabel will conflict header = title("", Text(title_text)) group_section = section("", header, ids=[ref_id], names=[ref_id]) if description := self._pre_format(group.description): group_section += description self._register_ref(ref_id, title_text, group_section) opt_group = bullet_list() for action in group._group_actions: # noqa: SLF001 if action.help == SUPPRESS: continue point = self._mk_option_line(action, prefix) opt_group += point group_section += opt_group return group_section def _build_opt_grp_title( self, group: _ArgumentGroup, prefix: str, prog: str, sub_title_prefix: str, title_prefix: str ) -> str: sub_cmd = prefix[len(prog) :].strip() or None if prefix != prog else None title_text = self._resolve_prefix(prog, sub_cmd, prefix, title_prefix, sub_title_prefix) title_text += group.title or "" return title_text def _mk_option_line(self, action: Action, prefix: str) -> list_item: line = paragraph() as_key = action.dest if action.metavar: as_key = action.metavar if isinstance(action.metavar, str) else action.metavar[0] if action.option_strings: first = True is_flag = action.nargs == 0 for opt in action.option_strings: if first: first = False else: line += Text(", ") self._mk_option_name(line, prefix, opt) if not is_flag: line += Text(" ") metavar_text = ( " ".join(meta.upper() for meta in action.metavar) if isinstance(action.metavar, tuple) else as_key.upper() ) line += literal(text=metavar_text) else: self._mk_option_name(line, prefix, as_key) point = list_item("", line, ids=[]) if action.help: help_text = load_help_text(action.help) temp = paragraph() self.state.nested_parse(StringList(help_text.split("\n")), 0, temp) line += Text(" - ") for content in cast("paragraph", temp.children[0]).children: line += content if ( "no_default_values" not in self.options and action.default is not None and action.default != SUPPRESS and not re.match(r".*[ (]default[s]? .*", (action.help or "")) and not isinstance(action, _StoreTrueAction | _StoreFalseAction) ): line += Text(" (default: ") line += literal(text=str(action.default).replace(str(Path.cwd()), "{cwd}")) line += Text(")") return point def _mk_option_name(self, line: paragraph, prefix: str, opt: str) -> None: ref_id = self.make_id(f"{prefix}-{opt}") ref_title = f"{prefix} {opt}" ref = reference("", refid=ref_id, reftitle=ref_title) line.attributes["ids"].append(ref_id) st = strong() st += literal(text=opt) ref += st self._register_ref(ref_id, ref_title, ref, is_cli_option=True) line += ref def _register_ref( self, ref_name: str, ref_title: str, node: Element, is_cli_option: bool = False, # noqa: FBT001, FBT002 ) -> None: doc_name = self.env.docname normalize_name = whitespace_normalize_name if is_cli_option else fully_normalize_name if self.env.config.sphinx_argparse_cli_prefix_document: name = normalize_name(f"{doc_name}:{ref_name}") else: name = normalize_name(ref_name) if name in self._std_domain.labels: logger.warning( __("duplicate label %s, other instance in %s"), name, self.env.doc2path(self._std_domain.labels[name][0]), location=node, type="sphinx-argparse-cli", subtype=self.env.docname, ) self._std_domain.anonlabels[name] = doc_name, ref_name self._std_domain.labels[name] = doc_name, ref_name, ref_title def _mk_sub_command(self, aliases: list[str], help_msg: str, parser: ArgumentParser) -> section: sub_title_prefix: str = self.options["group_sub_title_prefix"] title_prefix: str = self.options["group_title_prefix"] if sys.version_info >= (3, 14): # pragma: >=3.14 cover # https://github.com/python/cpython/issues/139809 parser.prog = _strip_ansi_colors(parser.prog) title_text = self._build_sub_cmd_title(parser, sub_title_prefix, title_prefix) title_ref: str = parser.prog if aliases: aliases_text: str = f" ({', '.join(aliases)})" title_text += aliases_text title_ref += aliases_text title_text = title_text.strip() ref_id = self.make_id(title_ref) group_section = section("", title("", Text(title_text)), ids=[ref_id], names=[title_ref]) self._register_ref(ref_id, title_ref, group_section) if "usage_first" in self.options: group_section += self._mk_usage(parser) command_desc = (parser.description or help_msg or "").strip() if command_desc: desc_paragraph = paragraph("", Text(command_desc)) group_section += desc_paragraph if "usage_first" not in self.options: group_section += self._mk_usage(parser) for group in parser._action_groups: # noqa: SLF001 if not group._group_actions: # do not show empty groups # noqa: SLF001 continue if isinstance(group._group_actions[0], _SubParsersAction): # noqa: SLF001 # If this is a subparser, ignore it continue group_section += self._mk_option_group(group, prefix=parser.prog, prog=self.parser.prog.split("/")[-1]) return group_section def _build_sub_cmd_title(self, parser: ArgumentParser, sub_title_prefix: str, title_prefix: str) -> str: root_prog = self.parser.prog.split("/")[-1] sub_cmd = parser.prog[len(root_prog) :].strip().split(" ", maxsplit=1)[0] return self._resolve_prefix(root_prog, sub_cmd, parser.prog, title_prefix, sub_title_prefix).rstrip() def _resolve_prefix( self, prog_name: str, sub_cmd: str | None, full_text: str, title_prefix: str | None, sub_title_prefix: str | None, ) -> str: title_text = "" if title_prefix is not None: title_prefix = title_prefix.replace("{prog}", prog_name) if title_prefix: title_text += f"{title_prefix} " if sub_cmd is not None: if sub_title_prefix is not None: title_text = self._apply_sub_title(title_text, sub_title_prefix, prog_name, sub_cmd) else: title_text += f"{sub_cmd} " elif sub_cmd is not None: if sub_title_prefix is not None: title_text += f"{prog_name} " title_text = self._apply_sub_title(title_text, sub_title_prefix, prog_name, sub_cmd) else: title_text += f"{full_text} " else: title_text += f"{full_text} " return title_text @staticmethod def _apply_sub_title(title_text: str, sub_title_prefix: str, prog: str, sub_cmd: str) -> str: if sub_title_prefix: sub_title_prefix = sub_title_prefix.replace("{prog}", prog) sub_title_prefix = sub_title_prefix.replace("{subcommand}", sub_cmd) title_text += f"{sub_title_prefix} " return title_text def _mk_usage(self, parser: ArgumentParser) -> literal_block: parser.formatter_class = lambda prog: HelpFormatter(prog, width=self.options.get("usage_width", 100)) with self.no_color(): texts = parser.format_usage()[len("usage: ") :].splitlines() texts = [line if at == 0 else f"{' ' * (len(parser.prog) + 1)}{line.lstrip()}" for at, line in enumerate(texts)] return literal_block("", Text("\n".join(texts)), classes=["sphinx-argparse-cli-wrap"]) @contextmanager def no_color(self) -> Iterator[None]: with patch.dict(os.environ, {"NO_COLOR": "1"}, clear=False): yield None SINGLE_QUOTE = re.compile(r"[']+(.+?)[']+") DOUBLE_QUOTE = re.compile(r'["]+(.+?)["]+') CURLY_BRACES = re.compile(r"[{](.+?)[}]") def load_help_text(help_text: str) -> str: single_quote = SINGLE_QUOTE.sub("``'\\1'``", help_text) double_quote = DOUBLE_QUOTE.sub('``"\\1"``', single_quote) return CURLY_BRACES.sub("``{\\1}``", double_quote) class HookError(Exception): def __init__(self, parser: ArgumentParser) -> None: self.parser = parser def _parse_known_args_hook(self: ArgumentParser, *args: Any, **kwargs: Any) -> None: # noqa: ARG001 raise HookError(self) _ANSI_COLOR_RE = re.compile(r"\x1b\[[0-9;]*m") def _strip_ansi_colors(text: str) -> str: # pragma: >=3.14 cover # needed due to https://github.com/python/cpython/issues/139809 return _ANSI_COLOR_RE.sub("", text) def _update_sub_parser_prog(parser: ArgumentParser, old_prog: str, new_prog: str) -> None: if not (sub_parsers := parser._subparsers): # noqa: SLF001 return sub_action: _SubParsersAction[ArgumentParser] = sub_parsers._group_actions[0] # type: ignore[assignment] # noqa: SLF001 for sub_parser in sub_action.choices.values(): sub_parser.prog = sub_parser.prog.replace(old_prog, new_prog, 1) _update_sub_parser_prog(sub_parser, old_prog, new_prog) __all__ = [ "SphinxArgparseCli", ] sphinx_argparse_cli-1.21.3/src/sphinx_argparse_cli/py.typed0000644000000000000000000000000013615410400021042 0ustar00sphinx_argparse_cli-1.21.3/src/sphinx_argparse_cli/version.py0000644000000000000000000000130213615410400021410 0ustar00# file generated by setuptools-scm # don't change, don't track in version control __all__ = [ "__version__", "__version_tuple__", "version", "version_tuple", "__commit_id__", "commit_id", ] TYPE_CHECKING = False if TYPE_CHECKING: from typing import Tuple from typing import Union VERSION_TUPLE = Tuple[Union[int, str], ...] COMMIT_ID = Union[str, None] else: VERSION_TUPLE = object COMMIT_ID = object version: str __version__: str __version_tuple__: VERSION_TUPLE version_tuple: VERSION_TUPLE commit_id: COMMIT_ID __commit_id__: COMMIT_ID __version__ = version = '1.21.3' __version_tuple__ = version_tuple = (1, 21, 3) __commit_id__ = commit_id = None sphinx_argparse_cli-1.21.3/tests/complex.txt0000644000000000000000000000314613615410400016120 0ustar00complex - CLI interface *********************** argparse tester complex [-h] [--root] [--no-help] [--outdir out_dir] [--in-dir IN_DIR] [--foo | --bar] {first,f,second,third} ... complex options =============== * **"-h"**, **"--help"** - show this help message and exit * **"--root"** - root flag * **"--no-help"** * **"--outdir"** "OUT_DIR", **"-o"** "OUT_DIR" - output directory * **"--in-dir"** "IN_DIR", **"-i"** "IN_DIR" - input directory complex Exclusive ================= this is an exclusive group * **"--foo"** - foo * **"--bar"** - bar complex first (f) ================= a-first-desc complex first [-h] [--flag] [--root] one pos_two complex first positional arguments ---------------------------------- * **"one"** - first positional argument * **"pos_two"** - second positional argument (default: "1") complex first options --------------------- * **"-h"**, **"--help"** - show this help message and exit * **"--flag"** - a parser first flag * **"--root"** - root flag complex second ============== complex second [-h] [--flag] [--root] one pos_two complex second positional arguments ----------------------------------- * **"one"** - first positional argument * **"pos_two"** - second positional argument (default: "green") complex second options ---------------------- * **"-h"**, **"--help"** - show this help message and exit * **"--flag"** - a parser second flag * **"--root"** - root flag complex third ============= complex third [-h] complex third options --------------------- * **"-h"**, **"--help"** - show this help message and exit test epilog sphinx_argparse_cli-1.21.3/tests/complex_pre_310.txt0000644000000000000000000000327613615410400017355 0ustar00complex - CLI interface *********************** argparse tester complex [-h] [--root] [--no-help] [--outdir out_dir] [--in-dir IN_DIR] [--foo | --bar] {first,f,second,third} ... complex optional arguments ========================== * **"-h"**, **"--help"** - show this help message and exit * **"--root"** - root flag * **"--no-help"** * **"--outdir"** "OUT_DIR", **"-o"** "OUT_DIR" - output directory * **"--in-dir"** "IN_DIR", **"-i"** "IN_DIR" - input directory complex Exclusive ================= this is an exclusive group * **"--foo"** - foo * **"--bar"** - bar complex first (f) ================= a-first-desc complex first [-h] [--flag] [--root] one pos_two complex first positional arguments ---------------------------------- * **"one"** - first positional argument * **"pos_two"** - second positional argument (default: "1") complex first optional arguments -------------------------------- * **"-h"**, **"--help"** - show this help message and exit * **"--flag"** - a parser first flag * **"--root"** - root flag complex second ============== complex second [-h] [--flag] [--root] one pos_two complex second positional arguments ----------------------------------- * **"one"** - first positional argument * **"pos_two"** - second positional argument (default: "green") complex second optional arguments --------------------------------- * **"-h"**, **"--help"** - show this help message and exit * **"--flag"** - a parser second flag * **"--root"** - root flag complex third ============= complex third [-h] complex third optional arguments -------------------------------- * **"-h"**, **"--help"** - show this help message and exit test epilog sphinx_argparse_cli-1.21.3/tests/conftest.py0000644000000000000000000000143713615410400016110 0ustar00from __future__ import annotations from pathlib import Path from typing import TYPE_CHECKING import pytest from docutils import __version__ as docutils_version from sphinx import __display_version__ as sphinx_version if TYPE_CHECKING: from _pytest.config import Config pytest_plugins = "sphinx.testing.fixtures" collect_ignore = ["roots"] def pytest_report_header( config: Config, # noqa: ARG001 ) -> str: # pragma: no cover # runs during collection before coverage starts return f"libraries: Sphinx-{sphinx_version}, docutils-{docutils_version}" @pytest.fixture(scope="session", name="rootdir") def root_dir() -> Path: return Path(__file__).parents[1].absolute() / "roots" def pytest_configure(config: Config) -> None: config.addinivalue_line("markers", "prepare") sphinx_argparse_cli-1.21.3/tests/test_logic.py0000644000000000000000000004303013615410400016412 0ustar00from __future__ import annotations import os import sys from pathlib import Path from typing import TYPE_CHECKING import pytest from sphinx_argparse_cli._logic import make_id, make_id_lower if TYPE_CHECKING: from io import StringIO from _pytest.fixtures import SubRequest from sphinx.testing.util import SphinxTestApp @pytest.fixture(scope="session") def opt_grp_name() -> tuple[str, str]: return "options", "options" @pytest.fixture def build_outcome(app: SphinxTestApp, request: SubRequest, monkeypatch: pytest.MonkeyPatch) -> str: prepare_marker = request.node.get_closest_marker("prepare") if prepare_marker: directive_args: list[str] | None = prepare_marker.kwargs.get("directive_args") if directive_args: # pragma: no branch index = Path(app.confdir) / "index.rst" if not any(i for i in directive_args if i.startswith(":module:")): # pragma: no branch directive_args.append(":module: parser") if not any(i for i in directive_args if i.startswith(":func:")): # pragma: no branch directive_args.append(":func: make") args = [f" {i}" for i in directive_args] index.write_text(os.linesep.join([".. sphinx_argparse_cli::", *args])) ext_mapping = {"html": "html", "text": "txt"} sphinx_marker = request.node.get_closest_marker("sphinx") assert sphinx_marker is not None ext = ext_mapping[sphinx_marker.kwargs.get("buildername")] monkeypatch.setenv("FORCE_COLOR", "1") monkeypatch.delenv("NO_COLOR", raising=False) app.build() return (Path(app.outdir) / f"index.{ext}").read_text() @pytest.mark.sphinx(buildername="html", testroot="basic") def test_basic_as_html(build_outcome: str) -> None: assert build_outcome assert "custom.css" not in build_outcome @pytest.mark.sphinx(buildername="text", testroot="complex") def test_complex_as_text(build_outcome: str) -> None: name = "complex.txt" if sys.version_info >= (3, 10) else "complex_pre_310.txt" expected = (Path(__file__).parent / name).read_text() assert build_outcome == expected @pytest.mark.sphinx(buildername="html", testroot="complex") def test_complex_as_html(build_outcome: str) -> None: assert build_outcome @pytest.mark.sphinx(buildername="html", testroot="hook") def test_hook(build_outcome: str) -> None: assert build_outcome @pytest.mark.sphinx(buildername="text", testroot="hook-fail") def test_hook_fail(app: SphinxTestApp, warning: StringIO) -> None: app.build() text = (Path(app.outdir) / "index.txt").read_text() assert "Failed to hook argparse to get ArgumentParser" in warning.getvalue() assert not text @pytest.mark.sphinx(buildername="text", testroot="prog") def test_prog_as_text(build_outcome: str) -> None: assert build_outcome == "magic - CLI interface\n*********************\n\n magic\n" @pytest.mark.sphinx(buildername="text", testroot="title-set") def test_set_title_as_text(build_outcome: str) -> None: assert build_outcome == "My own title\n************\n\n foo\n" @pytest.mark.sphinx(buildername="text", testroot="title-empty") def test_empty_title_as_text(build_outcome: str) -> None: assert build_outcome == " foo\n" @pytest.mark.sphinx(buildername="text", testroot="description-set") def test_set_description_as_text(build_outcome: str) -> None: assert build_outcome == "foo - CLI interface\n*******************\n\nMy own description\n\n foo\n" @pytest.mark.sphinx(buildername="text", testroot="description-empty") def test_empty_description_as_text(build_outcome: str) -> None: assert build_outcome == "foo - CLI interface\n*******************\n\n\n foo\n" @pytest.mark.sphinx(buildername="html", testroot="description-multiline") def test_multiline_description_as_html(build_outcome: str) -> None: ref = ( "This description\nspans multiple lines.\n\n this line is indented.\n and also this.\n\nNow this should be" " a separate paragraph.\n" ) assert ref in build_outcome ref = "This group description\n\nspans multiple lines.\n" assert ref in build_outcome @pytest.mark.sphinx(buildername="text", testroot="epilog-set") def test_set_epilog_as_text(build_outcome: str) -> None: assert build_outcome == "foo - CLI interface\n*******************\n\n foo\n\nMy own epilog\n" @pytest.mark.sphinx(buildername="text", testroot="epilog-empty") def test_empty_epilog_as_text(build_outcome: str) -> None: assert build_outcome == "foo - CLI interface\n*******************\n\n foo\n\n" @pytest.mark.sphinx(buildername="html", testroot="epilog-multiline") def test_multiline_epilog_as_html(build_outcome: str) -> None: ref = ( "This epilog\nspans multiple lines.\n\n this line is indented.\n and also this.\n\nNow this should be" " a separate paragraph.\n" ) assert ref in build_outcome @pytest.mark.sphinx(buildername="html", testroot="epilog-multiline-subclass") def test_multiline_epilog_subclass_formatter_as_html(build_outcome: str) -> None: ref = ( "This epilog\nspans multiple lines.\n\n this line is indented.\n and also this.\n\nNow this should be" " a separate paragraph.\n" ) assert ref in build_outcome @pytest.mark.sphinx(buildername="text", testroot="complex") @pytest.mark.prepare(directive_args=[":usage_width: 100"]) def test_usage_width_default(build_outcome: str) -> None: assert "complex second [-h] [--flag] [--root] one pos_two\n" in build_outcome @pytest.mark.sphinx(buildername="text", testroot="complex") @pytest.mark.prepare(directive_args=[":usage_width: 50"]) def test_usage_width_custom(build_outcome: str) -> None: assert "complex second [-h] [--flag] [--root]\n" in build_outcome @pytest.mark.sphinx(buildername="text", testroot="complex") @pytest.mark.prepare(directive_args=[":usage_first:"]) def test_set_usage_first(build_outcome: str) -> None: assert "complex [-h]" in build_outcome.split("argparse tester", maxsplit=1)[0] assert "complex first [-h]" in build_outcome.split("a-first-desc", maxsplit=1)[0] @pytest.mark.sphinx(buildername="text", testroot="suppressed-action") def test_suppressed_action(build_outcome: str) -> None: assert "--activities-since" not in build_outcome @pytest.mark.parametrize( ("example", "output"), [ ("", ""), ("{", "{"), ('"', '"'), ("'", "'"), ("{a}", "``{a}``"), ('"a"', '``"a"``'), ("'a'", "``'a'``"), ], ) def test_help_loader(example: str, output: str) -> None: from sphinx_argparse_cli._logic import load_help_text # noqa: PLC0415 result = load_help_text(example) assert result == output @pytest.mark.sphinx(buildername="html", testroot="ref") def test_ref_as_html(build_outcome: str) -> None: ref = ( '

Flag prog --root and' ' positional prog root.' "

" ) assert ref in build_outcome @pytest.mark.sphinx(buildername="html", testroot="ref-prefix-doc") def test_ref_prefix_doc(build_outcome: str) -> None: ref = ( '

Flag prog --root and' ' positional prog root.' "

" ) assert ref in build_outcome @pytest.mark.sphinx(buildername="text", testroot="ref-duplicate-label") def test_ref_duplicate_label(build_outcome: tuple[str, str], warning: StringIO) -> None: assert build_outcome assert "duplicate label prog---help" in warning.getvalue() @pytest.mark.sphinx(buildername="html", testroot="group-title-prefix-default") def test_group_title_prefix_default(build_outcome: str) -> None: assert '

prog positional arguments None: assert '

positional arguments None: assert '

custom positional arguments None: assert '

barfoo positional arguments None: grp, anchor = opt_grp_name assert '

complex Exclusivecomplex custom (f)complex custom {grp}complex customcustom-2 {grp}myprog custom-3 {grp} None: grp, anchor = opt_grp_name assert '

complex Exclusivecomplex (f)complex {grp}complexmyprog {grp} None: grp, anchor = opt_grp_name assert '

Exclusive(f)positional arguments{grp} None: grp, anchor = opt_grp_name assert f'

bar {grp}bar Exclusivebar baronlyroot (f)bar baronlyroot first positional arguments None: assert "False" not in build_outcome assert "True" not in build_outcome @pytest.mark.sphinx(buildername="html", testroot="lower-upper-refs") def test_lower_upper_refs(build_outcome: str, warning: StringIO) -> None: assert '

' in build_outcome assert '

' in build_outcome assert not warning.getvalue() @pytest.mark.parametrize( ("key", "mixed", "lower"), [ ("ProgramName", "ProgramName", "_program_name"), ("ProgramName -A", "ProgramName--A", "_program_name--_a"), ("ProgramName -a", "ProgramName--a", "_program_name--a"), ], ) def test_make_id(key: str, mixed: str, lower: str) -> None: assert make_id(key) == mixed assert make_id_lower(key) == lower @pytest.mark.sphinx(buildername="html", testroot="force-refs-lower") def test_ref_cases(build_outcome: str, warning: StringIO) -> None: assert '' in build_outcome assert '' in build_outcome assert not warning.getvalue() @pytest.mark.sphinx(buildername="text", testroot="default-handling") def test_with_default(build_outcome: str) -> None: assert ( build_outcome == """foo - CLI interface ******************* foo x foo positional arguments ======================== * **"x"** - arg (default: True) """ ) @pytest.mark.sphinx(buildername="html", testroot="nested") def test_nested_content(build_outcome: str) -> None: assert '

' in build_outcome assert "

basic-1 - CLI interface" in build_outcome assert "

basic-1 opt" in build_outcome assert "

Some text inside first directive.

" in build_outcome assert '
' in build_outcome assert "

basic-2 - CLI interface" in build_outcome assert "

basic-2 opt" in build_outcome assert "

Some text inside second directive.

" in build_outcome assert "

Some text after directives.

" in build_outcome @pytest.mark.sphinx(buildername="html", testroot="subparsers") def test_subparsers(build_outcome: str) -> None: assert '
' in build_outcome assert '
' in build_outcome assert '
' in build_outcome assert '
' in build_outcome assert '
' in build_outcome assert '
' in build_outcome assert '
' in build_outcome assert '
' in build_outcome assert '
' in build_outcome assert '
' in build_outcome assert '
' in build_outcome @pytest.mark.sphinx(buildername="text", testroot="bad-module") def test_bad_module(app: SphinxTestApp, warning: StringIO) -> None: app.build() assert "Failed to import module 'nonexistent_module'" in warning.getvalue() @pytest.mark.sphinx(buildername="text", testroot="bad-func") def test_bad_func(app: SphinxTestApp, warning: StringIO) -> None: app.build() assert "Module 'parser' has no attribute 'nonexistent_func'" in warning.getvalue() @pytest.mark.sphinx(buildername="text", testroot="nargs") def test_nargs(build_outcome: str) -> None: assert "pos_optional" in build_outcome assert "pos_zero_or_more" in build_outcome assert "pos_one_or_more" in build_outcome assert "KEY VALUE" in build_outcome assert "(default: " in build_outcome # default_val is not None, should show assert 'default: "None"' not in build_outcome @pytest.mark.sphinx(buildername="text", testroot="choices") def test_choices(build_outcome: str) -> None: assert "output format" in build_outcome assert "verbosity level" in build_outcome @pytest.mark.sphinx(buildername="text", testroot="actions") def test_actions(build_outcome: str) -> None: assert "increase verbosity" in build_outcome assert "paths to include" in build_outcome assert "a required optional argument" in build_outcome @pytest.mark.sphinx(buildername="text", testroot="tuple-metavar") def test_tuple_metavar(build_outcome: str) -> None: assert '"A B"' in build_outcome or "A B" in build_outcome assert "select a pair" in build_outcome assert "default: None" not in build_outcome assert '"VAL"' in build_outcome @pytest.mark.sphinx(buildername="text", testroot="prog-subcommands") def test_prog_subcommands(build_outcome: str) -> None: assert "my-tool" in build_outcome assert "original-name" not in build_outcome assert "my-tool foo" in build_outcome @pytest.mark.sphinx(buildername="text", testroot="multiword-prog") def test_multiword_prog(build_outcome: str) -> None: assert "python -m build positional arguments" in build_outcome assert "python -m build options" in build_outcome @pytest.mark.sphinx(buildername="html", testroot="title-empty-groups") def test_empty_title_groups_in_toctree(app: SphinxTestApp, monkeypatch: pytest.MonkeyPatch) -> None: monkeypatch.setenv("FORCE_COLOR", "1") monkeypatch.delenv("NO_COLOR", raising=False) app.build() cli_html = (Path(app.outdir) / "cli.html").read_text() assert '
' in cli_html assert "be verbose" in cli_html sphinx_argparse_cli-1.21.3/tests/test_sphinx_argparse_cli.py0000644000000000000000000000023513615410400021341 0ustar00from __future__ import annotations def test_version() -> None: import sphinx_argparse_cli # noqa: PLC0415 assert sphinx_argparse_cli.__version__ sphinx_argparse_cli-1.21.3/.gitignore0000644000000000000000000000017213615410400014532 0ustar00.idea *.egg-info/ .tox/ .coverage* coverage.xml .*_cache __pycache__ **.pyc build dist src/sphinx_argparse_cli/version.py sphinx_argparse_cli-1.21.3/LICENSE.txt0000644000000000000000000000177713615410400014401 0ustar00Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. sphinx_argparse_cli-1.21.3/README.md0000644000000000000000000001656113615410400014032 0ustar00# sphinx-argparse-cli [![PyPI](https://img.shields.io/pypi/v/sphinx-argparse-cli?style=flat-square)](https://pypi.org/project/sphinx-argparse-cli) [![PyPI - Implementation](https://img.shields.io/pypi/implementation/sphinx-argparse-cli?style=flat-square)](https://pypi.org/project/sphinx-argparse-cli) [![PyPI - Python Version](https://img.shields.io/pypi/pyversions/sphinx-argparse-cli?style=flat-square)](https://pypi.org/project/sphinx-argparse-cli) [![Downloads](https://static.pepy.tech/badge/sphinx-argparse-cli/month)](https://pepy.tech/project/sphinx-argparse-cli) [![PyPI - License](https://img.shields.io/pypi/l/sphinx-argparse-cli?style=flat-square)](https://opensource.org/licenses/MIT) [![check](https://github.com/tox-dev/sphinx-argparse-cli/actions/workflows/check.yaml/badge.svg)](https://github.com/tox-dev/sphinx-argparse-cli/actions/workflows/check.yaml) Render CLI arguments (sub-commands friendly) defined by the argparse module. ## Getting started Install the package: ```bash python -m pip install sphinx-argparse-cli ``` Add the extension to your `conf.py`: ```python extensions = ["sphinx_argparse_cli"] ``` Use the directive in any reStructuredText file: ```rst .. sphinx_argparse_cli:: :module: my_project.cli :func: build_parser ``` `:module:` points to the Python module containing the parser, and `:func:` names a zero-argument function that returns an `ArgumentParser`. Build your docs and the full CLI reference appears automatically. ## How-to guides ### Override the program name By default the program name comes from the parser. Use `:prog:` to replace it: ```rst .. sphinx_argparse_cli:: :module: my_project.cli :func: build_parser :prog: my-cli ``` ### Hook into a parser that is not returned When a function creates and uses a parser internally without returning it, set the `:hook:` flag to intercept `argparse.ArgumentParser`: ```rst .. sphinx_argparse_cli:: :module: my_project.cli :func: main :hook: :prog: my-cli ``` ### Customize section titles Control how group and subcommand headings are rendered with `:group_title_prefix:` and `:group_sub_title_prefix:`. Both accept `{prog}` and the sub-title also accepts `{subcommand}`: ```rst .. sphinx_argparse_cli:: :module: my_project.cli :func: build_parser :group_title_prefix: {prog} :group_sub_title_prefix: {prog} {subcommand} ``` ### Suppress default values Hide `(default: ...)` annotations from the output: ```rst .. sphinx_argparse_cli:: :module: my_project.cli :func: build_parser :no_default_values: ``` ### Control usage display Set the character width for usage lines and optionally show usage before the description: ```rst .. sphinx_argparse_cli:: :module: my_project.cli :func: build_parser :usage_width: 80 :usage_first: ``` ### Override title, description, or epilog Replace auto-detected values, or pass an empty string to suppress them entirely: ```rst .. sphinx_argparse_cli:: :module: my_project.cli :func: build_parser :title: Custom Title :description: Custom description text. :epilog: ``` ### Cross-reference generated anchors The directive registers Sphinx reference labels for every command, group, and flag. Use the `:ref:` role to link to them. With `sphinx_argparse_cli_prefix_document = False` (default): ```rst :ref:`tox-optional-arguments` :ref:`tox-run` :ref:`tox-run---magic` ``` With `sphinx_argparse_cli_prefix_document = True` (anchors prefixed by document name, avoids clashes across documents): ```rst :ref:`cli:tox-optional-arguments` :ref:`cli:tox-run` :ref:`cli:tox-run---magic` ``` The anchor text is visible after the `#` in the URL when you click a heading. ### Handle mixed-case references Sphinx `:ref:` only supports lower-case targets. When your program name or flags contain capital letters, set `:force_refs_lower:` to convert them — each upper-case letter becomes its lower-case form prefixed with `_` (e.g. `A` becomes `_a`): ```rst .. sphinx_argparse_cli:: :module: my_project.cli :func: build_parser :force_refs_lower: ``` For a program named `SampleProgram`: ```rst :ref:`_sample_program--a` .. flag -a :ref:`_sample_program--_a` .. flag -A ``` If you do not need Sphinx `:ref:` cross-references you can leave this off to keep mixed-case anchors in the HTML output, but enabling it later will change existing anchor URLs. ### Add extra content after generated docs Any content nested inside the directive is appended after the generated CLI documentation: ```rst .. sphinx_argparse_cli:: :module: my_project.cli :func: build_parser Extra notes or examples rendered after the CLI reference. ``` ## Reference ### Directive options | Option | Type | Default | Description | | -------------------------- | ------ | ------------------------ | ------------------------------------------------------------------------------ | | `:module:` | string | **required** | Python module path where the parser is defined | | `:func:` | string | **required** | Zero-argument function that returns an `ArgumentParser` | | `:prog:` | string | parser's `prog` | Override the displayed program name | | `:hook:` | flag | off | Intercept `ArgumentParser` instead of expecting `func` to return it | | `:title:` | string | ` - CLI interface` | Custom title; empty string suppresses it | | `:description:` | string | parser's description | Custom description; empty string suppresses it | | `:epilog:` | string | parser's epilog | Custom epilog; empty string suppresses it | | `:usage_width:` | int | `100` | Character width for usage lines | | `:usage_first:` | flag | off | Show usage before the description | | `:group_title_prefix:` | string | `{prog}` | Heading prefix for groups; `{prog}` is replaced with the program name | | `:group_sub_title_prefix:` | string | `{prog} {subcommand}` | Heading prefix for subcommand groups; supports `{prog}` and `{subcommand}` | | `:no_default_values:` | flag | off | Suppress `(default: ...)` annotations | | `:force_refs_lower:` | flag | off | Lower-case reference anchors with `_` prefix for capitals (for `:ref:` compat) | ### Configuration values (`conf.py`) | Name | Type | Default | Description | | ------------------------------------- | ---- | ------- | ---------------------------------------------------------------- | | `sphinx_argparse_cli_prefix_document` | bool | `False` | Prefix reference anchors with the document name to avoid clashes | ## Live examples - [tox](https://tox.wiki/en/latest/cli_interface.html) - [pypa-build](https://pypa-build.readthedocs.io/en/latest/#python-m-build) - [mdpo](https://mondeja.github.io/mdpo/latest/cli.html) sphinx_argparse_cli-1.21.3/pyproject.toml0000644000000000000000000000737413615410400015471 0ustar00[build-system] build-backend = "hatchling.build" requires = [ "hatch-vcs>=0.5", "hatchling>=1.28", ] [project] name = "sphinx-argparse-cli" description = "render CLI arguments (sub-commands friendly) defined by argparse module" readme = "README.md" keywords = [ "argparse", "sphinx", ] license = "MIT" maintainers = [ { name = "Bernat Gabor", email = "gaborjbernat@gmail.com" }, ] requires-python = ">=3.11" classifiers = [ "Development Status :: 5 - Production/Stable", "Environment :: Console", "Framework :: Sphinx", "Framework :: Sphinx :: Extension", "Intended Audience :: Developers", "License :: OSI Approved :: MIT License", "Operating System :: OS Independent", "Programming Language :: Python :: 3 :: Only", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.14", "Programming Language :: Python :: Implementation :: CPython", "Topic :: Documentation", "Topic :: Documentation :: Sphinx", ] dynamic = [ "version", ] dependencies = [ "sphinx>=9.0.4", ] urls.Documentation = "https://github.com/tox-dev/sphinx-argparse-cli#sphinx-argparse-cli" urls.Homepage = "https://github.com/tox-dev/sphinx-argparse-cli" urls.Source = "https://github.com/tox-dev/sphinx-argparse-cli" urls.Tracker = "https://github.com/tox-dev/sphinx-argparse-cli/issues" [dependency-groups] dev = [ { include-group = "lint" }, { include-group = "pkg-meta" }, { include-group = "test" }, { include-group = "type" }, ] test = [ "covdefaults>=2.3", "defusedxml>=0.7.1", "pytest>=9.0.2", "pytest-cov>=7", ] type = [ "ty>=0.0.17", { include-group = "test" }, ] lint = [ "pre-commit-uv>=4.2", ] pkg-meta = [ "check-wheel-contents>=0.6.3", "twine>=6.2", "uv>=0.10.2", ] [tool.hatch] build.hooks.vcs.version-file = "src/sphinx_argparse_cli/version.py" build.targets.sdist.include = [ "/src", "/tests", ] version.source = "vcs" [tool.black] line-length = 120 [tool.ruff] target-version = "py310" line-length = 120 format.preview = true format.docstring-code-line-length = 100 format.docstring-code-format = true lint.select = [ "ALL", ] lint.ignore = [ "ANN401", # allow Any as type annotation "COM812", # Conflict with formatter "CPY", # No copyright statements "D203", # `one-blank-line-before-class` (D203) and `no-blank-line-before-class` (D211) are incompatible "D212", # `multi-line-summary-first-line` (D212) and `multi-line-summary-second-line` (D213) are incompatible "DOC", # not yet supported "ISC001", # Conflict with formatter "S104", # Possible binding to all interface ] lint.per-file-ignores."roots/**/*.py" = [ "D", # no docs "INP001", # no namespace ] lint.per-file-ignores."tests/**/*.py" = [ "D", # don't care about documentation in tests "FBT", # don't care about booleans as positional arguments in tests "INP001", # no implicit namespace "PLC2701", # private import "PLR0913", # any number of arguments in tests "PLR0917", # any number of arguments in tests "PLR2004", # Magic value used in comparison, consider replacing with a constant variable "S101", # asserts allowed in tests "S603", # `subprocess` call: check for execution of untrusted input ] lint.isort = { known-first-party = [ "sphinx_argparse_cli", ], required-imports = [ "from __future__ import annotations", ] } [tool.codespell] builtin = "clear,usage,en-GB_to_en-US" count = true [tool.pyproject-fmt] max_supported_python = "3.14" [tool.coverage] run.parallel = true run.plugins = [ "covdefaults", ] run.relative_files = true paths.source = [ "src", "**/site-packages", ] report.fail_under = 100 html.show_contexts = true html.skip_covered = false [tool.ty] environment.python-version = "3.14" sphinx_argparse_cli-1.21.3/PKG-INFO0000644000000000000000000002135313615410400013643 0ustar00Metadata-Version: 2.4 Name: sphinx-argparse-cli Version: 1.21.3 Summary: render CLI arguments (sub-commands friendly) defined by argparse module Project-URL: Documentation, https://github.com/tox-dev/sphinx-argparse-cli#sphinx-argparse-cli Project-URL: Homepage, https://github.com/tox-dev/sphinx-argparse-cli Project-URL: Source, https://github.com/tox-dev/sphinx-argparse-cli Project-URL: Tracker, https://github.com/tox-dev/sphinx-argparse-cli/issues Maintainer-email: Bernat Gabor License-Expression: MIT License-File: LICENSE.txt Keywords: argparse,sphinx Classifier: Development Status :: 5 - Production/Stable Classifier: Environment :: Console Classifier: Framework :: Sphinx Classifier: Framework :: Sphinx :: Extension Classifier: Intended Audience :: Developers Classifier: License :: OSI Approved :: MIT License Classifier: Operating System :: OS Independent Classifier: Programming Language :: Python :: 3 :: Only Classifier: Programming Language :: Python :: 3.11 Classifier: Programming Language :: Python :: 3.12 Classifier: Programming Language :: Python :: 3.13 Classifier: Programming Language :: Python :: 3.14 Classifier: Programming Language :: Python :: Implementation :: CPython Classifier: Topic :: Documentation Classifier: Topic :: Documentation :: Sphinx Requires-Python: >=3.11 Requires-Dist: sphinx>=9.0.4 Description-Content-Type: text/markdown # sphinx-argparse-cli [![PyPI](https://img.shields.io/pypi/v/sphinx-argparse-cli?style=flat-square)](https://pypi.org/project/sphinx-argparse-cli) [![PyPI - Implementation](https://img.shields.io/pypi/implementation/sphinx-argparse-cli?style=flat-square)](https://pypi.org/project/sphinx-argparse-cli) [![PyPI - Python Version](https://img.shields.io/pypi/pyversions/sphinx-argparse-cli?style=flat-square)](https://pypi.org/project/sphinx-argparse-cli) [![Downloads](https://static.pepy.tech/badge/sphinx-argparse-cli/month)](https://pepy.tech/project/sphinx-argparse-cli) [![PyPI - License](https://img.shields.io/pypi/l/sphinx-argparse-cli?style=flat-square)](https://opensource.org/licenses/MIT) [![check](https://github.com/tox-dev/sphinx-argparse-cli/actions/workflows/check.yaml/badge.svg)](https://github.com/tox-dev/sphinx-argparse-cli/actions/workflows/check.yaml) Render CLI arguments (sub-commands friendly) defined by the argparse module. ## Getting started Install the package: ```bash python -m pip install sphinx-argparse-cli ``` Add the extension to your `conf.py`: ```python extensions = ["sphinx_argparse_cli"] ``` Use the directive in any reStructuredText file: ```rst .. sphinx_argparse_cli:: :module: my_project.cli :func: build_parser ``` `:module:` points to the Python module containing the parser, and `:func:` names a zero-argument function that returns an `ArgumentParser`. Build your docs and the full CLI reference appears automatically. ## How-to guides ### Override the program name By default the program name comes from the parser. Use `:prog:` to replace it: ```rst .. sphinx_argparse_cli:: :module: my_project.cli :func: build_parser :prog: my-cli ``` ### Hook into a parser that is not returned When a function creates and uses a parser internally without returning it, set the `:hook:` flag to intercept `argparse.ArgumentParser`: ```rst .. sphinx_argparse_cli:: :module: my_project.cli :func: main :hook: :prog: my-cli ``` ### Customize section titles Control how group and subcommand headings are rendered with `:group_title_prefix:` and `:group_sub_title_prefix:`. Both accept `{prog}` and the sub-title also accepts `{subcommand}`: ```rst .. sphinx_argparse_cli:: :module: my_project.cli :func: build_parser :group_title_prefix: {prog} :group_sub_title_prefix: {prog} {subcommand} ``` ### Suppress default values Hide `(default: ...)` annotations from the output: ```rst .. sphinx_argparse_cli:: :module: my_project.cli :func: build_parser :no_default_values: ``` ### Control usage display Set the character width for usage lines and optionally show usage before the description: ```rst .. sphinx_argparse_cli:: :module: my_project.cli :func: build_parser :usage_width: 80 :usage_first: ``` ### Override title, description, or epilog Replace auto-detected values, or pass an empty string to suppress them entirely: ```rst .. sphinx_argparse_cli:: :module: my_project.cli :func: build_parser :title: Custom Title :description: Custom description text. :epilog: ``` ### Cross-reference generated anchors The directive registers Sphinx reference labels for every command, group, and flag. Use the `:ref:` role to link to them. With `sphinx_argparse_cli_prefix_document = False` (default): ```rst :ref:`tox-optional-arguments` :ref:`tox-run` :ref:`tox-run---magic` ``` With `sphinx_argparse_cli_prefix_document = True` (anchors prefixed by document name, avoids clashes across documents): ```rst :ref:`cli:tox-optional-arguments` :ref:`cli:tox-run` :ref:`cli:tox-run---magic` ``` The anchor text is visible after the `#` in the URL when you click a heading. ### Handle mixed-case references Sphinx `:ref:` only supports lower-case targets. When your program name or flags contain capital letters, set `:force_refs_lower:` to convert them — each upper-case letter becomes its lower-case form prefixed with `_` (e.g. `A` becomes `_a`): ```rst .. sphinx_argparse_cli:: :module: my_project.cli :func: build_parser :force_refs_lower: ``` For a program named `SampleProgram`: ```rst :ref:`_sample_program--a` .. flag -a :ref:`_sample_program--_a` .. flag -A ``` If you do not need Sphinx `:ref:` cross-references you can leave this off to keep mixed-case anchors in the HTML output, but enabling it later will change existing anchor URLs. ### Add extra content after generated docs Any content nested inside the directive is appended after the generated CLI documentation: ```rst .. sphinx_argparse_cli:: :module: my_project.cli :func: build_parser Extra notes or examples rendered after the CLI reference. ``` ## Reference ### Directive options | Option | Type | Default | Description | | -------------------------- | ------ | ------------------------ | ------------------------------------------------------------------------------ | | `:module:` | string | **required** | Python module path where the parser is defined | | `:func:` | string | **required** | Zero-argument function that returns an `ArgumentParser` | | `:prog:` | string | parser's `prog` | Override the displayed program name | | `:hook:` | flag | off | Intercept `ArgumentParser` instead of expecting `func` to return it | | `:title:` | string | ` - CLI interface` | Custom title; empty string suppresses it | | `:description:` | string | parser's description | Custom description; empty string suppresses it | | `:epilog:` | string | parser's epilog | Custom epilog; empty string suppresses it | | `:usage_width:` | int | `100` | Character width for usage lines | | `:usage_first:` | flag | off | Show usage before the description | | `:group_title_prefix:` | string | `{prog}` | Heading prefix for groups; `{prog}` is replaced with the program name | | `:group_sub_title_prefix:` | string | `{prog} {subcommand}` | Heading prefix for subcommand groups; supports `{prog}` and `{subcommand}` | | `:no_default_values:` | flag | off | Suppress `(default: ...)` annotations | | `:force_refs_lower:` | flag | off | Lower-case reference anchors with `_` prefix for capitals (for `:ref:` compat) | ### Configuration values (`conf.py`) | Name | Type | Default | Description | | ------------------------------------- | ---- | ------- | ---------------------------------------------------------------- | | `sphinx_argparse_cli_prefix_document` | bool | `False` | Prefix reference anchors with the document name to avoid clashes | ## Live examples - [tox](https://tox.wiki/en/latest/cli_interface.html) - [pypa-build](https://pypa-build.readthedocs.io/en/latest/#python-m-build) - [mdpo](https://mondeja.github.io/mdpo/latest/cli.html)