sphinx-argparse-cli 1.22.0__tar.gz → 1.23.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {sphinx_argparse_cli-1.22.0 → sphinx_argparse_cli-1.23.0}/PKG-INFO +27 -2
- {sphinx_argparse_cli-1.22.0 → sphinx_argparse_cli-1.23.0}/README.md +25 -0
- {sphinx_argparse_cli-1.22.0 → sphinx_argparse_cli-1.23.0}/src/sphinx_argparse_cli/_logic.py +173 -147
- {sphinx_argparse_cli-1.22.0 → sphinx_argparse_cli-1.23.0}/src/sphinx_argparse_cli/version.py +2 -2
- {sphinx_argparse_cli-1.22.0 → sphinx_argparse_cli-1.23.0}/tests/complex.txt +1 -1
- {sphinx_argparse_cli-1.22.0 → sphinx_argparse_cli-1.23.0}/tests/complex_pre_310.txt +1 -1
- {sphinx_argparse_cli-1.22.0 → sphinx_argparse_cli-1.23.0}/tests/test_logic.py +381 -19
- {sphinx_argparse_cli-1.22.0 → sphinx_argparse_cli-1.23.0}/.gitignore +0 -0
- {sphinx_argparse_cli-1.22.0 → sphinx_argparse_cli-1.23.0}/LICENSE.txt +0 -0
- {sphinx_argparse_cli-1.22.0 → sphinx_argparse_cli-1.23.0}/pyproject.toml +1 -1
- {sphinx_argparse_cli-1.22.0 → sphinx_argparse_cli-1.23.0}/src/sphinx_argparse_cli/__init__.py +0 -0
- {sphinx_argparse_cli-1.22.0 → sphinx_argparse_cli-1.23.0}/src/sphinx_argparse_cli/py.typed +0 -0
- {sphinx_argparse_cli-1.22.0 → sphinx_argparse_cli-1.23.0}/tests/conftest.py +0 -0
- {sphinx_argparse_cli-1.22.0 → sphinx_argparse_cli-1.23.0}/tests/test_sphinx_argparse_cli.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
2
|
Name: sphinx-argparse-cli
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.23.0
|
|
4
4
|
Summary: render CLI arguments (sub-commands friendly) defined by argparse module
|
|
5
5
|
Project-URL: Documentation, https://github.com/tox-dev/sphinx-argparse-cli#sphinx-argparse-cli
|
|
6
6
|
Project-URL: Homepage, https://github.com/tox-dev/sphinx-argparse-cli
|
|
@@ -164,6 +164,23 @@ With `sphinx_argparse_cli_prefix_document = True` (anchors prefixed by document
|
|
|
164
164
|
|
|
165
165
|
The anchor text is visible after the `#` in the URL when you click a heading.
|
|
166
166
|
|
|
167
|
+
Flags and positional arguments are also registered as Sphinx program options, so the `:option:` role works with the
|
|
168
|
+
program (and sub-command) name followed by the argument, or scoped through a `.. program::` directive:
|
|
169
|
+
|
|
170
|
+
```rst
|
|
171
|
+
:option:`tox --magic`
|
|
172
|
+
:option:`tox run --magic`
|
|
173
|
+
|
|
174
|
+
.. program:: tox run
|
|
175
|
+
|
|
176
|
+
:option:`--magic`
|
|
177
|
+
:option:`--magic=value`
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
`:option:` targets keep their original case, so they do not need `:force_refs_lower:`. They ignore
|
|
181
|
+
`sphinx_argparse_cli_prefix_document`; when two documents render the same program, the role links to the first one
|
|
182
|
+
Sphinx reads.
|
|
183
|
+
|
|
167
184
|
### Handle mixed-case references
|
|
168
185
|
|
|
169
186
|
Sphinx `:ref:` only supports lower-case targets. When your program name or flags contain capital letters, set
|
|
@@ -187,6 +204,14 @@ For a program named `SampleProgram`:
|
|
|
187
204
|
If you do not need Sphinx `:ref:` cross-references you can leave this off to keep mixed-case anchors in the HTML output,
|
|
188
205
|
but enabling it later will change existing anchor URLs.
|
|
189
206
|
|
|
207
|
+
### Write help text as reStructuredText
|
|
208
|
+
|
|
209
|
+
The directive parses every argument's `help` string as reStructuredText, so inline markup such as a `:ref:` role or
|
|
210
|
+
`**bold**` works. The same applies to characters that carry meaning in reStructuredText: a lone `*` (`match *.py`), a
|
|
211
|
+
trailing `_` (`name_`), backticks, or a trailing `::` produce docutils warnings and fail a build run with `-W`. Escape
|
|
212
|
+
them with a backslash (`match \*.py`) or wrap them in inline literals. Text between matching single quotes, double
|
|
213
|
+
quotes, or braces becomes an inline literal for you, so `pick 'fast' or "slow"` shows `'fast'` and `"slow"` as code.
|
|
214
|
+
|
|
190
215
|
### Add extra content after generated docs
|
|
191
216
|
|
|
192
217
|
Any content nested inside the directive is appended after the generated CLI documentation:
|
|
@@ -132,6 +132,23 @@ With `sphinx_argparse_cli_prefix_document = True` (anchors prefixed by document
|
|
|
132
132
|
|
|
133
133
|
The anchor text is visible after the `#` in the URL when you click a heading.
|
|
134
134
|
|
|
135
|
+
Flags and positional arguments are also registered as Sphinx program options, so the `:option:` role works with the
|
|
136
|
+
program (and sub-command) name followed by the argument, or scoped through a `.. program::` directive:
|
|
137
|
+
|
|
138
|
+
```rst
|
|
139
|
+
:option:`tox --magic`
|
|
140
|
+
:option:`tox run --magic`
|
|
141
|
+
|
|
142
|
+
.. program:: tox run
|
|
143
|
+
|
|
144
|
+
:option:`--magic`
|
|
145
|
+
:option:`--magic=value`
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
`:option:` targets keep their original case, so they do not need `:force_refs_lower:`. They ignore
|
|
149
|
+
`sphinx_argparse_cli_prefix_document`; when two documents render the same program, the role links to the first one
|
|
150
|
+
Sphinx reads.
|
|
151
|
+
|
|
135
152
|
### Handle mixed-case references
|
|
136
153
|
|
|
137
154
|
Sphinx `:ref:` only supports lower-case targets. When your program name or flags contain capital letters, set
|
|
@@ -155,6 +172,14 @@ For a program named `SampleProgram`:
|
|
|
155
172
|
If you do not need Sphinx `:ref:` cross-references you can leave this off to keep mixed-case anchors in the HTML output,
|
|
156
173
|
but enabling it later will change existing anchor URLs.
|
|
157
174
|
|
|
175
|
+
### Write help text as reStructuredText
|
|
176
|
+
|
|
177
|
+
The directive parses every argument's `help` string as reStructuredText, so inline markup such as a `:ref:` role or
|
|
178
|
+
`**bold**` works. The same applies to characters that carry meaning in reStructuredText: a lone `*` (`match *.py`), a
|
|
179
|
+
trailing `_` (`name_`), backticks, or a trailing `::` produce docutils warnings and fail a build run with `-W`. Escape
|
|
180
|
+
them with a backslash (`match \*.py`) or wrap them in inline literals. Text between matching single quotes, double
|
|
181
|
+
quotes, or braces becomes an inline literal for you, so `pick 'fast' or "slow"` shows `'fast'` and `"slow"` as code.
|
|
182
|
+
|
|
158
183
|
### Add extra content after generated docs
|
|
159
184
|
|
|
160
185
|
Any content nested inside the directive is appended after the generated CLI documentation:
|
|
@@ -16,6 +16,7 @@ from argparse import (
|
|
|
16
16
|
)
|
|
17
17
|
from collections import defaultdict
|
|
18
18
|
from contextlib import contextmanager
|
|
19
|
+
from functools import cached_property
|
|
19
20
|
from pathlib import Path
|
|
20
21
|
from typing import TYPE_CHECKING, Any, ClassVar, Final, cast
|
|
21
22
|
from unittest.mock import patch
|
|
@@ -42,13 +43,13 @@ from docutils.nodes import (
|
|
|
42
43
|
from docutils.parsers.rst.directives import flag, positive_int, unchanged, unchanged_required
|
|
43
44
|
from docutils.statemachine import StringList
|
|
44
45
|
from sphinx.locale import __
|
|
46
|
+
from sphinx.util import ws_re
|
|
45
47
|
from sphinx.util.docutils import SphinxDirective
|
|
46
48
|
from sphinx.util.logging import getLogger
|
|
47
49
|
|
|
48
50
|
if TYPE_CHECKING:
|
|
49
|
-
from collections.abc import Iterator
|
|
51
|
+
from collections.abc import Callable, Iterator, Sequence
|
|
50
52
|
|
|
51
|
-
from docutils.parsers.rst.states import RSTState, RSTStateMachine
|
|
52
53
|
from sphinx.domains.std import StandardDomain
|
|
53
54
|
from sphinx.util.logging import SphinxLoggerAdapter
|
|
54
55
|
|
|
@@ -77,70 +78,56 @@ class SphinxArgparseCli(SphinxDirective):
|
|
|
77
78
|
"force_refs_lower": flag,
|
|
78
79
|
}
|
|
79
80
|
|
|
80
|
-
|
|
81
|
-
self,
|
|
82
|
-
name: str,
|
|
83
|
-
arguments: list[str],
|
|
84
|
-
options: dict[str, str | None],
|
|
85
|
-
content: StringList,
|
|
86
|
-
lineno: int,
|
|
87
|
-
content_offset: int,
|
|
88
|
-
block_text: str,
|
|
89
|
-
state: RSTState,
|
|
90
|
-
state_machine: RSTStateMachine,
|
|
91
|
-
) -> None:
|
|
92
|
-
options.setdefault("group_title_prefix", None)
|
|
93
|
-
options.setdefault("group_sub_title_prefix", None)
|
|
94
|
-
super().__init__(name, arguments, options, content, lineno, content_offset, block_text, state, state_machine)
|
|
95
|
-
self._parser: ArgumentParser | None = None
|
|
96
|
-
self._std_domain: StandardDomain = cast("StandardDomain", self.env.get_domain("std"))
|
|
97
|
-
self._raw_format: bool = False
|
|
98
|
-
self._make_id = make_id_lower if "force_refs_lower" in self.options else make_id
|
|
99
|
-
|
|
100
|
-
@property
|
|
81
|
+
@cached_property
|
|
101
82
|
def parser(self) -> ArgumentParser:
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
83
|
+
module_name, attr_name = self.options["module"], self.options["func"]
|
|
84
|
+
try:
|
|
85
|
+
module = __import__(module_name, fromlist=[attr_name])
|
|
86
|
+
except ImportError:
|
|
87
|
+
msg = f"Failed to import module {module_name!r}"
|
|
88
|
+
raise self.error(msg) # noqa: B904
|
|
89
|
+
try:
|
|
90
|
+
parser_creator = getattr(module, attr_name)
|
|
91
|
+
except AttributeError:
|
|
92
|
+
del sys.modules[module_name]
|
|
93
|
+
msg = f"Module {module_name!r} has no attribute {attr_name!r}"
|
|
94
|
+
raise self.error(msg) # noqa: B904
|
|
95
|
+
parser: ArgumentParser | None = None
|
|
96
|
+
if "hook" in self.options:
|
|
97
|
+
# parse_intermixed_args bypasses parse_known_args since Python 3.12, so both entry points need the hook
|
|
98
|
+
with (
|
|
99
|
+
patch.object(ArgumentParser, "parse_known_args", _parse_known_args_hook),
|
|
100
|
+
patch.object(ArgumentParser, "parse_known_intermixed_args", _parse_known_args_hook),
|
|
101
|
+
):
|
|
118
102
|
try:
|
|
119
103
|
parser_creator()
|
|
120
104
|
except HookError as hooked:
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
else:
|
|
125
|
-
self._parser = parser_creator()
|
|
105
|
+
parser = hooked.parser
|
|
106
|
+
else:
|
|
107
|
+
parser = parser_creator()
|
|
126
108
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
109
|
+
del sys.modules[module_name]
|
|
110
|
+
if parser is None:
|
|
111
|
+
msg = "Failed to hook argparse to get ArgumentParser"
|
|
112
|
+
raise self.error(msg)
|
|
113
|
+
|
|
114
|
+
if "prog" in self.options:
|
|
115
|
+
old_prog, new_prog = parser.prog, self.options["prog"]
|
|
116
|
+
parser.prog = new_prog
|
|
117
|
+
_update_sub_parser_prog(parser, old_prog, new_prog)
|
|
118
|
+
return parser
|
|
131
119
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
_update_sub_parser_prog(self._parser, old_prog, new_prog)
|
|
120
|
+
@cached_property
|
|
121
|
+
def _std_domain(self) -> StandardDomain:
|
|
122
|
+
return cast("StandardDomain", self.env.get_domain("std"))
|
|
136
123
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
return self.
|
|
124
|
+
@cached_property
|
|
125
|
+
def _make_id(self) -> Callable[[str], str]:
|
|
126
|
+
return make_id_lower if "force_refs_lower" in self.options else make_id
|
|
140
127
|
|
|
141
128
|
def _load_sub_parsers(
|
|
142
|
-
self, sub_parser: _SubParsersAction[ArgumentParser]
|
|
143
|
-
) -> Iterator[tuple[list[str], str, ArgumentParser]]:
|
|
129
|
+
self, sub_parser: _SubParsersAction[ArgumentParser], parent_cmd: str = ""
|
|
130
|
+
) -> Iterator[tuple[list[str], str, ArgumentParser, str]]:
|
|
144
131
|
parser_to_args: dict[int, list[str]] = defaultdict(list)
|
|
145
132
|
for key, parser in sub_parser._name_parser_map.items(): # noqa: SLF001
|
|
146
133
|
parser_to_args[id(parser)].append(key)
|
|
@@ -154,19 +141,18 @@ class SphinxArgparseCli(SphinxDirective):
|
|
|
154
141
|
aliases = parser_to_args[id(parser)]
|
|
155
142
|
aliases.remove(name)
|
|
156
143
|
# help is stored in a pseudo action
|
|
157
|
-
help_msg = next((a.help for a in sub_parser._choices_actions if a.dest == name), None)
|
|
158
|
-
|
|
144
|
+
help_msg = next((a.help for a in sub_parser._choices_actions if a.dest == name), None) # noqa: SLF001
|
|
145
|
+
if help_msg == SUPPRESS:
|
|
146
|
+
continue
|
|
147
|
+
sub_cmd = f"{parent_cmd} {name}".strip()
|
|
148
|
+
yield aliases, help_msg or "", parser, sub_cmd
|
|
159
149
|
|
|
160
|
-
if parser
|
|
161
|
-
|
|
162
|
-
yield from self._load_sub_parsers(sub_sub_parser)
|
|
150
|
+
if (sub_sub_parser := _sub_parser_action(parser)) is not None:
|
|
151
|
+
yield from self._load_sub_parsers(sub_sub_parser, sub_cmd)
|
|
163
152
|
|
|
164
|
-
def _iter_sub_commands(self) -> Iterator[tuple[list[str], str, ArgumentParser]]:
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
return
|
|
168
|
-
sub_parser: _SubParsersAction[ArgumentParser] = top_sub_parser._group_actions[0] # type: ignore[assignment] # noqa: SLF001
|
|
169
|
-
yield from self._load_sub_parsers(sub_parser)
|
|
153
|
+
def _iter_sub_commands(self) -> Iterator[tuple[list[str], str, ArgumentParser, str]]:
|
|
154
|
+
if (sub_parser := _sub_parser_action(self.parser)) is not None:
|
|
155
|
+
yield from self._load_sub_parsers(sub_parser)
|
|
170
156
|
|
|
171
157
|
def run(self) -> list[Node]:
|
|
172
158
|
self.env.note_reread() # this document needs to always be rebuilt
|
|
@@ -179,22 +165,24 @@ class SphinxArgparseCli(SphinxDirective):
|
|
|
179
165
|
if "usage_first" in self.options:
|
|
180
166
|
home_section += self._mk_usage(self.parser)
|
|
181
167
|
|
|
182
|
-
if description := self._pre_format(self.options.get("description", self.parser.description)):
|
|
168
|
+
if description := self._pre_format(self.options.get("description", self.parser.description), self.parser):
|
|
183
169
|
home_section += description
|
|
184
170
|
|
|
185
171
|
if "usage_first" not in self.options:
|
|
186
172
|
home_section += self._mk_usage(self.parser)
|
|
187
173
|
|
|
188
174
|
for group in self.parser._action_groups: # noqa: SLF001
|
|
189
|
-
if
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
175
|
+
if actions := _visible_actions(group):
|
|
176
|
+
home_section += self._mk_option_group(
|
|
177
|
+
group,
|
|
178
|
+
actions,
|
|
179
|
+
self.parser,
|
|
180
|
+
prefix=self.parser.prog.split("/")[-1],
|
|
181
|
+
)
|
|
182
|
+
for aliases, help_msg, parser, sub_cmd in self._iter_sub_commands():
|
|
183
|
+
home_section += self._mk_sub_command(aliases, help_msg, parser, sub_cmd)
|
|
184
|
+
|
|
185
|
+
if epilog := self._pre_format(self.options.get("epilog", self.parser.epilog), self.parser):
|
|
198
186
|
home_section += epilog
|
|
199
187
|
|
|
200
188
|
if self.content:
|
|
@@ -202,10 +190,12 @@ class SphinxArgparseCli(SphinxDirective):
|
|
|
202
190
|
|
|
203
191
|
return [home_section]
|
|
204
192
|
|
|
205
|
-
def _pre_format(self, block:
|
|
206
|
-
if block is None:
|
|
193
|
+
def _pre_format(self, block: str | None, parser: ArgumentParser) -> paragraph | literal_block | None:
|
|
194
|
+
if block is None or not block.strip():
|
|
207
195
|
return None
|
|
208
|
-
|
|
196
|
+
block = _expand_prog(block, parser.prog)
|
|
197
|
+
formatter = parser.formatter_class
|
|
198
|
+
if "\n" in block and isinstance(formatter, type) and issubclass(formatter, RawDescriptionHelpFormatter):
|
|
209
199
|
lit = literal_block("", Text(block), classes=["sphinx-argparse-cli-wrap"])
|
|
210
200
|
lit["language"] = "none"
|
|
211
201
|
return lit
|
|
@@ -213,75 +203,75 @@ class SphinxArgparseCli(SphinxDirective):
|
|
|
213
203
|
_protect_option_dashes(para)
|
|
214
204
|
return para
|
|
215
205
|
|
|
216
|
-
def _mk_option_group(
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
206
|
+
def _mk_option_group(
|
|
207
|
+
self,
|
|
208
|
+
group: _ArgumentGroup,
|
|
209
|
+
actions: list[Action],
|
|
210
|
+
parser: ArgumentParser,
|
|
211
|
+
prefix: str,
|
|
212
|
+
sub_cmd: str | None = None,
|
|
213
|
+
) -> section:
|
|
214
|
+
prog = self.parser.prog.split("/")[-1]
|
|
215
|
+
sub_title_prefix: str = self.options.get("group_sub_title_prefix")
|
|
216
|
+
title_prefix = self.options.get("group_title_prefix")
|
|
217
|
+
# an untitled group borrows its description as heading so its anchor stays unique
|
|
218
|
+
group_title = group.title or group.description or "arguments"
|
|
219
|
+
title_text = self._resolve_prefix(prog, sub_cmd, prefix, title_prefix, sub_title_prefix) + group_title
|
|
220
|
+
title_ref: str = f"{prefix}{' ' if prefix else ''}{group_title}"
|
|
221
221
|
ref_id = self._make_id(title_ref)
|
|
222
222
|
# the text sadly needs to be prefixed, because otherwise the autosectionlabel will conflict
|
|
223
223
|
header = title("", Text(title_text))
|
|
224
224
|
group_section = section("", header, ids=[ref_id], names=[ref_id])
|
|
225
|
-
if description := self._pre_format(group.description):
|
|
225
|
+
if group.title and (description := self._pre_format(group.description, parser)):
|
|
226
226
|
group_section += description
|
|
227
227
|
self._register_ref(ref_id, title_text, group_section)
|
|
228
228
|
opt_group = bullet_list()
|
|
229
|
-
for action in
|
|
230
|
-
|
|
231
|
-
continue
|
|
232
|
-
point = self._mk_option_line(action, prefix)
|
|
233
|
-
opt_group += point
|
|
229
|
+
for action in actions:
|
|
230
|
+
opt_group += self._mk_option_line(parser, action, prefix)
|
|
234
231
|
group_section += opt_group
|
|
235
232
|
return group_section
|
|
236
233
|
|
|
237
|
-
def
|
|
238
|
-
self, group: _ArgumentGroup, prefix: str, prog: str, sub_title_prefix: str, title_prefix: str
|
|
239
|
-
) -> str:
|
|
240
|
-
sub_cmd = prefix[len(prog) :].strip() or None if prefix != prog else None
|
|
241
|
-
title_text = self._resolve_prefix(prog, sub_cmd, prefix, title_prefix, sub_title_prefix)
|
|
242
|
-
title_text += group.title or ""
|
|
243
|
-
return title_text
|
|
244
|
-
|
|
245
|
-
def _mk_option_line(self, action: Action, prefix: str) -> list_item:
|
|
234
|
+
def _mk_option_line(self, parser: ArgumentParser, action: Action, prefix: str) -> list_item:
|
|
246
235
|
line = paragraph()
|
|
247
|
-
as_key = action.dest
|
|
248
|
-
if action.metavar:
|
|
249
|
-
as_key = action.metavar if isinstance(action.metavar, str) else action.metavar[0]
|
|
250
236
|
if action.option_strings:
|
|
237
|
+
args_text = _format_args(parser, action) if action.nargs != 0 else None
|
|
251
238
|
for at, opt in enumerate(action.option_strings):
|
|
252
239
|
if at:
|
|
253
240
|
line += Text(", ")
|
|
254
241
|
self._mk_option_name(line, prefix, opt)
|
|
255
|
-
if
|
|
242
|
+
if args_text is not None:
|
|
256
243
|
line += Text(" ")
|
|
257
|
-
|
|
258
|
-
" ".join(meta.upper() for meta in action.metavar)
|
|
259
|
-
if isinstance(action.metavar, tuple)
|
|
260
|
-
else as_key.upper()
|
|
261
|
-
)
|
|
262
|
-
line += literal(text=metavar_text)
|
|
244
|
+
line += literal(text=args_text)
|
|
263
245
|
else:
|
|
264
|
-
|
|
246
|
+
metavar = action.metavar
|
|
247
|
+
self._mk_option_name(
|
|
248
|
+
line, prefix, " ".join(metavar) if isinstance(metavar, tuple) else metavar or action.dest
|
|
249
|
+
)
|
|
265
250
|
|
|
266
|
-
|
|
267
|
-
|
|
251
|
+
extra: Sequence[Node] = ()
|
|
252
|
+
if help_text := _expand_help(action, parser.prog):
|
|
268
253
|
temp = paragraph()
|
|
269
|
-
self.state.nested_parse(StringList(help_text.split("\n")), 0, temp)
|
|
270
|
-
line
|
|
271
|
-
|
|
272
|
-
line +=
|
|
254
|
+
self.state.nested_parse(StringList(load_help_text(help_text).split("\n")), 0, temp)
|
|
255
|
+
# only a leading paragraph can share the option's line; anything else becomes a block under it
|
|
256
|
+
if temp.children and isinstance(temp.children[0], paragraph):
|
|
257
|
+
line += Text(" - ")
|
|
258
|
+
line += temp.children[0].children
|
|
259
|
+
extra = temp.children[1:]
|
|
260
|
+
else:
|
|
261
|
+
extra = temp.children
|
|
273
262
|
if (
|
|
274
263
|
"no_default_values" not in self.options
|
|
275
264
|
and action.default is not None
|
|
276
265
|
and action.default != SUPPRESS
|
|
277
|
-
and not
|
|
266
|
+
and not _DEFAULT_IN_HELP.search(help_text)
|
|
278
267
|
and not isinstance(action, _StoreTrueAction | _StoreFalseAction)
|
|
279
268
|
):
|
|
280
269
|
line += Text(" (default: ")
|
|
281
270
|
line += literal(text=str(action.default).replace(str(Path.cwd()), "{cwd}"))
|
|
282
271
|
line += Text(")")
|
|
283
|
-
|
|
284
|
-
|
|
272
|
+
item = list_item("", line, *extra, ids=[])
|
|
273
|
+
_protect_option_dashes(item)
|
|
274
|
+
return item
|
|
285
275
|
|
|
286
276
|
def _mk_option_name(self, line: paragraph, prefix: str, opt: str) -> None:
|
|
287
277
|
ref_id = self._make_id(f"{prefix}-{opt}")
|
|
@@ -290,6 +280,7 @@ class SphinxArgparseCli(SphinxDirective):
|
|
|
290
280
|
line.attributes["ids"].append(ref_id)
|
|
291
281
|
ref += strong("", "", literal(text=opt))
|
|
292
282
|
self._register_ref(ref_id, ref_title, ref, is_cli_option=True)
|
|
283
|
+
self._std_domain.add_program_option(ws_re.sub("-", prefix), opt, self.env.docname, ref_id)
|
|
293
284
|
line += ref
|
|
294
285
|
|
|
295
286
|
def _register_ref(
|
|
@@ -317,15 +308,16 @@ class SphinxArgparseCli(SphinxDirective):
|
|
|
317
308
|
self._std_domain.anonlabels[name] = doc_name, ref_name
|
|
318
309
|
self._std_domain.labels[name] = doc_name, ref_name, ref_title
|
|
319
310
|
|
|
320
|
-
def _mk_sub_command(self, aliases: list[str], help_msg: str, parser: ArgumentParser) -> section:
|
|
321
|
-
sub_title_prefix: str = self.options
|
|
322
|
-
title_prefix: str = self.options
|
|
311
|
+
def _mk_sub_command(self, aliases: list[str], help_msg: str, parser: ArgumentParser, sub_cmd: str) -> section:
|
|
312
|
+
sub_title_prefix: str = self.options.get("group_sub_title_prefix")
|
|
313
|
+
title_prefix: str = self.options.get("group_title_prefix")
|
|
323
314
|
|
|
324
315
|
if sys.version_info >= (3, 14): # pragma: >=3.14 cover
|
|
325
316
|
# https://github.com/python/cpython/issues/139809
|
|
326
317
|
parser.prog = _strip_ansi_colors(parser.prog)
|
|
327
318
|
|
|
328
|
-
|
|
319
|
+
root_prog = self.parser.prog.split("/")[-1]
|
|
320
|
+
title_text = self._resolve_prefix(root_prog, sub_cmd, parser.prog, title_prefix, sub_title_prefix).rstrip()
|
|
329
321
|
title_ref: str = parser.prog
|
|
330
322
|
if aliases:
|
|
331
323
|
aliases_text: str = f" ({', '.join(aliases)})"
|
|
@@ -339,28 +331,19 @@ class SphinxArgparseCli(SphinxDirective):
|
|
|
339
331
|
if "usage_first" in self.options:
|
|
340
332
|
group_section += self._mk_usage(parser)
|
|
341
333
|
|
|
342
|
-
command_desc
|
|
343
|
-
|
|
344
|
-
desc_paragraph = paragraph("", Text(command_desc))
|
|
345
|
-
_protect_option_dashes(desc_paragraph)
|
|
346
|
-
group_section += desc_paragraph
|
|
334
|
+
if command_desc := (parser.description or help_msg).strip():
|
|
335
|
+
group_section += self._pre_format(command_desc, parser)
|
|
347
336
|
|
|
348
337
|
if "usage_first" not in self.options:
|
|
349
338
|
group_section += self._mk_usage(parser)
|
|
350
339
|
|
|
351
340
|
for group in parser._action_groups: # noqa: SLF001
|
|
352
|
-
if
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
group_section += self._mk_option_group(group, prefix=parser.prog, prog=self.parser.prog.split("/")[-1])
|
|
341
|
+
if actions := _visible_actions(group):
|
|
342
|
+
group_section += self._mk_option_group(group, actions, parser, prefix=parser.prog, sub_cmd=sub_cmd)
|
|
343
|
+
if epilog := self._pre_format(parser.epilog, parser):
|
|
344
|
+
group_section += epilog
|
|
357
345
|
return group_section
|
|
358
346
|
|
|
359
|
-
def _build_sub_cmd_title(self, parser: ArgumentParser, sub_title_prefix: str, title_prefix: str) -> str:
|
|
360
|
-
root_prog = self.parser.prog.split("/")[-1]
|
|
361
|
-
sub_cmd = parser.prog[len(root_prog) :].strip().split(" ", maxsplit=1)[0]
|
|
362
|
-
return self._resolve_prefix(root_prog, sub_cmd, parser.prog, title_prefix, sub_title_prefix).rstrip()
|
|
363
|
-
|
|
364
347
|
def _resolve_prefix(
|
|
365
348
|
self,
|
|
366
349
|
prog_name: str,
|
|
@@ -397,18 +380,25 @@ class SphinxArgparseCli(SphinxDirective):
|
|
|
397
380
|
return title_text
|
|
398
381
|
|
|
399
382
|
def _mk_usage(self, parser: ArgumentParser) -> literal_block:
|
|
400
|
-
|
|
401
|
-
with self._no_color():
|
|
383
|
+
width = self.options.get("usage_width", 100)
|
|
384
|
+
with patch.object(parser, "formatter_class", lambda prog: HelpFormatter(prog, width=width)), self._no_color():
|
|
402
385
|
texts = parser.format_usage()[len("usage: ") :].splitlines()
|
|
403
386
|
texts = [line if at == 0 else f"{' ' * (len(parser.prog) + 1)}{line.lstrip()}" for at, line in enumerate(texts)]
|
|
404
387
|
return literal_block("", Text("\n".join(texts)), classes=["sphinx-argparse-cli-wrap"])
|
|
405
388
|
|
|
406
389
|
@contextmanager
|
|
407
390
|
def _no_color(self) -> Iterator[None]:
|
|
408
|
-
|
|
391
|
+
# PYTHON_COLORS=1 outranks NO_COLOR in argparse, so both must be set to get plain text
|
|
392
|
+
with patch.dict(os.environ, {"NO_COLOR": "1", "PYTHON_COLORS": "0"}, clear=False):
|
|
409
393
|
yield
|
|
410
394
|
|
|
411
395
|
|
|
396
|
+
def _format_args(parser: ArgumentParser, action: Action) -> str:
|
|
397
|
+
# argparse's formatter keeps the text in step with the usage line: nargs, choices and user metavars included
|
|
398
|
+
formatter = parser._get_formatter() # noqa: SLF001
|
|
399
|
+
return formatter._format_args(action, formatter._get_default_metavar_for_optional(action)) # noqa: SLF001
|
|
400
|
+
|
|
401
|
+
|
|
412
402
|
def make_id_lower(key: str) -> str:
|
|
413
403
|
return re.sub("[A-Z]", lambda m: f"_{m.group(0).lower()}", make_id(key))
|
|
414
404
|
|
|
@@ -417,9 +407,46 @@ def make_id(key: str) -> str:
|
|
|
417
407
|
return "-".join(key.split()).rstrip("-")
|
|
418
408
|
|
|
419
409
|
|
|
410
|
+
def _sub_parser_action(parser: ArgumentParser) -> _SubParsersAction[ArgumentParser] | None:
|
|
411
|
+
# add_subparsers reuses the positional group, so the action may sit after positional arguments;
|
|
412
|
+
# isinstance cannot recover the type parameter, hence the cast
|
|
413
|
+
found = next((action for action in parser._actions if isinstance(action, _SubParsersAction)), None) # noqa: SLF001
|
|
414
|
+
return cast("_SubParsersAction[ArgumentParser] | None", found)
|
|
415
|
+
|
|
416
|
+
|
|
417
|
+
def _visible_actions(group: _ArgumentGroup) -> list[Action]:
|
|
418
|
+
# sub-commands get their own sections, and a heading over an empty list helps nobody
|
|
419
|
+
return [
|
|
420
|
+
action
|
|
421
|
+
for action in group._group_actions # noqa: SLF001
|
|
422
|
+
if action.help != SUPPRESS and not isinstance(action, _SubParsersAction)
|
|
423
|
+
]
|
|
424
|
+
|
|
425
|
+
|
|
426
|
+
def _expand_prog(text: str, prog: str) -> str:
|
|
427
|
+
# what argparse.HelpFormatter._format_text does for descriptions and epilogs
|
|
428
|
+
return text % {"prog": prog} if "%(prog)" in text else text
|
|
429
|
+
|
|
430
|
+
|
|
431
|
+
def _expand_help(action: Action, prog: str) -> str:
|
|
432
|
+
# mirrors argparse.HelpFormatter._expand_help so the help reads as it does under --help
|
|
433
|
+
help_text = action.help or ""
|
|
434
|
+
if "%" not in help_text:
|
|
435
|
+
return help_text
|
|
436
|
+
params = {
|
|
437
|
+
key: getattr(value, "__name__", value) for key, value in vars(action).items() if value is not SUPPRESS
|
|
438
|
+
} | {"prog": prog}
|
|
439
|
+
if action.choices is not None:
|
|
440
|
+
params["choices"] = ", ".join(map(str, action.choices))
|
|
441
|
+
return help_text % params
|
|
442
|
+
|
|
443
|
+
|
|
444
|
+
_DEFAULT_IN_HELP: Final[re.Pattern[str]] = re.compile(r"\bdefaults?\b", re.IGNORECASE)
|
|
445
|
+
|
|
420
446
|
_HELP_SUBSTITUTIONS: Final[list[tuple[re.Pattern[str], str]]] = [
|
|
421
|
-
(
|
|
422
|
-
(re.compile(r
|
|
447
|
+
# a quote glued to a word character is an apostrophe (don't, it's), not the edge of a quoted span
|
|
448
|
+
(re.compile(r"(?<!\w)'([^']+?)'(?!\w)"), "``'\\1'``"),
|
|
449
|
+
(re.compile(r'(?<!\w)"([^"]+?)"(?!\w)'), '``"\\1"``'),
|
|
423
450
|
(re.compile(r"[{](.+?)[}]"), "``{\\1}``"),
|
|
424
451
|
]
|
|
425
452
|
|
|
@@ -470,9 +497,8 @@ def _strip_ansi_colors(text: str) -> str: # pragma: >=3.14 cover
|
|
|
470
497
|
|
|
471
498
|
|
|
472
499
|
def _update_sub_parser_prog(parser: ArgumentParser, old_prog: str, new_prog: str) -> None:
|
|
473
|
-
if
|
|
500
|
+
if (sub_action := _sub_parser_action(parser)) is None:
|
|
474
501
|
return
|
|
475
|
-
sub_action: _SubParsersAction[ArgumentParser] = sub_parsers._group_actions[0] # type: ignore[assignment] # noqa: SLF001
|
|
476
502
|
for sub_parser in sub_action.choices.values():
|
|
477
503
|
sub_parser.prog = sub_parser.prog.replace(old_prog, new_prog, 1)
|
|
478
504
|
_update_sub_parser_prog(sub_parser, old_prog, new_prog)
|
{sphinx_argparse_cli-1.22.0 → sphinx_argparse_cli-1.23.0}/src/sphinx_argparse_cli/version.py
RENAMED
|
@@ -18,7 +18,7 @@ version_tuple: tuple[int | str, ...]
|
|
|
18
18
|
commit_id: str | None
|
|
19
19
|
__commit_id__: str | None
|
|
20
20
|
|
|
21
|
-
__version__ = version = '1.
|
|
22
|
-
__version_tuple__ = version_tuple = (1,
|
|
21
|
+
__version__ = version = '1.23.0'
|
|
22
|
+
__version_tuple__ = version_tuple = (1, 23, 0)
|
|
23
23
|
|
|
24
24
|
__commit_id__ = commit_id = None
|
|
@@ -16,7 +16,7 @@ complex optional arguments
|
|
|
16
16
|
|
|
17
17
|
* **"--no-help"**
|
|
18
18
|
|
|
19
|
-
* **"--outdir"** "
|
|
19
|
+
* **"--outdir"** "out_dir", **"-o"** "out_dir" - output directory
|
|
20
20
|
|
|
21
21
|
* **"--in-dir"** "IN_DIR", **"-i"** "IN_DIR" - input directory
|
|
22
22
|
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import os
|
|
4
|
+
import re
|
|
4
5
|
import sys
|
|
5
6
|
from pathlib import Path
|
|
6
7
|
from typing import TYPE_CHECKING
|
|
@@ -69,6 +70,24 @@ def test_hook(build_outcome: str) -> None:
|
|
|
69
70
|
assert build_outcome
|
|
70
71
|
|
|
71
72
|
|
|
73
|
+
@pytest.mark.sphinx(buildername="text", testroot="hook-intermixed")
|
|
74
|
+
def test_hook_intermixed(build_outcome: str) -> None:
|
|
75
|
+
assert (
|
|
76
|
+
build_outcome
|
|
77
|
+
== """foo - CLI interface
|
|
78
|
+
*******************
|
|
79
|
+
|
|
80
|
+
foo [--flag FLAG]
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
foo options
|
|
84
|
+
===========
|
|
85
|
+
|
|
86
|
+
* **"--flag"** "FLAG" - a flag
|
|
87
|
+
"""
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
|
|
72
91
|
@pytest.mark.sphinx(buildername="text", testroot="hook-fail")
|
|
73
92
|
def test_hook_fail(app: SphinxTestApp, warning: StringIO) -> None:
|
|
74
93
|
app.build()
|
|
@@ -99,18 +118,18 @@ def test_set_description_as_text(build_outcome: str) -> None:
|
|
|
99
118
|
|
|
100
119
|
@pytest.mark.sphinx(buildername="text", testroot="description-empty")
|
|
101
120
|
def test_empty_description_as_text(build_outcome: str) -> None:
|
|
102
|
-
assert build_outcome == "foo - CLI interface\n*******************\n\n
|
|
121
|
+
assert build_outcome == "foo - CLI interface\n*******************\n\n foo\n"
|
|
103
122
|
|
|
104
123
|
|
|
105
124
|
@pytest.mark.sphinx(buildername="html", testroot="description-multiline")
|
|
106
125
|
def test_multiline_description_as_html(build_outcome: str) -> None:
|
|
107
126
|
ref = (
|
|
108
|
-
"This description\nspans multiple lines.\n\n this line is indented.\n and also this.\n\
|
|
109
|
-
" a separate paragraph.\n"
|
|
127
|
+
"<pre><span></span>This description\nspans multiple lines.\n\n this line is indented.\n and also this.\n\n"
|
|
128
|
+
"Now this should be a separate paragraph.\n</pre>"
|
|
110
129
|
)
|
|
111
130
|
assert ref in build_outcome
|
|
112
131
|
|
|
113
|
-
ref = "This group description\n\nspans multiple lines.\n"
|
|
132
|
+
ref = "<pre><span></span>This group description\n\nspans multiple lines.\n</pre>"
|
|
114
133
|
assert ref in build_outcome
|
|
115
134
|
|
|
116
135
|
|
|
@@ -121,14 +140,14 @@ def test_set_epilog_as_text(build_outcome: str) -> None:
|
|
|
121
140
|
|
|
122
141
|
@pytest.mark.sphinx(buildername="text", testroot="epilog-empty")
|
|
123
142
|
def test_empty_epilog_as_text(build_outcome: str) -> None:
|
|
124
|
-
assert build_outcome == "foo - CLI interface\n*******************\n\n foo\n
|
|
143
|
+
assert build_outcome == "foo - CLI interface\n*******************\n\n foo\n"
|
|
125
144
|
|
|
126
145
|
|
|
127
146
|
@pytest.mark.sphinx(buildername="html", testroot="epilog-multiline")
|
|
128
147
|
def test_multiline_epilog_as_html(build_outcome: str) -> None:
|
|
129
148
|
ref = (
|
|
130
|
-
"This epilog\nspans multiple lines.\n\n this line is indented.\n and also this.\n\
|
|
131
|
-
" a separate paragraph.\n"
|
|
149
|
+
"<pre><span></span>This epilog\nspans multiple lines.\n\n this line is indented.\n and also this.\n\n"
|
|
150
|
+
"Now this should be a separate paragraph.\n</pre>"
|
|
132
151
|
)
|
|
133
152
|
assert ref in build_outcome
|
|
134
153
|
|
|
@@ -136,12 +155,58 @@ def test_multiline_epilog_as_html(build_outcome: str) -> None:
|
|
|
136
155
|
@pytest.mark.sphinx(buildername="html", testroot="epilog-multiline-subclass")
|
|
137
156
|
def test_multiline_epilog_subclass_formatter_as_html(build_outcome: str) -> None:
|
|
138
157
|
ref = (
|
|
139
|
-
"This epilog\nspans multiple lines.\n\n this line is indented.\n and also this.\n\
|
|
140
|
-
" a separate paragraph.\n"
|
|
158
|
+
"<pre><span></span>This epilog\nspans multiple lines.\n\n this line is indented.\n and also this.\n\n"
|
|
159
|
+
"Now this should be a separate paragraph.\n</pre>"
|
|
141
160
|
)
|
|
142
161
|
assert ref in build_outcome
|
|
143
162
|
|
|
144
163
|
|
|
164
|
+
@pytest.mark.sphinx(buildername="text", testroot="subcommand-epilog-raw")
|
|
165
|
+
def test_sub_command_description_epilog_as_text(build_outcome: str) -> None:
|
|
166
|
+
assert (
|
|
167
|
+
build_outcome
|
|
168
|
+
== """prog - CLI interface
|
|
169
|
+
********************
|
|
170
|
+
|
|
171
|
+
prog {raw,plain} ...
|
|
172
|
+
|
|
173
|
+
root description
|
|
174
|
+
kept as is
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
prog raw
|
|
178
|
+
========
|
|
179
|
+
|
|
180
|
+
prog raw [--flag FLAG]
|
|
181
|
+
|
|
182
|
+
raw description
|
|
183
|
+
kept as is
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
prog raw options
|
|
187
|
+
----------------
|
|
188
|
+
|
|
189
|
+
* **"--flag"** "FLAG" - raw flag
|
|
190
|
+
|
|
191
|
+
raw epilog
|
|
192
|
+
kept as is
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
prog plain
|
|
196
|
+
==========
|
|
197
|
+
|
|
198
|
+
prog plain
|
|
199
|
+
|
|
200
|
+
plain description reflowed
|
|
201
|
+
|
|
202
|
+
plain epilog reflowed
|
|
203
|
+
|
|
204
|
+
root epilog
|
|
205
|
+
kept as is
|
|
206
|
+
"""
|
|
207
|
+
)
|
|
208
|
+
|
|
209
|
+
|
|
145
210
|
@pytest.mark.sphinx(buildername="html", testroot="smartquotes")
|
|
146
211
|
def test_option_dashes_survive_smartquotes(build_outcome: str) -> None:
|
|
147
212
|
# option names mentioned in parser-supplied text keep their double hyphen
|
|
@@ -174,21 +239,97 @@ def test_set_usage_first(build_outcome: str) -> None:
|
|
|
174
239
|
assert "complex first [-h]" in build_outcome.split("a-first-desc", maxsplit=1)[0]
|
|
175
240
|
|
|
176
241
|
|
|
242
|
+
@pytest.mark.sphinx(buildername="text", testroot="help-apostrophe")
|
|
243
|
+
def test_help_apostrophe(build_outcome: str, warning: StringIO) -> None:
|
|
244
|
+
assert (
|
|
245
|
+
build_outcome
|
|
246
|
+
== """prog - CLI interface
|
|
247
|
+
********************
|
|
248
|
+
|
|
249
|
+
prog [--a A] [--b B]
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
prog options
|
|
253
|
+
============
|
|
254
|
+
|
|
255
|
+
* **"--a"** "A" - don't use it's value
|
|
256
|
+
|
|
257
|
+
* **"--b"** "B" - it's a "'thing'" to see
|
|
258
|
+
"""
|
|
259
|
+
)
|
|
260
|
+
assert not warning.getvalue()
|
|
261
|
+
|
|
262
|
+
|
|
177
263
|
@pytest.mark.sphinx(buildername="text", testroot="suppressed-action")
|
|
178
264
|
def test_suppressed_action(build_outcome: str) -> None:
|
|
179
|
-
assert
|
|
265
|
+
assert (
|
|
266
|
+
build_outcome
|
|
267
|
+
== """foo - CLI interface
|
|
268
|
+
*******************
|
|
269
|
+
|
|
270
|
+
desc
|
|
271
|
+
|
|
272
|
+
foo
|
|
273
|
+
"""
|
|
274
|
+
)
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
@pytest.mark.sphinx(buildername="text", testroot="subparsers-positional")
|
|
278
|
+
def test_subparsers_after_positional(build_outcome: str) -> None:
|
|
279
|
+
assert (
|
|
280
|
+
build_outcome
|
|
281
|
+
== """prog - CLI interface
|
|
282
|
+
********************
|
|
283
|
+
|
|
284
|
+
prog root {run} ...
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
prog positional arguments
|
|
288
|
+
=========================
|
|
289
|
+
|
|
290
|
+
* **"root"** - root positional
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
prog root run
|
|
294
|
+
=============
|
|
295
|
+
|
|
296
|
+
prog root run target {go} ...
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
prog root run positional arguments
|
|
300
|
+
----------------------------------
|
|
301
|
+
|
|
302
|
+
* **"target"** - run positional
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
prog root run target go
|
|
306
|
+
=======================
|
|
307
|
+
|
|
308
|
+
prog root run target go [--flag FLAG]
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
prog root run target go options
|
|
312
|
+
-------------------------------
|
|
313
|
+
|
|
314
|
+
* **"--flag"** "FLAG" - go flag
|
|
315
|
+
"""
|
|
316
|
+
)
|
|
180
317
|
|
|
181
318
|
|
|
182
319
|
@pytest.mark.parametrize(
|
|
183
320
|
("example", "output"),
|
|
184
321
|
[
|
|
185
|
-
("", ""),
|
|
186
|
-
("{", "{"),
|
|
187
|
-
('"', '"'),
|
|
188
|
-
("'", "'"),
|
|
189
|
-
("{a}", "``{a}``"),
|
|
190
|
-
('"a"', '``"a"``'),
|
|
191
|
-
("'a'", "``'a'``"),
|
|
322
|
+
pytest.param("", "", id="empty"),
|
|
323
|
+
pytest.param("{", "{", id="lone-brace"),
|
|
324
|
+
pytest.param('"', '"', id="lone-double-quote"),
|
|
325
|
+
pytest.param("'", "'", id="lone-single-quote"),
|
|
326
|
+
pytest.param("{a}", "``{a}``", id="braces"),
|
|
327
|
+
pytest.param('"a"', '``"a"``', id="double-quoted"),
|
|
328
|
+
pytest.param("'a'", "``'a'``", id="single-quoted"),
|
|
329
|
+
pytest.param("don't use it's value", "don't use it's value", id="apostrophes"),
|
|
330
|
+
pytest.param("it's a 'thing' to see", "it's a ``'thing'`` to see", id="apostrophe-and-quoted"),
|
|
331
|
+
pytest.param("'a' and 'b'", "``'a'`` and ``'b'``", id="two-quoted"),
|
|
332
|
+
pytest.param('say "hi" or "bye"', 'say ``"hi"`` or ``"bye"``', id="two-double-quoted"),
|
|
192
333
|
],
|
|
193
334
|
)
|
|
194
335
|
def test_help_loader(example: str, output: str) -> None:
|
|
@@ -215,6 +356,129 @@ def test_ref_prefix_doc(build_outcome: str) -> None:
|
|
|
215
356
|
assert ref in build_outcome
|
|
216
357
|
|
|
217
358
|
|
|
359
|
+
@pytest.mark.sphinx(buildername="html", testroot="option")
|
|
360
|
+
def test_option_role_as_html(build_outcome: str, warning: StringIO) -> None:
|
|
361
|
+
hrefs = re.findall(r'<a class="reference internal" href="(#[^"]+)"><code class="xref std std-option', build_outcome)
|
|
362
|
+
assert hrefs == [
|
|
363
|
+
"#prog---root",
|
|
364
|
+
"#prog--r",
|
|
365
|
+
"#prog-run---magic",
|
|
366
|
+
"#prog-run-target",
|
|
367
|
+
"#prog-run---magic",
|
|
368
|
+
"#prog-run---magic",
|
|
369
|
+
]
|
|
370
|
+
assert not warning.getvalue()
|
|
371
|
+
|
|
372
|
+
|
|
373
|
+
@pytest.mark.sphinx(buildername="text", testroot="subparsers-suppressed")
|
|
374
|
+
def test_suppressed_sub_command(build_outcome: str) -> None:
|
|
375
|
+
assert (
|
|
376
|
+
build_outcome
|
|
377
|
+
== """prog - CLI interface
|
|
378
|
+
********************
|
|
379
|
+
|
|
380
|
+
prog {run,secret} ...
|
|
381
|
+
|
|
382
|
+
|
|
383
|
+
prog run
|
|
384
|
+
========
|
|
385
|
+
|
|
386
|
+
run it
|
|
387
|
+
|
|
388
|
+
prog run
|
|
389
|
+
"""
|
|
390
|
+
)
|
|
391
|
+
|
|
392
|
+
|
|
393
|
+
@pytest.mark.sphinx(buildername="text", testroot="python-colors")
|
|
394
|
+
def test_usage_ignores_python_colors(build_outcome: str) -> None:
|
|
395
|
+
assert (
|
|
396
|
+
build_outcome
|
|
397
|
+
== """prog - CLI interface
|
|
398
|
+
********************
|
|
399
|
+
|
|
400
|
+
prog [--flag FLAG] {run} ...
|
|
401
|
+
|
|
402
|
+
|
|
403
|
+
prog options
|
|
404
|
+
============
|
|
405
|
+
|
|
406
|
+
* **"--flag"** "FLAG" - a flag
|
|
407
|
+
|
|
408
|
+
|
|
409
|
+
prog run
|
|
410
|
+
========
|
|
411
|
+
|
|
412
|
+
prog run [--magic MAGIC]
|
|
413
|
+
|
|
414
|
+
|
|
415
|
+
prog run options
|
|
416
|
+
----------------
|
|
417
|
+
|
|
418
|
+
* **"--magic"** "MAGIC" - magic
|
|
419
|
+
"""
|
|
420
|
+
)
|
|
421
|
+
|
|
422
|
+
|
|
423
|
+
@pytest.mark.sphinx(buildername="html", testroot="subcommand-placeholder")
|
|
424
|
+
def test_sub_command_placeholder_nested(build_outcome: str) -> None:
|
|
425
|
+
headings = re.findall(r'<h[23]>([^<]*)<a class="headerlink" href="(#[^"]*)"', build_outcome)
|
|
426
|
+
assert headings == [
|
|
427
|
+
("prog positional arguments", "#prog-positional-arguments"),
|
|
428
|
+
("prog first (f)", "#prog-root-first-(f)"),
|
|
429
|
+
("prog first options", "#prog-root-first-options"),
|
|
430
|
+
("prog first nested", "#prog-root-first-nested"),
|
|
431
|
+
("prog first nested options", "#prog-root-first-nested-options"),
|
|
432
|
+
]
|
|
433
|
+
|
|
434
|
+
|
|
435
|
+
@pytest.mark.sphinx(buildername="text", testroot="help-nodes")
|
|
436
|
+
def test_help_nodes_as_text(build_outcome: str) -> None:
|
|
437
|
+
assert (
|
|
438
|
+
build_outcome
|
|
439
|
+
== """prog - CLI interface
|
|
440
|
+
********************
|
|
441
|
+
|
|
442
|
+
prog [--blank BLANK] [--list LIST] [--two TWO]
|
|
443
|
+
|
|
444
|
+
|
|
445
|
+
prog options
|
|
446
|
+
============
|
|
447
|
+
|
|
448
|
+
* **"--blank"** "BLANK"
|
|
449
|
+
|
|
450
|
+
* **"--list"** "LIST"
|
|
451
|
+
|
|
452
|
+
* item one
|
|
453
|
+
|
|
454
|
+
* item two
|
|
455
|
+
|
|
456
|
+
* **"--two"** "TWO" - first paragraph
|
|
457
|
+
|
|
458
|
+
second paragraph
|
|
459
|
+
"""
|
|
460
|
+
)
|
|
461
|
+
|
|
462
|
+
|
|
463
|
+
@pytest.mark.sphinx(buildername="html", testroot="help-nodes")
|
|
464
|
+
def test_help_nodes_as_html(build_outcome: str, warning: StringIO) -> None:
|
|
465
|
+
assert "<p></p>" not in build_outcome
|
|
466
|
+
assert not warning.getvalue()
|
|
467
|
+
|
|
468
|
+
|
|
469
|
+
@pytest.mark.sphinx(buildername="html", testroot="group-untitled")
|
|
470
|
+
def test_group_untitled(build_outcome: str, warning: StringIO) -> None:
|
|
471
|
+
headings = re.findall(r'<h2>(.*?)<a class="headerlink" href="(#[^"]+)"', build_outcome)
|
|
472
|
+
assert headings == [
|
|
473
|
+
("tool options", "#tool-options"),
|
|
474
|
+
("tool only a description", "#tool-only-a-description"),
|
|
475
|
+
("tool another", "#tool-another"),
|
|
476
|
+
("tool arguments", "#tool-arguments"),
|
|
477
|
+
]
|
|
478
|
+
assert "<p>only a description</p>" not in build_outcome
|
|
479
|
+
assert not warning.getvalue()
|
|
480
|
+
|
|
481
|
+
|
|
218
482
|
@pytest.mark.sphinx(buildername="text", testroot="ref-duplicate-label")
|
|
219
483
|
def test_ref_duplicate_label(build_outcome: tuple[str, str], warning: StringIO) -> None:
|
|
220
484
|
assert build_outcome
|
|
@@ -286,8 +550,8 @@ def test_group_title_prefix_sub_command_replacement(build_outcome: str, opt_grp_
|
|
|
286
550
|
grp, anchor = opt_grp_name
|
|
287
551
|
assert f'<h2>bar {grp}<a class="headerlink" href="#bar-{anchor}"' in build_outcome
|
|
288
552
|
assert '<h2>bar Exclusive<a class="headerlink" href="#bar-Exclusive"' in build_outcome
|
|
289
|
-
assert '<h2>bar
|
|
290
|
-
assert '<h3>bar
|
|
553
|
+
assert '<h2>bar baronlyfirst (f)<a class="headerlink" href="#bar-root-first-(f)"' in build_outcome
|
|
554
|
+
assert '<h3>bar baronlyfirst positional arguments<a class="headerlink"' in build_outcome
|
|
291
555
|
|
|
292
556
|
|
|
293
557
|
@pytest.mark.sphinx(buildername="html", testroot="store-true-false")
|
|
@@ -323,6 +587,59 @@ def test_ref_cases(build_outcome: str, warning: StringIO) -> None:
|
|
|
323
587
|
assert not warning.getvalue()
|
|
324
588
|
|
|
325
589
|
|
|
590
|
+
@pytest.mark.sphinx(buildername="text", testroot="help-format-specifiers")
|
|
591
|
+
def test_help_format_specifiers(build_outcome: str) -> None:
|
|
592
|
+
assert (
|
|
593
|
+
build_outcome
|
|
594
|
+
== """tool - CLI interface
|
|
595
|
+
********************
|
|
596
|
+
|
|
597
|
+
tool does things
|
|
598
|
+
|
|
599
|
+
tool [--n N] [--mode {a,b}] [--pct PCT] [--capital CAPITAL] [--kind KIND] [--level LEVEL]
|
|
600
|
+
{run} ...
|
|
601
|
+
|
|
602
|
+
|
|
603
|
+
tool options
|
|
604
|
+
============
|
|
605
|
+
|
|
606
|
+
* **"--n"** "N" - count (default: 3)
|
|
607
|
+
|
|
608
|
+
* **"--mode"** "{a,b}" - pick one of a, b (default: "a")
|
|
609
|
+
|
|
610
|
+
* **"--pct"** "PCT" - 100% of tool (default: "5")
|
|
611
|
+
|
|
612
|
+
* **"--capital"** "CAPITAL" - Default: 3
|
|
613
|
+
|
|
614
|
+
* **"--kind"** "KIND" - parsed with float
|
|
615
|
+
|
|
616
|
+
|
|
617
|
+
tool tuning
|
|
618
|
+
===========
|
|
619
|
+
|
|
620
|
+
tune tool
|
|
621
|
+
|
|
622
|
+
* **"--level"** "LEVEL" - level (default: "1")
|
|
623
|
+
|
|
624
|
+
|
|
625
|
+
tool run
|
|
626
|
+
========
|
|
627
|
+
|
|
628
|
+
tool run runs
|
|
629
|
+
|
|
630
|
+
tool run [--target TARGET]
|
|
631
|
+
|
|
632
|
+
|
|
633
|
+
tool run options
|
|
634
|
+
----------------
|
|
635
|
+
|
|
636
|
+
* **"--target"** "TARGET" - target for tool run
|
|
637
|
+
|
|
638
|
+
run tool --help
|
|
639
|
+
"""
|
|
640
|
+
)
|
|
641
|
+
|
|
642
|
+
|
|
326
643
|
@pytest.mark.sphinx(buildername="text", testroot="default-handling")
|
|
327
644
|
def test_with_default(build_outcome: str) -> None:
|
|
328
645
|
assert (
|
|
@@ -369,6 +686,13 @@ def test_subparsers(build_outcome: str) -> None:
|
|
|
369
686
|
assert '<section id="test-no_child-options">' in build_outcome
|
|
370
687
|
|
|
371
688
|
|
|
689
|
+
@pytest.mark.sphinx(buildername="html", testroot="subparsers-storeaction")
|
|
690
|
+
def test_subparsers_stoeaction(build_outcome: str) -> None:
|
|
691
|
+
assert '<section id="test-options">' in build_outcome
|
|
692
|
+
assert '<section id="test-subparser">' in build_outcome
|
|
693
|
+
assert '<section id="test-subparser-positional-arguments">' in build_outcome
|
|
694
|
+
|
|
695
|
+
|
|
372
696
|
@pytest.mark.sphinx(buildername="text", testroot="bad-module")
|
|
373
697
|
def test_bad_module(app: SphinxTestApp, warning: StringIO) -> None:
|
|
374
698
|
app.build()
|
|
@@ -391,6 +715,44 @@ def test_nargs(build_outcome: str) -> None:
|
|
|
391
715
|
assert 'default: "None"' not in build_outcome
|
|
392
716
|
|
|
393
717
|
|
|
718
|
+
@pytest.mark.sphinx(buildername="text", testroot="nargs-metavar")
|
|
719
|
+
def test_nargs_metavar(build_outcome: str) -> None:
|
|
720
|
+
assert (
|
|
721
|
+
build_outcome
|
|
722
|
+
== """tool - CLI interface
|
|
723
|
+
********************
|
|
724
|
+
|
|
725
|
+
tool [--opt [OPT]] [--many [MANY ...]] [--two TWO TWO] [--rest ...] [--out <file>]
|
|
726
|
+
[--dir path/to/dir] [--format {json,xml}]
|
|
727
|
+
SRC DST
|
|
728
|
+
|
|
729
|
+
|
|
730
|
+
tool positional arguments
|
|
731
|
+
=========================
|
|
732
|
+
|
|
733
|
+
* **"SRC DST"** - copy pair
|
|
734
|
+
|
|
735
|
+
|
|
736
|
+
tool options
|
|
737
|
+
============
|
|
738
|
+
|
|
739
|
+
* **"--opt"** "[OPT]" - optional value
|
|
740
|
+
|
|
741
|
+
* **"--many"** "[MANY ...]" - zero or more
|
|
742
|
+
|
|
743
|
+
* **"--two"** "TWO TWO" - exactly two
|
|
744
|
+
|
|
745
|
+
* **"--rest"** "..." - the rest
|
|
746
|
+
|
|
747
|
+
* **"--out"** "<file>" - output
|
|
748
|
+
|
|
749
|
+
* **"--dir"** "path/to/dir" - dir
|
|
750
|
+
|
|
751
|
+
* **"--format"** "{json,xml}" - output format
|
|
752
|
+
"""
|
|
753
|
+
)
|
|
754
|
+
|
|
755
|
+
|
|
394
756
|
@pytest.mark.sphinx(buildername="text", testroot="choices")
|
|
395
757
|
def test_choices(build_outcome: str) -> None:
|
|
396
758
|
assert "output format" in build_outcome
|
|
File without changes
|
|
File without changes
|
|
@@ -136,10 +136,10 @@ environment.python-version = "3.14"
|
|
|
136
136
|
|
|
137
137
|
[tool.coverage]
|
|
138
138
|
run.parallel = true
|
|
139
|
+
run.relative_files = true
|
|
139
140
|
run.plugins = [
|
|
140
141
|
"covdefaults",
|
|
141
142
|
]
|
|
142
|
-
run.relative_files = true
|
|
143
143
|
paths.source = [
|
|
144
144
|
"src",
|
|
145
145
|
"**/site-packages",
|
{sphinx_argparse_cli-1.22.0 → sphinx_argparse_cli-1.23.0}/src/sphinx_argparse_cli/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|