sphinx_argparse_cli-1.17.0/src/sphinx_argparse_cli/__init__.py0000644000000000000000000000106513615410400021472 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]: app.add_css_file("custom.css") from ._logic import SphinxArgparseCli app.add_directive(SphinxArgparseCli.name, SphinxArgparseCli) app.add_config_value("sphinx_argparse_cli_prefix_document", False, "env") # noqa: FBT003 return {"parallel_read_safe": True} __all__ = [ "__version__", ] sphinx_argparse_cli-1.17.0/src/sphinx_argparse_cli/_logic.py0000644000000000000000000003757313615410400021204 0ustar00from __future__ import annotations import re import sys from argparse import ( SUPPRESS, Action, ArgumentParser, HelpFormatter, RawDescriptionHelpFormatter, _ArgumentGroup, _StoreFalseAction, _StoreTrueAction, _SubParsersAction, ) from collections import defaultdict from pathlib import Path from typing import TYPE_CHECKING, Any, ClassVar, Iterator, NamedTuple, cast from docutils.nodes import ( Element, Node, Text, bullet_list, 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.domains.std import StandardDomain from sphinx.locale import __ from sphinx.util.docutils import SphinxDirective from sphinx.util.logging import getLogger if TYPE_CHECKING: from docutils.parsers.rst.states import RSTState, RSTStateMachine 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"] parser_creator = getattr(__import__(module_name, fromlist=[attr_name]), attr_name) 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 # type: ignore[method-assign] else: self._parser = parser_creator() del sys.modules[module_name] # no longer needed cleanup if self._parser is None: msg = "Failed to hook argparse to get ArgumentParser" raise self.error(msg) if "prog" in self.options: self._parser.prog = self.options["prog"] self._raw_format = self._parser.formatter_class == RawDescriptionHelpFormatter return self._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 parser_to_args: dict[int, list[str]] = defaultdict(list) str_to_parser: dict[str, ArgumentParser] = {} sub_parser: _SubParsersAction[ArgumentParser] sub_parser = top_sub_parser._group_actions[0] # type: ignore[assignment] # noqa: SLF001 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 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.strip(): home_section: Element = paragraph() 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]) # 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)) lit["language"] = "none" return lit return paragraph("", Text(block)) def _mk_option_group(self, group: _ArgumentGroup, prefix: 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, 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, sub_title_prefix: str, title_prefix: str) -> str: title_text, elements = "", prefix.split(" ") if title_prefix is not None: title_prefix = title_prefix.replace("{prog}", elements[0]) if title_prefix: title_text += f"{title_prefix} " if " " in prefix: if sub_title_prefix is not None: title_text = self._append_title(title_text, sub_title_prefix, elements[0], " ".join(elements[1:])) else: title_text += f"{' '.join(prefix.split(' ')[1:])} " elif " " in prefix: if sub_title_prefix is not None: title_text += f"{elements[0]} " title_text = self._append_title(title_text, sub_title_prefix, elements[0], " ".join(elements[1:])) else: title_text += f"{' '.join(elements[:2])} " else: title_text += f"{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(" ") line += literal(text=as_key.upper()) 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 != 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"] 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 group_section += self._mk_option_group(group, prefix=parser.prog) return group_section def _build_sub_cmd_title(self, parser: ArgumentParser, sub_title_prefix: str, title_prefix: str) -> str: title_text, elements = "", parser.prog.split(" ") if title_prefix is not None: title_prefix = title_prefix.replace("{prog}", elements[0]) if title_prefix: title_text += f"{title_prefix} " if sub_title_prefix is not None: title_text = self._append_title(title_text, sub_title_prefix, elements[0], elements[1]) else: title_text += elements[1] elif sub_title_prefix is not None: title_text += f"{elements[0]} " title_text = self._append_title(title_text, sub_title_prefix, elements[0], elements[1]) else: title_text += parser.prog return title_text.rstrip() @staticmethod def _append_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)) 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))) 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) __all__ = [ "SphinxArgparseCli", ] sphinx_argparse_cli-1.17.0/src/sphinx_argparse_cli/py.typed0000644000000000000000000000000013615410400021044 0ustar00sphinx_argparse_cli-1.17.0/src/sphinx_argparse_cli/version.py0000644000000000000000000000063513615410400021422 0ustar00# file generated by setuptools_scm # don't change, don't track in version control TYPE_CHECKING = False if TYPE_CHECKING: from typing import Tuple, Union VERSION_TUPLE = Tuple[Union[int, str], ...] else: VERSION_TUPLE = object version: str __version__: str __version_tuple__: VERSION_TUPLE version_tuple: VERSION_TUPLE __version__ = version = '1.17.0' __version_tuple__ = version_tuple = (1, 17, 0) sphinx_argparse_cli-1.17.0/tests/complex.txt0000644000000000000000000000326213615410400016121 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 (default: "None") * **"--in-dir"** "IN_DIR", **"-i"** "IN_DIR" - input directory (default: "None") 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 (default: "None") * **"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 (default: "None") * **"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.17.0/tests/complex_pre_310.txt0000644000000000000000000000341213615410400017347 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 (default: "None") * **"--in-dir"** "IN_DIR", **"-i"** "IN_DIR" - input directory (default: "None") 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 (default: "None") * **"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 (default: "None") * **"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.17.0/tests/conftest.py0000644000000000000000000000132313615410400016104 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) -> str: # noqa: ARG001 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.17.0/tests/test_logic.py0000644000000000000000000003335413615410400016424 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]: if sys.version_info >= (3, 10): # pragma: no branch # https://bugs.python.org/issue9694 return "options", "options" # pragma: no cover return "optional arguments", "optional-arguments" # pragma: no cover @pytest.fixture() def build_outcome(app: SphinxTestApp, request: SubRequest) -> 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")] 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 @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 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" @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="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")[0] assert "complex first [-h]" in build_outcome.split("a-first-desc")[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 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 sphinx_argparse_cli-1.17.0/tests/test_sphinx_argparse_cli.py0000644000000000000000000000021413615410400021340 0ustar00from __future__ import annotations def test_version() -> None: import sphinx_argparse_cli assert sphinx_argparse_cli.__version__ sphinx_argparse_cli-1.17.0/.gitignore0000644000000000000000000000017213615410400014534 0ustar00.idea *.egg-info/ .tox/ .coverage* coverage.xml .*_cache __pycache__ **.pyc build dist src/sphinx_argparse_cli/version.py sphinx_argparse_cli-1.17.0/LICENSE.txt0000644000000000000000000000177713615410400014403 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.17.0/README.md0000644000000000000000000001556013615410400014032 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/workflows/check/badge.svg?branch=main) Render CLI arguments (sub-commands friendly) defined by the argparse module. For live demo checkout the documentation of [tox](https://tox.wiki/en/latest/cli_interface.html), [pypa-build](https://pypa-build.readthedocs.io/en/latest/#python-m-build) and [mdpo](https://mondeja.github.io/mdpo/latest/cli.html). ## installation ```bash python -m pip install sphinx-argparse-cli ``` ## enable in your `conf.py` ```python # just add it to your list of extensions to load within conf.py extensions = ["sphinx_argparse_cli"] ``` ## use Within the reStructuredText files use the `sphinx_argparse_cli` directive that takes, at least, two arguments: | Name | Description | | ---------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | module | the module path to where the parser is defined | | func | the name of the function that once called with no arguments constructs the parser | | prog | (optional) when provided, overwrites the `` name. | | hook | (optional) hook `argparse` to retrieve the parser if `func` uses a parser instead of returning it. | | title | (optional) when provided, overwrites the ` - CLI interface` title added by default and when empty, will not be included | | description | (optional) when provided, overwrites the description and when empty, will not be included | | epilog | (optional) when provided, overwrites the epilog and when empty, will not be included | | usage_width | (optional) how large should usage examples be - defaults to 100 character | | usage_first | (optional) show usage before description | | group_title_prefix | (optional) groups subsections title prefixes, accepts the string `{prog}` as a replacement for the program name - defaults to `{prog}` | | group_sub_title_prefix | (optional) subcommands groups subsections title prefixes, accepts replacement of `{prog}` and `{subcommand}` for program and subcommand name - defaults to `{prog} {subcommand}` | | no_default_values | (optional) suppresses generation of `default` entries | | force_refs_lower | (optional) Sphinx `:ref:` only supports lower-case references. With this, any capital letter in generated reference anchors are lowered and given an `_` prefix (i.e. `A` becomes `_a`) | For example: ```rst .. sphinx_argparse_cli:: :module: a_project.cli :func: build_parser :prog: my-cli-program ``` If you have code that creates and uses a parser but does not return it, you can specify the `:hook:` flag: ```rst .. sphinx_argparse_cli:: :module: a_project.cli :func: main :hook: :prog: my-cli-program ``` ### Refer to generated content The tool will register reference links to all anchors. This means that you can use the sphinx `ref` role to refer to both the (sub)command title/groups and every flag/argument. The tool offers a configuration flag `sphinx_argparse_cli_prefix_document` (change by setting this variable in `conf.py` - by default `False`). This option influences the reference ids generated. If it's false the reference will be the anchor id (the text appearing after the `'#` in the URI once you click on it). If it's true the anchor id will be prefixed by the document name (this is useful to avoid reference label clash when the same anchors are generated in multiple documents). For example in case of a `tox` command, and `sphinx_argparse_cli_prefix_document=False` (default): - to refer to the optional arguments group use ``:ref:`tox-optional-arguments` ``, - to refer to the run subcommand use ``:ref:`tox-run` ``, - to refer to flag `--magic` of the `run` sub-command use ``:ref:`tox-run---magic` ``. For example in case of a `tox` command, and `sphinx_argparse_cli_prefix_document=True`, and the current document name being `cli`: - to refer to the optional arguments group use ``:ref:`cli:tox-optional-arguments` ``, - to refer to the run subcommand use ``:ref:`cli:tox-run` ``, - to refer to flag `--magic` of the `run` sub-command use ``:ref:`cli:tox-run---magic` ``. Due to Sphinx's `:ref:` only supporting lower-case values, if you need to distinguish mixed case program names or arguments, set the `:force_refs_lower:` argument. With this flag, captial-letters in references will be converted to their lower-case counterpart and prefixed with an `_`. For example: - A `prog` name `SampleProgram` will be referenced as ``:ref:`_sample_program...` ``. - To distinguish between mixed case flags `-a` and `-A` use ``:ref:`_sample_program--a` `` and ``:ref:`_sample_program--_a` `` respectively Note that if you are _not_ concernced about using internal Sphinx `:ref:` cross-references, you may choose to leave this off to maintain mixed-case anchors in your output HTML; but be aware that later enabling it will change your anchors in the output HTML. sphinx_argparse_cli-1.17.0/pyproject.toml0000644000000000000000000000665213615410400015471 0ustar00[build-system] build-backend = "hatchling.build" requires = [ "hatch-vcs>=0.4", "hatchling>=1.25", ] [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" }, ] # noqa: E999 requires-python = ">=3.9" 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.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: Implementation :: CPython", "Topic :: Documentation", "Topic :: Documentation :: Sphinx", ] dynamic = [ "version", ] dependencies = [ "sphinx>=7.3.7", ] optional-dependencies.test = [ "covdefaults>=2.3", "defusedxml>=0.7.1", # needed for sphinx.testing "pytest>=8.2.2", "pytest-cov>=5", ] 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" [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 = "py38" line-length = 120 lint.select = [ "ALL", ] lint.ignore = [ "ANN101", # no type annotation for self "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 "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.coverage] html.show_contexts = true html.skip_covered = false paths.source = [ "src", "**/site-packages", ] report.fail_under = 76 run.dynamic_context = "test_function" run.parallel = true run.plugins = [ "covdefaults", ] run.relative_files = true [tool.mypy] python_version = "3.11" show_error_codes = true strict = true sphinx_argparse_cli-1.17.0/PKG-INFO0000644000000000000000000002067413615410400013652 0ustar00Metadata-Version: 2.3 Name: sphinx-argparse-cli Version: 1.17.0 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.9 Classifier: Programming Language :: Python :: 3.10 Classifier: Programming Language :: Python :: 3.11 Classifier: Programming Language :: Python :: 3.12 Classifier: Programming Language :: Python :: Implementation :: CPython Classifier: Topic :: Documentation Classifier: Topic :: Documentation :: Sphinx Requires-Python: >=3.9 Requires-Dist: sphinx>=7.3.7 Provides-Extra: test Requires-Dist: covdefaults>=2.3; extra == 'test' Requires-Dist: defusedxml>=0.7.1; extra == 'test' Requires-Dist: pytest-cov>=5; extra == 'test' Requires-Dist: pytest>=8.2.2; extra == 'test' 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/workflows/check/badge.svg?branch=main) Render CLI arguments (sub-commands friendly) defined by the argparse module. For live demo checkout the documentation of [tox](https://tox.wiki/en/latest/cli_interface.html), [pypa-build](https://pypa-build.readthedocs.io/en/latest/#python-m-build) and [mdpo](https://mondeja.github.io/mdpo/latest/cli.html). ## installation ```bash python -m pip install sphinx-argparse-cli ``` ## enable in your `conf.py` ```python # just add it to your list of extensions to load within conf.py extensions = ["sphinx_argparse_cli"] ``` ## use Within the reStructuredText files use the `sphinx_argparse_cli` directive that takes, at least, two arguments: | Name | Description | | ---------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | module | the module path to where the parser is defined | | func | the name of the function that once called with no arguments constructs the parser | | prog | (optional) when provided, overwrites the `` name. | | hook | (optional) hook `argparse` to retrieve the parser if `func` uses a parser instead of returning it. | | title | (optional) when provided, overwrites the ` - CLI interface` title added by default and when empty, will not be included | | description | (optional) when provided, overwrites the description and when empty, will not be included | | epilog | (optional) when provided, overwrites the epilog and when empty, will not be included | | usage_width | (optional) how large should usage examples be - defaults to 100 character | | usage_first | (optional) show usage before description | | group_title_prefix | (optional) groups subsections title prefixes, accepts the string `{prog}` as a replacement for the program name - defaults to `{prog}` | | group_sub_title_prefix | (optional) subcommands groups subsections title prefixes, accepts replacement of `{prog}` and `{subcommand}` for program and subcommand name - defaults to `{prog} {subcommand}` | | no_default_values | (optional) suppresses generation of `default` entries | | force_refs_lower | (optional) Sphinx `:ref:` only supports lower-case references. With this, any capital letter in generated reference anchors are lowered and given an `_` prefix (i.e. `A` becomes `_a`) | For example: ```rst .. sphinx_argparse_cli:: :module: a_project.cli :func: build_parser :prog: my-cli-program ``` If you have code that creates and uses a parser but does not return it, you can specify the `:hook:` flag: ```rst .. sphinx_argparse_cli:: :module: a_project.cli :func: main :hook: :prog: my-cli-program ``` ### Refer to generated content The tool will register reference links to all anchors. This means that you can use the sphinx `ref` role to refer to both the (sub)command title/groups and every flag/argument. The tool offers a configuration flag `sphinx_argparse_cli_prefix_document` (change by setting this variable in `conf.py` - by default `False`). This option influences the reference ids generated. If it's false the reference will be the anchor id (the text appearing after the `'#` in the URI once you click on it). If it's true the anchor id will be prefixed by the document name (this is useful to avoid reference label clash when the same anchors are generated in multiple documents). For example in case of a `tox` command, and `sphinx_argparse_cli_prefix_document=False` (default): - to refer to the optional arguments group use ``:ref:`tox-optional-arguments` ``, - to refer to the run subcommand use ``:ref:`tox-run` ``, - to refer to flag `--magic` of the `run` sub-command use ``:ref:`tox-run---magic` ``. For example in case of a `tox` command, and `sphinx_argparse_cli_prefix_document=True`, and the current document name being `cli`: - to refer to the optional arguments group use ``:ref:`cli:tox-optional-arguments` ``, - to refer to the run subcommand use ``:ref:`cli:tox-run` ``, - to refer to flag `--magic` of the `run` sub-command use ``:ref:`cli:tox-run---magic` ``. Due to Sphinx's `:ref:` only supporting lower-case values, if you need to distinguish mixed case program names or arguments, set the `:force_refs_lower:` argument. With this flag, captial-letters in references will be converted to their lower-case counterpart and prefixed with an `_`. For example: - A `prog` name `SampleProgram` will be referenced as ``:ref:`_sample_program...` ``. - To distinguish between mixed case flags `-a` and `-A` use ``:ref:`_sample_program--a` `` and ``:ref:`_sample_program--_a` `` respectively Note that if you are _not_ concernced about using internal Sphinx `:ref:` cross-references, you may choose to leave this off to maintain mixed-case anchors in your output HTML; but be aware that later enabling it will change your anchors in the output HTML.