diff --git a/README.md b/README.md index ee41975..4ce961b 100644 --- a/README.md +++ b/README.md @@ -39,6 +39,7 @@ Within the reStructuredText files use the `sphinx_argparse_cli` directive that t | 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 | diff --git a/src/sphinx_argparse_cli/_logic.py b/src/sphinx_argparse_cli/_logic.py index 4136fd3..92fe33b 100644 --- a/src/sphinx_argparse_cli/_logic.py +++ b/src/sphinx_argparse_cli/_logic.py @@ -67,6 +67,7 @@ class SphinxArgparseCli(SphinxDirective): "description": unchanged, "epilog": unchanged, "usage_width": positive_int, + "usage_first": flag, "group_title_prefix": unchanged, "group_sub_title_prefix": unchanged, "no_default_values": unchanged, @@ -89,6 +90,7 @@ def __init__( # noqa: PLR0913 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 @property def parser(self) -> ArgumentParser: @@ -114,6 +116,8 @@ def parser(self) -> ArgumentParser: 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]]: @@ -148,18 +152,16 @@ def run(self) -> list[Node]: else: home_section = section("", title("", Text(title_text)), ids=[make_id(title_text)], names=[title_text]) - raw_format = self.parser.formatter_class == RawDescriptionHelpFormatter + 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) - description = self.options.get("description", self.parser.description) - if description: - if raw_format and "\n" in description: - lit = literal_block("", Text(description)) - lit["language"] = "none" - home_section += lit - else: - home_section += paragraph("", Text(description)) # construct groups excluding sub-parsers - home_section += self._mk_usage(self.parser) for group in self.parser._action_groups: # noqa: SLF001 if not group._group_actions or group is self.parser._subparsers: # noqa: SLF001 continue @@ -168,17 +170,20 @@ def run(self) -> list[Node]: for aliases, help_msg, parser in self.load_sub_parsers(): home_section += self._mk_sub_command(aliases, help_msg, parser) - epilog = self.options.get("epilog", self.parser.epilog) - if epilog: - if raw_format and "\n" in epilog: - lit = literal_block("", Text(epilog)) - lit["language"] = "none" - home_section += lit - else: - home_section += paragraph("", Text(epilog)) + if epilog := self._pre_format(self.options.get("epilog", self.parser.epilog)): + home_section += epilog 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"] @@ -313,11 +318,17 @@ def _mk_sub_command(self, aliases: list[str], help_msg: str, parser: ArgumentPar 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 - group_section += self._mk_usage(parser) + + 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 diff --git a/tests/test_logic.py b/tests/test_logic.py index 17de0de..4e0ea35 100644 --- a/tests/test_logic.py +++ b/tests/test_logic.py @@ -139,6 +139,13 @@ 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