sphinx-argparse-cli 1.21.2__tar.gz → 1.21.4__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.21.2 → sphinx_argparse_cli-1.21.4}/PKG-INFO +4 -4
- {sphinx_argparse_cli-1.21.2 → sphinx_argparse_cli-1.21.4}/README.md +3 -3
- {sphinx_argparse_cli-1.21.2 → sphinx_argparse_cli-1.21.4}/pyproject.toml +10 -10
- {sphinx_argparse_cli-1.21.2 → sphinx_argparse_cli-1.21.4}/src/sphinx_argparse_cli/__init__.py +14 -0
- {sphinx_argparse_cli-1.21.2 → sphinx_argparse_cli-1.21.4}/src/sphinx_argparse_cli/_logic.py +75 -66
- sphinx_argparse_cli-1.21.4/src/sphinx_argparse_cli/version.py +24 -0
- {sphinx_argparse_cli-1.21.2 → sphinx_argparse_cli-1.21.4}/tests/test_logic.py +24 -5
- {sphinx_argparse_cli-1.21.2 → sphinx_argparse_cli-1.21.4}/tests/test_sphinx_argparse_cli.py +2 -2
- sphinx_argparse_cli-1.21.2/src/sphinx_argparse_cli/version.py +0 -34
- {sphinx_argparse_cli-1.21.2 → sphinx_argparse_cli-1.21.4}/.gitignore +0 -0
- {sphinx_argparse_cli-1.21.2 → sphinx_argparse_cli-1.21.4}/LICENSE.txt +0 -0
- {sphinx_argparse_cli-1.21.2 → sphinx_argparse_cli-1.21.4}/src/sphinx_argparse_cli/py.typed +0 -0
- {sphinx_argparse_cli-1.21.2 → sphinx_argparse_cli-1.21.4}/tests/complex.txt +0 -0
- {sphinx_argparse_cli-1.21.2 → sphinx_argparse_cli-1.21.4}/tests/complex_pre_310.txt +0 -0
- {sphinx_argparse_cli-1.21.2 → sphinx_argparse_cli-1.21.4}/tests/conftest.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: sphinx-argparse-cli
|
|
3
|
-
Version: 1.21.
|
|
3
|
+
Version: 1.21.4
|
|
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
|
|
@@ -63,7 +63,7 @@ Use the directive in any reStructuredText file:
|
|
|
63
63
|
```
|
|
64
64
|
|
|
65
65
|
`:module:` points to the Python module containing the parser, and `:func:` names a zero-argument function that returns
|
|
66
|
-
an `ArgumentParser`. Build your docs and the full CLI reference appears
|
|
66
|
+
an `ArgumentParser`. Build your docs and the full CLI reference appears.
|
|
67
67
|
|
|
68
68
|
## How-to guides
|
|
69
69
|
|
|
@@ -129,7 +129,7 @@ Set the character width for usage lines and optionally show usage before the des
|
|
|
129
129
|
|
|
130
130
|
### Override title, description, or epilog
|
|
131
131
|
|
|
132
|
-
Replace auto-detected values, or pass an empty string to suppress them
|
|
132
|
+
Replace auto-detected values, or pass an empty string to suppress them:
|
|
133
133
|
|
|
134
134
|
```rst
|
|
135
135
|
.. sphinx_argparse_cli::
|
|
@@ -166,7 +166,7 @@ The anchor text is visible after the `#` in the URL when you click a heading.
|
|
|
166
166
|
### Handle mixed-case references
|
|
167
167
|
|
|
168
168
|
Sphinx `:ref:` only supports lower-case targets. When your program name or flags contain capital letters, set
|
|
169
|
-
`:force_refs_lower:` to convert them
|
|
169
|
+
`:force_refs_lower:` to convert them. Each upper-case letter becomes its lower-case form prefixed with `_` (e.g. `A`
|
|
170
170
|
becomes `_a`):
|
|
171
171
|
|
|
172
172
|
```rst
|
|
@@ -32,7 +32,7 @@ Use the directive in any reStructuredText file:
|
|
|
32
32
|
```
|
|
33
33
|
|
|
34
34
|
`:module:` points to the Python module containing the parser, and `:func:` names a zero-argument function that returns
|
|
35
|
-
an `ArgumentParser`. Build your docs and the full CLI reference appears
|
|
35
|
+
an `ArgumentParser`. Build your docs and the full CLI reference appears.
|
|
36
36
|
|
|
37
37
|
## How-to guides
|
|
38
38
|
|
|
@@ -98,7 +98,7 @@ Set the character width for usage lines and optionally show usage before the des
|
|
|
98
98
|
|
|
99
99
|
### Override title, description, or epilog
|
|
100
100
|
|
|
101
|
-
Replace auto-detected values, or pass an empty string to suppress them
|
|
101
|
+
Replace auto-detected values, or pass an empty string to suppress them:
|
|
102
102
|
|
|
103
103
|
```rst
|
|
104
104
|
.. sphinx_argparse_cli::
|
|
@@ -135,7 +135,7 @@ The anchor text is visible after the `#` in the URL when you click a heading.
|
|
|
135
135
|
### Handle mixed-case references
|
|
136
136
|
|
|
137
137
|
Sphinx `:ref:` only supports lower-case targets. When your program name or flags contain capital letters, set
|
|
138
|
-
`:force_refs_lower:` to convert them
|
|
138
|
+
`:force_refs_lower:` to convert them. Each upper-case letter becomes its lower-case form prefixed with `_` (e.g. `A`
|
|
139
139
|
becomes `_a`):
|
|
140
140
|
|
|
141
141
|
```rst
|
|
@@ -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.30.1",
|
|
6
6
|
]
|
|
7
7
|
|
|
8
8
|
[project]
|
|
@@ -56,29 +56,29 @@ dev = [
|
|
|
56
56
|
test = [
|
|
57
57
|
"covdefaults>=2.3",
|
|
58
58
|
"defusedxml>=0.7.1",
|
|
59
|
-
"pytest>=9.
|
|
60
|
-
"pytest-cov>=7",
|
|
59
|
+
"pytest>=9.1.1",
|
|
60
|
+
"pytest-cov>=7.1",
|
|
61
61
|
]
|
|
62
62
|
type = [
|
|
63
|
-
"ty
|
|
63
|
+
"ty<0.0.25,>=0.0.24",
|
|
64
64
|
{ include-group = "test" },
|
|
65
65
|
]
|
|
66
66
|
lint = [
|
|
67
|
-
"pre-commit-uv>=4.2",
|
|
67
|
+
"pre-commit-uv>=4.2.2",
|
|
68
68
|
]
|
|
69
69
|
pkg-meta = [
|
|
70
70
|
"check-wheel-contents>=0.6.3",
|
|
71
71
|
"twine>=6.2",
|
|
72
|
-
"uv>=0.
|
|
72
|
+
"uv>=0.11.26",
|
|
73
73
|
]
|
|
74
74
|
|
|
75
75
|
[tool.hatch]
|
|
76
|
+
version.source = "vcs"
|
|
76
77
|
build.hooks.vcs.version-file = "src/sphinx_argparse_cli/version.py"
|
|
77
78
|
build.targets.sdist.include = [
|
|
78
79
|
"/src",
|
|
79
80
|
"/tests",
|
|
80
81
|
]
|
|
81
|
-
version.source = "vcs"
|
|
82
82
|
|
|
83
83
|
[tool.black]
|
|
84
84
|
line-length = 120
|
|
@@ -130,6 +130,9 @@ count = true
|
|
|
130
130
|
[tool.pyproject-fmt]
|
|
131
131
|
max_supported_python = "3.14"
|
|
132
132
|
|
|
133
|
+
[tool.ty]
|
|
134
|
+
environment.python-version = "3.14"
|
|
135
|
+
|
|
133
136
|
[tool.coverage]
|
|
134
137
|
run.parallel = true
|
|
135
138
|
run.plugins = [
|
|
@@ -143,6 +146,3 @@ paths.source = [
|
|
|
143
146
|
report.fail_under = 100
|
|
144
147
|
html.show_contexts = true
|
|
145
148
|
html.skip_covered = false
|
|
146
|
-
|
|
147
|
-
[tool.ty]
|
|
148
|
-
environment.python-version = "3.14"
|
{sphinx_argparse_cli-1.21.2 → sphinx_argparse_cli-1.21.4}/src/sphinx_argparse_cli/__init__.py
RENAMED
|
@@ -15,10 +15,24 @@ def setup(app: Sphinx) -> dict[str, Any]:
|
|
|
15
15
|
|
|
16
16
|
app.add_directive(SphinxArgparseCli.name, SphinxArgparseCli)
|
|
17
17
|
app.add_config_value("sphinx_argparse_cli_prefix_document", False, "env") # noqa: FBT003
|
|
18
|
+
app.add_css_file("sphinx_argparse_cli.css")
|
|
19
|
+
app.connect("build-finished", _write_css)
|
|
18
20
|
|
|
19
21
|
return {"parallel_read_safe": True}
|
|
20
22
|
|
|
21
23
|
|
|
24
|
+
def _write_css(app: Sphinx, exception: Exception | None) -> None:
|
|
25
|
+
if exception or not app.builder or app.builder.format != "html":
|
|
26
|
+
return
|
|
27
|
+
from pathlib import Path # noqa: PLC0415
|
|
28
|
+
|
|
29
|
+
static = Path(app.outdir) / "_static"
|
|
30
|
+
static.mkdir(parents=True, exist_ok=True)
|
|
31
|
+
(static / "sphinx_argparse_cli.css").write_text(
|
|
32
|
+
".sphinx-argparse-cli-wrap pre { white-space: pre-wrap; word-wrap: break-word; }\n"
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
|
|
22
36
|
__all__ = [
|
|
23
37
|
"__version__",
|
|
24
38
|
]
|
|
@@ -17,16 +17,18 @@ from argparse import (
|
|
|
17
17
|
from collections import defaultdict
|
|
18
18
|
from contextlib import contextmanager
|
|
19
19
|
from pathlib import Path
|
|
20
|
-
from typing import TYPE_CHECKING, Any, ClassVar,
|
|
20
|
+
from typing import TYPE_CHECKING, Any, ClassVar, Final, cast
|
|
21
21
|
from unittest.mock import patch
|
|
22
22
|
|
|
23
23
|
from docutils.nodes import (
|
|
24
24
|
Element,
|
|
25
|
+
FixedTextElement,
|
|
25
26
|
Node,
|
|
26
27
|
Text,
|
|
27
28
|
bullet_list,
|
|
28
29
|
container,
|
|
29
30
|
fully_normalize_name,
|
|
31
|
+
inline,
|
|
30
32
|
list_item,
|
|
31
33
|
literal,
|
|
32
34
|
literal_block,
|
|
@@ -48,22 +50,10 @@ if TYPE_CHECKING:
|
|
|
48
50
|
|
|
49
51
|
from docutils.parsers.rst.states import RSTState, RSTStateMachine
|
|
50
52
|
from sphinx.domains.std import StandardDomain
|
|
53
|
+
from sphinx.util.logging import SphinxLoggerAdapter
|
|
51
54
|
|
|
52
55
|
|
|
53
|
-
|
|
54
|
-
text: str
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
def make_id(key: str) -> str:
|
|
58
|
-
return "-".join(key.split()).rstrip("-")
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
def make_id_lower(key: str) -> str:
|
|
62
|
-
# replace all capital letters "X" with "_lower(X)"
|
|
63
|
-
return re.sub("[A-Z]", lambda m: "_" + m.group(0).lower(), make_id(key))
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
logger = getLogger(__name__)
|
|
56
|
+
_LOGGER: Final[SphinxLoggerAdapter] = getLogger(__name__)
|
|
67
57
|
|
|
68
58
|
|
|
69
59
|
class SphinxArgparseCli(SphinxDirective):
|
|
@@ -82,9 +72,8 @@ class SphinxArgparseCli(SphinxDirective):
|
|
|
82
72
|
"group_title_prefix": unchanged,
|
|
83
73
|
"group_sub_title_prefix": unchanged,
|
|
84
74
|
"no_default_values": unchanged,
|
|
85
|
-
# :ref: only supports lower-case
|
|
86
|
-
#
|
|
87
|
-
# this is backwards incompatible for URL's, this is opt-in.
|
|
75
|
+
# :ref: only supports lower-case, so this prefixes would-be-upper-case chars with _;
|
|
76
|
+
# opt-in because it breaks existing URLs.
|
|
88
77
|
"force_refs_lower": flag,
|
|
89
78
|
}
|
|
90
79
|
|
|
@@ -106,7 +95,7 @@ class SphinxArgparseCli(SphinxDirective):
|
|
|
106
95
|
self._parser: ArgumentParser | None = None
|
|
107
96
|
self._std_domain: StandardDomain = cast("StandardDomain", self.env.get_domain("std"))
|
|
108
97
|
self._raw_format: bool = False
|
|
109
|
-
self.
|
|
98
|
+
self._make_id = make_id_lower if "force_refs_lower" in self.options else make_id
|
|
110
99
|
|
|
111
100
|
@property
|
|
112
101
|
def parser(self) -> ArgumentParser:
|
|
@@ -145,17 +134,16 @@ class SphinxArgparseCli(SphinxDirective):
|
|
|
145
134
|
self._parser.prog = new_prog
|
|
146
135
|
_update_sub_parser_prog(self._parser, old_prog, new_prog)
|
|
147
136
|
|
|
148
|
-
|
|
137
|
+
formatter = self._parser.formatter_class
|
|
138
|
+
self._raw_format = isinstance(formatter, type) and issubclass(formatter, RawDescriptionHelpFormatter)
|
|
149
139
|
return self._parser
|
|
150
140
|
|
|
151
141
|
def _load_sub_parsers(
|
|
152
142
|
self, sub_parser: _SubParsersAction[ArgumentParser]
|
|
153
143
|
) -> Iterator[tuple[list[str], str, ArgumentParser]]:
|
|
154
144
|
parser_to_args: dict[int, list[str]] = defaultdict(list)
|
|
155
|
-
str_to_parser: dict[str, ArgumentParser] = {}
|
|
156
145
|
for key, parser in sub_parser._name_parser_map.items(): # noqa: SLF001
|
|
157
146
|
parser_to_args[id(parser)].append(key)
|
|
158
|
-
str_to_parser[key] = parser
|
|
159
147
|
done_parser: set[int] = set()
|
|
160
148
|
|
|
161
149
|
for name, parser in sub_parser.choices.items():
|
|
@@ -169,28 +157,24 @@ class SphinxArgparseCli(SphinxDirective):
|
|
|
169
157
|
help_msg = next((a.help for a in sub_parser._choices_actions if a.dest == name), None) or "" # noqa: SLF001
|
|
170
158
|
yield aliases, help_msg, parser
|
|
171
159
|
|
|
172
|
-
# If this parser has a subparser, recurse into it
|
|
173
160
|
if parser._subparsers: # noqa: SLF001
|
|
174
161
|
sub_sub_parser: _SubParsersAction[ArgumentParser] = parser._subparsers._group_actions[0] # type: ignore[assignment] # noqa: SLF001
|
|
175
162
|
yield from self._load_sub_parsers(sub_sub_parser)
|
|
176
163
|
|
|
177
|
-
def
|
|
164
|
+
def _iter_sub_commands(self) -> Iterator[tuple[list[str], str, ArgumentParser]]:
|
|
178
165
|
top_sub_parser = self.parser._subparsers # noqa: SLF001
|
|
179
166
|
if not top_sub_parser:
|
|
180
167
|
return
|
|
181
|
-
sub_parser: _SubParsersAction[ArgumentParser]
|
|
182
|
-
sub_parser = top_sub_parser._group_actions[0] # type: ignore[assignment] # noqa: SLF001
|
|
183
|
-
|
|
168
|
+
sub_parser: _SubParsersAction[ArgumentParser] = top_sub_parser._group_actions[0] # type: ignore[assignment] # noqa: SLF001
|
|
184
169
|
yield from self._load_sub_parsers(sub_parser)
|
|
185
170
|
|
|
186
171
|
def run(self) -> list[Node]:
|
|
187
|
-
#
|
|
188
|
-
self.env.note_reread() # this document needs to be always updated
|
|
172
|
+
self.env.note_reread() # this document needs to always be rebuilt
|
|
189
173
|
title_text = self.options.get("title", f"{self.parser.prog} - CLI interface").strip()
|
|
190
174
|
if not title_text:
|
|
191
175
|
home_section: Element = container("")
|
|
192
176
|
else:
|
|
193
|
-
home_section = section("", title("", Text(title_text)), ids=[self.
|
|
177
|
+
home_section = section("", title("", Text(title_text)), ids=[self._make_id(title_text)], names=[title_text])
|
|
194
178
|
|
|
195
179
|
if "usage_first" in self.options:
|
|
196
180
|
home_section += self._mk_usage(self.parser)
|
|
@@ -201,15 +185,13 @@ class SphinxArgparseCli(SphinxDirective):
|
|
|
201
185
|
if "usage_first" not in self.options:
|
|
202
186
|
home_section += self._mk_usage(self.parser)
|
|
203
187
|
|
|
204
|
-
# construct groups excluding sub-parsers
|
|
205
188
|
for group in self.parser._action_groups: # noqa: SLF001
|
|
206
189
|
if not group._group_actions or group is self.parser._subparsers: # noqa: SLF001
|
|
207
190
|
continue
|
|
208
191
|
home_section += self._mk_option_group(
|
|
209
192
|
group, prefix=self.parser.prog.split("/")[-1], prog=self.parser.prog.split("/")[-1]
|
|
210
193
|
)
|
|
211
|
-
|
|
212
|
-
for aliases, help_msg, parser in self.load_sub_parsers():
|
|
194
|
+
for aliases, help_msg, parser in self._iter_sub_commands():
|
|
213
195
|
home_section += self._mk_sub_command(aliases, help_msg, parser)
|
|
214
196
|
|
|
215
197
|
if epilog := self._pre_format(self.options.get("epilog", self.parser.epilog)):
|
|
@@ -224,17 +206,19 @@ class SphinxArgparseCli(SphinxDirective):
|
|
|
224
206
|
if block is None:
|
|
225
207
|
return None
|
|
226
208
|
if self._raw_format and "\n" in block:
|
|
227
|
-
lit = literal_block("", Text(block))
|
|
209
|
+
lit = literal_block("", Text(block), classes=["sphinx-argparse-cli-wrap"])
|
|
228
210
|
lit["language"] = "none"
|
|
229
211
|
return lit
|
|
230
|
-
|
|
212
|
+
para = paragraph("", Text(block))
|
|
213
|
+
_protect_option_dashes(para)
|
|
214
|
+
return para
|
|
231
215
|
|
|
232
216
|
def _mk_option_group(self, group: _ArgumentGroup, prefix: str, prog: str) -> section:
|
|
233
217
|
sub_title_prefix: str = self.options["group_sub_title_prefix"]
|
|
234
218
|
title_prefix = self.options["group_title_prefix"]
|
|
235
219
|
title_text = self._build_opt_grp_title(group, prefix, prog, sub_title_prefix, title_prefix)
|
|
236
220
|
title_ref: str = f"{prefix}{' ' if prefix else ''}{group.title}"
|
|
237
|
-
ref_id = self.
|
|
221
|
+
ref_id = self._make_id(title_ref)
|
|
238
222
|
# the text sadly needs to be prefixed, because otherwise the autosectionlabel will conflict
|
|
239
223
|
header = title("", Text(title_text))
|
|
240
224
|
group_section = section("", header, ids=[ref_id], names=[ref_id])
|
|
@@ -264,15 +248,11 @@ class SphinxArgparseCli(SphinxDirective):
|
|
|
264
248
|
if action.metavar:
|
|
265
249
|
as_key = action.metavar if isinstance(action.metavar, str) else action.metavar[0]
|
|
266
250
|
if action.option_strings:
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
for opt in action.option_strings:
|
|
270
|
-
if first:
|
|
271
|
-
first = False
|
|
272
|
-
else:
|
|
251
|
+
for at, opt in enumerate(action.option_strings):
|
|
252
|
+
if at:
|
|
273
253
|
line += Text(", ")
|
|
274
254
|
self._mk_option_name(line, prefix, opt)
|
|
275
|
-
if
|
|
255
|
+
if action.nargs != 0:
|
|
276
256
|
line += Text(" ")
|
|
277
257
|
metavar_text = (
|
|
278
258
|
" ".join(meta.upper() for meta in action.metavar)
|
|
@@ -283,7 +263,6 @@ class SphinxArgparseCli(SphinxDirective):
|
|
|
283
263
|
else:
|
|
284
264
|
self._mk_option_name(line, prefix, as_key)
|
|
285
265
|
|
|
286
|
-
point = list_item("", line, ids=[])
|
|
287
266
|
if action.help:
|
|
288
267
|
help_text = load_help_text(action.help)
|
|
289
268
|
temp = paragraph()
|
|
@@ -301,16 +280,15 @@ class SphinxArgparseCli(SphinxDirective):
|
|
|
301
280
|
line += Text(" (default: ")
|
|
302
281
|
line += literal(text=str(action.default).replace(str(Path.cwd()), "{cwd}"))
|
|
303
282
|
line += Text(")")
|
|
304
|
-
|
|
283
|
+
_protect_option_dashes(line)
|
|
284
|
+
return list_item("", line, ids=[])
|
|
305
285
|
|
|
306
286
|
def _mk_option_name(self, line: paragraph, prefix: str, opt: str) -> None:
|
|
307
|
-
ref_id = self.
|
|
287
|
+
ref_id = self._make_id(f"{prefix}-{opt}")
|
|
308
288
|
ref_title = f"{prefix} {opt}"
|
|
309
289
|
ref = reference("", refid=ref_id, reftitle=ref_title)
|
|
310
290
|
line.attributes["ids"].append(ref_id)
|
|
311
|
-
|
|
312
|
-
st += literal(text=opt)
|
|
313
|
-
ref += st
|
|
291
|
+
ref += strong("", "", literal(text=opt))
|
|
314
292
|
self._register_ref(ref_id, ref_title, ref, is_cli_option=True)
|
|
315
293
|
line += ref
|
|
316
294
|
|
|
@@ -328,7 +306,7 @@ class SphinxArgparseCli(SphinxDirective):
|
|
|
328
306
|
else:
|
|
329
307
|
name = normalize_name(ref_name)
|
|
330
308
|
if name in self._std_domain.labels:
|
|
331
|
-
|
|
309
|
+
_LOGGER.warning(
|
|
332
310
|
__("duplicate label %s, other instance in %s"),
|
|
333
311
|
name,
|
|
334
312
|
self.env.doc2path(self._std_domain.labels[name][0]),
|
|
@@ -354,7 +332,7 @@ class SphinxArgparseCli(SphinxDirective):
|
|
|
354
332
|
title_text += aliases_text
|
|
355
333
|
title_ref += aliases_text
|
|
356
334
|
title_text = title_text.strip()
|
|
357
|
-
ref_id = self.
|
|
335
|
+
ref_id = self._make_id(title_ref)
|
|
358
336
|
group_section = section("", title("", Text(title_text)), ids=[ref_id], names=[title_ref])
|
|
359
337
|
self._register_ref(ref_id, title_ref, group_section)
|
|
360
338
|
|
|
@@ -364,16 +342,16 @@ class SphinxArgparseCli(SphinxDirective):
|
|
|
364
342
|
command_desc = (parser.description or help_msg or "").strip()
|
|
365
343
|
if command_desc:
|
|
366
344
|
desc_paragraph = paragraph("", Text(command_desc))
|
|
345
|
+
_protect_option_dashes(desc_paragraph)
|
|
367
346
|
group_section += desc_paragraph
|
|
368
347
|
|
|
369
348
|
if "usage_first" not in self.options:
|
|
370
349
|
group_section += self._mk_usage(parser)
|
|
371
350
|
|
|
372
351
|
for group in parser._action_groups: # noqa: SLF001
|
|
373
|
-
if not group._group_actions: #
|
|
352
|
+
if not group._group_actions: # noqa: SLF001
|
|
374
353
|
continue
|
|
375
354
|
if isinstance(group._group_actions[0], _SubParsersAction): # noqa: SLF001
|
|
376
|
-
# If this is a subparser, ignore it
|
|
377
355
|
continue
|
|
378
356
|
group_section += self._mk_option_group(group, prefix=parser.prog, prog=self.parser.prog.split("/")[-1])
|
|
379
357
|
return group_section
|
|
@@ -414,33 +392,64 @@ class SphinxArgparseCli(SphinxDirective):
|
|
|
414
392
|
@staticmethod
|
|
415
393
|
def _apply_sub_title(title_text: str, sub_title_prefix: str, prog: str, sub_cmd: str) -> str:
|
|
416
394
|
if sub_title_prefix:
|
|
417
|
-
sub_title_prefix = sub_title_prefix.replace("{prog}", prog)
|
|
418
|
-
sub_title_prefix = sub_title_prefix.replace("{subcommand}", sub_cmd)
|
|
395
|
+
sub_title_prefix = sub_title_prefix.replace("{prog}", prog).replace("{subcommand}", sub_cmd)
|
|
419
396
|
title_text += f"{sub_title_prefix} "
|
|
420
397
|
return title_text
|
|
421
398
|
|
|
422
399
|
def _mk_usage(self, parser: ArgumentParser) -> literal_block:
|
|
423
400
|
parser.formatter_class = lambda prog: HelpFormatter(prog, width=self.options.get("usage_width", 100))
|
|
424
|
-
with self.
|
|
401
|
+
with self._no_color():
|
|
425
402
|
texts = parser.format_usage()[len("usage: ") :].splitlines()
|
|
426
403
|
texts = [line if at == 0 else f"{' ' * (len(parser.prog) + 1)}{line.lstrip()}" for at, line in enumerate(texts)]
|
|
427
|
-
return literal_block("", Text("\n".join(texts)))
|
|
404
|
+
return literal_block("", Text("\n".join(texts)), classes=["sphinx-argparse-cli-wrap"])
|
|
428
405
|
|
|
429
406
|
@contextmanager
|
|
430
|
-
def
|
|
407
|
+
def _no_color(self) -> Iterator[None]:
|
|
431
408
|
with patch.dict(os.environ, {"NO_COLOR": "1"}, clear=False):
|
|
432
|
-
yield
|
|
409
|
+
yield
|
|
433
410
|
|
|
434
411
|
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
412
|
+
def make_id_lower(key: str) -> str:
|
|
413
|
+
return re.sub("[A-Z]", lambda m: f"_{m.group(0).lower()}", make_id(key))
|
|
414
|
+
|
|
415
|
+
|
|
416
|
+
def make_id(key: str) -> str:
|
|
417
|
+
return "-".join(key.split()).rstrip("-")
|
|
418
|
+
|
|
419
|
+
|
|
420
|
+
_HELP_SUBSTITUTIONS: Final[list[tuple[re.Pattern[str], str]]] = [
|
|
421
|
+
(re.compile(r"[']+(.+?)[']+"), "``'\\1'``"),
|
|
422
|
+
(re.compile(r'["]+(.+?)["]+'), '``"\\1"``'),
|
|
423
|
+
(re.compile(r"[{](.+?)[}]"), "``{\\1}``"),
|
|
424
|
+
]
|
|
438
425
|
|
|
439
426
|
|
|
440
427
|
def load_help_text(help_text: str) -> str:
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
return
|
|
428
|
+
for pattern, replacement in _HELP_SUBSTITUTIONS:
|
|
429
|
+
help_text = pattern.sub(replacement, help_text)
|
|
430
|
+
return help_text
|
|
431
|
+
|
|
432
|
+
|
|
433
|
+
_OPTION_TOKEN: Final[re.Pattern[str]] = re.compile(r"((?<!\w)--[a-zA-Z0-9][\w-]*)")
|
|
434
|
+
|
|
435
|
+
|
|
436
|
+
def _protect_option_dashes(node: Element) -> None:
|
|
437
|
+
"""Wrap each ``--option`` in a smartquotes-exempt inline so its ``--`` survives Sphinx's en-dash transform."""
|
|
438
|
+
for text in list(node.findall(Text)):
|
|
439
|
+
if isinstance(text.parent, (literal, FixedTextElement)):
|
|
440
|
+
continue
|
|
441
|
+
# A capturing group makes split() interleave the option tokens (odd indices) with the surrounding text.
|
|
442
|
+
parts = _OPTION_TOKEN.split(text)
|
|
443
|
+
if len(parts) == 1:
|
|
444
|
+
continue
|
|
445
|
+
text.parent.replace(
|
|
446
|
+
text,
|
|
447
|
+
[
|
|
448
|
+
inline("", part, support_smartquotes=False) if index % 2 else Text(part)
|
|
449
|
+
for index, part in enumerate(parts)
|
|
450
|
+
if part
|
|
451
|
+
],
|
|
452
|
+
)
|
|
444
453
|
|
|
445
454
|
|
|
446
455
|
class HookError(Exception):
|
|
@@ -452,7 +461,7 @@ def _parse_known_args_hook(self: ArgumentParser, *args: Any, **kwargs: Any) -> N
|
|
|
452
461
|
raise HookError(self)
|
|
453
462
|
|
|
454
463
|
|
|
455
|
-
_ANSI_COLOR_RE = re.compile(r"\x1b\[[0-9;]*m")
|
|
464
|
+
_ANSI_COLOR_RE: Final[re.Pattern[str]] = re.compile(r"\x1b\[[0-9;]*m")
|
|
456
465
|
|
|
457
466
|
|
|
458
467
|
def _strip_ansi_colors(text: str) -> str: # pragma: >=3.14 cover
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# file generated by vcs-versioning
|
|
2
|
+
# don't change, don't track in version control
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
__all__ = [
|
|
6
|
+
"__version__",
|
|
7
|
+
"__version_tuple__",
|
|
8
|
+
"version",
|
|
9
|
+
"version_tuple",
|
|
10
|
+
"__commit_id__",
|
|
11
|
+
"commit_id",
|
|
12
|
+
]
|
|
13
|
+
|
|
14
|
+
version: str
|
|
15
|
+
__version__: str
|
|
16
|
+
__version_tuple__: tuple[int | str, ...]
|
|
17
|
+
version_tuple: tuple[int | str, ...]
|
|
18
|
+
commit_id: str | None
|
|
19
|
+
__commit_id__: str | None
|
|
20
|
+
|
|
21
|
+
__version__ = version = '1.21.4'
|
|
22
|
+
__version_tuple__ = version_tuple = (1, 21, 4)
|
|
23
|
+
|
|
24
|
+
__commit_id__ = commit_id = None
|
|
@@ -7,7 +7,7 @@ from typing import TYPE_CHECKING
|
|
|
7
7
|
|
|
8
8
|
import pytest
|
|
9
9
|
|
|
10
|
-
from sphinx_argparse_cli._logic import make_id, make_id_lower
|
|
10
|
+
from sphinx_argparse_cli._logic import load_help_text, make_id, make_id_lower
|
|
11
11
|
|
|
12
12
|
if TYPE_CHECKING:
|
|
13
13
|
from io import StringIO
|
|
@@ -133,6 +133,28 @@ def test_multiline_epilog_as_html(build_outcome: str) -> None:
|
|
|
133
133
|
assert ref in build_outcome
|
|
134
134
|
|
|
135
135
|
|
|
136
|
+
@pytest.mark.sphinx(buildername="html", testroot="epilog-multiline-subclass")
|
|
137
|
+
def test_multiline_epilog_subclass_formatter_as_html(build_outcome: str) -> None:
|
|
138
|
+
ref = (
|
|
139
|
+
"This epilog\nspans multiple lines.\n\n this line is indented.\n and also this.\n\nNow this should be"
|
|
140
|
+
" a separate paragraph.\n"
|
|
141
|
+
)
|
|
142
|
+
assert ref in build_outcome
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
@pytest.mark.sphinx(buildername="html", testroot="smartquotes")
|
|
146
|
+
def test_option_dashes_survive_smartquotes(build_outcome: str) -> None:
|
|
147
|
+
# option names mentioned in parser-supplied text keep their double hyphen
|
|
148
|
+
assert "Pass input via <span>--text</span> or stdin." in build_outcome
|
|
149
|
+
assert "see also <span>--2fa</span> and <span>--dry_run</span>." in build_outcome
|
|
150
|
+
assert "combine with <span>--output</span> for files" in build_outcome
|
|
151
|
+
assert "build things with <span>--flair</span>" in build_outcome
|
|
152
|
+
for mangled in ("\u2013text", "\u2013output", "\u20132fa", "\u2013dry_run", "\u2013flair"):
|
|
153
|
+
assert mangled not in build_outcome
|
|
154
|
+
# the surrounding text keeps normal typography
|
|
155
|
+
assert "north\u2013south and 10\u201320" in build_outcome
|
|
156
|
+
|
|
157
|
+
|
|
136
158
|
@pytest.mark.sphinx(buildername="text", testroot="complex")
|
|
137
159
|
@pytest.mark.prepare(directive_args=[":usage_width: 100"])
|
|
138
160
|
def test_usage_width_default(build_outcome: str) -> None:
|
|
@@ -170,10 +192,7 @@ def test_suppressed_action(build_outcome: str) -> None:
|
|
|
170
192
|
],
|
|
171
193
|
)
|
|
172
194
|
def test_help_loader(example: str, output: str) -> None:
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
result = load_help_text(example)
|
|
176
|
-
assert result == output
|
|
195
|
+
assert load_help_text(example) == output
|
|
177
196
|
|
|
178
197
|
|
|
179
198
|
@pytest.mark.sphinx(buildername="html", testroot="ref")
|
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
# file generated by setuptools-scm
|
|
2
|
-
# don't change, don't track in version control
|
|
3
|
-
|
|
4
|
-
__all__ = [
|
|
5
|
-
"__version__",
|
|
6
|
-
"__version_tuple__",
|
|
7
|
-
"version",
|
|
8
|
-
"version_tuple",
|
|
9
|
-
"__commit_id__",
|
|
10
|
-
"commit_id",
|
|
11
|
-
]
|
|
12
|
-
|
|
13
|
-
TYPE_CHECKING = False
|
|
14
|
-
if TYPE_CHECKING:
|
|
15
|
-
from typing import Tuple
|
|
16
|
-
from typing import Union
|
|
17
|
-
|
|
18
|
-
VERSION_TUPLE = Tuple[Union[int, str], ...]
|
|
19
|
-
COMMIT_ID = Union[str, None]
|
|
20
|
-
else:
|
|
21
|
-
VERSION_TUPLE = object
|
|
22
|
-
COMMIT_ID = object
|
|
23
|
-
|
|
24
|
-
version: str
|
|
25
|
-
__version__: str
|
|
26
|
-
__version_tuple__: VERSION_TUPLE
|
|
27
|
-
version_tuple: VERSION_TUPLE
|
|
28
|
-
commit_id: COMMIT_ID
|
|
29
|
-
__commit_id__: COMMIT_ID
|
|
30
|
-
|
|
31
|
-
__version__ = version = '1.21.2'
|
|
32
|
-
__version_tuple__ = version_tuple = (1, 21, 2)
|
|
33
|
-
|
|
34
|
-
__commit_id__ = commit_id = None
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|