From 8773eebf067464c099d6da31231935fee66955e2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bern=C3=A1t=20G=C3=A1bor?= Date: Sun, 1 Mar 2026 20:00:48 -0800 Subject: [PATCH] =?UTF-8?q?=F0=9F=90=9B=20fix(titles):=20handle=20multi-wo?= =?UTF-8?q?rd=20prog=20names=20in=20group=20titles?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Programs with spaces in their prog (e.g. `python -m build`) had their group titles corrupted because the title-building code split on spaces to separate the prog name from subcommands. Now uses the actual root prog length to find the subcommand portion. Fixes #68 --- roots/test-multiword-prog/conf.py | 8 ++++++++ roots/test-multiword-prog/index.rst | 3 +++ roots/test-multiword-prog/parser.py | 9 +++++++++ src/sphinx_argparse_cli/_logic.py | 24 ++++++++++++++---------- tests/test_logic.py | 6 ++++++ 5 files changed, 40 insertions(+), 10 deletions(-) create mode 100644 roots/test-multiword-prog/conf.py create mode 100644 roots/test-multiword-prog/index.rst create mode 100644 roots/test-multiword-prog/parser.py diff --git a/roots/test-multiword-prog/conf.py b/roots/test-multiword-prog/conf.py new file mode 100644 index 0000000..9f2a54a --- /dev/null +++ b/roots/test-multiword-prog/conf.py @@ -0,0 +1,8 @@ +from __future__ import annotations + +import sys +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).parent)) +extensions = ["sphinx_argparse_cli"] +nitpicky = True diff --git a/roots/test-multiword-prog/index.rst b/roots/test-multiword-prog/index.rst new file mode 100644 index 0000000..708ad9c --- /dev/null +++ b/roots/test-multiword-prog/index.rst @@ -0,0 +1,3 @@ +.. sphinx_argparse_cli:: + :module: parser + :func: make diff --git a/roots/test-multiword-prog/parser.py b/roots/test-multiword-prog/parser.py new file mode 100644 index 0000000..a274fea --- /dev/null +++ b/roots/test-multiword-prog/parser.py @@ -0,0 +1,9 @@ +from __future__ import annotations + +from argparse import ArgumentParser + + +def make() -> ArgumentParser: + parser = ArgumentParser(prog="python -m build") + parser.add_argument("srcdir", help="source directory") + return parser diff --git a/src/sphinx_argparse_cli/_logic.py b/src/sphinx_argparse_cli/_logic.py index 9e37ca1..1429395 100644 --- a/src/sphinx_argparse_cli/_logic.py +++ b/src/sphinx_argparse_cli/_logic.py @@ -204,7 +204,9 @@ def run(self) -> list[Node]: 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]) + 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) @@ -226,10 +228,10 @@ def _pre_format(self, block: None | str) -> None | paragraph | literal_block: return lit return paragraph("", Text(block)) - def _mk_option_group(self, group: _ArgumentGroup, prefix: str) -> section: + 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, sub_title_prefix, 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 @@ -247,10 +249,11 @@ def _mk_option_group(self, group: _ArgumentGroup, prefix: str) -> section: 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: - elements = prefix.split(" ") - sub_cmd = " ".join(elements[1:]) if " " in prefix else None - title_text = self._resolve_prefix(elements[0], sub_cmd, prefix, title_prefix, sub_title_prefix) + 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 @@ -371,12 +374,13 @@ def _mk_sub_command(self, aliases: list[str], help_msg: str, parser: ArgumentPar 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) + 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: - elements = parser.prog.split(" ") - return self._resolve_prefix(elements[0], elements[1], parser.prog, title_prefix, sub_title_prefix).rstrip() + 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, diff --git a/tests/test_logic.py b/tests/test_logic.py index 2e6802c..b0f46c9 100644 --- a/tests/test_logic.py +++ b/tests/test_logic.py @@ -397,3 +397,9 @@ 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