paramspecli 0.2.1__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.
- paramspecli/__init__.py +27 -0
- paramspecli/apstub.py +84 -0
- paramspecli/args.py +110 -0
- paramspecli/cli.py +956 -0
- paramspecli/conv.py +58 -0
- paramspecli/doc.py +368 -0
- paramspecli/fake.py +127 -0
- paramspecli/flags.py +226 -0
- paramspecli/md.py +75 -0
- paramspecli/opts.py +324 -0
- paramspecli/py.typed +0 -0
- paramspecli/util.py +50 -0
- paramspecli-0.2.1.dist-info/METADATA +88 -0
- paramspecli-0.2.1.dist-info/RECORD +16 -0
- paramspecli-0.2.1.dist-info/WHEEL +4 -0
- paramspecli-0.2.1.dist-info/licenses/LICENSE +21 -0
paramspecli/cli.py
ADDED
|
@@ -0,0 +1,956 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
import dataclasses
|
|
3
|
+
import sys
|
|
4
|
+
from typing import (
|
|
5
|
+
Any,
|
|
6
|
+
Callable,
|
|
7
|
+
Concatenate,
|
|
8
|
+
Final,
|
|
9
|
+
Iterable,
|
|
10
|
+
Iterator,
|
|
11
|
+
Literal,
|
|
12
|
+
Mapping,
|
|
13
|
+
Protocol,
|
|
14
|
+
Self,
|
|
15
|
+
Sequence,
|
|
16
|
+
TypeGuard,
|
|
17
|
+
overload,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
from . import util
|
|
21
|
+
from .apstub import (
|
|
22
|
+
ArgumentGroupLike,
|
|
23
|
+
ArgumentParserLike,
|
|
24
|
+
SupportsAddArgument,
|
|
25
|
+
SupportsAddArgumentGroup,
|
|
26
|
+
SupportsAddOneofGroup,
|
|
27
|
+
SupportsAddParser,
|
|
28
|
+
SupportsSetDefaults,
|
|
29
|
+
TypeConverter,
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
# TODO:
|
|
33
|
+
# if section has a headline, it's not auto-purged from the help.
|
|
34
|
+
# Ok for user sections, bad for the default options/arguments sections
|
|
35
|
+
|
|
36
|
+
type ArgparseActionCls = type[argparse.Action]
|
|
37
|
+
|
|
38
|
+
type HandlerSpec = tuple[Callable[..., None] | None, list[Any], dict[str, Any]]
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class Markup(Protocol):
|
|
42
|
+
def plain(self) -> str: ...
|
|
43
|
+
|
|
44
|
+
def __str__(self) -> str: ...
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class DefaultFunc[**P](Protocol):
|
|
48
|
+
def __call__(self, parser: ArgumentParserLike, /, *args: P.args, **kwargs: P.kwargs) -> None: ...
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _make_spec(prefix: str, func: Callable[..., None] | None, nargs: int, options: Iterable[str]) -> HandlerSpec:
|
|
52
|
+
return (
|
|
53
|
+
func,
|
|
54
|
+
[f"{prefix}[{i}]" for i in range(nargs)],
|
|
55
|
+
{param: f"{prefix}.{param}" for param in options},
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _repr_class(obj: object, fields: Mapping[str, Any], *, skip_none: bool = True, skip_empty: bool = True) -> str:
|
|
60
|
+
items: list[str] = []
|
|
61
|
+
for k, v in fields.items():
|
|
62
|
+
if skip_none and v is None:
|
|
63
|
+
continue
|
|
64
|
+
if skip_empty and isinstance(v, (list, dict, set)) and not len(v):
|
|
65
|
+
continue
|
|
66
|
+
items.append(f"{k}={v!r}")
|
|
67
|
+
|
|
68
|
+
return f"{obj.__class__.__name__}({', '.join(items)})"
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def name_and_aliases(arg: str | tuple[str, ...]) -> tuple[str, tuple[str, ...]]:
|
|
72
|
+
if isinstance(arg, str):
|
|
73
|
+
return arg, ()
|
|
74
|
+
return arg[0], arg[1:]
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def is_markup(obj: object) -> TypeGuard[Markup]:
|
|
78
|
+
return hasattr(obj, "plain")
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
@overload
|
|
82
|
+
def _as_plain(arg: str | Markup) -> str: ...
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
@overload
|
|
86
|
+
def _as_plain(arg: None) -> None: ...
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def _as_plain(arg: str | Markup | None) -> str | None:
|
|
90
|
+
if arg is None:
|
|
91
|
+
return arg
|
|
92
|
+
if is_markup(arg):
|
|
93
|
+
return arg.plain()
|
|
94
|
+
return str(arg)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def print_group_help(parser: ArgumentParserLike, /, *args: Any, **kwargs: Any) -> None:
|
|
98
|
+
parser.print_help()
|
|
99
|
+
parser.exit()
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def _with_argparser_arg[**P](
|
|
103
|
+
f: Callable[Concatenate[ArgumentParserLike, P], None], argparser: ArgumentParserLike
|
|
104
|
+
) -> Callable[P, None]:
|
|
105
|
+
def _wrapper(*args: P.args, **kwargs: P.kwargs) -> None:
|
|
106
|
+
f(argparser, *args, **kwargs)
|
|
107
|
+
|
|
108
|
+
# ty as of 0.0.18 suggests some nonsense here
|
|
109
|
+
return _wrapper # ty: ignore[invalid-return-type]
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
# took the idea from mypy. nice and simple.
|
|
113
|
+
# if there are newlines in text, emit as is (with indent)
|
|
114
|
+
# otherwise use default wrapping HelpFormatter.
|
|
115
|
+
class HelpFormatter(argparse.HelpFormatter):
|
|
116
|
+
def __init__(self, prog: str, *args: Any, **kwargs: Any) -> None:
|
|
117
|
+
super().__init__(prog=prog, max_help_position=28)
|
|
118
|
+
|
|
119
|
+
def _fill_text(self, text: str, width: int, indent: str) -> str:
|
|
120
|
+
if "\n" in text:
|
|
121
|
+
return "".join(indent + line for line in text.splitlines(keepends=True))
|
|
122
|
+
return super()._fill_text(text, width, indent)
|
|
123
|
+
|
|
124
|
+
def _split_lines(self, text: str, width: int) -> list[str]:
|
|
125
|
+
if "\n" in text:
|
|
126
|
+
return text.splitlines()
|
|
127
|
+
return super()._split_lines(text, width)
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
@dataclasses.dataclass
|
|
131
|
+
class Config:
|
|
132
|
+
"""Settings to fine-tune the generated argparser"""
|
|
133
|
+
|
|
134
|
+
show_default: bool = True
|
|
135
|
+
"""Show default value in help. Set to `False` to globally opt-out. Options may set own `show default` to selectively opt-in"""
|
|
136
|
+
propagate_epilog: bool = False
|
|
137
|
+
"""Propagate root epilog for all groups and commands"""
|
|
138
|
+
commands_title: str = "commands"
|
|
139
|
+
"""Section for the list of commands in group"""
|
|
140
|
+
arguments_title: str = "arguments"
|
|
141
|
+
"""Title for the list of arguments"""
|
|
142
|
+
options_title: str = "options"
|
|
143
|
+
"""Title for the list of options"""
|
|
144
|
+
arguments_headline: str | Markup | None = None
|
|
145
|
+
"""Headline for the list of arguments"""
|
|
146
|
+
options_headline: str | Markup | None = None
|
|
147
|
+
"""Headline for the list of options"""
|
|
148
|
+
catch_typeconv_exceptions: bool = False
|
|
149
|
+
"""Catch all type converter exceptions and print as CLI errors"""
|
|
150
|
+
allow_abbrev: bool = False
|
|
151
|
+
"""Guess incomplete arguments"""
|
|
152
|
+
parser_class: type[argparse.ArgumentParser] = argparse.ArgumentParser
|
|
153
|
+
"""Argparge parser class to use"""
|
|
154
|
+
formatter_class: type[argparse.HelpFormatter] = HelpFormatter
|
|
155
|
+
"""Argparse help formatter class to use"""
|
|
156
|
+
root_parser_extra_kwargs: Mapping[str, Any] = dataclasses.field(default_factory=dict)
|
|
157
|
+
"""Dict of extra kwargs for the root (CLI) argparse.ArgumentParser"""
|
|
158
|
+
sub_parser_extra_kwargs: Mapping[str, Any] = dataclasses.field(default_factory=dict)
|
|
159
|
+
"""Dict of extra kwargs for sub (groups) argparse.ArgumentParsers"""
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
@dataclasses.dataclass(repr=False, slots=True)
|
|
163
|
+
class Arg:
|
|
164
|
+
"""Represents the argument"""
|
|
165
|
+
|
|
166
|
+
metavar: str
|
|
167
|
+
_: dataclasses.KW_ONLY
|
|
168
|
+
type: TypeConverter[Any] | None = None
|
|
169
|
+
help: str | Markup | bool | None = None
|
|
170
|
+
choices: Iterable[Any] | None = None
|
|
171
|
+
nargs: int | Literal["*", "+", "?"] | None = None
|
|
172
|
+
default: Any = None
|
|
173
|
+
extra: Mapping[str, Any] | None = None
|
|
174
|
+
|
|
175
|
+
def __repr__(self) -> str:
|
|
176
|
+
return _repr_class(self, {k: getattr(self, k) for k in self.__dataclass_fields__}, skip_empty=False)
|
|
177
|
+
|
|
178
|
+
# (weak) hash is based on a metavar
|
|
179
|
+
def __hash__(self) -> int:
|
|
180
|
+
return hash(self.metavar)
|
|
181
|
+
|
|
182
|
+
def _build(self, owner: SupportsAddArgument, config: Config, *, dest: str) -> None:
|
|
183
|
+
_type = self.type
|
|
184
|
+
if _type and config.catch_typeconv_exceptions:
|
|
185
|
+
_type = util.catch_all(_type)
|
|
186
|
+
|
|
187
|
+
help = self.help
|
|
188
|
+
|
|
189
|
+
if help is False:
|
|
190
|
+
help = argparse.SUPPRESS
|
|
191
|
+
elif help is True:
|
|
192
|
+
help = ""
|
|
193
|
+
|
|
194
|
+
# most kwargs shouldn't be set at all if None
|
|
195
|
+
kwargs: dict[str, Any] = {
|
|
196
|
+
"choices": self.choices,
|
|
197
|
+
"default": self.default,
|
|
198
|
+
"nargs": self.nargs,
|
|
199
|
+
"metavar": self.metavar,
|
|
200
|
+
# NOTE: if type is not specified, argparse's MetavarTypeHelpFormatter may fail!
|
|
201
|
+
"type": _type,
|
|
202
|
+
}
|
|
203
|
+
kwargs = {k: v for k, v in kwargs.items() if v is not None}
|
|
204
|
+
if self.extra:
|
|
205
|
+
kwargs |= self.extra
|
|
206
|
+
|
|
207
|
+
owner.add_argument(dest=dest, help=_as_plain(help), **kwargs)
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
# Too many fields to manage: dataclass to the rescue
|
|
211
|
+
@dataclasses.dataclass(repr=False, slots=True)
|
|
212
|
+
class Opt:
|
|
213
|
+
"""Abstract option"""
|
|
214
|
+
|
|
215
|
+
names: tuple[str, ...]
|
|
216
|
+
_: dataclasses.KW_ONLY
|
|
217
|
+
type: TypeConverter[Any] | None = None
|
|
218
|
+
help: str | Markup | bool | None = None
|
|
219
|
+
hard_show_default: bool | str | None = None
|
|
220
|
+
soft_show_default: bool | str = False
|
|
221
|
+
required: bool = False
|
|
222
|
+
choices: Iterable[Any] | None = None
|
|
223
|
+
metavar: str | tuple[str, ...] | None = None
|
|
224
|
+
action: str | ArgparseActionCls | None = None
|
|
225
|
+
nargs: int | Literal["*", "+", "?"] | None = None
|
|
226
|
+
default: Any = None
|
|
227
|
+
const: Any = None
|
|
228
|
+
deprecated: bool = False
|
|
229
|
+
extra: Mapping[str, Any] | None = None
|
|
230
|
+
|
|
231
|
+
def __post_init__(self) -> None:
|
|
232
|
+
if not self.names:
|
|
233
|
+
raise ValueError("at least one name is required")
|
|
234
|
+
|
|
235
|
+
for name in self.names:
|
|
236
|
+
if not name.startswith("-"):
|
|
237
|
+
raise ValueError(f"Option name {name!r} should start with '-'")
|
|
238
|
+
|
|
239
|
+
def __repr__(self) -> str:
|
|
240
|
+
return _repr_class(self, {k: getattr(self, k) for k in self.__dataclass_fields__}, skip_empty=False)
|
|
241
|
+
|
|
242
|
+
# (weak) hash is based on names
|
|
243
|
+
def __hash__(self) -> int:
|
|
244
|
+
return hash(self.names)
|
|
245
|
+
|
|
246
|
+
def _build(self, owner: SupportsAddArgument, config: Config, *, dest: str | Literal[False]) -> None:
|
|
247
|
+
help = self.help
|
|
248
|
+
|
|
249
|
+
if help is False:
|
|
250
|
+
help = argparse.SUPPRESS
|
|
251
|
+
elif help is True:
|
|
252
|
+
help = ""
|
|
253
|
+
elif help is None:
|
|
254
|
+
pass
|
|
255
|
+
else:
|
|
256
|
+
help = _as_plain(help)
|
|
257
|
+
|
|
258
|
+
# str
|
|
259
|
+
if self.hard_show_default is not None:
|
|
260
|
+
show_default = self.hard_show_default
|
|
261
|
+
else:
|
|
262
|
+
show_default = self.soft_show_default if config.show_default is True else False
|
|
263
|
+
|
|
264
|
+
if show_default is True:
|
|
265
|
+
help += " (default: %(default)s)"
|
|
266
|
+
elif show_default is False:
|
|
267
|
+
pass
|
|
268
|
+
else:
|
|
269
|
+
help += f" (default: {show_default})"
|
|
270
|
+
|
|
271
|
+
_type = self.type
|
|
272
|
+
if _type and config.catch_typeconv_exceptions:
|
|
273
|
+
_type = util.catch_all(_type)
|
|
274
|
+
|
|
275
|
+
# most kwargs shouldn't be set at all if None
|
|
276
|
+
kwargs: dict[str, Any] = {
|
|
277
|
+
"action": self.action,
|
|
278
|
+
"choices": self.choices,
|
|
279
|
+
"default": self.default,
|
|
280
|
+
"nargs": self.nargs,
|
|
281
|
+
"const": self.const,
|
|
282
|
+
"metavar": self.metavar,
|
|
283
|
+
"type": _type,
|
|
284
|
+
"required": self.required or None,
|
|
285
|
+
"deprecated": self.deprecated if sys.version_info >= (3, 13) else None,
|
|
286
|
+
}
|
|
287
|
+
kwargs = {k: v for k, v in kwargs.items() if v is not None}
|
|
288
|
+
|
|
289
|
+
if self.extra:
|
|
290
|
+
kwargs |= self.extra
|
|
291
|
+
|
|
292
|
+
owner.add_argument(*self.names, dest=dest or argparse.SUPPRESS, help=help, **kwargs)
|
|
293
|
+
|
|
294
|
+
@property
|
|
295
|
+
def is_hidden(self) -> bool:
|
|
296
|
+
return self.help is False
|
|
297
|
+
|
|
298
|
+
def __getitem__(self, section: "Section") -> Self:
|
|
299
|
+
section.include(self)
|
|
300
|
+
return self
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
class MixedOpts:
|
|
304
|
+
"""Abstract mix of options"""
|
|
305
|
+
|
|
306
|
+
__slots__ = ("options",)
|
|
307
|
+
|
|
308
|
+
def __init__(self, *options: Opt):
|
|
309
|
+
self.options: Final = options
|
|
310
|
+
|
|
311
|
+
def __eq__(self, other: object) -> bool:
|
|
312
|
+
if not isinstance(other, MixedOpts):
|
|
313
|
+
return NotImplemented
|
|
314
|
+
return self.options == other.options
|
|
315
|
+
|
|
316
|
+
def __repr__(self) -> str:
|
|
317
|
+
return _repr_class(self, {"options": self.options})
|
|
318
|
+
|
|
319
|
+
def __getitem__(self, section: "Section") -> Self:
|
|
320
|
+
for option in self.options:
|
|
321
|
+
section.include(option)
|
|
322
|
+
return self
|
|
323
|
+
|
|
324
|
+
|
|
325
|
+
class Cnst:
|
|
326
|
+
__slots__ = ("value",)
|
|
327
|
+
|
|
328
|
+
def __init__(self, value: Any):
|
|
329
|
+
self.value: Final = value
|
|
330
|
+
|
|
331
|
+
def __repr__(self) -> str:
|
|
332
|
+
return _repr_class(self, {"value": self.value}, skip_none=False)
|
|
333
|
+
|
|
334
|
+
def __eq__(self, other: object) -> bool:
|
|
335
|
+
if not isinstance(other, Cnst):
|
|
336
|
+
return NotImplemented
|
|
337
|
+
return bool(self.value == other.value)
|
|
338
|
+
|
|
339
|
+
def _build(self, owner: SupportsSetDefaults, config: Config, *, dest: str) -> None:
|
|
340
|
+
owner.set_defaults(**{dest: self.value})
|
|
341
|
+
|
|
342
|
+
|
|
343
|
+
class Action(Opt):
|
|
344
|
+
"""Special kind of option with side effects"""
|
|
345
|
+
|
|
346
|
+
__slots__ = ()
|
|
347
|
+
|
|
348
|
+
def _build(self, owner: SupportsAddArgument, config: Config, *, dest: str | Literal[False]) -> None:
|
|
349
|
+
if dest is not False:
|
|
350
|
+
raise TypeError(f"Action is used in place of Option {dest!r}")
|
|
351
|
+
return super()._build(owner, config, dest=dest)
|
|
352
|
+
|
|
353
|
+
|
|
354
|
+
class Ctx:
|
|
355
|
+
__slots__ = ()
|
|
356
|
+
|
|
357
|
+
def _build(self, owner: SupportsSetDefaults, config: Config, *, dest: str, context: Any) -> None:
|
|
358
|
+
if context is None:
|
|
359
|
+
raise ValueError("context can't be None")
|
|
360
|
+
owner.set_defaults(**{dest: context})
|
|
361
|
+
|
|
362
|
+
|
|
363
|
+
class Section:
|
|
364
|
+
"""Shows its options in a separate block of a `--help` output"""
|
|
365
|
+
|
|
366
|
+
def __init__(self, title: str, *, headline: str | Markup | None = None):
|
|
367
|
+
self.title: Final = title
|
|
368
|
+
self.headline: Final = headline
|
|
369
|
+
self.options: Final[set[Opt]] = set()
|
|
370
|
+
|
|
371
|
+
def __repr__(self) -> str:
|
|
372
|
+
return _repr_class(self, self.__dict__)
|
|
373
|
+
|
|
374
|
+
def include[T: Opt | MixedOpts](self, option: T) -> T:
|
|
375
|
+
if isinstance(option, (Opt)):
|
|
376
|
+
self.options.add(option)
|
|
377
|
+
else:
|
|
378
|
+
assert isinstance(option, MixedOpts)
|
|
379
|
+
self.options.update(option.options)
|
|
380
|
+
return option
|
|
381
|
+
|
|
382
|
+
def _build(self, owner: SupportsAddArgumentGroup, config: Config) -> ArgumentGroupLike:
|
|
383
|
+
return owner.add_argument_group(title=self.title, description=_as_plain(self.headline))
|
|
384
|
+
|
|
385
|
+
__call__ = include
|
|
386
|
+
|
|
387
|
+
|
|
388
|
+
class Oneof:
|
|
389
|
+
"""Mutually exclusive section. Ensures only one of options present on a command line"""
|
|
390
|
+
|
|
391
|
+
def __init__(self, *, required: bool = False):
|
|
392
|
+
self.options: Final[set[Opt]] = set()
|
|
393
|
+
self.required: Final = required
|
|
394
|
+
|
|
395
|
+
def __repr__(self) -> str:
|
|
396
|
+
return _repr_class(self, self.__dict__)
|
|
397
|
+
|
|
398
|
+
def _build(self, owner: SupportsAddOneofGroup, config: Config) -> SupportsAddArgument:
|
|
399
|
+
return owner.add_mutually_exclusive_group(required=self.required)
|
|
400
|
+
|
|
401
|
+
def include[T: Opt | MixedOpts](self, option: T) -> T:
|
|
402
|
+
if isinstance(option, Opt):
|
|
403
|
+
self.options.add(option)
|
|
404
|
+
else:
|
|
405
|
+
assert isinstance(option, MixedOpts)
|
|
406
|
+
self.options.update(option.options)
|
|
407
|
+
return option
|
|
408
|
+
|
|
409
|
+
__call__ = include
|
|
410
|
+
|
|
411
|
+
|
|
412
|
+
class ParserLike:
|
|
413
|
+
def __init__(
|
|
414
|
+
self,
|
|
415
|
+
*,
|
|
416
|
+
help: str | Markup | None = None,
|
|
417
|
+
info: str | Markup | None = None,
|
|
418
|
+
usage: str | Markup | None = None,
|
|
419
|
+
epilog: str | Markup | None = None,
|
|
420
|
+
prog: str | None = None,
|
|
421
|
+
):
|
|
422
|
+
self.help = help
|
|
423
|
+
self.info = info
|
|
424
|
+
self.usage = usage
|
|
425
|
+
self.epilog = epilog
|
|
426
|
+
self.prog = prog
|
|
427
|
+
|
|
428
|
+
self.arguments: list[Arg | Cnst | Ctx] | None = None
|
|
429
|
+
self.actions: list[Action] = []
|
|
430
|
+
self.options: dict[str, Opt | MixedOpts | Cnst | Ctx] | None = {}
|
|
431
|
+
self.sections: list[Section] = []
|
|
432
|
+
self.oneofs: list[Oneof] = []
|
|
433
|
+
|
|
434
|
+
# manually set if required
|
|
435
|
+
self._func: Callable[..., None] | None = None
|
|
436
|
+
|
|
437
|
+
def __repr__(self) -> str:
|
|
438
|
+
return _repr_class(self, self.__dict__)
|
|
439
|
+
|
|
440
|
+
def _set_params(self, *arguments: Any, **options: Any) -> None:
|
|
441
|
+
"""Set arguments and options. Positional parameters are arguments. Keyword parameters are options"""
|
|
442
|
+
|
|
443
|
+
for i, argument in enumerate(arguments):
|
|
444
|
+
if not isinstance(argument, (Arg, Cnst, Ctx)):
|
|
445
|
+
raise TypeError(f"parameter {i} should be an Argument | Const | Context")
|
|
446
|
+
|
|
447
|
+
for name, option in options.items():
|
|
448
|
+
if not isinstance(option, (Opt, MixedOpts, Cnst, Ctx)):
|
|
449
|
+
raise TypeError(f"parameter {name!r} should be Option | MixedOptions | Const | Context")
|
|
450
|
+
|
|
451
|
+
self.arguments = [*arguments]
|
|
452
|
+
self.options = {**options}
|
|
453
|
+
|
|
454
|
+
def _build_sections(
|
|
455
|
+
self, owner: ArgumentParserLike, config: Config, *, default_group: ArgumentGroupLike
|
|
456
|
+
) -> dict[Opt, SupportsAddArgument]:
|
|
457
|
+
sectmap: dict[Opt, ArgumentGroupLike] = {}
|
|
458
|
+
oneofmap: dict[Opt, SupportsAddArgument] = {}
|
|
459
|
+
|
|
460
|
+
for section in self.sections:
|
|
461
|
+
if section_dupes := section.options.intersection(sectmap):
|
|
462
|
+
raise KeyError(f"options {section_dupes} are in several sections at once")
|
|
463
|
+
|
|
464
|
+
sectmap |= dict.fromkeys(section.options, section._build(owner, config))
|
|
465
|
+
|
|
466
|
+
for oneof in self.oneofs:
|
|
467
|
+
if not oneof.options:
|
|
468
|
+
continue
|
|
469
|
+
|
|
470
|
+
if oneof_dupes := oneof.options.intersection(oneofmap):
|
|
471
|
+
raise KeyError(f"options {oneof_dupes} are in several oneofs at once")
|
|
472
|
+
|
|
473
|
+
groups = list({sectmap.get(option, default_group) for option in oneof.options})
|
|
474
|
+
if len(groups) != 1:
|
|
475
|
+
raise KeyError("all options in oneof should belong to the same section")
|
|
476
|
+
|
|
477
|
+
oneofmap |= dict.fromkeys(oneof.options, oneof._build(groups[0], config))
|
|
478
|
+
|
|
479
|
+
tgtmap = sectmap | oneofmap
|
|
480
|
+
return tgtmap
|
|
481
|
+
|
|
482
|
+
def _build_params(
|
|
483
|
+
self, owner: ArgumentParserLike, config: Config, *, parents: Sequence["ParserLike"], context: Any
|
|
484
|
+
) -> None:
|
|
485
|
+
if self._func:
|
|
486
|
+
if self.arguments is None or self.options is None:
|
|
487
|
+
raise ValueError("bind() was not called")
|
|
488
|
+
|
|
489
|
+
key = str(len(parents))
|
|
490
|
+
|
|
491
|
+
spec = _make_spec(key, self._func, len(self.arguments or []), self.options or {})
|
|
492
|
+
owner.set_defaults(**{key: spec})
|
|
493
|
+
|
|
494
|
+
if self.arguments:
|
|
495
|
+
arguments_group = owner.add_argument_group(
|
|
496
|
+
title=config.arguments_title, description=_as_plain(config.arguments_headline)
|
|
497
|
+
)
|
|
498
|
+
|
|
499
|
+
for i, argument in enumerate(self.arguments):
|
|
500
|
+
name = f"{key}[{i}]"
|
|
501
|
+
if isinstance(argument, Cnst):
|
|
502
|
+
argument._build(owner, config, dest=name)
|
|
503
|
+
elif isinstance(argument, Ctx):
|
|
504
|
+
argument._build(owner, config, dest=name, context=context)
|
|
505
|
+
else:
|
|
506
|
+
argument._build(arguments_group, config, dest=name)
|
|
507
|
+
|
|
508
|
+
default_options_group = owner.add_argument_group(
|
|
509
|
+
title=config.options_title, description=_as_plain(config.options_headline)
|
|
510
|
+
)
|
|
511
|
+
|
|
512
|
+
tgtmap = self._build_sections(owner, config, default_group=default_options_group)
|
|
513
|
+
|
|
514
|
+
for action in self.actions:
|
|
515
|
+
action._build(tgtmap.pop(action, default_options_group), config, dest=False)
|
|
516
|
+
|
|
517
|
+
if self.options:
|
|
518
|
+
for param, item in self.options.items():
|
|
519
|
+
name = f"{key}.{param}"
|
|
520
|
+
if isinstance(item, Cnst):
|
|
521
|
+
item._build(owner, config, dest=name)
|
|
522
|
+
elif isinstance(item, Ctx):
|
|
523
|
+
item._build(owner, config, dest=name, context=context)
|
|
524
|
+
else:
|
|
525
|
+
seq = (item,) if isinstance(item, Opt) else item.options
|
|
526
|
+
for opt in seq:
|
|
527
|
+
opt._build(tgtmap.pop(opt, default_options_group), config, dest=name)
|
|
528
|
+
|
|
529
|
+
if tgtmap:
|
|
530
|
+
raise KeyError(f"unconsumed items in sections: {tgtmap.keys()}")
|
|
531
|
+
|
|
532
|
+
def _build_subparser(
|
|
533
|
+
self,
|
|
534
|
+
owner: SupportsAddParser,
|
|
535
|
+
config: Config,
|
|
536
|
+
*,
|
|
537
|
+
parents: Sequence["ParserLike"],
|
|
538
|
+
name: str | tuple[str, ...],
|
|
539
|
+
) -> ArgumentParserLike:
|
|
540
|
+
basename, aliases = name_and_aliases(name)
|
|
541
|
+
|
|
542
|
+
epilog = self.epilog
|
|
543
|
+
|
|
544
|
+
if epilog is None and config.propagate_epilog and parents:
|
|
545
|
+
epilog = parents[0].epilog
|
|
546
|
+
|
|
547
|
+
return owner.add_parser(
|
|
548
|
+
basename,
|
|
549
|
+
aliases=aliases,
|
|
550
|
+
help=_as_plain(self.help),
|
|
551
|
+
description=_as_plain(self.info if self.info is not None else self.help),
|
|
552
|
+
epilog=_as_plain(epilog),
|
|
553
|
+
usage=_as_plain(self.usage),
|
|
554
|
+
prog=self.prog,
|
|
555
|
+
formatter_class=config.formatter_class,
|
|
556
|
+
add_help=False,
|
|
557
|
+
allow_abbrev=config.allow_abbrev,
|
|
558
|
+
**config.sub_parser_extra_kwargs,
|
|
559
|
+
)
|
|
560
|
+
|
|
561
|
+
def _build_root_parser(self, config: Config) -> argparse.ArgumentParser:
|
|
562
|
+
return config.parser_class(
|
|
563
|
+
description=_as_plain(self.info),
|
|
564
|
+
epilog=_as_plain(self.epilog),
|
|
565
|
+
usage=_as_plain(self.usage),
|
|
566
|
+
prog=self.prog,
|
|
567
|
+
formatter_class=config.formatter_class,
|
|
568
|
+
add_help=False,
|
|
569
|
+
allow_abbrev=config.allow_abbrev,
|
|
570
|
+
**config.root_parser_extra_kwargs,
|
|
571
|
+
)
|
|
572
|
+
|
|
573
|
+
def append_action(self, action: Action) -> None:
|
|
574
|
+
"""Append Action"""
|
|
575
|
+
self.actions.append(action)
|
|
576
|
+
|
|
577
|
+
def add_oneof(self, *, required: bool = False) -> "Oneof":
|
|
578
|
+
"""Creates and adds new mutually exclusive section"""
|
|
579
|
+
oneof = Oneof(required=required)
|
|
580
|
+
self.oneofs.append(oneof)
|
|
581
|
+
return oneof
|
|
582
|
+
|
|
583
|
+
def add_section(self, title: str, *, headline: str | Markup | None = None) -> "Section":
|
|
584
|
+
"""Creates and adds new help section"""
|
|
585
|
+
section = Section(title, headline=headline)
|
|
586
|
+
self.sections.append(section)
|
|
587
|
+
return section
|
|
588
|
+
|
|
589
|
+
def get_real_arguments(self) -> list[Arg]:
|
|
590
|
+
if not self.arguments:
|
|
591
|
+
return []
|
|
592
|
+
return [arg for arg in self.arguments if isinstance(arg, Arg)]
|
|
593
|
+
|
|
594
|
+
def get_real_options(self) -> list[Opt]:
|
|
595
|
+
if not self.options:
|
|
596
|
+
return []
|
|
597
|
+
out: list[Opt] = []
|
|
598
|
+
for item in self.options.values():
|
|
599
|
+
if isinstance(item, Opt):
|
|
600
|
+
out.append(item)
|
|
601
|
+
elif isinstance(item, MixedOpts):
|
|
602
|
+
out.extend(item.options)
|
|
603
|
+
return out
|
|
604
|
+
|
|
605
|
+
def __enter__(self) -> Self:
|
|
606
|
+
return self
|
|
607
|
+
|
|
608
|
+
def __exit__(self, *args: Any, **kwargs: Any) -> None:
|
|
609
|
+
pass
|
|
610
|
+
|
|
611
|
+
|
|
612
|
+
class Handler:
|
|
613
|
+
"""Represents parsed arguments and options.
|
|
614
|
+
Calling it will invoke the command handler"""
|
|
615
|
+
|
|
616
|
+
def __init__(self, func: Callable[..., None] | None, arguments: list[Any], options: dict[str, Any]):
|
|
617
|
+
self.func = func
|
|
618
|
+
self.arguments = arguments
|
|
619
|
+
self.options = options
|
|
620
|
+
|
|
621
|
+
def __eq__(self, other: object) -> bool:
|
|
622
|
+
if not isinstance(other, Handler):
|
|
623
|
+
return NotImplemented
|
|
624
|
+
return self.func == other.func and self.arguments == other.arguments and self.options == other.options
|
|
625
|
+
|
|
626
|
+
def __repr__(self) -> str:
|
|
627
|
+
return _repr_class(self, self.__dict__)
|
|
628
|
+
|
|
629
|
+
def __call__(self) -> None:
|
|
630
|
+
if not self.func:
|
|
631
|
+
return
|
|
632
|
+
self.func(*self.arguments, **self.options)
|
|
633
|
+
|
|
634
|
+
@classmethod
|
|
635
|
+
def from_ns(cls, ns: argparse.Namespace, spec: HandlerSpec) -> Self:
|
|
636
|
+
vars = ns.__dict__
|
|
637
|
+
|
|
638
|
+
func, argnames, kwargnames = spec
|
|
639
|
+
|
|
640
|
+
args = [vars[name] for name in argnames]
|
|
641
|
+
kwargs = {k: vars[v] for k, v in kwargnames.items()}
|
|
642
|
+
|
|
643
|
+
return cls(func, args, kwargs)
|
|
644
|
+
|
|
645
|
+
@classmethod
|
|
646
|
+
def from_spec(cls, func: Callable[..., None] | None, /, *arguments: Any, **options: Any) -> Self:
|
|
647
|
+
return cls(func, list(arguments), options)
|
|
648
|
+
|
|
649
|
+
|
|
650
|
+
class Route:
|
|
651
|
+
"""Represents sequence of handers, arguments and options.
|
|
652
|
+
Calling it will invoke the handlers"""
|
|
653
|
+
|
|
654
|
+
def __init__(self, handlers: Sequence[Handler]):
|
|
655
|
+
self.handlers = handlers
|
|
656
|
+
|
|
657
|
+
def __eq__(self, other: object) -> bool:
|
|
658
|
+
if not isinstance(other, Route):
|
|
659
|
+
return NotImplemented
|
|
660
|
+
return self.handlers == other.handlers
|
|
661
|
+
|
|
662
|
+
def __repr__(self) -> str:
|
|
663
|
+
return _repr_class(self, self.__dict__)
|
|
664
|
+
|
|
665
|
+
def __call__(self) -> None:
|
|
666
|
+
for handler in self.handlers:
|
|
667
|
+
handler()
|
|
668
|
+
|
|
669
|
+
def __iter__(self) -> Iterator[Handler]:
|
|
670
|
+
yield from self.handlers
|
|
671
|
+
|
|
672
|
+
def __len__(self) -> int:
|
|
673
|
+
return len(self.handlers)
|
|
674
|
+
|
|
675
|
+
def __getitem__(self, idx: int) -> Handler:
|
|
676
|
+
return self.handlers[idx]
|
|
677
|
+
|
|
678
|
+
@property
|
|
679
|
+
def nonempty(self) -> list[Handler]:
|
|
680
|
+
return [h for h in self.handlers if h.func is not None]
|
|
681
|
+
|
|
682
|
+
@classmethod
|
|
683
|
+
def from_ns(cls, ns: argparse.Namespace) -> Self:
|
|
684
|
+
specs: list[tuple[int, HandlerSpec]] = []
|
|
685
|
+
for k, v in ns.__dict__.items():
|
|
686
|
+
try:
|
|
687
|
+
idx = int(k)
|
|
688
|
+
except ValueError:
|
|
689
|
+
pass
|
|
690
|
+
else:
|
|
691
|
+
specs.append((idx, v))
|
|
692
|
+
specs.sort()
|
|
693
|
+
|
|
694
|
+
handlers = [Handler.from_ns(ns, spec[1]) for spec in specs]
|
|
695
|
+
return cls(handlers)
|
|
696
|
+
|
|
697
|
+
|
|
698
|
+
## More concrete
|
|
699
|
+
|
|
700
|
+
|
|
701
|
+
class Command[**P](ParserLike):
|
|
702
|
+
"""Represents the handler with parameters bound to arguments and options"""
|
|
703
|
+
|
|
704
|
+
def __init__(
|
|
705
|
+
self,
|
|
706
|
+
func: Callable[P, None],
|
|
707
|
+
*,
|
|
708
|
+
help: str | Markup | None = None,
|
|
709
|
+
info: str | Markup | None = None,
|
|
710
|
+
usage: str | Markup | None = None,
|
|
711
|
+
epilog: str | Markup | None = None,
|
|
712
|
+
prog: str | None = None,
|
|
713
|
+
add_help: bool = True,
|
|
714
|
+
):
|
|
715
|
+
super().__init__(help=help, info=info, usage=usage, epilog=epilog, prog=prog)
|
|
716
|
+
self._func = func
|
|
717
|
+
|
|
718
|
+
if add_help:
|
|
719
|
+
self.append_action(_help_action)
|
|
720
|
+
|
|
721
|
+
def _build(
|
|
722
|
+
self,
|
|
723
|
+
owner: ArgumentParserLike,
|
|
724
|
+
config: Config,
|
|
725
|
+
*,
|
|
726
|
+
parents: Sequence["Group"],
|
|
727
|
+
context: Any,
|
|
728
|
+
**kwargs: Any,
|
|
729
|
+
) -> None:
|
|
730
|
+
self._build_params(owner, config, parents=parents, context=context)
|
|
731
|
+
|
|
732
|
+
def bind(self, *arguments: P.args, **options: P.kwargs) -> Self:
|
|
733
|
+
"""Bind function parameters to arguments/options"""
|
|
734
|
+
self._set_params(*arguments, **options)
|
|
735
|
+
return self
|
|
736
|
+
|
|
737
|
+
def parse(self, input: Sequence[str] | None = None, *, config: Config | None = None, context: Any = None) -> Route:
|
|
738
|
+
"""Parse command line. Allows to use the Command as standalone groupless CLI"""
|
|
739
|
+
|
|
740
|
+
config = config or Config()
|
|
741
|
+
parser = self._build_root_parser(config)
|
|
742
|
+
self._build(parser, config, parents=[], context=context)
|
|
743
|
+
|
|
744
|
+
ns = parser.parse_args(input)
|
|
745
|
+
return Route.from_ns(ns)
|
|
746
|
+
|
|
747
|
+
|
|
748
|
+
class Group(ParserLike):
|
|
749
|
+
"""Represents group of commands. May contain nested groups"""
|
|
750
|
+
|
|
751
|
+
def __init__(
|
|
752
|
+
self,
|
|
753
|
+
*,
|
|
754
|
+
help: str | Markup | None = None,
|
|
755
|
+
info: str | Markup | None = None,
|
|
756
|
+
usage: str | Markup | None = None,
|
|
757
|
+
epilog: str | Markup | None = None,
|
|
758
|
+
prog: str | None = None,
|
|
759
|
+
title: str | None = None,
|
|
760
|
+
headline: str | Markup | None = None,
|
|
761
|
+
metavar: str | None = None,
|
|
762
|
+
default_func: DefaultFunc[...] | None = print_group_help,
|
|
763
|
+
add_help: bool = True,
|
|
764
|
+
):
|
|
765
|
+
super().__init__(help=help, info=info, usage=usage, epilog=epilog, prog=prog)
|
|
766
|
+
|
|
767
|
+
self.headline: Final = headline
|
|
768
|
+
self.title: Final = title
|
|
769
|
+
self.metavar: Final = metavar
|
|
770
|
+
self.default_func: Final = default_func
|
|
771
|
+
self.nodes: dict[str | tuple[str, ...], Group | Command[...]] = {}
|
|
772
|
+
|
|
773
|
+
if add_help:
|
|
774
|
+
self.append_action(_help_action)
|
|
775
|
+
|
|
776
|
+
def _build(self, owner: ArgumentParserLike, config: Config, *, parents: Sequence["Group"], context: Any) -> None:
|
|
777
|
+
self._build_params(owner, config, parents=parents, context=context)
|
|
778
|
+
self._build_default_handler(owner, config, parents=parents)
|
|
779
|
+
self._build_nodes(owner, config, parents=parents, context=context)
|
|
780
|
+
|
|
781
|
+
def _build_default_handler(
|
|
782
|
+
self, owner: ArgumentParserLike, config: Config, *, parents: Sequence["ParserLike"]
|
|
783
|
+
) -> None:
|
|
784
|
+
if not self.default_func:
|
|
785
|
+
return
|
|
786
|
+
|
|
787
|
+
default_func = _with_argparser_arg(self.default_func, owner)
|
|
788
|
+
|
|
789
|
+
key = str(len(parents))
|
|
790
|
+
deeper_key = str(len(parents) + 1)
|
|
791
|
+
spec = _make_spec(key, default_func, len(self.arguments or []), self.options or {})
|
|
792
|
+
owner.set_defaults(**{deeper_key: spec})
|
|
793
|
+
|
|
794
|
+
def _build_nodes(
|
|
795
|
+
self, owner: ArgumentParserLike, config: Config, *, parents: Sequence["Group"], context: Any
|
|
796
|
+
) -> None:
|
|
797
|
+
if not self.nodes:
|
|
798
|
+
return
|
|
799
|
+
|
|
800
|
+
sub = owner.add_subparsers(
|
|
801
|
+
title=self.title if self.title is not None else config.commands_title,
|
|
802
|
+
description=_as_plain(self.headline),
|
|
803
|
+
parser_class=config.parser_class,
|
|
804
|
+
metavar=self.metavar,
|
|
805
|
+
required=False if self.default_func else True,
|
|
806
|
+
)
|
|
807
|
+
for name, node in self.nodes.items():
|
|
808
|
+
try:
|
|
809
|
+
parser = node._build_subparser(sub, config, name=name, parents=[*parents, self])
|
|
810
|
+
node._build(parser, config, parents=[*parents, self], context=context)
|
|
811
|
+
except Exception as e:
|
|
812
|
+
e.add_note(f"{' ' * len(parents)}- {name!r}")
|
|
813
|
+
raise
|
|
814
|
+
|
|
815
|
+
def parse(self, input: Sequence[str] | None = None, *, config: Config | None = None, context: Any = None) -> Route:
|
|
816
|
+
"""Parse command line"""
|
|
817
|
+
|
|
818
|
+
config = config or Config()
|
|
819
|
+
|
|
820
|
+
try:
|
|
821
|
+
parser = self._build_root_parser(config)
|
|
822
|
+
self._build(parser, config, parents=[], context=context)
|
|
823
|
+
except Exception as e:
|
|
824
|
+
e.add_note("- Cli")
|
|
825
|
+
raise
|
|
826
|
+
|
|
827
|
+
ns = parser.parse_args(input)
|
|
828
|
+
return Route.from_ns(ns)
|
|
829
|
+
|
|
830
|
+
def add_command[**P](
|
|
831
|
+
self,
|
|
832
|
+
name: str | tuple[str, ...],
|
|
833
|
+
func: Callable[P, None],
|
|
834
|
+
*,
|
|
835
|
+
help: str | Markup | None = None,
|
|
836
|
+
info: str | Markup | None = None,
|
|
837
|
+
usage: str | Markup | None = None,
|
|
838
|
+
epilog: str | Markup | None = None,
|
|
839
|
+
add_help: bool = True,
|
|
840
|
+
) -> Command[P]:
|
|
841
|
+
"""Creates and adds new command"""
|
|
842
|
+
cmd = Command(func, help=help, info=info, usage=usage, epilog=epilog, add_help=add_help)
|
|
843
|
+
self.nodes[name] = cmd
|
|
844
|
+
return cmd
|
|
845
|
+
|
|
846
|
+
def add_group(
|
|
847
|
+
self,
|
|
848
|
+
name: str | tuple[str, ...],
|
|
849
|
+
*,
|
|
850
|
+
help: str | Markup | None = None,
|
|
851
|
+
info: str | Markup | None = None,
|
|
852
|
+
metavar: str | None = None,
|
|
853
|
+
usage: str | Markup | None = None,
|
|
854
|
+
epilog: str | Markup | None = None,
|
|
855
|
+
title: str | None = None,
|
|
856
|
+
headline: str | Markup | None = None,
|
|
857
|
+
default_func: DefaultFunc[...] | None = print_group_help,
|
|
858
|
+
add_help: bool = True,
|
|
859
|
+
) -> "Group":
|
|
860
|
+
"""Creates and adds new group"""
|
|
861
|
+
group = Group(
|
|
862
|
+
help=help,
|
|
863
|
+
info=info,
|
|
864
|
+
headline=headline,
|
|
865
|
+
title=title,
|
|
866
|
+
metavar=metavar,
|
|
867
|
+
usage=usage,
|
|
868
|
+
epilog=epilog,
|
|
869
|
+
default_func=default_func,
|
|
870
|
+
add_help=add_help,
|
|
871
|
+
)
|
|
872
|
+
self.nodes[name] = group
|
|
873
|
+
return group
|
|
874
|
+
|
|
875
|
+
def add_callable_group[**P](
|
|
876
|
+
self,
|
|
877
|
+
name: str | tuple[str, ...],
|
|
878
|
+
func: Callable[P, None],
|
|
879
|
+
*,
|
|
880
|
+
help: str | Markup | None = None,
|
|
881
|
+
info: str | Markup | None = None,
|
|
882
|
+
metavar: str | None = None,
|
|
883
|
+
usage: str | Markup | None = None,
|
|
884
|
+
epilog: str | Markup | None = None,
|
|
885
|
+
title: str | None = None,
|
|
886
|
+
headline: str | Markup | None = None,
|
|
887
|
+
default_func: DefaultFunc[P] | None = print_group_help,
|
|
888
|
+
add_help: bool = True,
|
|
889
|
+
) -> "CallableGroup[P]":
|
|
890
|
+
"""Creates and adds new group with handler and parameters"""
|
|
891
|
+
group = CallableGroup(
|
|
892
|
+
func=func,
|
|
893
|
+
help=help,
|
|
894
|
+
info=info,
|
|
895
|
+
headline=headline,
|
|
896
|
+
title=title,
|
|
897
|
+
metavar=metavar,
|
|
898
|
+
usage=usage,
|
|
899
|
+
epilog=epilog,
|
|
900
|
+
default_func=default_func,
|
|
901
|
+
add_help=add_help,
|
|
902
|
+
)
|
|
903
|
+
self.nodes[name] = group
|
|
904
|
+
return group
|
|
905
|
+
|
|
906
|
+
def __setitem__(self, name: str | tuple[str, ...], val: "Group | Command[...]") -> None:
|
|
907
|
+
self.nodes[name] = val
|
|
908
|
+
|
|
909
|
+
|
|
910
|
+
class CallableGroup[**P](Group):
|
|
911
|
+
"""Group with with parameters"""
|
|
912
|
+
|
|
913
|
+
def __init__(
|
|
914
|
+
self,
|
|
915
|
+
func: Callable[P, None],
|
|
916
|
+
*,
|
|
917
|
+
help: str | Markup | None = None,
|
|
918
|
+
info: str | Markup | None = None,
|
|
919
|
+
usage: str | Markup | None = None,
|
|
920
|
+
epilog: str | Markup | None = None,
|
|
921
|
+
prog: str | None = None,
|
|
922
|
+
title: str | None = None,
|
|
923
|
+
headline: str | Markup | None = None,
|
|
924
|
+
metavar: str | None = None,
|
|
925
|
+
default_func: DefaultFunc[P] | None = print_group_help,
|
|
926
|
+
add_help: bool = True,
|
|
927
|
+
):
|
|
928
|
+
super().__init__(
|
|
929
|
+
help=help,
|
|
930
|
+
info=info,
|
|
931
|
+
usage=usage,
|
|
932
|
+
epilog=epilog,
|
|
933
|
+
prog=prog,
|
|
934
|
+
title=title,
|
|
935
|
+
headline=headline,
|
|
936
|
+
metavar=metavar,
|
|
937
|
+
default_func=default_func,
|
|
938
|
+
add_help=add_help,
|
|
939
|
+
)
|
|
940
|
+
self._func = func
|
|
941
|
+
|
|
942
|
+
def bind(self, *arguments: P.args, **options: P.kwargs) -> Self:
|
|
943
|
+
"""Bind function parameters to arguments/options"""
|
|
944
|
+
self._set_params(*arguments, **options)
|
|
945
|
+
return self
|
|
946
|
+
|
|
947
|
+
|
|
948
|
+
def help_action(*, help: str | Markup | bool = "Show help and exit") -> Action:
|
|
949
|
+
return Action(("--help", "-h"), action="help", help=help)
|
|
950
|
+
|
|
951
|
+
|
|
952
|
+
def version_action(version: str, *, help: str | Markup | bool = "Show program's version number and exit") -> Action:
|
|
953
|
+
return Action(("--version",), action="version", help=help, extra={"version": version})
|
|
954
|
+
|
|
955
|
+
|
|
956
|
+
_help_action: Final = help_action()
|