Skip to content

Commit be4e5d9

Browse files
committed
Parse help text as reStructuredText and escape {`, skip default if help text has default
Signed-off-by: Bernát Gábor <[email protected]>
1 parent 082d578 commit be4e5d9

File tree

6 files changed

+55
-12
lines changed

6 files changed

+55
-12
lines changed

CHANGELOG.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,11 @@ All notable changes to this project will be documented in this file.
44

55
## 1.3.0 (2021-02-13)
66

7-
- Add support for changing the usage width
7+
- Add support for changing the usage width via the `usage_width` option on the directive
8+
- Mark document as always needs update (as the underlying content is coming from outside the sphinx documents)
9+
- Help messages is now interpreted as reStructuredText
10+
- Matching curly braces, single and double quotes in help text will be marked as string literals
11+
- Help messages containing the ``default(s)`` word do not show the default value (as often this indicates the default is already documented in the help text)
812

913
## 1.2.0 (2021-02-05)
1014

src/sphinx_argparse_cli/_logic.py

Lines changed: 23 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
_SubParsersAction,
1313
)
1414
from collections import defaultdict, namedtuple
15-
from typing import Iterator, Set, cast
15+
from typing import Iterator, cast
1616

1717
from docutils.nodes import (
1818
Element,
@@ -27,7 +27,7 @@
2727
section,
2828
title,
2929
)
30-
from docutils.parsers.rst.directives import unchanged, unchanged_required
30+
from docutils.parsers.rst.directives import positive_int, unchanged, unchanged_required
3131
from docutils.parsers.rst.states import RSTState, RSTStateMachine
3232
from docutils.statemachine import StringList
3333
from sphinx.util.docutils import SphinxDirective
@@ -47,7 +47,7 @@ class SphinxArgparseCli(SphinxDirective):
4747
"func": unchanged_required,
4848
"prog": unchanged,
4949
"title": unchanged,
50-
"usage_width": unchanged,
50+
"usage_width": positive_int,
5151
}
5252

5353
def __init__(
@@ -100,6 +100,7 @@ def load_sub_parsers(self) -> Iterator[tuple[list[str], str, ArgumentParser]]:
100100

101101
def run(self) -> list[Node]:
102102
# construct headers
103+
self.env.note_reread() # this document needs to be always updated
103104
title_text = self.options.get("title", f"{self.parser.prog} - CLI interface").strip()
104105
if title_text.strip() == "":
105106
home_section: Element = paragraph()
@@ -162,9 +163,13 @@ def _mk_option_line(self, action: Action, prefix: str) -> list_item: # noqa
162163
line += ref
163164
point = list_item("", line, ids=[])
164165
if action.help:
166+
help_text = load_help_text(action.help)
167+
temp = paragraph()
168+
self.state.nested_parse(StringList(help_text.split("\n")), 0, temp)
165169
line += Text(" - ")
166-
line += Text(action.help)
167-
if action.default != SUPPRESS:
170+
for content in cast(paragraph, temp.children[0]).children:
171+
line += content
172+
if action.default != SUPPRESS and not re.match(r".*[ (]default[s]? .*", (action.help or "")):
168173
line += Text(" (default: ")
169174
line += literal(text=str(action.default).replace(os.getcwd(), "{cwd}"))
170175
line += Text(")")
@@ -186,10 +191,22 @@ def _mk_sub_command(self, aliases: list[str], help_msg: str, parser: ArgumentPar
186191
return group_section
187192

188193
def _mk_usage(self, parser: ArgumentParser) -> literal_block:
189-
parser.formatter_class = lambda prog: HelpFormatter(prog, width=int(self.options.get("usage_width", 100)))
194+
parser.formatter_class = lambda prog: HelpFormatter(prog, width=self.options.get("usage_width", 100))
190195
texts = parser.format_usage()[len("usage: ") :].splitlines()
191196
texts = [line if at == 0 else f"{' ' * (len(parser.prog) + 1)}{line.lstrip()}" for at, line in enumerate(texts)]
192197
return literal_block("", Text("\n".join(texts)))
193198

194199

200+
SINGLE_QUOTE = re.compile(r"[']+(.+?)[']+")
201+
DOUBLE_QUOTE = re.compile(r'["]+(.+?)["]+')
202+
CURLY_BRACES = re.compile(r"[{](.+?)[}]")
203+
204+
205+
def load_help_text(help_text: str) -> str:
206+
single_quote = SINGLE_QUOTE.sub("``'\\1'``", help_text)
207+
double_quote = DOUBLE_QUOTE.sub('``"\\1"``', single_quote)
208+
literal_curly_braces = CURLY_BRACES.sub("``{\\1}``", double_quote)
209+
return literal_curly_braces
210+
211+
195212
__all__ = ("SphinxArgparseCli",)

tests/conftest.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,3 +17,7 @@ def pytest_report_header(config: Config) -> str: # noqa: U100
1717
@pytest.fixture(scope="session", name="rootdir")
1818
def root_dir() -> path:
1919
return path(__file__).parent.parent.abspath() / "roots"
20+
21+
22+
def pytest_configure(config: Config) -> None:
23+
config.addinivalue_line("markers", "prepare")

tests/test_logic.py

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,11 @@ def build_outcome(app: SphinxTestApp, request: SubRequest) -> str:
1414
prepare_marker = request.node.get_closest_marker("prepare")
1515
if prepare_marker:
1616
directive_args: list[str] | None = prepare_marker.kwargs.get("directive_args")
17-
if directive_args:
17+
if directive_args: # pragma: no branch
1818
index = Path(cast(str, app.confdir)) / "index.rst"
19-
if not any(i for i in directive_args if i.startswith(":module:")):
19+
if not any(i for i in directive_args if i.startswith(":module:")): # pragma: no branch
2020
directive_args.append(":module: parser")
21-
if not any(i for i in directive_args if i.startswith(":func:")):
21+
if not any(i for i in directive_args if i.startswith(":func:")): # pragma: no branch
2222
directive_args.append(":func: make")
2323
args = [f" {i}" for i in directive_args]
2424
index.write_text(os.linesep.join([".. sphinx_argparse_cli::"] + args))
@@ -73,3 +73,22 @@ def test_usage_width_default(build_outcome: str) -> None:
7373
@pytest.mark.prepare(directive_args=[":usage_width: 50"])
7474
def test_usage_width_custom(build_outcome: str) -> None:
7575
assert "complex second [-h] [--flag] [--root]\n" in build_outcome
76+
77+
78+
@pytest.mark.parametrize(
79+
("example", "output"),
80+
[
81+
("", ""),
82+
("{", "{"),
83+
('"', '"'),
84+
("'", "'"),
85+
("{a}", "``{a}``"),
86+
('"a"', '``"a"``'),
87+
("'a'", "``'a'``"),
88+
],
89+
)
90+
def test_help_loader(example: str, output: str) -> None:
91+
from sphinx_argparse_cli._logic import load_help_text # noqa
92+
93+
result = load_help_text(example)
94+
assert result == output

tox.ini

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -78,5 +78,3 @@ commands =
7878

7979
[pytest]
8080
junit_family = xunit2
81-
markers =
82-
prepare

whitelist.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,3 +23,4 @@ statemachine
2323
subparsers
2424
testroot
2525
util
26+
addinivalue

0 commit comments

Comments
 (0)