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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sphinx-argparse-cli
3
- Version: 1.21.2
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 automatically.
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 entirely:
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 — each upper-case letter becomes its lower-case form prefixed with `_` (e.g. `A`
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 automatically.
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 entirely:
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 — each upper-case letter becomes its lower-case form prefixed with `_` (e.g. `A`
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.28",
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.0.2",
60
- "pytest-cov>=7",
59
+ "pytest>=9.1.1",
60
+ "pytest-cov>=7.1",
61
61
  ]
62
62
  type = [
63
- "ty>=0.0.17",
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.10.2",
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"
@@ -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, NamedTuple, cast
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
- class TextAsDefault(NamedTuple):
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. If this is set, any
86
- # would-be-upper-case chars will be prefixed with _. Since
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.make_id = make_id_lower if "force_refs_lower" in self.options else make_id
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
- self._raw_format = self._parser.formatter_class == RawDescriptionHelpFormatter
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 load_sub_parsers(self) -> Iterator[tuple[list[str], str, ArgumentParser]]:
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
- # construct headers
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.make_id(title_text)], names=[title_text])
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
- # construct sub-parser
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
- return paragraph("", Text(block))
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.make_id(title_ref)
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
- first = True
268
- is_flag = action.nargs == 0
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 not is_flag:
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
- return point
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.make_id(f"{prefix}-{opt}")
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
- st = strong()
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
- logger.warning(
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.make_id(title_ref)
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: # do not show empty groups # noqa: SLF001
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.no_color():
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 no_color(self) -> Iterator[None]:
407
+ def _no_color(self) -> Iterator[None]:
431
408
  with patch.dict(os.environ, {"NO_COLOR": "1"}, clear=False):
432
- yield None
409
+ yield
433
410
 
434
411
 
435
- SINGLE_QUOTE = re.compile(r"[']+(.+?)[']+")
436
- DOUBLE_QUOTE = re.compile(r'["]+(.+?)["]+')
437
- CURLY_BRACES = re.compile(r"[{](.+?)[}]")
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
- single_quote = SINGLE_QUOTE.sub("``'\\1'``", help_text)
442
- double_quote = DOUBLE_QUOTE.sub('``"\\1"``', single_quote)
443
- return CURLY_BRACES.sub("``{\\1}``", double_quote)
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
- from sphinx_argparse_cli._logic import load_help_text # noqa: PLC0415
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,7 +1,7 @@
1
1
  from __future__ import annotations
2
2
 
3
+ import sphinx_argparse_cli
3
4
 
4
- def test_version() -> None:
5
- import sphinx_argparse_cli # noqa: PLC0415
6
5
 
6
+ def test_version() -> None:
7
7
  assert sphinx_argparse_cli.__version__
@@ -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