diff --git a/roots/test-subparsers/conf.py b/roots/test-subparsers/conf.py new file mode 100644 index 0000000..9f2a54a --- /dev/null +++ b/roots/test-subparsers/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-subparsers/index.rst b/roots/test-subparsers/index.rst new file mode 100644 index 0000000..708ad9c --- /dev/null +++ b/roots/test-subparsers/index.rst @@ -0,0 +1,3 @@ +.. sphinx_argparse_cli:: + :module: parser + :func: make diff --git a/roots/test-subparsers/parser.py b/roots/test-subparsers/parser.py new file mode 100644 index 0000000..5e0c26a --- /dev/null +++ b/roots/test-subparsers/parser.py @@ -0,0 +1,24 @@ +from __future__ import annotations + +from argparse import ArgumentParser + + +def make() -> ArgumentParser: + parser = ArgumentParser(prog="test") + + sub_parsers = parser.add_subparsers() + sub_parser = sub_parsers.add_parser("subparser") + sub_parser.add_argument("--foo") + + sub_parser_no_child = sub_parsers.add_parser("no_child") + sub_parser_no_child.add_argument("argument_one", help="no_child argument") + + sub_sub_parsers = sub_parser.add_subparsers() + sub_sub_parser = sub_sub_parsers.add_parser("child_two") + + sub_sub_sub_parsers = sub_sub_parser.add_subparsers() + sub_sub_sub_parser = sub_sub_sub_parsers.add_parser("child_three") + sub_sub_sub_parser.add_argument("argument", help="sub sub sub child pos argument") + sub_sub_sub_parser.add_argument("--flag", help="sub sub sub child argument") + + return parser diff --git a/src/sphinx_argparse_cli/_logic.py b/src/sphinx_argparse_cli/_logic.py index d9d4ffb..2c35278 100644 --- a/src/sphinx_argparse_cli/_logic.py +++ b/src/sphinx_argparse_cli/_logic.py @@ -132,18 +132,16 @@ def parser(self) -> ArgumentParser: 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 + 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] = {} - 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: @@ -155,6 +153,21 @@ def load_sub_parsers(self) -> Iterator[tuple[list[str], str, ArgumentParser]]: 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] + sub_sub_parser = 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 @@ -202,7 +215,6 @@ def _pre_format(self, block: None | str) -> None | paragraph | literal_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) @@ -237,7 +249,7 @@ def _build_opt_grp_title(self, group: _ArgumentGroup, prefix: str, sub_title_pre 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])} " + title_text += f"{' '.join(elements)} " else: title_text += f"{prefix} " title_text += group.title or "" @@ -347,6 +359,9 @@ def _mk_sub_command(self, aliases: list[str], help_msg: str, parser: ArgumentPar 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) return group_section diff --git a/tests/test_logic.py b/tests/test_logic.py index 2c89b50..a205441 100644 --- a/tests/test_logic.py +++ b/tests/test_logic.py @@ -331,3 +331,18 @@ def test_nested_content(build_outcome: str) -> None: 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 '