paramspecli 0.2.1__tar.gz → 0.2.2__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.
- {paramspecli-0.2.1 → paramspecli-0.2.2}/PKG-INFO +2 -2
- {paramspecli-0.2.1 → paramspecli-0.2.2}/README.md +1 -1
- {paramspecli-0.2.1 → paramspecli-0.2.2}/src/paramspecli/__init__.py +3 -1
- {paramspecli-0.2.1 → paramspecli-0.2.2}/src/paramspecli/apstub.py +1 -1
- {paramspecli-0.2.1 → paramspecli-0.2.2}/src/paramspecli/args.py +1 -1
- {paramspecli-0.2.1 → paramspecli-0.2.2}/src/paramspecli/cli.py +148 -103
- {paramspecli-0.2.1 → paramspecli-0.2.2}/src/paramspecli/conv.py +10 -10
- {paramspecli-0.2.1 → paramspecli-0.2.2}/src/paramspecli/doc.py +19 -9
- {paramspecli-0.2.1 → paramspecli-0.2.2}/src/paramspecli/flags.py +4 -13
- {paramspecli-0.2.1 → paramspecli-0.2.2}/src/paramspecli/opts.py +18 -10
- {paramspecli-0.2.1 → paramspecli-0.2.2}/LICENSE +0 -0
- {paramspecli-0.2.1 → paramspecli-0.2.2}/pyproject.toml +0 -0
- {paramspecli-0.2.1 → paramspecli-0.2.2}/src/paramspecli/fake.py +0 -0
- {paramspecli-0.2.1 → paramspecli-0.2.2}/src/paramspecli/md.py +0 -0
- {paramspecli-0.2.1 → paramspecli-0.2.2}/src/paramspecli/py.typed +0 -0
- {paramspecli-0.2.1 → paramspecli-0.2.2}/src/paramspecli/util.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: paramspecli
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.2
|
|
4
4
|
Summary: Type-safe facade for the venerable argparse
|
|
5
5
|
Author: jkmnt
|
|
6
6
|
Requires-Python: >=3.12
|
|
@@ -78,7 +78,7 @@ pip install paramspecli
|
|
|
78
78
|
- Commands match handler functions parameters with arguments and options.
|
|
79
79
|
- Arguments are bound to the handler's positional parameters.
|
|
80
80
|
- Options are bound to the handler's keyword parameters.
|
|
81
|
-
- Groups organize the commands. Groups may be nested. They could act like
|
|
81
|
+
- Groups organize the commands. Groups may be nested. They could act like an intermediate commands, i.e. have own handlers, options and arguments.
|
|
82
82
|
- CLI 'compiles' to the `argparse`, runs it, then outputs the parse result.
|
|
83
83
|
- Parse result is a callable. Calling it invokes handlers along the route.
|
|
84
84
|
|
|
@@ -64,7 +64,7 @@ pip install paramspecli
|
|
|
64
64
|
- Commands match handler functions parameters with arguments and options.
|
|
65
65
|
- Arguments are bound to the handler's positional parameters.
|
|
66
66
|
- Options are bound to the handler's keyword parameters.
|
|
67
|
-
- Groups organize the commands. Groups may be nested. They could act like
|
|
67
|
+
- Groups organize the commands. Groups may be nested. They could act like an intermediate commands, i.e. have own handlers, options and arguments.
|
|
68
68
|
- CLI 'compiles' to the `argparse`, runs it, then outputs the parse result.
|
|
69
69
|
- Parse result is a callable. Calling it invokes handlers along the route.
|
|
70
70
|
|
|
@@ -1,15 +1,17 @@
|
|
|
1
1
|
"""Type-safe facade for the venerable argparse"""
|
|
2
2
|
|
|
3
|
-
__version__ = "0.2.
|
|
3
|
+
__version__ = "0.2.2"
|
|
4
4
|
|
|
5
5
|
|
|
6
6
|
from .args import argument as argument
|
|
7
|
+
from .cli import MISSING as MISSING
|
|
7
8
|
from .cli import Action as Action
|
|
8
9
|
from .cli import CallableGroup as CallableGroup
|
|
9
10
|
from .cli import Command as Command
|
|
10
11
|
from .cli import Config as Config
|
|
11
12
|
from .cli import Group as Group
|
|
12
13
|
from .cli import Handler as Handler
|
|
14
|
+
from .cli import Missing as Missing
|
|
13
15
|
from .cli import Route as Route
|
|
14
16
|
from .cli import help_action as help_action
|
|
15
17
|
from .cli import version_action as version_action
|
|
@@ -81,4 +81,4 @@ class ArgumentParserLike(
|
|
|
81
81
|
def exit(self, status: int = ..., message: str | None = ...) -> NoReturn: ...
|
|
82
82
|
|
|
83
83
|
|
|
84
|
-
class ArgumentGroupLike(SupportsAddArgument, SupportsAddOneofGroup, Protocol): ...
|
|
84
|
+
class ArgumentGroupLike(SupportsAddArgument, SupportsAddOneofGroup, SupportsSetDefaults, Protocol): ...
|
|
@@ -107,4 +107,4 @@ def argument(
|
|
|
107
107
|
choices: Iterable[Any] | None = None,
|
|
108
108
|
) -> Argument[Any, Any]:
|
|
109
109
|
"""Positional argument. Always required, unless made optional via `nargs="?"`."""
|
|
110
|
-
return Argument(metavar, help=help,
|
|
110
|
+
return Argument(metavar, help=help, conv=type, choices=choices, nargs=nargs, default=default)
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import argparse
|
|
2
2
|
import dataclasses
|
|
3
3
|
import sys
|
|
4
|
+
from types import EllipsisType
|
|
4
5
|
from typing import (
|
|
5
6
|
Any,
|
|
6
7
|
Callable,
|
|
@@ -10,6 +11,7 @@ from typing import (
|
|
|
10
11
|
Iterator,
|
|
11
12
|
Literal,
|
|
12
13
|
Mapping,
|
|
14
|
+
NamedTuple,
|
|
13
15
|
Protocol,
|
|
14
16
|
Self,
|
|
15
17
|
Sequence,
|
|
@@ -28,14 +30,20 @@ from .apstub import (
|
|
|
28
30
|
SupportsSetDefaults,
|
|
29
31
|
TypeConverter,
|
|
30
32
|
)
|
|
33
|
+
from .conv import PathConv
|
|
31
34
|
|
|
32
35
|
# TODO:
|
|
33
36
|
# if section has a headline, it's not auto-purged from the help.
|
|
34
37
|
# Ok for user sections, bad for the default options/arguments sections
|
|
35
38
|
|
|
36
|
-
type
|
|
39
|
+
type Missing = EllipsisType
|
|
40
|
+
MISSING: Final = ...
|
|
37
41
|
|
|
38
|
-
|
|
42
|
+
|
|
43
|
+
class HandlerSpec(NamedTuple):
|
|
44
|
+
func: Callable[..., None] | None
|
|
45
|
+
args: list[str] # [arg_key]
|
|
46
|
+
opts: dict[str, str] # {opt_name: opt_key}
|
|
39
47
|
|
|
40
48
|
|
|
41
49
|
class Markup(Protocol):
|
|
@@ -48,11 +56,16 @@ class DefaultFunc[**P](Protocol):
|
|
|
48
56
|
def __call__(self, parser: ArgumentParserLike, /, *args: P.args, **kwargs: P.kwargs) -> None: ...
|
|
49
57
|
|
|
50
58
|
|
|
51
|
-
def _make_spec(
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
59
|
+
def _make_spec(
|
|
60
|
+
level: int,
|
|
61
|
+
func: Callable[..., None] | None,
|
|
62
|
+
args: Sequence["Arg | Cnst | Ctx"],
|
|
63
|
+
options: Mapping[str, "Opt | MixedOpts | Cnst | Ctx"],
|
|
64
|
+
) -> HandlerSpec:
|
|
65
|
+
return HandlerSpec(
|
|
66
|
+
func=func,
|
|
67
|
+
args=[f"{level}[{i}]" for i in range(len(args))],
|
|
68
|
+
opts={param: f"{level}.{param}" for param in options},
|
|
56
69
|
)
|
|
57
70
|
|
|
58
71
|
|
|
@@ -94,11 +107,30 @@ def _as_plain(arg: str | Markup | None) -> str | None:
|
|
|
94
107
|
return str(arg)
|
|
95
108
|
|
|
96
109
|
|
|
110
|
+
def _may_raise_custom_exceptions(conv: TypeConverter[Any]) -> bool:
|
|
111
|
+
if conv is int:
|
|
112
|
+
return False
|
|
113
|
+
if conv is float:
|
|
114
|
+
return False
|
|
115
|
+
if conv is bool:
|
|
116
|
+
return False
|
|
117
|
+
if isinstance(conv, PathConv):
|
|
118
|
+
return False
|
|
119
|
+
return True
|
|
120
|
+
|
|
121
|
+
|
|
97
122
|
def print_group_help(parser: ArgumentParserLike, /, *args: Any, **kwargs: Any) -> None:
|
|
98
123
|
parser.print_help()
|
|
99
124
|
parser.exit()
|
|
100
125
|
|
|
101
126
|
|
|
127
|
+
# print some values in a nicer way. currently it's a lists w/o quotes
|
|
128
|
+
def nice_str(val: Any) -> str:
|
|
129
|
+
if isinstance(val, list):
|
|
130
|
+
return f"[{ ", ".join([str(item) for item in val])}]"
|
|
131
|
+
return str(val)
|
|
132
|
+
|
|
133
|
+
|
|
102
134
|
def _with_argparser_arg[**P](
|
|
103
135
|
f: Callable[Concatenate[ArgumentParserLike, P], None], argparser: ArgumentParserLike
|
|
104
136
|
) -> Callable[P, None]:
|
|
@@ -126,6 +158,17 @@ class HelpFormatter(argparse.HelpFormatter):
|
|
|
126
158
|
return text.splitlines()
|
|
127
159
|
return super()._split_lines(text, width)
|
|
128
160
|
|
|
161
|
+
# interpolate lists manually to avoid extra quotes
|
|
162
|
+
def _get_help_string(self, action: argparse.Action) -> str | None:
|
|
163
|
+
help = action.help
|
|
164
|
+
|
|
165
|
+
if help is not None and "%(default)s" in help:
|
|
166
|
+
if isinstance(action.default, list):
|
|
167
|
+
s = nice_str(action.default)
|
|
168
|
+
help = help.replace("%(default)s", s)
|
|
169
|
+
|
|
170
|
+
return help
|
|
171
|
+
|
|
129
172
|
|
|
130
173
|
@dataclasses.dataclass
|
|
131
174
|
class Config:
|
|
@@ -152,6 +195,8 @@ class Config:
|
|
|
152
195
|
parser_class: type[argparse.ArgumentParser] = argparse.ArgumentParser
|
|
153
196
|
"""Argparge parser class to use"""
|
|
154
197
|
formatter_class: type[argparse.HelpFormatter] = HelpFormatter
|
|
198
|
+
"""Silently ignore unknown args and place them into the Route.unknown_args"""
|
|
199
|
+
ignore_unknown_args: bool = False
|
|
155
200
|
"""Argparse help formatter class to use"""
|
|
156
201
|
root_parser_extra_kwargs: Mapping[str, Any] = dataclasses.field(default_factory=dict)
|
|
157
202
|
"""Dict of extra kwargs for the root (CLI) argparse.ArgumentParser"""
|
|
@@ -165,7 +210,7 @@ class Arg:
|
|
|
165
210
|
|
|
166
211
|
metavar: str
|
|
167
212
|
_: dataclasses.KW_ONLY
|
|
168
|
-
|
|
213
|
+
conv: TypeConverter[Any] | None = None
|
|
169
214
|
help: str | Markup | bool | None = None
|
|
170
215
|
choices: Iterable[Any] | None = None
|
|
171
216
|
nargs: int | Literal["*", "+", "?"] | None = None
|
|
@@ -179,10 +224,10 @@ class Arg:
|
|
|
179
224
|
def __hash__(self) -> int:
|
|
180
225
|
return hash(self.metavar)
|
|
181
226
|
|
|
182
|
-
def _build(self, owner: SupportsAddArgument, config: Config, *, dest: str) -> None:
|
|
183
|
-
|
|
184
|
-
if
|
|
185
|
-
|
|
227
|
+
def _build(self, owner: SupportsAddArgument, config: Config, *, dest: str, **_rest: Any) -> None:
|
|
228
|
+
conv = self.conv
|
|
229
|
+
if conv and config.catch_typeconv_exceptions and _may_raise_custom_exceptions(conv):
|
|
230
|
+
conv = util.catch_all(conv)
|
|
186
231
|
|
|
187
232
|
help = self.help
|
|
188
233
|
|
|
@@ -194,17 +239,16 @@ class Arg:
|
|
|
194
239
|
# most kwargs shouldn't be set at all if None
|
|
195
240
|
kwargs: dict[str, Any] = {
|
|
196
241
|
"choices": self.choices,
|
|
197
|
-
"default": self.default,
|
|
198
242
|
"nargs": self.nargs,
|
|
199
243
|
"metavar": self.metavar,
|
|
200
244
|
# NOTE: if type is not specified, argparse's MetavarTypeHelpFormatter may fail!
|
|
201
|
-
"type":
|
|
245
|
+
"type": conv,
|
|
202
246
|
}
|
|
203
247
|
kwargs = {k: v for k, v in kwargs.items() if v is not None}
|
|
204
248
|
if self.extra:
|
|
205
249
|
kwargs |= self.extra
|
|
206
250
|
|
|
207
|
-
owner.add_argument(dest=dest, help=_as_plain(help), **kwargs)
|
|
251
|
+
owner.add_argument(dest=dest, help=_as_plain(help), default=self.default, **kwargs)
|
|
208
252
|
|
|
209
253
|
|
|
210
254
|
# Too many fields to manage: dataclass to the rescue
|
|
@@ -214,14 +258,14 @@ class Opt:
|
|
|
214
258
|
|
|
215
259
|
names: tuple[str, ...]
|
|
216
260
|
_: dataclasses.KW_ONLY
|
|
217
|
-
|
|
261
|
+
conv: TypeConverter[Any] | None = None
|
|
218
262
|
help: str | Markup | bool | None = None
|
|
219
263
|
hard_show_default: bool | str | None = None
|
|
220
264
|
soft_show_default: bool | str = False
|
|
221
265
|
required: bool = False
|
|
222
266
|
choices: Iterable[Any] | None = None
|
|
223
267
|
metavar: str | tuple[str, ...] | None = None
|
|
224
|
-
action: str |
|
|
268
|
+
action: str | type[argparse.Action] | None = None
|
|
225
269
|
nargs: int | Literal["*", "+", "?"] | None = None
|
|
226
270
|
default: Any = None
|
|
227
271
|
const: Any = None
|
|
@@ -243,7 +287,7 @@ class Opt:
|
|
|
243
287
|
def __hash__(self) -> int:
|
|
244
288
|
return hash(self.names)
|
|
245
289
|
|
|
246
|
-
def _build(self, owner: SupportsAddArgument, config: Config, *, dest: str | Literal[False]) -> None:
|
|
290
|
+
def _build(self, owner: SupportsAddArgument, config: Config, *, dest: str | Literal[False], **_rest: Any) -> None:
|
|
247
291
|
help = self.help
|
|
248
292
|
|
|
249
293
|
if help is False:
|
|
@@ -268,19 +312,18 @@ class Opt:
|
|
|
268
312
|
else:
|
|
269
313
|
help += f" (default: {show_default})"
|
|
270
314
|
|
|
271
|
-
|
|
272
|
-
if
|
|
273
|
-
|
|
315
|
+
conv = self.conv
|
|
316
|
+
if conv and config.catch_typeconv_exceptions and _may_raise_custom_exceptions(conv):
|
|
317
|
+
conv = util.catch_all(conv)
|
|
274
318
|
|
|
275
319
|
# most kwargs shouldn't be set at all if None
|
|
276
320
|
kwargs: dict[str, Any] = {
|
|
277
321
|
"action": self.action,
|
|
278
322
|
"choices": self.choices,
|
|
279
|
-
"default": self.default,
|
|
280
323
|
"nargs": self.nargs,
|
|
281
324
|
"const": self.const,
|
|
282
325
|
"metavar": self.metavar,
|
|
283
|
-
"type":
|
|
326
|
+
"type": conv,
|
|
284
327
|
"required": self.required or None,
|
|
285
328
|
"deprecated": self.deprecated if sys.version_info >= (3, 13) else None,
|
|
286
329
|
}
|
|
@@ -289,7 +332,7 @@ class Opt:
|
|
|
289
332
|
if self.extra:
|
|
290
333
|
kwargs |= self.extra
|
|
291
334
|
|
|
292
|
-
owner.add_argument(*self.names, dest=dest or argparse.SUPPRESS, help=help, **kwargs)
|
|
335
|
+
owner.add_argument(*self.names, dest=dest or argparse.SUPPRESS, default=self.default, help=help, **kwargs)
|
|
293
336
|
|
|
294
337
|
@property
|
|
295
338
|
def is_hidden(self) -> bool:
|
|
@@ -300,6 +343,34 @@ class Opt:
|
|
|
300
343
|
return self
|
|
301
344
|
|
|
302
345
|
|
|
346
|
+
|
|
347
|
+
class Cnst:
|
|
348
|
+
__slots__ = ("value",)
|
|
349
|
+
|
|
350
|
+
def __init__(self, value: Any):
|
|
351
|
+
self.value: Final = value
|
|
352
|
+
|
|
353
|
+
def __repr__(self) -> str:
|
|
354
|
+
return _repr_class(self, {"value": self.value}, skip_none=False)
|
|
355
|
+
|
|
356
|
+
def __eq__(self, other: object) -> bool:
|
|
357
|
+
if not isinstance(other, Cnst):
|
|
358
|
+
return NotImplemented
|
|
359
|
+
return bool(self.value == other.value)
|
|
360
|
+
|
|
361
|
+
def _build(self, owner: SupportsSetDefaults, config: Config, *, dest: str, **_rest: Any) -> None:
|
|
362
|
+
owner.set_defaults(**{dest: self.value})
|
|
363
|
+
|
|
364
|
+
|
|
365
|
+
class Ctx:
|
|
366
|
+
__slots__ = ()
|
|
367
|
+
|
|
368
|
+
def _build(self, owner: SupportsSetDefaults, config: Config, *, dest: str, context: Any) -> None:
|
|
369
|
+
if context is None:
|
|
370
|
+
raise ValueError("context can't be None")
|
|
371
|
+
owner.set_defaults(**{dest: context})
|
|
372
|
+
|
|
373
|
+
|
|
303
374
|
class MixedOpts:
|
|
304
375
|
"""Abstract mix of options"""
|
|
305
376
|
|
|
@@ -322,44 +393,17 @@ class MixedOpts:
|
|
|
322
393
|
return self
|
|
323
394
|
|
|
324
395
|
|
|
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
396
|
class Action(Opt):
|
|
344
397
|
"""Special kind of option with side effects"""
|
|
345
398
|
|
|
346
399
|
__slots__ = ()
|
|
347
400
|
|
|
348
|
-
def _build(self, owner: SupportsAddArgument, config: Config, *, dest: str | Literal[False]) -> None:
|
|
401
|
+
def _build(self, owner: SupportsAddArgument, config: Config, *, dest: str | Literal[False], **_rest: Any) -> None:
|
|
349
402
|
if dest is not False:
|
|
350
403
|
raise TypeError(f"Action is used in place of Option {dest!r}")
|
|
351
404
|
return super()._build(owner, config, dest=dest)
|
|
352
405
|
|
|
353
406
|
|
|
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
407
|
class Section:
|
|
364
408
|
"""Shows its options in a separate block of a `--help` output"""
|
|
365
409
|
|
|
@@ -427,7 +471,7 @@ class ParserLike:
|
|
|
427
471
|
|
|
428
472
|
self.arguments: list[Arg | Cnst | Ctx] | None = None
|
|
429
473
|
self.actions: list[Action] = []
|
|
430
|
-
self.options: dict[str, Opt | MixedOpts | Cnst | Ctx] | None =
|
|
474
|
+
self.options: dict[str, Opt | MixedOpts | Cnst | Ctx] | None = None
|
|
431
475
|
self.sections: list[Section] = []
|
|
432
476
|
self.oneofs: list[Oneof] = []
|
|
433
477
|
|
|
@@ -452,7 +496,7 @@ class ParserLike:
|
|
|
452
496
|
self.options = {**options}
|
|
453
497
|
|
|
454
498
|
def _build_sections(
|
|
455
|
-
self,
|
|
499
|
+
self, parser: ArgumentParserLike, config: Config, *, default_group: ArgumentGroupLike
|
|
456
500
|
) -> dict[Opt, SupportsAddArgument]:
|
|
457
501
|
sectmap: dict[Opt, ArgumentGroupLike] = {}
|
|
458
502
|
oneofmap: dict[Opt, SupportsAddArgument] = {}
|
|
@@ -461,7 +505,7 @@ class ParserLike:
|
|
|
461
505
|
if section_dupes := section.options.intersection(sectmap):
|
|
462
506
|
raise KeyError(f"options {section_dupes} are in several sections at once")
|
|
463
507
|
|
|
464
|
-
sectmap |= dict.fromkeys(section.options, section._build(
|
|
508
|
+
sectmap |= dict.fromkeys(section.options, section._build(parser, config))
|
|
465
509
|
|
|
466
510
|
for oneof in self.oneofs:
|
|
467
511
|
if not oneof.options:
|
|
@@ -480,58 +524,51 @@ class ParserLike:
|
|
|
480
524
|
return tgtmap
|
|
481
525
|
|
|
482
526
|
def _build_params(
|
|
483
|
-
self,
|
|
527
|
+
self, parser: ArgumentParserLike, config: Config, *, parents: Sequence["ParserLike"], context: Any
|
|
484
528
|
) -> None:
|
|
485
529
|
if self._func:
|
|
486
530
|
if self.arguments is None or self.options is None:
|
|
487
531
|
raise ValueError("bind() was not called")
|
|
488
532
|
|
|
489
|
-
|
|
533
|
+
level = len(parents)
|
|
490
534
|
|
|
491
|
-
spec = _make_spec(
|
|
492
|
-
|
|
535
|
+
spec = _make_spec(level, self._func, self.arguments or [], self.options or {})
|
|
536
|
+
parser.set_defaults(**{str(level): spec})
|
|
493
537
|
|
|
494
538
|
if self.arguments:
|
|
495
|
-
arguments_group =
|
|
539
|
+
arguments_group = parser.add_argument_group(
|
|
496
540
|
title=config.arguments_title, description=_as_plain(config.arguments_headline)
|
|
497
541
|
)
|
|
498
542
|
|
|
499
543
|
for i, argument in enumerate(self.arguments):
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
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(
|
|
544
|
+
dest = f"{level}[{i}]"
|
|
545
|
+
argument._build(arguments_group, config, dest=dest, context=context)
|
|
546
|
+
|
|
547
|
+
default_options_group = parser.add_argument_group(
|
|
509
548
|
title=config.options_title, description=_as_plain(config.options_headline)
|
|
510
549
|
)
|
|
511
550
|
|
|
512
|
-
tgtmap = self._build_sections(
|
|
551
|
+
tgtmap = self._build_sections(parser, config, default_group=default_options_group)
|
|
513
552
|
|
|
514
553
|
for action in self.actions:
|
|
515
554
|
action._build(tgtmap.pop(action, default_options_group), config, dest=False)
|
|
516
555
|
|
|
517
556
|
if self.options:
|
|
518
557
|
for param, item in self.options.items():
|
|
519
|
-
|
|
520
|
-
if isinstance(item,
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
for opt in seq:
|
|
527
|
-
opt._build(tgtmap.pop(opt, default_options_group), config, dest=name)
|
|
558
|
+
dest = f"{level}.{param}"
|
|
559
|
+
seq = (item,) if not isinstance(item, MixedOpts) else item.options
|
|
560
|
+
for opt in seq:
|
|
561
|
+
if isinstance(opt, (Cnst, Ctx)):
|
|
562
|
+
opt._build(parser, config, dest=dest, context=context)
|
|
563
|
+
else:
|
|
564
|
+
opt._build(tgtmap.pop(opt, default_options_group), config, dest=dest, context=context)
|
|
528
565
|
|
|
529
566
|
if tgtmap:
|
|
530
567
|
raise KeyError(f"unconsumed items in sections: {tgtmap.keys()}")
|
|
531
568
|
|
|
532
569
|
def _build_subparser(
|
|
533
570
|
self,
|
|
534
|
-
|
|
571
|
+
parent: SupportsAddParser,
|
|
535
572
|
config: Config,
|
|
536
573
|
*,
|
|
537
574
|
parents: Sequence["ParserLike"],
|
|
@@ -544,7 +581,7 @@ class ParserLike:
|
|
|
544
581
|
if epilog is None and config.propagate_epilog and parents:
|
|
545
582
|
epilog = parents[0].epilog
|
|
546
583
|
|
|
547
|
-
return
|
|
584
|
+
return parent.add_parser(
|
|
548
585
|
basename,
|
|
549
586
|
aliases=aliases,
|
|
550
587
|
help=_as_plain(self.help),
|
|
@@ -635,12 +672,10 @@ class Handler:
|
|
|
635
672
|
def from_ns(cls, ns: argparse.Namespace, spec: HandlerSpec) -> Self:
|
|
636
673
|
vars = ns.__dict__
|
|
637
674
|
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
args = [vars[name] for name in argnames]
|
|
641
|
-
kwargs = {k: vars[v] for k, v in kwargnames.items()}
|
|
675
|
+
args = [vars[name] for name in spec.args]
|
|
676
|
+
kwargs = {k: vars[v] for k, v in spec.opts.items()}
|
|
642
677
|
|
|
643
|
-
return cls(func, args, kwargs)
|
|
678
|
+
return cls(spec.func, args, kwargs)
|
|
644
679
|
|
|
645
680
|
@classmethod
|
|
646
681
|
def from_spec(cls, func: Callable[..., None] | None, /, *arguments: Any, **options: Any) -> Self:
|
|
@@ -651,9 +686,11 @@ class Route:
|
|
|
651
686
|
"""Represents sequence of handers, arguments and options.
|
|
652
687
|
Calling it will invoke the handlers"""
|
|
653
688
|
|
|
654
|
-
def __init__(self, handlers: Sequence[Handler]):
|
|
689
|
+
def __init__(self, handlers: Sequence[Handler], unknown_args: Sequence[str] | None = None):
|
|
655
690
|
self.handlers = handlers
|
|
691
|
+
self.unknown_args = unknown_args
|
|
656
692
|
|
|
693
|
+
# unrecognized args are ignored for the comparison
|
|
657
694
|
def __eq__(self, other: object) -> bool:
|
|
658
695
|
if not isinstance(other, Route):
|
|
659
696
|
return NotImplemented
|
|
@@ -680,7 +717,7 @@ class Route:
|
|
|
680
717
|
return [h for h in self.handlers if h.func is not None]
|
|
681
718
|
|
|
682
719
|
@classmethod
|
|
683
|
-
def from_ns(cls, ns: argparse.Namespace) -> Self:
|
|
720
|
+
def from_ns(cls, ns: argparse.Namespace, unknown_args: Sequence[str] | None = None) -> Self:
|
|
684
721
|
specs: list[tuple[int, HandlerSpec]] = []
|
|
685
722
|
for k, v in ns.__dict__.items():
|
|
686
723
|
try:
|
|
@@ -692,7 +729,7 @@ class Route:
|
|
|
692
729
|
specs.sort()
|
|
693
730
|
|
|
694
731
|
handlers = [Handler.from_ns(ns, spec[1]) for spec in specs]
|
|
695
|
-
return cls(handlers)
|
|
732
|
+
return cls(handlers, unknown_args=unknown_args)
|
|
696
733
|
|
|
697
734
|
|
|
698
735
|
## More concrete
|
|
@@ -720,14 +757,14 @@ class Command[**P](ParserLike):
|
|
|
720
757
|
|
|
721
758
|
def _build(
|
|
722
759
|
self,
|
|
723
|
-
|
|
760
|
+
parser: ArgumentParserLike,
|
|
724
761
|
config: Config,
|
|
725
762
|
*,
|
|
726
763
|
parents: Sequence["Group"],
|
|
727
764
|
context: Any,
|
|
728
765
|
**kwargs: Any,
|
|
729
766
|
) -> None:
|
|
730
|
-
self._build_params(
|
|
767
|
+
self._build_params(parser, config, parents=parents, context=context)
|
|
731
768
|
|
|
732
769
|
def bind(self, *arguments: P.args, **options: P.kwargs) -> Self:
|
|
733
770
|
"""Bind function parameters to arguments/options"""
|
|
@@ -741,6 +778,10 @@ class Command[**P](ParserLike):
|
|
|
741
778
|
parser = self._build_root_parser(config)
|
|
742
779
|
self._build(parser, config, parents=[], context=context)
|
|
743
780
|
|
|
781
|
+
if config.ignore_unknown_args:
|
|
782
|
+
ns, unknown_args = parser.parse_known_args(input)
|
|
783
|
+
return Route.from_ns(ns, unknown_args=unknown_args)
|
|
784
|
+
|
|
744
785
|
ns = parser.parse_args(input)
|
|
745
786
|
return Route.from_ns(ns)
|
|
746
787
|
|
|
@@ -773,31 +814,31 @@ class Group(ParserLike):
|
|
|
773
814
|
if add_help:
|
|
774
815
|
self.append_action(_help_action)
|
|
775
816
|
|
|
776
|
-
def _build(self,
|
|
777
|
-
self._build_params(
|
|
778
|
-
self._build_default_handler(
|
|
779
|
-
self._build_nodes(
|
|
817
|
+
def _build(self, parser: ArgumentParserLike, config: Config, *, parents: Sequence["Group"], context: Any) -> None:
|
|
818
|
+
self._build_params(parser, config, parents=parents, context=context)
|
|
819
|
+
self._build_default_handler(parser, config, parents=parents)
|
|
820
|
+
self._build_nodes(parser, config, parents=parents, context=context)
|
|
780
821
|
|
|
781
822
|
def _build_default_handler(
|
|
782
|
-
self,
|
|
823
|
+
self, parser: ArgumentParserLike, config: Config, *, parents: Sequence["ParserLike"]
|
|
783
824
|
) -> None:
|
|
784
825
|
if not self.default_func:
|
|
785
826
|
return
|
|
786
827
|
|
|
787
|
-
default_func = _with_argparser_arg(self.default_func,
|
|
828
|
+
default_func = _with_argparser_arg(self.default_func, parser)
|
|
788
829
|
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
spec = _make_spec(
|
|
792
|
-
|
|
830
|
+
level = len(parents)
|
|
831
|
+
next_level = level + 1
|
|
832
|
+
spec = _make_spec(level, default_func, self.arguments or [], self.options or {})
|
|
833
|
+
parser.set_defaults(**{str(next_level): spec})
|
|
793
834
|
|
|
794
835
|
def _build_nodes(
|
|
795
|
-
self,
|
|
836
|
+
self, parent: ArgumentParserLike, config: Config, *, parents: Sequence["Group"], context: Any
|
|
796
837
|
) -> None:
|
|
797
838
|
if not self.nodes:
|
|
798
839
|
return
|
|
799
840
|
|
|
800
|
-
sub =
|
|
841
|
+
sub = parent.add_subparsers(
|
|
801
842
|
title=self.title if self.title is not None else config.commands_title,
|
|
802
843
|
description=_as_plain(self.headline),
|
|
803
844
|
parser_class=config.parser_class,
|
|
@@ -824,6 +865,10 @@ class Group(ParserLike):
|
|
|
824
865
|
e.add_note("- Cli")
|
|
825
866
|
raise
|
|
826
867
|
|
|
868
|
+
if config.ignore_unknown_args:
|
|
869
|
+
ns, unknown_args = parser.parse_known_args(input)
|
|
870
|
+
return Route.from_ns(ns, unknown_args=unknown_args)
|
|
871
|
+
|
|
827
872
|
ns = parser.parse_args(input)
|
|
828
873
|
return Route.from_ns(ns)
|
|
829
874
|
|
|
@@ -6,11 +6,11 @@ from typing import Final, Literal
|
|
|
6
6
|
class PathConv:
|
|
7
7
|
__name__ = "PATH"
|
|
8
8
|
|
|
9
|
-
__slots__ = ("exists", "
|
|
9
|
+
__slots__ = ("exists", "kind", "resolve")
|
|
10
10
|
|
|
11
|
-
def __init__(self,
|
|
11
|
+
def __init__(self, kind: Literal["file", "dir", None] = None, *, exists: bool | None = None, resolve: bool = True):
|
|
12
12
|
self.exists: Final = exists
|
|
13
|
-
self.
|
|
13
|
+
self.kind: Final = kind
|
|
14
14
|
self.resolve: Final = resolve
|
|
15
15
|
|
|
16
16
|
# scenarios:
|
|
@@ -26,24 +26,24 @@ class PathConv:
|
|
|
26
26
|
if self.resolve:
|
|
27
27
|
path = path.resolve()
|
|
28
28
|
|
|
29
|
-
|
|
29
|
+
check_kind = False
|
|
30
30
|
|
|
31
31
|
if self.exists is True:
|
|
32
32
|
if not path.exists():
|
|
33
33
|
raise ArgumentTypeError(f"{path} does not exists")
|
|
34
|
-
|
|
34
|
+
check_kind = True
|
|
35
35
|
elif self.exists is False:
|
|
36
36
|
if path.exists():
|
|
37
37
|
raise ArgumentTypeError(f"{path} already exists")
|
|
38
38
|
else:
|
|
39
|
-
if self.
|
|
40
|
-
|
|
39
|
+
if self.kind and path.exists():
|
|
40
|
+
check_kind = True
|
|
41
41
|
|
|
42
|
-
if
|
|
43
|
-
if self.
|
|
42
|
+
if check_kind:
|
|
43
|
+
if self.kind == "file":
|
|
44
44
|
if not path.is_file():
|
|
45
45
|
raise ArgumentTypeError(f"{path} is not a file")
|
|
46
|
-
elif self.
|
|
46
|
+
elif self.kind == "dir":
|
|
47
47
|
if not path.is_dir():
|
|
48
48
|
raise ArgumentTypeError(f"{path} is not a directory")
|
|
49
49
|
|
|
@@ -4,7 +4,7 @@ from typing import Any, Final, Iterable, Mapping, NamedTuple, Protocol, Sequence
|
|
|
4
4
|
|
|
5
5
|
from paramspecli import md
|
|
6
6
|
|
|
7
|
-
from .cli import Arg, Command, Group, Opt, Section, name_and_aliases
|
|
7
|
+
from .cli import Arg, Command, Group, Opt, Section, name_and_aliases, nice_str
|
|
8
8
|
|
|
9
9
|
|
|
10
10
|
class Renderer(Protocol):
|
|
@@ -65,6 +65,7 @@ class Settings:
|
|
|
65
65
|
usage_title: str = "Usage"
|
|
66
66
|
aliases_title: str = "Aliases"
|
|
67
67
|
options_headline: str | None = None
|
|
68
|
+
choice_metavar: str = "CHOICE"
|
|
68
69
|
|
|
69
70
|
|
|
70
71
|
class Doc:
|
|
@@ -121,8 +122,10 @@ class Doc:
|
|
|
121
122
|
else:
|
|
122
123
|
out += _join([r.code(name) for name in names], sep=", ")
|
|
123
124
|
|
|
124
|
-
|
|
125
|
-
|
|
125
|
+
metavar = self.r_metavar(option)
|
|
126
|
+
|
|
127
|
+
if metavar:
|
|
128
|
+
out += " " + r.i(r.e(metavar))
|
|
126
129
|
return out
|
|
127
130
|
|
|
128
131
|
def r_option_default(self, option: Opt) -> str:
|
|
@@ -134,12 +137,12 @@ class Doc:
|
|
|
134
137
|
if isinstance(option.hard_show_default, str):
|
|
135
138
|
default = option.hard_show_default
|
|
136
139
|
elif option.hard_show_default is True:
|
|
137
|
-
default =
|
|
140
|
+
default = nice_str(option.default)
|
|
138
141
|
else:
|
|
139
142
|
if isinstance(option.soft_show_default, str):
|
|
140
143
|
default = option.soft_show_default
|
|
141
144
|
elif option.soft_show_default is True:
|
|
142
|
-
default =
|
|
145
|
+
default = nice_str(option.default)
|
|
143
146
|
|
|
144
147
|
if default is None:
|
|
145
148
|
return ""
|
|
@@ -164,10 +167,17 @@ class Doc:
|
|
|
164
167
|
|
|
165
168
|
return out
|
|
166
169
|
|
|
167
|
-
def r_metavar(self,
|
|
170
|
+
def r_metavar(self, item: Opt | Arg) -> str:
|
|
171
|
+
metavar = item.metavar
|
|
168
172
|
if isinstance(metavar, tuple):
|
|
169
173
|
return _join(metavar, sep=" ")
|
|
170
174
|
|
|
175
|
+
if metavar is None:
|
|
176
|
+
if item.choices:
|
|
177
|
+
metavar = self.config.choice_metavar
|
|
178
|
+
else:
|
|
179
|
+
return ""
|
|
180
|
+
|
|
171
181
|
nargs = item.nargs
|
|
172
182
|
|
|
173
183
|
if isinstance(nargs, int):
|
|
@@ -177,7 +187,7 @@ class Doc:
|
|
|
177
187
|
if nargs == "+":
|
|
178
188
|
return f"{metavar} [{metavar} ...]"
|
|
179
189
|
if nargs == "?":
|
|
180
|
-
return f"[{metavar}
|
|
190
|
+
return f"[{metavar}]"
|
|
181
191
|
return metavar
|
|
182
192
|
|
|
183
193
|
def r_arguments(self, arguments: Sequence[Arg]) -> str:
|
|
@@ -231,7 +241,7 @@ class Doc:
|
|
|
231
241
|
def _group_usage_partial(self, me: BoundGroup) -> str:
|
|
232
242
|
return _join(
|
|
233
243
|
me.name,
|
|
234
|
-
[self.r_metavar(arg
|
|
244
|
+
[self.r_metavar(arg) for arg in self.get_arguments(me)],
|
|
235
245
|
sep=" ",
|
|
236
246
|
)
|
|
237
247
|
|
|
@@ -273,7 +283,7 @@ class Doc:
|
|
|
273
283
|
[self._group_usage_partial(p) for p in parents],
|
|
274
284
|
me.name,
|
|
275
285
|
[f"[{section.title.lower()}]" for section in options_sections],
|
|
276
|
-
[self.r_metavar(arg
|
|
286
|
+
[self.r_metavar(arg) for arg in self.get_arguments(me)],
|
|
277
287
|
sep=" ",
|
|
278
288
|
)
|
|
279
289
|
)
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
from argparse import BooleanOptionalAction
|
|
2
2
|
from typing import Any, Literal, overload
|
|
3
3
|
|
|
4
|
+
from .cli import MISSING
|
|
4
5
|
from .fake import Option, RepeatedOption
|
|
5
6
|
|
|
6
7
|
|
|
@@ -54,7 +55,7 @@ def flag[D](
|
|
|
54
55
|
def flag(
|
|
55
56
|
*names: str,
|
|
56
57
|
value: Any = True,
|
|
57
|
-
default: Any =
|
|
58
|
+
default: Any = MISSING,
|
|
58
59
|
help: str | bool | None = None,
|
|
59
60
|
show_default: bool | str | None = None,
|
|
60
61
|
) -> Option[Any, Any]:
|
|
@@ -80,7 +81,7 @@ def flag(
|
|
|
80
81
|
|
|
81
82
|
|
|
82
83
|
"""
|
|
83
|
-
if default is
|
|
84
|
+
if default is MISSING:
|
|
84
85
|
soft_show_default = False
|
|
85
86
|
if value is True:
|
|
86
87
|
default = False
|
|
@@ -130,16 +131,6 @@ def switch(
|
|
|
130
131
|
show_default: bool | str | None = None,
|
|
131
132
|
) -> Option[bool, Any]:
|
|
132
133
|
"""On/Off switch with complimentary flags: `--foo/--no-foo`. Default is `False`"""
|
|
133
|
-
longs = [name for name in names if name.startswith("--")]
|
|
134
|
-
if not longs:
|
|
135
|
-
raise ValueError("at least one name starting with '--' is required")
|
|
136
|
-
|
|
137
|
-
soft_show_default: bool | str
|
|
138
|
-
|
|
139
|
-
if default is True or default is False:
|
|
140
|
-
soft_show_default = longs[0] if default else f"--no-{longs[0][2:]}"
|
|
141
|
-
else:
|
|
142
|
-
soft_show_default = True
|
|
143
134
|
|
|
144
135
|
return Option(
|
|
145
136
|
names,
|
|
@@ -148,7 +139,7 @@ def switch(
|
|
|
148
139
|
default=default,
|
|
149
140
|
#
|
|
150
141
|
hard_show_default=show_default,
|
|
151
|
-
soft_show_default=
|
|
142
|
+
soft_show_default=True,
|
|
152
143
|
)
|
|
153
144
|
|
|
154
145
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
from types import EllipsisType
|
|
2
1
|
from typing import Any, Iterable, Literal, overload
|
|
3
2
|
|
|
4
3
|
from .apstub import TypeConverter
|
|
4
|
+
from .cli import Missing
|
|
5
5
|
from .fake import Option, RepeatedOption
|
|
6
6
|
|
|
7
7
|
|
|
@@ -49,7 +49,7 @@ def option(
|
|
|
49
49
|
choices: Iterable[str] | None = None,
|
|
50
50
|
metavar: str | tuple[str, ...] | None = None,
|
|
51
51
|
show_default: bool | str | None = None,
|
|
52
|
-
) -> Option[str |
|
|
52
|
+
) -> Option[str | Missing, None]: ...
|
|
53
53
|
|
|
54
54
|
|
|
55
55
|
## nargs, default: D
|
|
@@ -75,7 +75,7 @@ def option[D](
|
|
|
75
75
|
choices: Iterable[str] | None = None,
|
|
76
76
|
metavar: str | tuple[str, ...] | None = None,
|
|
77
77
|
show_default: bool | str | None = None,
|
|
78
|
-
) -> Option[str |
|
|
78
|
+
) -> Option[str | Missing, D]: ...
|
|
79
79
|
|
|
80
80
|
|
|
81
81
|
## type: T
|
|
@@ -127,7 +127,7 @@ def option[T](
|
|
|
127
127
|
choices: Iterable[T] | None = None,
|
|
128
128
|
metavar: str | tuple[str, ...] | None = None,
|
|
129
129
|
show_default: bool | str | None = None,
|
|
130
|
-
) -> Option[T |
|
|
130
|
+
) -> Option[T | Missing, None]: ...
|
|
131
131
|
|
|
132
132
|
|
|
133
133
|
## type: T, default: str, nargs
|
|
@@ -156,7 +156,7 @@ def option[T](
|
|
|
156
156
|
choices: Iterable[T] | None = None,
|
|
157
157
|
metavar: str | tuple[str, ...] | None = None,
|
|
158
158
|
show_default: bool | str | None = None,
|
|
159
|
-
) -> Option[T |
|
|
159
|
+
) -> Option[T | Missing, T]: ...
|
|
160
160
|
|
|
161
161
|
|
|
162
162
|
## type: T, default: D, nargs
|
|
@@ -184,7 +184,7 @@ def option[T, D](
|
|
|
184
184
|
choices: Iterable[T] | None = None,
|
|
185
185
|
metavar: str | tuple[str, ...] | None = None,
|
|
186
186
|
show_default: bool | str | None = None,
|
|
187
|
-
) -> Option[T |
|
|
187
|
+
) -> Option[T | Missing, D]: ...
|
|
188
188
|
|
|
189
189
|
|
|
190
190
|
def option(
|
|
@@ -198,15 +198,19 @@ def option(
|
|
|
198
198
|
show_default: bool | str | None = None,
|
|
199
199
|
) -> Option[Any, Any]:
|
|
200
200
|
"""Just an option"""
|
|
201
|
+
|
|
202
|
+
if metavar is None and choices is None:
|
|
203
|
+
metavar = names[0].lstrip("-").upper()
|
|
204
|
+
|
|
201
205
|
return Option(
|
|
202
206
|
names,
|
|
203
207
|
help=help,
|
|
204
|
-
|
|
208
|
+
conv=type,
|
|
205
209
|
nargs=nargs,
|
|
206
210
|
default=default,
|
|
207
211
|
const=... if nargs == "?" else None,
|
|
208
212
|
choices=choices,
|
|
209
|
-
metavar=metavar
|
|
213
|
+
metavar=metavar,
|
|
210
214
|
action="store",
|
|
211
215
|
#
|
|
212
216
|
hard_show_default=show_default,
|
|
@@ -310,13 +314,17 @@ def repeated_option(
|
|
|
310
314
|
) -> RepeatedOption[Any]:
|
|
311
315
|
"""Option which could present multiple times on a command line.
|
|
312
316
|
Result is collected into the list"""
|
|
317
|
+
|
|
318
|
+
if metavar is None and choices is None:
|
|
319
|
+
metavar = names[0].lstrip("-").upper()
|
|
320
|
+
|
|
313
321
|
return RepeatedOption(
|
|
314
322
|
names,
|
|
315
323
|
help=help,
|
|
316
|
-
|
|
324
|
+
conv=type,
|
|
317
325
|
nargs=nargs,
|
|
318
326
|
choices=choices,
|
|
319
|
-
metavar=metavar
|
|
327
|
+
metavar=metavar,
|
|
320
328
|
action="extend" if flatten else "append",
|
|
321
329
|
default=[],
|
|
322
330
|
#
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|