argparse-help-markdown 0.1.0__py3-none-any.whl
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.
- argparse_help_markdown-0.1.0.dist-info/METADATA +45 -0
- argparse_help_markdown-0.1.0.dist-info/RECORD +6 -0
- argparse_help_markdown-0.1.0.dist-info/WHEEL +5 -0
- argparse_help_markdown-0.1.0.dist-info/entry_points.txt +2 -0
- argparse_help_markdown-0.1.0.dist-info/top_level.txt +1 -0
- argparse_help_markdown.py +425 -0
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: argparse-help-markdown
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Pull argparse configuration out of a CLI tool and write it as Markdown. Great for embedding in a README.md with cog.
|
|
5
|
+
Author-email: ellieayla <1447600+me@users.noreply.github.com>
|
|
6
|
+
Requires-Python: >=3.8
|
|
7
|
+
Description-Content-Type: text/markdown
|
|
8
|
+
|
|
9
|
+
Pull argparse configuration out of a CLI tool and write it as Markdown.
|
|
10
|
+
|
|
11
|
+
Great for embedded in a README.md with cog.
|
|
12
|
+
|
|
13
|
+
Possible invocation options:
|
|
14
|
+
|
|
15
|
+
```sh
|
|
16
|
+
# entrypoint command, dotted module
|
|
17
|
+
uv run argparse-help-markdown -m tests.data.example
|
|
18
|
+
|
|
19
|
+
# script, script
|
|
20
|
+
argparse_help_markdown.py script.py
|
|
21
|
+
|
|
22
|
+
# import, function, custom writer
|
|
23
|
+
from argparse_help_markdown import run_script
|
|
24
|
+
with open("out.md", mode="w") as f:
|
|
25
|
+
run_script("mytool.py", include_usage=True, writer=f)
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
<!-- [[[cog
|
|
29
|
+
import src.argparse_help_markdown as m
|
|
30
|
+
m.run(filename="src/argparse_help_markdown.py", include_usage=True, writer=None)
|
|
31
|
+
]]] -->
|
|
32
|
+
```
|
|
33
|
+
usage: cog script.py
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
| Options | Values | Help |
|
|
37
|
+
| ------- | ------- | ---- |
|
|
38
|
+
| *positional arguments* | |
|
|
39
|
+
| <pre>filename</pre> | Optional. | Path to the subject script. |
|
|
40
|
+
| *options* | |
|
|
41
|
+
| <pre>-h --help</pre> | Flag. | show this help message and exit |
|
|
42
|
+
| <pre>--usage</pre> | Flag. | Emit the terse usage info in triple-ticks. Excluded by default. |
|
|
43
|
+
| <pre>--write</pre> | Optional. | Write to named file instead of stdout. |
|
|
44
|
+
| <pre>-m --module</pre> | Optional. | Run as module. |
|
|
45
|
+
<!-- [[[end]]] -->
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
argparse_help_markdown.py,sha256=Kr6ra1IodPZUs1cuw73SHbS9bQsvu37T31_c0nd24O8,14915
|
|
2
|
+
argparse_help_markdown-0.1.0.dist-info/METADATA,sha256=Ptp9rCk0ygzXzeuNB-MjY0k92fm1Nc_ShP8Tm-mmxlo,1456
|
|
3
|
+
argparse_help_markdown-0.1.0.dist-info/WHEEL,sha256=YCfwYGOYMi5Jhw2fU4yNgwErybb2IX5PEwBKV4ZbdBo,91
|
|
4
|
+
argparse_help_markdown-0.1.0.dist-info/entry_points.txt,sha256=SIlqLN8FnijFjQxI5xkpJrO2dxARXFY7rd7jVi2jH0Y,71
|
|
5
|
+
argparse_help_markdown-0.1.0.dist-info/top_level.txt,sha256=tpSjVPrQW916VGQwrG1zI_6ioXQYXO_lAh_xw7JcLbw,23
|
|
6
|
+
argparse_help_markdown-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
argparse_help_markdown
|
|
@@ -0,0 +1,425 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import importlib.machinery
|
|
5
|
+
import importlib.util
|
|
6
|
+
import os
|
|
7
|
+
import sys
|
|
8
|
+
from contextlib import contextmanager
|
|
9
|
+
from importlib.machinery import ModuleSpec
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from types import CodeType, ModuleType
|
|
12
|
+
from typing import TYPE_CHECKING, Any, Callable, Generator, Iterable
|
|
13
|
+
from unittest.mock import patch
|
|
14
|
+
|
|
15
|
+
if TYPE_CHECKING:
|
|
16
|
+
from io import TextIOWrapper
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class DummyLoader:
|
|
20
|
+
# See coveragepy, pep302
|
|
21
|
+
def __init__(self, fullname: str, *_args: Any) -> None:
|
|
22
|
+
self.fullname = fullname
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def pre_tag(s: str) -> str:
|
|
26
|
+
"""Wrap strings in <pre> tags."""
|
|
27
|
+
return f"<pre>{s}</pre>"
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def wrap_in_backticks(s: str) -> str:
|
|
31
|
+
"""Wrap `strings` in backticks."""
|
|
32
|
+
return f"`{s}`"
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def escape_pipe(s: str) -> str:
|
|
36
|
+
"""Escape | characters."""
|
|
37
|
+
return s.replace("|", r"\|")
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class MarkdownFormatter(argparse.HelpFormatter):
|
|
41
|
+
"""
|
|
42
|
+
ArgumentParser.format_help() calls the public api of HelpFormatter:
|
|
43
|
+
|
|
44
|
+
# usage
|
|
45
|
+
formatter.add_usage(self.usage, self._actions,
|
|
46
|
+
self._mutually_exclusive_groups)
|
|
47
|
+
|
|
48
|
+
# description
|
|
49
|
+
formatter.add_text(self.description)
|
|
50
|
+
|
|
51
|
+
# positionals, optionals and user-defined groups
|
|
52
|
+
for action_group in self._action_groups:
|
|
53
|
+
formatter.start_section(action_group.title)
|
|
54
|
+
formatter.add_text(action_group.description)
|
|
55
|
+
formatter.add_arguments(action_group._group_actions)
|
|
56
|
+
formatter.end_section()
|
|
57
|
+
|
|
58
|
+
# epilog
|
|
59
|
+
formatter.add_text(self.epilog)
|
|
60
|
+
|
|
61
|
+
# determine help from format above
|
|
62
|
+
return formatter.format_help()
|
|
63
|
+
|
|
64
|
+
Normally HelpFormatter creates a list of callback functions to produce help text,
|
|
65
|
+
but how is *not* part of the public api. We can do whatever we want,
|
|
66
|
+
as long as format_help() returns a string.
|
|
67
|
+
"""
|
|
68
|
+
|
|
69
|
+
prog: str
|
|
70
|
+
items: list[tuple[Callable[..., str], Iterable[Any]]]
|
|
71
|
+
in_table: bool = False
|
|
72
|
+
|
|
73
|
+
def __init__(self, prog: str, *_args: Iterable[Any]) -> None:
|
|
74
|
+
"""Sneak away the prog name, ignore other arguments."""
|
|
75
|
+
# may need to deal with args/kwargs?
|
|
76
|
+
self.prog = prog
|
|
77
|
+
self.items = []
|
|
78
|
+
|
|
79
|
+
def _set_color(self, color: bool) -> None: # noqa: ARG002, FBT001
|
|
80
|
+
"""Compatability: Called directly by ArgumentParser._get_formatter(). Make it a no-op."""
|
|
81
|
+
return
|
|
82
|
+
|
|
83
|
+
def ensure_header(self) -> str:
|
|
84
|
+
if not self.in_table:
|
|
85
|
+
self.in_table = True
|
|
86
|
+
return (
|
|
87
|
+
"| Options | Values | Help |\n"
|
|
88
|
+
"| ------- | ------- | ---- |\n"
|
|
89
|
+
) # fmt: skip
|
|
90
|
+
return "" # pragma: no cover
|
|
91
|
+
|
|
92
|
+
def _add_item(self, func: Callable[[Any], str], args: Iterable[Any]) -> None:
|
|
93
|
+
self.items.append((func, args))
|
|
94
|
+
|
|
95
|
+
def arguments_column(self, action: argparse.Action) -> str:
|
|
96
|
+
default = action.dest
|
|
97
|
+
if action.option_strings:
|
|
98
|
+
return pre_tag(" ".join(action.option_strings))
|
|
99
|
+
return pre_tag(default)
|
|
100
|
+
|
|
101
|
+
def _format_action(self, action: argparse.Action) -> str:
|
|
102
|
+
if action.type and isinstance(action.type, type):
|
|
103
|
+
typename = action.type.__name__
|
|
104
|
+
else:
|
|
105
|
+
typename = "Unknown"
|
|
106
|
+
|
|
107
|
+
# 1
|
|
108
|
+
column_one = self.arguments_column(action)
|
|
109
|
+
|
|
110
|
+
# 2
|
|
111
|
+
column_two_parts: list[str] = []
|
|
112
|
+
|
|
113
|
+
if (
|
|
114
|
+
action.const is None # fmt: skip
|
|
115
|
+
and action.required
|
|
116
|
+
and action.nargs != 0
|
|
117
|
+
):
|
|
118
|
+
column_two_parts.append(action.dest.upper())
|
|
119
|
+
|
|
120
|
+
if action.nargs == 0:
|
|
121
|
+
column_two_parts.append("Flag.")
|
|
122
|
+
elif action.required:
|
|
123
|
+
column_two_parts.append("Required.")
|
|
124
|
+
elif not action.default or action.default is argparse.SUPPRESS:
|
|
125
|
+
column_two_parts.append("Optional.")
|
|
126
|
+
|
|
127
|
+
if action.type is not None:
|
|
128
|
+
column_two_parts.append(f"Type: {typename}")
|
|
129
|
+
if action.choices:
|
|
130
|
+
column_two_parts.append(f"Choice: {', '.join([wrap_in_backticks(x) for x in action.choices])}")
|
|
131
|
+
if action.default and action.default is not argparse.SUPPRESS and action.const is None:
|
|
132
|
+
column_two_parts.append(f"Default: {wrap_in_backticks(action.default)}")
|
|
133
|
+
|
|
134
|
+
# 3
|
|
135
|
+
if action.help and action.help.strip():
|
|
136
|
+
column_three = action.help.strip()
|
|
137
|
+
else:
|
|
138
|
+
column_three = "" # no help
|
|
139
|
+
return f"| {column_one} | {'<br/>'.join(column_two_parts)} | {column_three} |\n"
|
|
140
|
+
|
|
141
|
+
# TODO:
|
|
142
|
+
# if there are any sub-actions, add their help as well
|
|
143
|
+
# for subaction in self._iter_indented_subactions(action):
|
|
144
|
+
# parts.append(self._format_action(subaction))
|
|
145
|
+
|
|
146
|
+
def add_argument(self, action: argparse.Action) -> None:
|
|
147
|
+
if action.help is not argparse.SUPPRESS:
|
|
148
|
+
self._add_item(self._format_action, [action])
|
|
149
|
+
|
|
150
|
+
def add_arguments(self, actions: Iterable[argparse.Action]) -> None:
|
|
151
|
+
for action in actions:
|
|
152
|
+
self.add_argument(action)
|
|
153
|
+
|
|
154
|
+
def _format_text(self, text: str) -> str:
|
|
155
|
+
return f"| {escape_pipe(text)} | |\n"
|
|
156
|
+
|
|
157
|
+
def add_text(self, text: str | None) -> None:
|
|
158
|
+
if text:
|
|
159
|
+
self._add_item(self._format_text, [text])
|
|
160
|
+
|
|
161
|
+
def add_usage(
|
|
162
|
+
self,
|
|
163
|
+
usage: str | None,
|
|
164
|
+
actions: Iterable[argparse.Action],
|
|
165
|
+
groups: Iterable[Any],
|
|
166
|
+
prefix: str | None = None,
|
|
167
|
+
) -> None:
|
|
168
|
+
"""No implementation; usage doesn't go in the table. Just ignore the arguments."""
|
|
169
|
+
|
|
170
|
+
def _format_heading(self, text: str) -> str:
|
|
171
|
+
return f"| *{escape_pipe(text)}* | |\n"
|
|
172
|
+
|
|
173
|
+
def start_section(self, heading: str | None) -> None:
|
|
174
|
+
if heading:
|
|
175
|
+
self._add_item(self._format_heading, [heading])
|
|
176
|
+
|
|
177
|
+
def end_section(self) -> None:
|
|
178
|
+
return
|
|
179
|
+
|
|
180
|
+
def _join_parts(self, part_strings: Iterable[str]) -> str:
|
|
181
|
+
return "".join([part for part in part_strings if part and part is not argparse.SUPPRESS])
|
|
182
|
+
|
|
183
|
+
def format_help(self) -> str:
|
|
184
|
+
"""
|
|
185
|
+
The workhorse. Run all the callbacks in self.items and return a giant string,
|
|
186
|
+
similar to argparse.HelpFormatter._Section.format_help()
|
|
187
|
+
"""
|
|
188
|
+
|
|
189
|
+
item_help = self._join_parts(
|
|
190
|
+
[func(*args) for func, args in self.items],
|
|
191
|
+
)
|
|
192
|
+
return self.ensure_header() + item_help
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
class ParseArgsNotCalledError(Exception):
|
|
196
|
+
"""Failure - parse_args() was never called."""
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
class FinishedGettingOutput(Exception): # noqa: N818
|
|
200
|
+
"""Success 'exception' used to escape from inside argparse."""
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
class NoSourceError(ValueError):
|
|
204
|
+
"""Unable to find/load the source for the given module/script"""
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
@contextmanager
|
|
208
|
+
def open_file_for_writing_or_none(write_filename: str | None) -> Generator[TextIOWrapper | None, None, None]:
|
|
209
|
+
"""If a write_filename was passed, open it for writing. Otherwise use stdout."""
|
|
210
|
+
if write_filename is not None:
|
|
211
|
+
with open(file=write_filename, mode="w") as fh:
|
|
212
|
+
yield fh
|
|
213
|
+
else:
|
|
214
|
+
yield None
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def main() -> int:
|
|
218
|
+
"""
|
|
219
|
+
Parse CLI arguments, open an output file if needed, and run against a script.
|
|
220
|
+
|
|
221
|
+
TODO: Support modules.
|
|
222
|
+
"""
|
|
223
|
+
p = argparse.ArgumentParser(usage="%(prog)s script.py")
|
|
224
|
+
p.add_argument("--usage", action="store_true", help="Emit the terse usage info in triple-ticks. Excluded by default.")
|
|
225
|
+
p.add_argument("--write", metavar="out.md", help="Write to named file instead of stdout.")
|
|
226
|
+
|
|
227
|
+
subject_group = p.add_mutually_exclusive_group(required=True)
|
|
228
|
+
subject_group.add_argument("-m", "--module", help="Run as module.")
|
|
229
|
+
subject_group.add_argument("filename", nargs="?", metavar="script.py", help="Path to the subject script.")
|
|
230
|
+
|
|
231
|
+
options = p.parse_args()
|
|
232
|
+
|
|
233
|
+
try:
|
|
234
|
+
with open_file_for_writing_or_none(options.write) as fh:
|
|
235
|
+
run(
|
|
236
|
+
filename=(options.module if options.module else options.filename),
|
|
237
|
+
include_usage=options.usage,
|
|
238
|
+
writer=fh,
|
|
239
|
+
as_module=(options.module is not None),
|
|
240
|
+
)
|
|
241
|
+
return 0
|
|
242
|
+
except ParseArgsNotCalledError:
|
|
243
|
+
p.error("Subject script never called parse_args(). You might need to pass additional arguments or environment variables.")
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
def find_module(module_name: str) -> tuple[str | None, str, ModuleSpec]:
|
|
247
|
+
try:
|
|
248
|
+
spec = importlib.util.find_spec(module_name)
|
|
249
|
+
except ImportError: # pragma: no cover
|
|
250
|
+
raise NoSourceError(module_name)
|
|
251
|
+
if not spec:
|
|
252
|
+
raise NoSourceError(module_name)
|
|
253
|
+
|
|
254
|
+
path_name = spec.origin
|
|
255
|
+
package_name = spec.name
|
|
256
|
+
if spec.submodule_search_locations:
|
|
257
|
+
mod_main = module_name + ".__main__"
|
|
258
|
+
spec = importlib.util.find_spec(mod_main)
|
|
259
|
+
if not spec:
|
|
260
|
+
raise NoSourceError("Is a package and cannot be directly executed")
|
|
261
|
+
|
|
262
|
+
path_name = spec.origin
|
|
263
|
+
package_name = spec.name
|
|
264
|
+
|
|
265
|
+
package_name = package_name.rpartition(".")[0]
|
|
266
|
+
return path_name, package_name, spec
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
class Loader:
|
|
270
|
+
"""inspired by coveragepy"""
|
|
271
|
+
|
|
272
|
+
filename_or_module: str
|
|
273
|
+
as_module: bool = False
|
|
274
|
+
|
|
275
|
+
package: str | None = None
|
|
276
|
+
loader: DummyLoader
|
|
277
|
+
|
|
278
|
+
def __init__(self, filename_or_module: str, as_module: bool):
|
|
279
|
+
self.filename_or_module = filename_or_module
|
|
280
|
+
self.as_module = as_module
|
|
281
|
+
|
|
282
|
+
def prepare_sys_path(self) -> None:
|
|
283
|
+
"""Set sys.path properly so the first element is appropriate from the perspective of filename_or_module."""
|
|
284
|
+
|
|
285
|
+
path0: str | None
|
|
286
|
+
|
|
287
|
+
if getattr(sys.flags, "safe_path", False): # -P # pragma: no cover
|
|
288
|
+
path0 = None
|
|
289
|
+
elif self.as_module:
|
|
290
|
+
path0 = os.getcwd()
|
|
291
|
+
elif os.path.isdir(self.filename_or_module):
|
|
292
|
+
# if passed a directory, we really want the __main__.py file inside it, and set the path to the container
|
|
293
|
+
path0 = self.filename_or_module
|
|
294
|
+
else:
|
|
295
|
+
path0 = os.path.abspath(os.path.dirname(self.filename_or_module))
|
|
296
|
+
|
|
297
|
+
if path0 is not None:
|
|
298
|
+
sys.path[0] = os.path.abspath(path0)
|
|
299
|
+
|
|
300
|
+
def generate_spec(self) -> None:
|
|
301
|
+
"""
|
|
302
|
+
Resolve the context (eg, a package) for the code to run.
|
|
303
|
+
"""
|
|
304
|
+
if self.as_module:
|
|
305
|
+
"""python -m modulename"""
|
|
306
|
+
self.module_name = self.filename_or_module
|
|
307
|
+
path_name, self.package, self.spec = find_module(self.module_name)
|
|
308
|
+
if self.spec is not None:
|
|
309
|
+
self.module_name = self.spec.name
|
|
310
|
+
self.loader = DummyLoader(self.module_name)
|
|
311
|
+
if path_name is None: # pragma: no cover
|
|
312
|
+
raise NoSourceError("path_name is None")
|
|
313
|
+
self.path_name = os.path.abspath(path_name)
|
|
314
|
+
self.filename_or_module = self.path_name
|
|
315
|
+
|
|
316
|
+
elif os.path.isdir(self.filename_or_module):
|
|
317
|
+
"""python modulename/"""
|
|
318
|
+
for ext in (".py",): # don't support pyc/pyo
|
|
319
|
+
try_filename = os.path.abspath(os.path.join(self.filename_or_module, f"__main__{ext}"))
|
|
320
|
+
if os.path.exists(try_filename):
|
|
321
|
+
self.filename_or_module = try_filename
|
|
322
|
+
break
|
|
323
|
+
else:
|
|
324
|
+
raise NoSourceError(f"Can't find __main__ module in {self.filename_or_module}")
|
|
325
|
+
|
|
326
|
+
self.spec = importlib.machinery.ModuleSpec(
|
|
327
|
+
name="__main__",
|
|
328
|
+
loader=None,
|
|
329
|
+
origin=try_filename,
|
|
330
|
+
)
|
|
331
|
+
self.spec.has_location = True
|
|
332
|
+
self.package = ""
|
|
333
|
+
self.path_name = try_filename
|
|
334
|
+
self.loader = DummyLoader("__main__")
|
|
335
|
+
else:
|
|
336
|
+
self.path_name = self.filename_or_module
|
|
337
|
+
self.loader = DummyLoader("__main__")
|
|
338
|
+
|
|
339
|
+
def load_main_code(self) -> tuple[CodeType, ModuleType]:
|
|
340
|
+
filename: str = self.path_name
|
|
341
|
+
py_source = Path(filename).read_text()
|
|
342
|
+
|
|
343
|
+
py_code = compile(py_source, filename=filename, mode="exec", dont_inherit=True)
|
|
344
|
+
|
|
345
|
+
main_mod = ModuleType("__main__")
|
|
346
|
+
main_mod.__file__ = filename
|
|
347
|
+
main_mod.__loader__ = DummyLoader("__main__") # type: ignore[assignment]
|
|
348
|
+
main_mod.__builtins__ = sys.modules["builtins"] # type: ignore[attr-defined]
|
|
349
|
+
|
|
350
|
+
if self.package is not None:
|
|
351
|
+
main_mod.__package__ = self.package
|
|
352
|
+
|
|
353
|
+
return py_code, main_mod
|
|
354
|
+
|
|
355
|
+
|
|
356
|
+
def run_script(filename: str, include_usage: bool = False, cwd: Path | str | None = None) -> None:
|
|
357
|
+
"""Public API, helpful for calling from cog"""
|
|
358
|
+
if cwd:
|
|
359
|
+
os.chdir(cwd)
|
|
360
|
+
run(filename=filename, as_module=False, include_usage=include_usage, writer=None)
|
|
361
|
+
|
|
362
|
+
|
|
363
|
+
def run_module(modulename: str, include_usage: bool = False, cwd: Path | str | None = None) -> None:
|
|
364
|
+
"""Public API, helpful for calling from cog"""
|
|
365
|
+
if cwd:
|
|
366
|
+
os.chdir(cwd)
|
|
367
|
+
run(filename=modulename, as_module=True, include_usage=include_usage, writer=None)
|
|
368
|
+
|
|
369
|
+
|
|
370
|
+
def run(*, filename: str, as_module: bool = False, include_usage: bool, writer: TextIOWrapper | None = None) -> None:
|
|
371
|
+
"""
|
|
372
|
+
Read source from filename, construct a __main__ module for it, patch argparse, and exec() the source.
|
|
373
|
+
|
|
374
|
+
Inspired by approach in coverage.py
|
|
375
|
+
"""
|
|
376
|
+
r = Loader(filename, as_module=as_module)
|
|
377
|
+
r.prepare_sys_path()
|
|
378
|
+
r.generate_spec()
|
|
379
|
+
py_code, main_mod = r.load_main_code()
|
|
380
|
+
|
|
381
|
+
sys.modules["__main__"] = main_mod
|
|
382
|
+
|
|
383
|
+
# part of argparse's public api in >=3.14, and ignored earlier
|
|
384
|
+
os.environ["NO_COLOR"] = "1"
|
|
385
|
+
|
|
386
|
+
def print_help_cb(
|
|
387
|
+
parser: argparse.ArgumentParser,
|
|
388
|
+
args: list[str] | None = None, # noqa: ARG001 - unused argument
|
|
389
|
+
namespace: argparse.Namespace | None = None, # noqa: ARG001 - unused argument
|
|
390
|
+
) -> None:
|
|
391
|
+
"""Format and print the help instead."""
|
|
392
|
+
if include_usage:
|
|
393
|
+
usage_text = parser.format_usage()
|
|
394
|
+
print(f"```\n{usage_text.strip()}\n```\n", file=writer)
|
|
395
|
+
|
|
396
|
+
# patch attributes of the parser instance:
|
|
397
|
+
# formatter_class will get invoked and used to render to stdout/writer
|
|
398
|
+
# description/epilog=None so they don't mess up the middle of the table
|
|
399
|
+
with patch.multiple(
|
|
400
|
+
parser,
|
|
401
|
+
formatter_class=MarkdownFormatter,
|
|
402
|
+
description=None,
|
|
403
|
+
epilog=None,
|
|
404
|
+
):
|
|
405
|
+
parser.print_help(file=writer)
|
|
406
|
+
raise FinishedGettingOutput # success
|
|
407
|
+
|
|
408
|
+
# Force the help to be printed as soon as one of the parser.parse_*args() functions is called,
|
|
409
|
+
# regardless of whether --help or any other argument was passed.
|
|
410
|
+
with patch.multiple(
|
|
411
|
+
argparse.ArgumentParser, # not instantiated yet
|
|
412
|
+
parse_args=print_help_cb,
|
|
413
|
+
parse_known_args=print_help_cb,
|
|
414
|
+
parse_intermixed_args=print_help_cb,
|
|
415
|
+
parse_known_intermixed_args=print_help_cb,
|
|
416
|
+
):
|
|
417
|
+
try:
|
|
418
|
+
exec(py_code, main_mod.__dict__) # noqa: S102 = exec
|
|
419
|
+
raise ParseArgsNotCalledError # failure
|
|
420
|
+
except FinishedGettingOutput:
|
|
421
|
+
return # success
|
|
422
|
+
|
|
423
|
+
|
|
424
|
+
if __name__ == "__main__":
|
|
425
|
+
raise SystemExit(main()) # pragma: no cover
|