Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ All notable changes to this project will be documented in this file.
render sub-command epilogs.
- Render the argument spec after an option with argparse's formatter, so `nargs`, `choices` and tuple metavars show as
in the usage line; user-supplied metavars keep their case instead of being upper-cased.
- Expand argparse format specifiers such as `%(prog)s`, `%(default)s` and `%(choices)s` in help, descriptions and
epilogs, and skip the generated `(default: ...)` when the help already mentions a default in any case.

## 1.13.1

Expand Down
8 changes: 8 additions & 0 deletions roots/test-help-format-specifiers/conf.py
Original file line number Diff line number Diff line change
@@ -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
3 changes: 3 additions & 0 deletions roots/test-help-format-specifiers/index.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
.. sphinx_argparse_cli::
:module: parser
:func: make
23 changes: 23 additions & 0 deletions roots/test-help-format-specifiers/parser.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
from __future__ import annotations

from argparse import ArgumentDefaultsHelpFormatter, ArgumentParser


def make() -> ArgumentParser:
parser = ArgumentParser(
prog="tool",
formatter_class=ArgumentDefaultsHelpFormatter,
description="%(prog)s does things",
epilog="run %(prog)s --help",
add_help=False,
)
parser.add_argument("--n", type=int, default=3, help="count (default: %(default)s)")
parser.add_argument("--mode", choices=["a", "b"], default="a", help="pick one of %(choices)s")
parser.add_argument("--pct", default=5, help="100%% of %(prog)s")
parser.add_argument("--capital", default=3, help="Default: 3")
parser.add_argument("--kind", type=float, help="parsed with %(type)s")
group = parser.add_argument_group("tuning", description="tune %(prog)s")
group.add_argument("--level", default=1, help="level")
run = parser.add_subparsers().add_parser("run", description="%(prog)s runs", add_help=False)
run.add_argument("--target", help="target for %(prog)s")
return parser
27 changes: 24 additions & 3 deletions src/sphinx_argparse_cli/_logic.py
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,7 @@ def run(self) -> list[Node]:
def _pre_format(self, block: str | None, parser: ArgumentParser) -> paragraph | literal_block | None:
if block is None or not block.strip():
return None
block = _expand_prog(block, parser.prog)
formatter = parser.formatter_class
if "\n" in block and isinstance(formatter, type) and issubclass(formatter, RawDescriptionHelpFormatter):
lit = literal_block("", Text(block), classes=["sphinx-argparse-cli-wrap"])
Expand Down Expand Up @@ -248,9 +249,9 @@ def _mk_option_line(self, parser: ArgumentParser, action: Action, prefix: str) -
)

extra: Sequence[Node] = ()
if action.help:
if help_text := _expand_help(action, parser.prog):
temp = paragraph()
self.state.nested_parse(StringList(load_help_text(action.help).split("\n")), 0, temp)
self.state.nested_parse(StringList(load_help_text(help_text).split("\n")), 0, temp)
# only a leading paragraph can share the option's line; anything else becomes a block under it
if temp.children and isinstance(temp.children[0], paragraph):
line += Text(" - ")
Expand All @@ -262,7 +263,7 @@ def _mk_option_line(self, parser: ArgumentParser, action: Action, prefix: str) -
"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 _DEFAULT_IN_HELP.search(help_text)
and not isinstance(action, _StoreTrueAction | _StoreFalseAction)
):
line += Text(" (default: ")
Expand Down Expand Up @@ -428,6 +429,26 @@ def _visible_actions(group: _ArgumentGroup) -> list[Action]:
]


def _expand_prog(text: str, prog: str) -> str:
# what argparse.HelpFormatter._format_text does for descriptions and epilogs
return text % {"prog": prog} if "%(prog)" in text else text


def _expand_help(action: Action, prog: str) -> str:
# mirrors argparse.HelpFormatter._expand_help so the help reads as it does under --help
help_text = action.help or ""
if "%" not in help_text:
return help_text
params = {
key: getattr(value, "__name__", value) for key, value in vars(action).items() if value is not SUPPRESS
} | {"prog": prog}
if action.choices is not None:
params["choices"] = ", ".join(map(str, action.choices))
return help_text % params


_DEFAULT_IN_HELP: Final[re.Pattern[str]] = re.compile(r"\bdefaults?\b", re.IGNORECASE)

_HELP_SUBSTITUTIONS: Final[list[tuple[re.Pattern[str], str]]] = [
# a quote glued to a word character is an apostrophe (don't, it's), not the edge of a quoted span
(re.compile(r"(?<!\w)'([^']+?)'(?!\w)"), "``'\\1'``"),
Expand Down
53 changes: 53 additions & 0 deletions tests/test_logic.py
Original file line number Diff line number Diff line change
Expand Up @@ -575,6 +575,59 @@ def test_ref_cases(build_outcome: str, warning: StringIO) -> None:
assert not warning.getvalue()


@pytest.mark.sphinx(buildername="text", testroot="help-format-specifiers")
def test_help_format_specifiers(build_outcome: str) -> None:
assert (
build_outcome
== """tool - CLI interface
********************

tool does things

tool [--n N] [--mode {a,b}] [--pct PCT] [--capital CAPITAL] [--kind KIND] [--level LEVEL]
{run} ...


tool options
============

* **"--n"** "N" - count (default: 3)

* **"--mode"** "{a,b}" - pick one of a, b (default: "a")

* **"--pct"** "PCT" - 100% of tool (default: "5")

* **"--capital"** "CAPITAL" - Default: 3

* **"--kind"** "KIND" - parsed with float


tool tuning
===========

tune tool

* **"--level"** "LEVEL" - level (default: "1")


tool run
========

tool run runs

tool run [--target TARGET]


tool run options
----------------

* **"--target"** "TARGET" - target for tool run

run tool --help
"""
)


@pytest.mark.sphinx(buildername="text", testroot="default-handling")
def test_with_default(build_outcome: str) -> None:
assert (
Expand Down