sphinx-argparse-cli 1.22.1__tar.gz → 1.24.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.1 → sphinx_argparse_cli-1.24.0}/PKG-INFO +27 -2
- {sphinx_argparse_cli-1.22.1 → sphinx_argparse_cli-1.24.0}/README.md +25 -0
- {sphinx_argparse_cli-1.22.1 → sphinx_argparse_cli-1.24.0}/pyproject.toml +7 -7
- {sphinx_argparse_cli-1.22.1 → sphinx_argparse_cli-1.24.0}/src/sphinx_argparse_cli/_logic.py +138 -103
- {sphinx_argparse_cli-1.22.1 → sphinx_argparse_cli-1.24.0}/src/sphinx_argparse_cli/version.py +2 -2
- {sphinx_argparse_cli-1.22.1 → sphinx_argparse_cli-1.24.0}/tests/complex.txt +1 -1
- {sphinx_argparse_cli-1.22.1 → sphinx_argparse_cli-1.24.0}/tests/test_logic.py +375 -22
- sphinx_argparse_cli-1.22.1/tests/complex_pre_310.txt +0 -95
- {sphinx_argparse_cli-1.22.1 → sphinx_argparse_cli-1.24.0}/.gitignore +0 -0
- {sphinx_argparse_cli-1.22.1 → sphinx_argparse_cli-1.24.0}/LICENSE.txt +0 -0
- {sphinx_argparse_cli-1.22.1 → sphinx_argparse_cli-1.24.0}/src/sphinx_argparse_cli/__init__.py +0 -0
- {sphinx_argparse_cli-1.22.1 → sphinx_argparse_cli-1.24.0}/src/sphinx_argparse_cli/py.typed +0 -0
- {sphinx_argparse_cli-1.22.1 → sphinx_argparse_cli-1.24.0}/tests/conftest.py +0 -0
- {sphinx_argparse_cli-1.22.1 → sphinx_argparse_cli-1.24.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.24.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:
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
build-backend = "hatchling.build"
|
|
3
3
|
requires = [
|
|
4
4
|
"hatch-vcs>=0.5",
|
|
5
|
-
"hatchling>=1.
|
|
5
|
+
"hatchling>=1.32.4",
|
|
6
6
|
]
|
|
7
7
|
|
|
8
8
|
[project]
|
|
@@ -65,12 +65,12 @@ type = [
|
|
|
65
65
|
{ include-group = "test" },
|
|
66
66
|
]
|
|
67
67
|
lint = [
|
|
68
|
-
"pre-commit-uv>=4.
|
|
68
|
+
"pre-commit-uv>=4.3",
|
|
69
69
|
]
|
|
70
70
|
pkg-meta = [
|
|
71
71
|
"check-wheel-contents>=0.6.3",
|
|
72
|
-
"twine>=
|
|
73
|
-
"uv>=0.
|
|
72
|
+
"twine>=7",
|
|
73
|
+
"uv>=0.12.22",
|
|
74
74
|
]
|
|
75
75
|
|
|
76
76
|
[tool.hatch]
|
|
@@ -85,7 +85,7 @@ build.targets.sdist.include = [
|
|
|
85
85
|
line-length = 120
|
|
86
86
|
|
|
87
87
|
[tool.ruff]
|
|
88
|
-
target-version = "
|
|
88
|
+
target-version = "py311"
|
|
89
89
|
line-length = 120
|
|
90
90
|
format.preview = true
|
|
91
91
|
format.docstring-code-line-length = 100
|
|
@@ -132,14 +132,14 @@ count = true
|
|
|
132
132
|
max_supported_python = "3.15"
|
|
133
133
|
|
|
134
134
|
[tool.ty]
|
|
135
|
-
environment.python-version = "3.
|
|
135
|
+
environment.python-version = "3.15"
|
|
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",
|
|
@@ -43,11 +43,12 @@ from docutils.nodes import (
|
|
|
43
43
|
from docutils.parsers.rst.directives import flag, positive_int, unchanged, unchanged_required
|
|
44
44
|
from docutils.statemachine import StringList
|
|
45
45
|
from sphinx.locale import __
|
|
46
|
+
from sphinx.util import ws_re
|
|
46
47
|
from sphinx.util.docutils import SphinxDirective
|
|
47
48
|
from sphinx.util.logging import getLogger
|
|
48
49
|
|
|
49
50
|
if TYPE_CHECKING:
|
|
50
|
-
from collections.abc import Callable, Iterator
|
|
51
|
+
from collections.abc import Callable, Iterator, Sequence
|
|
51
52
|
|
|
52
53
|
from sphinx.domains.std import StandardDomain
|
|
53
54
|
from sphinx.util.logging import SphinxLoggerAdapter
|
|
@@ -93,14 +94,15 @@ class SphinxArgparseCli(SphinxDirective):
|
|
|
93
94
|
raise self.error(msg) # noqa: B904
|
|
94
95
|
parser: ArgumentParser | None = None
|
|
95
96
|
if "hook" in self.options:
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
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
|
+
):
|
|
102
|
+
try:
|
|
103
|
+
parser_creator()
|
|
104
|
+
except HookError as hooked:
|
|
105
|
+
parser = hooked.parser
|
|
104
106
|
else:
|
|
105
107
|
parser = parser_creator()
|
|
106
108
|
|
|
@@ -123,14 +125,9 @@ class SphinxArgparseCli(SphinxDirective):
|
|
|
123
125
|
def _make_id(self) -> Callable[[str], str]:
|
|
124
126
|
return make_id_lower if "force_refs_lower" in self.options else make_id
|
|
125
127
|
|
|
126
|
-
@property
|
|
127
|
-
def _raw_format(self) -> bool:
|
|
128
|
-
formatter = self.parser.formatter_class
|
|
129
|
-
return isinstance(formatter, type) and issubclass(formatter, RawDescriptionHelpFormatter)
|
|
130
|
-
|
|
131
128
|
def _load_sub_parsers(
|
|
132
|
-
self, sub_parser: _SubParsersAction[ArgumentParser]
|
|
133
|
-
) -> Iterator[tuple[list[str], str, ArgumentParser]]:
|
|
129
|
+
self, sub_parser: _SubParsersAction[ArgumentParser], parent_cmd: str = ""
|
|
130
|
+
) -> Iterator[tuple[list[str], str, ArgumentParser, str]]:
|
|
134
131
|
parser_to_args: dict[int, list[str]] = defaultdict(list)
|
|
135
132
|
for key, parser in sub_parser._name_parser_map.items(): # noqa: SLF001
|
|
136
133
|
parser_to_args[id(parser)].append(key)
|
|
@@ -144,20 +141,18 @@ class SphinxArgparseCli(SphinxDirective):
|
|
|
144
141
|
aliases = parser_to_args[id(parser)]
|
|
145
142
|
aliases.remove(name)
|
|
146
143
|
# help is stored in a pseudo action
|
|
147
|
-
help_msg = next((a.help for a in sub_parser._choices_actions if a.dest == name), None)
|
|
148
|
-
|
|
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
|
|
149
149
|
|
|
150
|
-
if parser
|
|
151
|
-
|
|
152
|
-
if isinstance(sub_sub_parser, _SubParsersAction):
|
|
153
|
-
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)
|
|
154
152
|
|
|
155
|
-
def _iter_sub_commands(self) -> Iterator[tuple[list[str], str, ArgumentParser]]:
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
return
|
|
159
|
-
sub_parser: _SubParsersAction[ArgumentParser] = top_sub_parser._group_actions[0] # type: ignore[assignment] # noqa: SLF001
|
|
160
|
-
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)
|
|
161
156
|
|
|
162
157
|
def run(self) -> list[Node]:
|
|
163
158
|
self.env.note_reread() # this document needs to always be rebuilt
|
|
@@ -170,22 +165,24 @@ class SphinxArgparseCli(SphinxDirective):
|
|
|
170
165
|
if "usage_first" in self.options:
|
|
171
166
|
home_section += self._mk_usage(self.parser)
|
|
172
167
|
|
|
173
|
-
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):
|
|
174
169
|
home_section += description
|
|
175
170
|
|
|
176
171
|
if "usage_first" not in self.options:
|
|
177
172
|
home_section += self._mk_usage(self.parser)
|
|
178
173
|
|
|
179
174
|
for group in self.parser._action_groups: # noqa: SLF001
|
|
180
|
-
if
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
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):
|
|
189
186
|
home_section += epilog
|
|
190
187
|
|
|
191
188
|
if self.content:
|
|
@@ -193,10 +190,12 @@ class SphinxArgparseCli(SphinxDirective):
|
|
|
193
190
|
|
|
194
191
|
return [home_section]
|
|
195
192
|
|
|
196
|
-
def _pre_format(self, block: str | None) -> paragraph | literal_block | None:
|
|
197
|
-
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():
|
|
198
195
|
return None
|
|
199
|
-
|
|
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):
|
|
200
199
|
lit = literal_block("", Text(block), classes=["sphinx-argparse-cli-wrap"])
|
|
201
200
|
lit["language"] = "none"
|
|
202
201
|
return lit
|
|
@@ -204,75 +203,75 @@ class SphinxArgparseCli(SphinxDirective):
|
|
|
204
203
|
_protect_option_dashes(para)
|
|
205
204
|
return para
|
|
206
205
|
|
|
207
|
-
def _mk_option_group(
|
|
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]
|
|
208
215
|
sub_title_prefix: str = self.options.get("group_sub_title_prefix")
|
|
209
216
|
title_prefix = self.options.get("group_title_prefix")
|
|
210
|
-
|
|
211
|
-
|
|
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}"
|
|
212
221
|
ref_id = self._make_id(title_ref)
|
|
213
222
|
# the text sadly needs to be prefixed, because otherwise the autosectionlabel will conflict
|
|
214
223
|
header = title("", Text(title_text))
|
|
215
224
|
group_section = section("", header, ids=[ref_id], names=[ref_id])
|
|
216
|
-
if description := self._pre_format(group.description):
|
|
225
|
+
if group.title and (description := self._pre_format(group.description, parser)):
|
|
217
226
|
group_section += description
|
|
218
227
|
self._register_ref(ref_id, title_text, group_section)
|
|
219
228
|
opt_group = bullet_list()
|
|
220
|
-
for action in
|
|
221
|
-
|
|
222
|
-
continue
|
|
223
|
-
point = self._mk_option_line(action, prefix)
|
|
224
|
-
opt_group += point
|
|
229
|
+
for action in actions:
|
|
230
|
+
opt_group += self._mk_option_line(parser, action, prefix)
|
|
225
231
|
group_section += opt_group
|
|
226
232
|
return group_section
|
|
227
233
|
|
|
228
|
-
def
|
|
229
|
-
self, group: _ArgumentGroup, prefix: str, prog: str, sub_title_prefix: str, title_prefix: str
|
|
230
|
-
) -> str:
|
|
231
|
-
sub_cmd = prefix[len(prog) :].strip() or None if prefix != prog else None
|
|
232
|
-
title_text = self._resolve_prefix(prog, sub_cmd, prefix, title_prefix, sub_title_prefix)
|
|
233
|
-
title_text += group.title or ""
|
|
234
|
-
return title_text
|
|
235
|
-
|
|
236
|
-
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:
|
|
237
235
|
line = paragraph()
|
|
238
|
-
as_key = action.dest
|
|
239
|
-
if action.metavar:
|
|
240
|
-
as_key = action.metavar if isinstance(action.metavar, str) else action.metavar[0]
|
|
241
236
|
if action.option_strings:
|
|
237
|
+
args_text = _format_args(parser, action) if action.nargs != 0 else None
|
|
242
238
|
for at, opt in enumerate(action.option_strings):
|
|
243
239
|
if at:
|
|
244
240
|
line += Text(", ")
|
|
245
241
|
self._mk_option_name(line, prefix, opt)
|
|
246
|
-
if
|
|
242
|
+
if args_text is not None:
|
|
247
243
|
line += Text(" ")
|
|
248
|
-
|
|
249
|
-
" ".join(meta.upper() for meta in action.metavar)
|
|
250
|
-
if isinstance(action.metavar, tuple)
|
|
251
|
-
else as_key.upper()
|
|
252
|
-
)
|
|
253
|
-
line += literal(text=metavar_text)
|
|
244
|
+
line += literal(text=args_text)
|
|
254
245
|
else:
|
|
255
|
-
|
|
246
|
+
metavar = action.metavar
|
|
247
|
+
self._mk_option_name(
|
|
248
|
+
line, prefix, " ".join(metavar) if isinstance(metavar, tuple) else metavar or action.dest
|
|
249
|
+
)
|
|
256
250
|
|
|
257
|
-
|
|
258
|
-
|
|
251
|
+
extra: Sequence[Node] = ()
|
|
252
|
+
if help_text := _expand_help(action, parser.prog):
|
|
259
253
|
temp = paragraph()
|
|
260
|
-
self.state.nested_parse(StringList(help_text.split("\n")), 0, temp)
|
|
261
|
-
line
|
|
262
|
-
|
|
263
|
-
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
|
|
264
262
|
if (
|
|
265
263
|
"no_default_values" not in self.options
|
|
266
264
|
and action.default is not None
|
|
267
265
|
and action.default != SUPPRESS
|
|
268
|
-
and not
|
|
266
|
+
and not _DEFAULT_IN_HELP.search(help_text)
|
|
269
267
|
and not isinstance(action, _StoreTrueAction | _StoreFalseAction)
|
|
270
268
|
):
|
|
271
269
|
line += Text(" (default: ")
|
|
272
270
|
line += literal(text=str(action.default).replace(str(Path.cwd()), "{cwd}"))
|
|
273
271
|
line += Text(")")
|
|
274
|
-
|
|
275
|
-
|
|
272
|
+
item = list_item("", line, *extra, ids=[])
|
|
273
|
+
_protect_option_dashes(item)
|
|
274
|
+
return item
|
|
276
275
|
|
|
277
276
|
def _mk_option_name(self, line: paragraph, prefix: str, opt: str) -> None:
|
|
278
277
|
ref_id = self._make_id(f"{prefix}-{opt}")
|
|
@@ -281,6 +280,7 @@ class SphinxArgparseCli(SphinxDirective):
|
|
|
281
280
|
line.attributes["ids"].append(ref_id)
|
|
282
281
|
ref += strong("", "", literal(text=opt))
|
|
283
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)
|
|
284
284
|
line += ref
|
|
285
285
|
|
|
286
286
|
def _register_ref(
|
|
@@ -308,7 +308,7 @@ class SphinxArgparseCli(SphinxDirective):
|
|
|
308
308
|
self._std_domain.anonlabels[name] = doc_name, ref_name
|
|
309
309
|
self._std_domain.labels[name] = doc_name, ref_name, ref_title
|
|
310
310
|
|
|
311
|
-
def _mk_sub_command(self, aliases: list[str], help_msg: str, parser: ArgumentParser) -> section:
|
|
311
|
+
def _mk_sub_command(self, aliases: list[str], help_msg: str, parser: ArgumentParser, sub_cmd: str) -> section:
|
|
312
312
|
sub_title_prefix: str = self.options.get("group_sub_title_prefix")
|
|
313
313
|
title_prefix: str = self.options.get("group_title_prefix")
|
|
314
314
|
|
|
@@ -316,7 +316,8 @@ class SphinxArgparseCli(SphinxDirective):
|
|
|
316
316
|
# https://github.com/python/cpython/issues/139809
|
|
317
317
|
parser.prog = _strip_ansi_colors(parser.prog)
|
|
318
318
|
|
|
319
|
-
|
|
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()
|
|
320
321
|
title_ref: str = parser.prog
|
|
321
322
|
if aliases:
|
|
322
323
|
aliases_text: str = f" ({', '.join(aliases)})"
|
|
@@ -330,28 +331,19 @@ class SphinxArgparseCli(SphinxDirective):
|
|
|
330
331
|
if "usage_first" in self.options:
|
|
331
332
|
group_section += self._mk_usage(parser)
|
|
332
333
|
|
|
333
|
-
command_desc
|
|
334
|
-
|
|
335
|
-
desc_paragraph = paragraph("", Text(command_desc))
|
|
336
|
-
_protect_option_dashes(desc_paragraph)
|
|
337
|
-
group_section += desc_paragraph
|
|
334
|
+
if command_desc := (parser.description or help_msg).strip():
|
|
335
|
+
group_section += self._pre_format(command_desc, parser)
|
|
338
336
|
|
|
339
337
|
if "usage_first" not in self.options:
|
|
340
338
|
group_section += self._mk_usage(parser)
|
|
341
339
|
|
|
342
340
|
for group in parser._action_groups: # noqa: SLF001
|
|
343
|
-
if
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
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
|
|
348
345
|
return group_section
|
|
349
346
|
|
|
350
|
-
def _build_sub_cmd_title(self, parser: ArgumentParser, sub_title_prefix: str, title_prefix: str) -> str:
|
|
351
|
-
root_prog = self.parser.prog.split("/")[-1]
|
|
352
|
-
sub_cmd = parser.prog[len(root_prog) :].strip().split(" ", maxsplit=1)[0]
|
|
353
|
-
return self._resolve_prefix(root_prog, sub_cmd, parser.prog, title_prefix, sub_title_prefix).rstrip()
|
|
354
|
-
|
|
355
347
|
def _resolve_prefix(
|
|
356
348
|
self,
|
|
357
349
|
prog_name: str,
|
|
@@ -388,18 +380,25 @@ class SphinxArgparseCli(SphinxDirective):
|
|
|
388
380
|
return title_text
|
|
389
381
|
|
|
390
382
|
def _mk_usage(self, parser: ArgumentParser) -> literal_block:
|
|
391
|
-
|
|
392
|
-
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():
|
|
393
385
|
texts = parser.format_usage()[len("usage: ") :].splitlines()
|
|
394
386
|
texts = [line if at == 0 else f"{' ' * (len(parser.prog) + 1)}{line.lstrip()}" for at, line in enumerate(texts)]
|
|
395
387
|
return literal_block("", Text("\n".join(texts)), classes=["sphinx-argparse-cli-wrap"])
|
|
396
388
|
|
|
397
389
|
@contextmanager
|
|
398
390
|
def _no_color(self) -> Iterator[None]:
|
|
399
|
-
|
|
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):
|
|
400
393
|
yield
|
|
401
394
|
|
|
402
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
|
+
|
|
403
402
|
def make_id_lower(key: str) -> str:
|
|
404
403
|
return re.sub("[A-Z]", lambda m: f"_{m.group(0).lower()}", make_id(key))
|
|
405
404
|
|
|
@@ -408,9 +407,46 @@ def make_id(key: str) -> str:
|
|
|
408
407
|
return "-".join(key.split()).rstrip("-")
|
|
409
408
|
|
|
410
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
|
+
|
|
411
446
|
_HELP_SUBSTITUTIONS: Final[list[tuple[re.Pattern[str], str]]] = [
|
|
412
|
-
(
|
|
413
|
-
(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"``'),
|
|
414
450
|
(re.compile(r"[{](.+?)[}]"), "``{\\1}``"),
|
|
415
451
|
]
|
|
416
452
|
|
|
@@ -461,9 +497,8 @@ def _strip_ansi_colors(text: str) -> str: # pragma: >=3.14 cover
|
|
|
461
497
|
|
|
462
498
|
|
|
463
499
|
def _update_sub_parser_prog(parser: ArgumentParser, old_prog: str, new_prog: str) -> None:
|
|
464
|
-
if
|
|
500
|
+
if (sub_action := _sub_parser_action(parser)) is None:
|
|
465
501
|
return
|
|
466
|
-
sub_action: _SubParsersAction[ArgumentParser] = sub_parsers._group_actions[0] # type: ignore[assignment] # noqa: SLF001
|
|
467
502
|
for sub_parser in sub_action.choices.values():
|
|
468
503
|
sub_parser.prog = sub_parser.prog.replace(old_prog, new_prog, 1)
|
|
469
504
|
_update_sub_parser_prog(sub_parser, old_prog, new_prog)
|
{sphinx_argparse_cli-1.22.1 → sphinx_argparse_cli-1.24.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.24.0'
|
|
22
|
+
__version_tuple__ = version_tuple = (1, 24, 0)
|
|
23
23
|
|
|
24
24
|
__commit_id__ = commit_id = None
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import os
|
|
4
|
-
import
|
|
4
|
+
import re
|
|
5
5
|
from pathlib import Path
|
|
6
6
|
from typing import TYPE_CHECKING
|
|
7
7
|
|
|
@@ -54,8 +54,7 @@ def test_basic_as_html(build_outcome: str) -> None:
|
|
|
54
54
|
|
|
55
55
|
@pytest.mark.sphinx(buildername="text", testroot="complex")
|
|
56
56
|
def test_complex_as_text(build_outcome: str) -> None:
|
|
57
|
-
|
|
58
|
-
expected = (Path(__file__).parent / name).read_text()
|
|
57
|
+
expected = (Path(__file__).parent / "complex.txt").read_text()
|
|
59
58
|
assert build_outcome == expected
|
|
60
59
|
|
|
61
60
|
|
|
@@ -69,6 +68,24 @@ def test_hook(build_outcome: str) -> None:
|
|
|
69
68
|
assert build_outcome
|
|
70
69
|
|
|
71
70
|
|
|
71
|
+
@pytest.mark.sphinx(buildername="text", testroot="hook-intermixed")
|
|
72
|
+
def test_hook_intermixed(build_outcome: str) -> None:
|
|
73
|
+
assert (
|
|
74
|
+
build_outcome
|
|
75
|
+
== """foo - CLI interface
|
|
76
|
+
*******************
|
|
77
|
+
|
|
78
|
+
foo [--flag FLAG]
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
foo options
|
|
82
|
+
===========
|
|
83
|
+
|
|
84
|
+
* **"--flag"** "FLAG" - a flag
|
|
85
|
+
"""
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
|
|
72
89
|
@pytest.mark.sphinx(buildername="text", testroot="hook-fail")
|
|
73
90
|
def test_hook_fail(app: SphinxTestApp, warning: StringIO) -> None:
|
|
74
91
|
app.build()
|
|
@@ -99,18 +116,18 @@ def test_set_description_as_text(build_outcome: str) -> None:
|
|
|
99
116
|
|
|
100
117
|
@pytest.mark.sphinx(buildername="text", testroot="description-empty")
|
|
101
118
|
def test_empty_description_as_text(build_outcome: str) -> None:
|
|
102
|
-
assert build_outcome == "foo - CLI interface\n*******************\n\n
|
|
119
|
+
assert build_outcome == "foo - CLI interface\n*******************\n\n foo\n"
|
|
103
120
|
|
|
104
121
|
|
|
105
122
|
@pytest.mark.sphinx(buildername="html", testroot="description-multiline")
|
|
106
123
|
def test_multiline_description_as_html(build_outcome: str) -> None:
|
|
107
124
|
ref = (
|
|
108
|
-
"This description\nspans multiple lines.\n\n this line is indented.\n and also this.\n\
|
|
109
|
-
" a separate paragraph.\n"
|
|
125
|
+
"<pre><span></span>This description\nspans multiple lines.\n\n this line is indented.\n and also this.\n\n"
|
|
126
|
+
"Now this should be a separate paragraph.\n</pre>"
|
|
110
127
|
)
|
|
111
128
|
assert ref in build_outcome
|
|
112
129
|
|
|
113
|
-
ref = "This group description\n\nspans multiple lines.\n"
|
|
130
|
+
ref = "<pre><span></span>This group description\n\nspans multiple lines.\n</pre>"
|
|
114
131
|
assert ref in build_outcome
|
|
115
132
|
|
|
116
133
|
|
|
@@ -121,14 +138,14 @@ def test_set_epilog_as_text(build_outcome: str) -> None:
|
|
|
121
138
|
|
|
122
139
|
@pytest.mark.sphinx(buildername="text", testroot="epilog-empty")
|
|
123
140
|
def test_empty_epilog_as_text(build_outcome: str) -> None:
|
|
124
|
-
assert build_outcome == "foo - CLI interface\n*******************\n\n foo\n
|
|
141
|
+
assert build_outcome == "foo - CLI interface\n*******************\n\n foo\n"
|
|
125
142
|
|
|
126
143
|
|
|
127
144
|
@pytest.mark.sphinx(buildername="html", testroot="epilog-multiline")
|
|
128
145
|
def test_multiline_epilog_as_html(build_outcome: str) -> None:
|
|
129
146
|
ref = (
|
|
130
|
-
"This epilog\nspans multiple lines.\n\n this line is indented.\n and also this.\n\
|
|
131
|
-
" a separate paragraph.\n"
|
|
147
|
+
"<pre><span></span>This epilog\nspans multiple lines.\n\n this line is indented.\n and also this.\n\n"
|
|
148
|
+
"Now this should be a separate paragraph.\n</pre>"
|
|
132
149
|
)
|
|
133
150
|
assert ref in build_outcome
|
|
134
151
|
|
|
@@ -136,12 +153,58 @@ def test_multiline_epilog_as_html(build_outcome: str) -> None:
|
|
|
136
153
|
@pytest.mark.sphinx(buildername="html", testroot="epilog-multiline-subclass")
|
|
137
154
|
def test_multiline_epilog_subclass_formatter_as_html(build_outcome: str) -> None:
|
|
138
155
|
ref = (
|
|
139
|
-
"This epilog\nspans multiple lines.\n\n this line is indented.\n and also this.\n\
|
|
140
|
-
" a separate paragraph.\n"
|
|
156
|
+
"<pre><span></span>This epilog\nspans multiple lines.\n\n this line is indented.\n and also this.\n\n"
|
|
157
|
+
"Now this should be a separate paragraph.\n</pre>"
|
|
141
158
|
)
|
|
142
159
|
assert ref in build_outcome
|
|
143
160
|
|
|
144
161
|
|
|
162
|
+
@pytest.mark.sphinx(buildername="text", testroot="subcommand-epilog-raw")
|
|
163
|
+
def test_sub_command_description_epilog_as_text(build_outcome: str) -> None:
|
|
164
|
+
assert (
|
|
165
|
+
build_outcome
|
|
166
|
+
== """prog - CLI interface
|
|
167
|
+
********************
|
|
168
|
+
|
|
169
|
+
prog {raw,plain} ...
|
|
170
|
+
|
|
171
|
+
root description
|
|
172
|
+
kept as is
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
prog raw
|
|
176
|
+
========
|
|
177
|
+
|
|
178
|
+
prog raw [--flag FLAG]
|
|
179
|
+
|
|
180
|
+
raw description
|
|
181
|
+
kept as is
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
prog raw options
|
|
185
|
+
----------------
|
|
186
|
+
|
|
187
|
+
* **"--flag"** "FLAG" - raw flag
|
|
188
|
+
|
|
189
|
+
raw epilog
|
|
190
|
+
kept as is
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
prog plain
|
|
194
|
+
==========
|
|
195
|
+
|
|
196
|
+
prog plain
|
|
197
|
+
|
|
198
|
+
plain description reflowed
|
|
199
|
+
|
|
200
|
+
plain epilog reflowed
|
|
201
|
+
|
|
202
|
+
root epilog
|
|
203
|
+
kept as is
|
|
204
|
+
"""
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
|
|
145
208
|
@pytest.mark.sphinx(buildername="html", testroot="smartquotes")
|
|
146
209
|
def test_option_dashes_survive_smartquotes(build_outcome: str) -> None:
|
|
147
210
|
# option names mentioned in parser-supplied text keep their double hyphen
|
|
@@ -174,21 +237,97 @@ def test_set_usage_first(build_outcome: str) -> None:
|
|
|
174
237
|
assert "complex first [-h]" in build_outcome.split("a-first-desc", maxsplit=1)[0]
|
|
175
238
|
|
|
176
239
|
|
|
240
|
+
@pytest.mark.sphinx(buildername="text", testroot="help-apostrophe")
|
|
241
|
+
def test_help_apostrophe(build_outcome: str, warning: StringIO) -> None:
|
|
242
|
+
assert (
|
|
243
|
+
build_outcome
|
|
244
|
+
== """prog - CLI interface
|
|
245
|
+
********************
|
|
246
|
+
|
|
247
|
+
prog [--a A] [--b B]
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
prog options
|
|
251
|
+
============
|
|
252
|
+
|
|
253
|
+
* **"--a"** "A" - don't use it's value
|
|
254
|
+
|
|
255
|
+
* **"--b"** "B" - it's a "'thing'" to see
|
|
256
|
+
"""
|
|
257
|
+
)
|
|
258
|
+
assert not warning.getvalue()
|
|
259
|
+
|
|
260
|
+
|
|
177
261
|
@pytest.mark.sphinx(buildername="text", testroot="suppressed-action")
|
|
178
262
|
def test_suppressed_action(build_outcome: str) -> None:
|
|
179
|
-
assert
|
|
263
|
+
assert (
|
|
264
|
+
build_outcome
|
|
265
|
+
== """foo - CLI interface
|
|
266
|
+
*******************
|
|
267
|
+
|
|
268
|
+
desc
|
|
269
|
+
|
|
270
|
+
foo
|
|
271
|
+
"""
|
|
272
|
+
)
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
@pytest.mark.sphinx(buildername="text", testroot="subparsers-positional")
|
|
276
|
+
def test_subparsers_after_positional(build_outcome: str) -> None:
|
|
277
|
+
assert (
|
|
278
|
+
build_outcome
|
|
279
|
+
== """prog - CLI interface
|
|
280
|
+
********************
|
|
281
|
+
|
|
282
|
+
prog root {run} ...
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
prog positional arguments
|
|
286
|
+
=========================
|
|
287
|
+
|
|
288
|
+
* **"root"** - root positional
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
prog root run
|
|
292
|
+
=============
|
|
293
|
+
|
|
294
|
+
prog root run target {go} ...
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
prog root run positional arguments
|
|
298
|
+
----------------------------------
|
|
299
|
+
|
|
300
|
+
* **"target"** - run positional
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
prog root run target go
|
|
304
|
+
=======================
|
|
305
|
+
|
|
306
|
+
prog root run target go [--flag FLAG]
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
prog root run target go options
|
|
310
|
+
-------------------------------
|
|
311
|
+
|
|
312
|
+
* **"--flag"** "FLAG" - go flag
|
|
313
|
+
"""
|
|
314
|
+
)
|
|
180
315
|
|
|
181
316
|
|
|
182
317
|
@pytest.mark.parametrize(
|
|
183
318
|
("example", "output"),
|
|
184
319
|
[
|
|
185
|
-
("", ""),
|
|
186
|
-
("{", "{"),
|
|
187
|
-
('"', '"'),
|
|
188
|
-
("'", "'"),
|
|
189
|
-
("{a}", "``{a}``"),
|
|
190
|
-
('"a"', '``"a"``'),
|
|
191
|
-
("'a'", "``'a'``"),
|
|
320
|
+
pytest.param("", "", id="empty"),
|
|
321
|
+
pytest.param("{", "{", id="lone-brace"),
|
|
322
|
+
pytest.param('"', '"', id="lone-double-quote"),
|
|
323
|
+
pytest.param("'", "'", id="lone-single-quote"),
|
|
324
|
+
pytest.param("{a}", "``{a}``", id="braces"),
|
|
325
|
+
pytest.param('"a"', '``"a"``', id="double-quoted"),
|
|
326
|
+
pytest.param("'a'", "``'a'``", id="single-quoted"),
|
|
327
|
+
pytest.param("don't use it's value", "don't use it's value", id="apostrophes"),
|
|
328
|
+
pytest.param("it's a 'thing' to see", "it's a ``'thing'`` to see", id="apostrophe-and-quoted"),
|
|
329
|
+
pytest.param("'a' and 'b'", "``'a'`` and ``'b'``", id="two-quoted"),
|
|
330
|
+
pytest.param('say "hi" or "bye"', 'say ``"hi"`` or ``"bye"``', id="two-double-quoted"),
|
|
192
331
|
],
|
|
193
332
|
)
|
|
194
333
|
def test_help_loader(example: str, output: str) -> None:
|
|
@@ -215,6 +354,129 @@ def test_ref_prefix_doc(build_outcome: str) -> None:
|
|
|
215
354
|
assert ref in build_outcome
|
|
216
355
|
|
|
217
356
|
|
|
357
|
+
@pytest.mark.sphinx(buildername="html", testroot="option")
|
|
358
|
+
def test_option_role_as_html(build_outcome: str, warning: StringIO) -> None:
|
|
359
|
+
hrefs = re.findall(r'<a class="reference internal" href="(#[^"]+)"><code class="xref std std-option', build_outcome)
|
|
360
|
+
assert hrefs == [
|
|
361
|
+
"#prog---root",
|
|
362
|
+
"#prog--r",
|
|
363
|
+
"#prog-run---magic",
|
|
364
|
+
"#prog-run-target",
|
|
365
|
+
"#prog-run---magic",
|
|
366
|
+
"#prog-run---magic",
|
|
367
|
+
]
|
|
368
|
+
assert not warning.getvalue()
|
|
369
|
+
|
|
370
|
+
|
|
371
|
+
@pytest.mark.sphinx(buildername="text", testroot="subparsers-suppressed")
|
|
372
|
+
def test_suppressed_sub_command(build_outcome: str) -> None:
|
|
373
|
+
assert (
|
|
374
|
+
build_outcome
|
|
375
|
+
== """prog - CLI interface
|
|
376
|
+
********************
|
|
377
|
+
|
|
378
|
+
prog {run,secret} ...
|
|
379
|
+
|
|
380
|
+
|
|
381
|
+
prog run
|
|
382
|
+
========
|
|
383
|
+
|
|
384
|
+
run it
|
|
385
|
+
|
|
386
|
+
prog run
|
|
387
|
+
"""
|
|
388
|
+
)
|
|
389
|
+
|
|
390
|
+
|
|
391
|
+
@pytest.mark.sphinx(buildername="text", testroot="python-colors")
|
|
392
|
+
def test_usage_ignores_python_colors(build_outcome: str) -> None:
|
|
393
|
+
assert (
|
|
394
|
+
build_outcome
|
|
395
|
+
== """prog - CLI interface
|
|
396
|
+
********************
|
|
397
|
+
|
|
398
|
+
prog [--flag FLAG] {run} ...
|
|
399
|
+
|
|
400
|
+
|
|
401
|
+
prog options
|
|
402
|
+
============
|
|
403
|
+
|
|
404
|
+
* **"--flag"** "FLAG" - a flag
|
|
405
|
+
|
|
406
|
+
|
|
407
|
+
prog run
|
|
408
|
+
========
|
|
409
|
+
|
|
410
|
+
prog run [--magic MAGIC]
|
|
411
|
+
|
|
412
|
+
|
|
413
|
+
prog run options
|
|
414
|
+
----------------
|
|
415
|
+
|
|
416
|
+
* **"--magic"** "MAGIC" - magic
|
|
417
|
+
"""
|
|
418
|
+
)
|
|
419
|
+
|
|
420
|
+
|
|
421
|
+
@pytest.mark.sphinx(buildername="html", testroot="subcommand-placeholder")
|
|
422
|
+
def test_sub_command_placeholder_nested(build_outcome: str) -> None:
|
|
423
|
+
headings = re.findall(r'<h[23]>([^<]*)<a class="headerlink" href="(#[^"]*)"', build_outcome)
|
|
424
|
+
assert headings == [
|
|
425
|
+
("prog positional arguments", "#prog-positional-arguments"),
|
|
426
|
+
("prog first (f)", "#prog-root-first-(f)"),
|
|
427
|
+
("prog first options", "#prog-root-first-options"),
|
|
428
|
+
("prog first nested", "#prog-root-first-nested"),
|
|
429
|
+
("prog first nested options", "#prog-root-first-nested-options"),
|
|
430
|
+
]
|
|
431
|
+
|
|
432
|
+
|
|
433
|
+
@pytest.mark.sphinx(buildername="text", testroot="help-nodes")
|
|
434
|
+
def test_help_nodes_as_text(build_outcome: str) -> None:
|
|
435
|
+
assert (
|
|
436
|
+
build_outcome
|
|
437
|
+
== """prog - CLI interface
|
|
438
|
+
********************
|
|
439
|
+
|
|
440
|
+
prog [--blank BLANK] [--list LIST] [--two TWO]
|
|
441
|
+
|
|
442
|
+
|
|
443
|
+
prog options
|
|
444
|
+
============
|
|
445
|
+
|
|
446
|
+
* **"--blank"** "BLANK"
|
|
447
|
+
|
|
448
|
+
* **"--list"** "LIST"
|
|
449
|
+
|
|
450
|
+
* item one
|
|
451
|
+
|
|
452
|
+
* item two
|
|
453
|
+
|
|
454
|
+
* **"--two"** "TWO" - first paragraph
|
|
455
|
+
|
|
456
|
+
second paragraph
|
|
457
|
+
"""
|
|
458
|
+
)
|
|
459
|
+
|
|
460
|
+
|
|
461
|
+
@pytest.mark.sphinx(buildername="html", testroot="help-nodes")
|
|
462
|
+
def test_help_nodes_as_html(build_outcome: str, warning: StringIO) -> None:
|
|
463
|
+
assert "<p></p>" not in build_outcome
|
|
464
|
+
assert not warning.getvalue()
|
|
465
|
+
|
|
466
|
+
|
|
467
|
+
@pytest.mark.sphinx(buildername="html", testroot="group-untitled")
|
|
468
|
+
def test_group_untitled(build_outcome: str, warning: StringIO) -> None:
|
|
469
|
+
headings = re.findall(r'<h2>(.*?)<a class="headerlink" href="(#[^"]+)"', build_outcome)
|
|
470
|
+
assert headings == [
|
|
471
|
+
("tool options", "#tool-options"),
|
|
472
|
+
("tool only a description", "#tool-only-a-description"),
|
|
473
|
+
("tool another", "#tool-another"),
|
|
474
|
+
("tool arguments", "#tool-arguments"),
|
|
475
|
+
]
|
|
476
|
+
assert "<p>only a description</p>" not in build_outcome
|
|
477
|
+
assert not warning.getvalue()
|
|
478
|
+
|
|
479
|
+
|
|
218
480
|
@pytest.mark.sphinx(buildername="text", testroot="ref-duplicate-label")
|
|
219
481
|
def test_ref_duplicate_label(build_outcome: tuple[str, str], warning: StringIO) -> None:
|
|
220
482
|
assert build_outcome
|
|
@@ -286,8 +548,8 @@ def test_group_title_prefix_sub_command_replacement(build_outcome: str, opt_grp_
|
|
|
286
548
|
grp, anchor = opt_grp_name
|
|
287
549
|
assert f'<h2>bar {grp}<a class="headerlink" href="#bar-{anchor}"' in build_outcome
|
|
288
550
|
assert '<h2>bar Exclusive<a class="headerlink" href="#bar-Exclusive"' in build_outcome
|
|
289
|
-
assert '<h2>bar
|
|
290
|
-
assert '<h3>bar
|
|
551
|
+
assert '<h2>bar baronlyfirst (f)<a class="headerlink" href="#bar-root-first-(f)"' in build_outcome
|
|
552
|
+
assert '<h3>bar baronlyfirst positional arguments<a class="headerlink"' in build_outcome
|
|
291
553
|
|
|
292
554
|
|
|
293
555
|
@pytest.mark.sphinx(buildername="html", testroot="store-true-false")
|
|
@@ -323,6 +585,59 @@ def test_ref_cases(build_outcome: str, warning: StringIO) -> None:
|
|
|
323
585
|
assert not warning.getvalue()
|
|
324
586
|
|
|
325
587
|
|
|
588
|
+
@pytest.mark.sphinx(buildername="text", testroot="help-format-specifiers")
|
|
589
|
+
def test_help_format_specifiers(build_outcome: str) -> None:
|
|
590
|
+
assert (
|
|
591
|
+
build_outcome
|
|
592
|
+
== """tool - CLI interface
|
|
593
|
+
********************
|
|
594
|
+
|
|
595
|
+
tool does things
|
|
596
|
+
|
|
597
|
+
tool [--n N] [--mode {a,b}] [--pct PCT] [--capital CAPITAL] [--kind KIND] [--level LEVEL]
|
|
598
|
+
{run} ...
|
|
599
|
+
|
|
600
|
+
|
|
601
|
+
tool options
|
|
602
|
+
============
|
|
603
|
+
|
|
604
|
+
* **"--n"** "N" - count (default: 3)
|
|
605
|
+
|
|
606
|
+
* **"--mode"** "{a,b}" - pick one of a, b (default: "a")
|
|
607
|
+
|
|
608
|
+
* **"--pct"** "PCT" - 100% of tool (default: "5")
|
|
609
|
+
|
|
610
|
+
* **"--capital"** "CAPITAL" - Default: 3
|
|
611
|
+
|
|
612
|
+
* **"--kind"** "KIND" - parsed with float
|
|
613
|
+
|
|
614
|
+
|
|
615
|
+
tool tuning
|
|
616
|
+
===========
|
|
617
|
+
|
|
618
|
+
tune tool
|
|
619
|
+
|
|
620
|
+
* **"--level"** "LEVEL" - level (default: "1")
|
|
621
|
+
|
|
622
|
+
|
|
623
|
+
tool run
|
|
624
|
+
========
|
|
625
|
+
|
|
626
|
+
tool run runs
|
|
627
|
+
|
|
628
|
+
tool run [--target TARGET]
|
|
629
|
+
|
|
630
|
+
|
|
631
|
+
tool run options
|
|
632
|
+
----------------
|
|
633
|
+
|
|
634
|
+
* **"--target"** "TARGET" - target for tool run
|
|
635
|
+
|
|
636
|
+
run tool --help
|
|
637
|
+
"""
|
|
638
|
+
)
|
|
639
|
+
|
|
640
|
+
|
|
326
641
|
@pytest.mark.sphinx(buildername="text", testroot="default-handling")
|
|
327
642
|
def test_with_default(build_outcome: str) -> None:
|
|
328
643
|
assert (
|
|
@@ -398,6 +713,44 @@ def test_nargs(build_outcome: str) -> None:
|
|
|
398
713
|
assert 'default: "None"' not in build_outcome
|
|
399
714
|
|
|
400
715
|
|
|
716
|
+
@pytest.mark.sphinx(buildername="text", testroot="nargs-metavar")
|
|
717
|
+
def test_nargs_metavar(build_outcome: str) -> None:
|
|
718
|
+
assert (
|
|
719
|
+
build_outcome
|
|
720
|
+
== """tool - CLI interface
|
|
721
|
+
********************
|
|
722
|
+
|
|
723
|
+
tool [--opt [OPT]] [--many [MANY ...]] [--two TWO TWO] [--rest ...] [--out <file>]
|
|
724
|
+
[--dir path/to/dir] [--format {json,xml}]
|
|
725
|
+
SRC DST
|
|
726
|
+
|
|
727
|
+
|
|
728
|
+
tool positional arguments
|
|
729
|
+
=========================
|
|
730
|
+
|
|
731
|
+
* **"SRC DST"** - copy pair
|
|
732
|
+
|
|
733
|
+
|
|
734
|
+
tool options
|
|
735
|
+
============
|
|
736
|
+
|
|
737
|
+
* **"--opt"** "[OPT]" - optional value
|
|
738
|
+
|
|
739
|
+
* **"--many"** "[MANY ...]" - zero or more
|
|
740
|
+
|
|
741
|
+
* **"--two"** "TWO TWO" - exactly two
|
|
742
|
+
|
|
743
|
+
* **"--rest"** "..." - the rest
|
|
744
|
+
|
|
745
|
+
* **"--out"** "<file>" - output
|
|
746
|
+
|
|
747
|
+
* **"--dir"** "path/to/dir" - dir
|
|
748
|
+
|
|
749
|
+
* **"--format"** "{json,xml}" - output format
|
|
750
|
+
"""
|
|
751
|
+
)
|
|
752
|
+
|
|
753
|
+
|
|
401
754
|
@pytest.mark.sphinx(buildername="text", testroot="choices")
|
|
402
755
|
def test_choices(build_outcome: str) -> None:
|
|
403
756
|
assert "output format" in build_outcome
|
|
@@ -1,95 +0,0 @@
|
|
|
1
|
-
complex - CLI interface
|
|
2
|
-
***********************
|
|
3
|
-
|
|
4
|
-
argparse tester
|
|
5
|
-
|
|
6
|
-
complex [-h] [--root] [--no-help] [--outdir out_dir] [--in-dir IN_DIR] [--foo | --bar]
|
|
7
|
-
{first,f,second,third} ...
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
complex optional arguments
|
|
11
|
-
==========================
|
|
12
|
-
|
|
13
|
-
* **"-h"**, **"--help"** - show this help message and exit
|
|
14
|
-
|
|
15
|
-
* **"--root"** - root flag
|
|
16
|
-
|
|
17
|
-
* **"--no-help"**
|
|
18
|
-
|
|
19
|
-
* **"--outdir"** "OUT_DIR", **"-o"** "OUT_DIR" - output directory
|
|
20
|
-
|
|
21
|
-
* **"--in-dir"** "IN_DIR", **"-i"** "IN_DIR" - input directory
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
complex Exclusive
|
|
25
|
-
=================
|
|
26
|
-
|
|
27
|
-
this is an exclusive group
|
|
28
|
-
|
|
29
|
-
* **"--foo"** - foo
|
|
30
|
-
|
|
31
|
-
* **"--bar"** - bar
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
complex first (f)
|
|
35
|
-
=================
|
|
36
|
-
|
|
37
|
-
a-first-desc
|
|
38
|
-
|
|
39
|
-
complex first [-h] [--flag] [--root] one pos_two
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
complex first positional arguments
|
|
43
|
-
----------------------------------
|
|
44
|
-
|
|
45
|
-
* **"one"** - first positional argument
|
|
46
|
-
|
|
47
|
-
* **"pos_two"** - second positional argument (default: "1")
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
complex first optional arguments
|
|
51
|
-
--------------------------------
|
|
52
|
-
|
|
53
|
-
* **"-h"**, **"--help"** - show this help message and exit
|
|
54
|
-
|
|
55
|
-
* **"--flag"** - a parser first flag
|
|
56
|
-
|
|
57
|
-
* **"--root"** - root flag
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
complex second
|
|
61
|
-
==============
|
|
62
|
-
|
|
63
|
-
complex second [-h] [--flag] [--root] one pos_two
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
complex second positional arguments
|
|
67
|
-
-----------------------------------
|
|
68
|
-
|
|
69
|
-
* **"one"** - first positional argument
|
|
70
|
-
|
|
71
|
-
* **"pos_two"** - second positional argument (default: "green")
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
complex second optional arguments
|
|
75
|
-
---------------------------------
|
|
76
|
-
|
|
77
|
-
* **"-h"**, **"--help"** - show this help message and exit
|
|
78
|
-
|
|
79
|
-
* **"--flag"** - a parser second flag
|
|
80
|
-
|
|
81
|
-
* **"--root"** - root flag
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
complex third
|
|
85
|
-
=============
|
|
86
|
-
|
|
87
|
-
complex third [-h]
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
complex third optional arguments
|
|
91
|
-
--------------------------------
|
|
92
|
-
|
|
93
|
-
* **"-h"**, **"--help"** - show this help message and exit
|
|
94
|
-
|
|
95
|
-
test epilog
|
|
File without changes
|
|
File without changes
|
{sphinx_argparse_cli-1.22.1 → sphinx_argparse_cli-1.24.0}/src/sphinx_argparse_cli/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|