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.
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.4
1
+ Metadata-Version: 2.5
2
2
  Name: sphinx-argparse-cli
3
- Version: 1.22.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.30.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.2.2",
68
+ "pre-commit-uv>=4.3",
69
69
  ]
70
70
  pkg-meta = [
71
71
  "check-wheel-contents>=0.6.3",
72
- "twine>=6.2",
73
- "uv>=0.11.26",
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 = "py310"
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.14"
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
- original_parse_known_args = ArgumentParser.parse_known_args
97
- ArgumentParser.parse_known_args = _parse_known_args_hook # type: ignore[method-assign,assignment]
98
- try:
99
- parser_creator()
100
- except HookError as hooked:
101
- parser = hooked.parser
102
- finally:
103
- ArgumentParser.parse_known_args = original_parse_known_args
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) or "" # noqa: SLF001
148
- yield aliases, help_msg, parser
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._subparsers: # noqa: SLF001
151
- sub_sub_parser: _SubParsersAction[ArgumentParser] = parser._subparsers._group_actions[0] # type: ignore[assignment] # noqa: SLF001
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
- top_sub_parser = self.parser._subparsers # noqa: SLF001
157
- if not top_sub_parser:
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 not group._group_actions or group is self.parser._subparsers: # noqa: SLF001
181
- continue
182
- home_section += self._mk_option_group(
183
- group, prefix=self.parser.prog.split("/")[-1], prog=self.parser.prog.split("/")[-1]
184
- )
185
- for aliases, help_msg, parser in self._iter_sub_commands():
186
- home_section += self._mk_sub_command(aliases, help_msg, parser)
187
-
188
- if epilog := self._pre_format(self.options.get("epilog", self.parser.epilog)):
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
- if self._raw_format and "\n" in block:
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(self, group: _ArgumentGroup, prefix: str, prog: str) -> section:
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
- title_text = self._build_opt_grp_title(group, prefix, prog, sub_title_prefix, title_prefix)
211
- title_ref: str = f"{prefix}{' ' if prefix else ''}{group.title}"
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 group._group_actions: # noqa: SLF001
221
- if action.help == SUPPRESS:
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 _build_opt_grp_title(
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 action.nargs != 0:
242
+ if args_text is not None:
247
243
  line += Text(" ")
248
- metavar_text = (
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
- self._mk_option_name(line, prefix, as_key)
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
- if action.help:
258
- help_text = load_help_text(action.help)
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 += Text(" - ")
262
- for content in cast("paragraph", temp.children[0]).children:
263
- line += content
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 re.match(r".*[ (]default[s]? .*", (action.help or ""))
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
- _protect_option_dashes(line)
275
- return list_item("", line, ids=[])
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
- title_text = self._build_sub_cmd_title(parser, sub_title_prefix, title_prefix)
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 = (parser.description or help_msg or "").strip()
334
- if command_desc:
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 not group._group_actions: # noqa: SLF001
344
- continue
345
- if isinstance(group._group_actions[0], _SubParsersAction): # noqa: SLF001
346
- continue
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
- parser.formatter_class = lambda prog: HelpFormatter(prog, width=self.options.get("usage_width", 100))
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
- with patch.dict(os.environ, {"NO_COLOR": "1"}, clear=False):
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
- (re.compile(r"[']+(.+?)[']+"), "``'\\1'``"),
413
- (re.compile(r'["]+(.+?)["]+'), '``"\\1"``'),
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 not (sub_parsers := parser._subparsers): # noqa: SLF001
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)
@@ -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.1'
22
- __version_tuple__ = version_tuple = (1, 22, 1)
21
+ __version__ = version = '1.24.0'
22
+ __version_tuple__ = version_tuple = (1, 24, 0)
23
23
 
24
24
  __commit_id__ = commit_id = None
@@ -16,7 +16,7 @@ complex options
16
16
 
17
17
  * **"--no-help"**
18
18
 
19
- * **"--outdir"** "OUT_DIR", **"-o"** "OUT_DIR" - output directory
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,7 +1,7 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import os
4
- import sys
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
- name = "complex.txt" if sys.version_info >= (3, 10) else "complex_pre_310.txt"
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\n foo\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\nNow this should be"
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\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\nNow this should be"
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\nNow this should be"
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 "--activities-since" not in build_outcome
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 baronlyroot (f)<a class="headerlink" href="#bar-root-first-(f)"' in build_outcome
290
- assert '<h3>bar baronlyroot first positional arguments<a class="headerlink"' in build_outcome
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