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/conv.py
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
from argparse import ArgumentTypeError
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
from typing import Final, Literal
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class PathConv:
|
|
7
|
+
__name__ = "PATH"
|
|
8
|
+
|
|
9
|
+
__slots__ = ("exists", "resolve", "type")
|
|
10
|
+
|
|
11
|
+
def __init__(self, type: Literal["file", "dir", None] = None, *, exists: bool | None = None, resolve: bool = True):
|
|
12
|
+
self.exists: Final = exists
|
|
13
|
+
self.type: Final = type
|
|
14
|
+
self.resolve: Final = resolve
|
|
15
|
+
|
|
16
|
+
# scenarios:
|
|
17
|
+
# - existing path: check exists
|
|
18
|
+
# - existing file: check exists and is_file
|
|
19
|
+
# - existing dir: check exists and is_dir
|
|
20
|
+
# - non-existing path/file/dir: check not exists
|
|
21
|
+
# - maybe existing path: nothing
|
|
22
|
+
# - maybe existing file: if exists, check is_file
|
|
23
|
+
# - maybe existing dir: if exists, check is_dir
|
|
24
|
+
def __call__(self, arg: str) -> Path:
|
|
25
|
+
path = Path(arg)
|
|
26
|
+
if self.resolve:
|
|
27
|
+
path = path.resolve()
|
|
28
|
+
|
|
29
|
+
check_type = False
|
|
30
|
+
|
|
31
|
+
if self.exists is True:
|
|
32
|
+
if not path.exists():
|
|
33
|
+
raise ArgumentTypeError(f"{path} does not exists")
|
|
34
|
+
check_type = True
|
|
35
|
+
elif self.exists is False:
|
|
36
|
+
if path.exists():
|
|
37
|
+
raise ArgumentTypeError(f"{path} already exists")
|
|
38
|
+
else:
|
|
39
|
+
if self.type and path.exists():
|
|
40
|
+
check_type = True
|
|
41
|
+
|
|
42
|
+
if check_type:
|
|
43
|
+
if self.type == "file":
|
|
44
|
+
if not path.is_file():
|
|
45
|
+
raise ArgumentTypeError(f"{path} is not a file")
|
|
46
|
+
elif self.type == "dir":
|
|
47
|
+
if not path.is_dir():
|
|
48
|
+
raise ArgumentTypeError(f"{path} is not a directory")
|
|
49
|
+
|
|
50
|
+
return path
|
|
51
|
+
|
|
52
|
+
@classmethod
|
|
53
|
+
def file(cls, *, exists: bool | None = None, resolve: bool = True) -> "PathConv":
|
|
54
|
+
return cls("file", exists=exists, resolve=resolve)
|
|
55
|
+
|
|
56
|
+
@classmethod
|
|
57
|
+
def dir(cls, *, exists: bool | None = None, resolve: bool = True) -> "PathConv":
|
|
58
|
+
return cls("dir", exists=exists, resolve=resolve)
|
paramspecli/doc.py
ADDED
|
@@ -0,0 +1,368 @@
|
|
|
1
|
+
import dataclasses
|
|
2
|
+
from argparse import BooleanOptionalAction
|
|
3
|
+
from typing import Any, Final, Iterable, Mapping, NamedTuple, Protocol, Sequence
|
|
4
|
+
|
|
5
|
+
from paramspecli import md
|
|
6
|
+
|
|
7
|
+
from .cli import Arg, Command, Group, Opt, Section, name_and_aliases
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class Renderer(Protocol):
|
|
11
|
+
"""
|
|
12
|
+
Doc render backend interface (protocol).
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
def p(self, text: str) -> str: ...
|
|
16
|
+
def h(self, level: int, text: str) -> str: ...
|
|
17
|
+
def br(self) -> str: ...
|
|
18
|
+
def b(self, text: str) -> str: ...
|
|
19
|
+
def i(self, text: str) -> str: ...
|
|
20
|
+
def blockquote(self, text: str) -> str: ...
|
|
21
|
+
def ul(self, lis: Iterable[str]) -> str: ...
|
|
22
|
+
def dl(self, dtdds: Iterable[tuple[str, str]]) -> str: ...
|
|
23
|
+
def code(self, text: str) -> str: ...
|
|
24
|
+
def codeblock(self, text: str, lang: str | None = None) -> str: ...
|
|
25
|
+
def strike(self, text: str) -> str: ...
|
|
26
|
+
def hr(self) -> str: ...
|
|
27
|
+
def link(self, url: str, *, text: str | None) -> str: ...
|
|
28
|
+
def e(self, text: str) -> str: ...
|
|
29
|
+
def body(self, text: str) -> str: ...
|
|
30
|
+
def postprocess(self, text: str) -> str: ...
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class BoundGroup(NamedTuple):
|
|
34
|
+
name: str
|
|
35
|
+
aliases: tuple[str, ...]
|
|
36
|
+
obj: Group
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class BoundCommand(NamedTuple):
|
|
40
|
+
name: str
|
|
41
|
+
aliases: tuple[str, ...]
|
|
42
|
+
obj: Command[...]
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
# A little helper joining all truthy strings with the sep
|
|
46
|
+
def _join(*args: str | None | bool | Iterable[str | None | bool], sep: str) -> str:
|
|
47
|
+
out: list[str] = []
|
|
48
|
+
for arg in args:
|
|
49
|
+
if isinstance(arg, str) and arg:
|
|
50
|
+
out.append(arg)
|
|
51
|
+
elif arg is not None and arg is not True and arg is not False:
|
|
52
|
+
out.extend(sub for sub in arg if isinstance(sub, str) and arg)
|
|
53
|
+
return sep.join(out)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
@dataclasses.dataclass(kw_only=True)
|
|
57
|
+
class Settings:
|
|
58
|
+
"""Documentation generator config"""
|
|
59
|
+
|
|
60
|
+
commands_title: str = "Commands"
|
|
61
|
+
arguments_title: str | None = "Arguments"
|
|
62
|
+
arguments_headline: str | None = None
|
|
63
|
+
options_title: str = "Options"
|
|
64
|
+
choices_title: str = "Possible values"
|
|
65
|
+
usage_title: str = "Usage"
|
|
66
|
+
aliases_title: str = "Aliases"
|
|
67
|
+
options_headline: str | None = None
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
class Doc:
|
|
71
|
+
"""Documentation generator"""
|
|
72
|
+
|
|
73
|
+
def __init__(
|
|
74
|
+
self,
|
|
75
|
+
*,
|
|
76
|
+
renderer: Renderer | None = None,
|
|
77
|
+
settings: Settings | None = None,
|
|
78
|
+
):
|
|
79
|
+
self.r: Final[Renderer] = renderer or md.Renderer()
|
|
80
|
+
self.config: Final = settings or Settings()
|
|
81
|
+
|
|
82
|
+
def r_choices(self, choices: Iterable[Any]) -> str:
|
|
83
|
+
r = self.r
|
|
84
|
+
out = ""
|
|
85
|
+
|
|
86
|
+
out += r.p(self.config.choices_title + ":")
|
|
87
|
+
if isinstance(choices, Mapping):
|
|
88
|
+
out += r.ul(r.code(str(c)) + ": " + str(desc) for c, desc in choices.items())
|
|
89
|
+
else:
|
|
90
|
+
out += r.ul(r.code(str(c)) for c in choices)
|
|
91
|
+
return out
|
|
92
|
+
|
|
93
|
+
def r_argument_title(self, arg: Arg) -> str:
|
|
94
|
+
r = self.r
|
|
95
|
+
|
|
96
|
+
return r.code(arg.metavar)
|
|
97
|
+
|
|
98
|
+
def r_argument_info(self, arg: Arg) -> str:
|
|
99
|
+
r = self.r
|
|
100
|
+
out = ""
|
|
101
|
+
|
|
102
|
+
if isinstance(arg.help, str):
|
|
103
|
+
out += r.p(arg.help)
|
|
104
|
+
if arg.choices:
|
|
105
|
+
out += self.r_choices(arg.choices)
|
|
106
|
+
return out
|
|
107
|
+
|
|
108
|
+
def r_option_title(self, option: Opt) -> str:
|
|
109
|
+
r = self.r
|
|
110
|
+
out = ""
|
|
111
|
+
|
|
112
|
+
names = option.names
|
|
113
|
+
|
|
114
|
+
if option.action == BooleanOptionalAction:
|
|
115
|
+
basename = [name for name in names if name.startswith("--")][0]
|
|
116
|
+
out += _join(
|
|
117
|
+
r.code(basename) + "/" + r.code(f"--no-{basename[2:]}"),
|
|
118
|
+
[r.code(name) for name in names if name != basename],
|
|
119
|
+
sep=", ",
|
|
120
|
+
)
|
|
121
|
+
else:
|
|
122
|
+
out += _join([r.code(name) for name in names], sep=", ")
|
|
123
|
+
|
|
124
|
+
if option.metavar is not None:
|
|
125
|
+
out += " " + r.i(r.e(self.r_metavar(option.metavar, option)))
|
|
126
|
+
return out
|
|
127
|
+
|
|
128
|
+
def r_option_default(self, option: Opt) -> str:
|
|
129
|
+
r = self.r
|
|
130
|
+
|
|
131
|
+
default: str | None = None
|
|
132
|
+
|
|
133
|
+
if option.hard_show_default is not None:
|
|
134
|
+
if isinstance(option.hard_show_default, str):
|
|
135
|
+
default = option.hard_show_default
|
|
136
|
+
elif option.hard_show_default is True:
|
|
137
|
+
default = str(option.default)
|
|
138
|
+
else:
|
|
139
|
+
if isinstance(option.soft_show_default, str):
|
|
140
|
+
default = option.soft_show_default
|
|
141
|
+
elif option.soft_show_default is True:
|
|
142
|
+
default = str(option.default)
|
|
143
|
+
|
|
144
|
+
if default is None:
|
|
145
|
+
return ""
|
|
146
|
+
|
|
147
|
+
# this wrapping in code avoid the need for most escapes
|
|
148
|
+
return f"(default: {r.code(default)})"
|
|
149
|
+
|
|
150
|
+
def r_option_info(self, option: Opt) -> str:
|
|
151
|
+
r = self.r
|
|
152
|
+
out = ""
|
|
153
|
+
|
|
154
|
+
out += r.p(
|
|
155
|
+
_join(
|
|
156
|
+
option.help if isinstance(option.help, str) else None,
|
|
157
|
+
self.r_option_default(option),
|
|
158
|
+
sep=" ",
|
|
159
|
+
)
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
if option.choices:
|
|
163
|
+
out += self.r_choices(option.choices)
|
|
164
|
+
|
|
165
|
+
return out
|
|
166
|
+
|
|
167
|
+
def r_metavar(self, metavar: str | tuple[str, ...], item: Opt | Arg) -> str:
|
|
168
|
+
if isinstance(metavar, tuple):
|
|
169
|
+
return _join(metavar, sep=" ")
|
|
170
|
+
|
|
171
|
+
nargs = item.nargs
|
|
172
|
+
|
|
173
|
+
if isinstance(nargs, int):
|
|
174
|
+
return _join([metavar] * nargs, sep=" ")
|
|
175
|
+
if nargs == "*":
|
|
176
|
+
return f"[{metavar} ...]"
|
|
177
|
+
if nargs == "+":
|
|
178
|
+
return f"{metavar} [{metavar} ...]"
|
|
179
|
+
if nargs == "?":
|
|
180
|
+
return f"[{metavar} ...]"
|
|
181
|
+
return metavar
|
|
182
|
+
|
|
183
|
+
def r_arguments(self, arguments: Sequence[Arg]) -> str:
|
|
184
|
+
r = self.r
|
|
185
|
+
out = ""
|
|
186
|
+
|
|
187
|
+
if not arguments:
|
|
188
|
+
return ""
|
|
189
|
+
|
|
190
|
+
title = self.config.arguments_title
|
|
191
|
+
headline = self.config.arguments_headline
|
|
192
|
+
|
|
193
|
+
if title:
|
|
194
|
+
out += r.p(title + ":")
|
|
195
|
+
if headline:
|
|
196
|
+
out += r.p(headline)
|
|
197
|
+
out += r.dl((self.r_argument_title(arg), self.r_argument_info(arg)) for arg in arguments)
|
|
198
|
+
return out
|
|
199
|
+
|
|
200
|
+
def r_options_section(self, section: Section, options: Sequence[Opt]) -> str:
|
|
201
|
+
r = self.r
|
|
202
|
+
out = ""
|
|
203
|
+
|
|
204
|
+
if not options:
|
|
205
|
+
return ""
|
|
206
|
+
|
|
207
|
+
if section.title:
|
|
208
|
+
out += r.p(section.title + ":")
|
|
209
|
+
if section.headline:
|
|
210
|
+
out += r.p(str(section.headline))
|
|
211
|
+
out += r.dl((self.r_option_title(opt), self.r_option_info(opt)) for opt in options)
|
|
212
|
+
return out
|
|
213
|
+
|
|
214
|
+
def get_options(self, me: BoundGroup | BoundCommand) -> list[Opt]:
|
|
215
|
+
return [*me.obj.actions, *me.obj.get_real_options()]
|
|
216
|
+
|
|
217
|
+
def get_arguments(self, me: BoundGroup | BoundCommand) -> list[Arg]:
|
|
218
|
+
return me.obj.get_real_arguments()
|
|
219
|
+
|
|
220
|
+
def _opts_by_sections(self, me: BoundGroup | BoundCommand) -> dict[Section, list[Opt]]:
|
|
221
|
+
default_section = Section(self.config.options_title, headline=self.config.options_headline)
|
|
222
|
+
|
|
223
|
+
out: dict[Section, list[Opt]] = {}
|
|
224
|
+
|
|
225
|
+
for option in self.get_options(me):
|
|
226
|
+
sect = next((sect for sect in me.obj.sections if option in sect.options), default_section)
|
|
227
|
+
out.setdefault(sect, []).append(option)
|
|
228
|
+
|
|
229
|
+
return out
|
|
230
|
+
|
|
231
|
+
def _group_usage_partial(self, me: BoundGroup) -> str:
|
|
232
|
+
return _join(
|
|
233
|
+
me.name,
|
|
234
|
+
[self.r_metavar(arg.metavar, arg) for arg in self.get_arguments(me)],
|
|
235
|
+
sep=" ",
|
|
236
|
+
)
|
|
237
|
+
|
|
238
|
+
def r_aliases(self, me: BoundGroup | BoundCommand) -> str:
|
|
239
|
+
r = self.r
|
|
240
|
+
out = ""
|
|
241
|
+
|
|
242
|
+
if me.aliases:
|
|
243
|
+
out += r.p(self.config.aliases_title + ":")
|
|
244
|
+
out += r.ul(r.b(alias) for alias in me.aliases)
|
|
245
|
+
return out
|
|
246
|
+
|
|
247
|
+
def r_command_title(self, me: BoundCommand, parents: Sequence[BoundGroup]) -> str:
|
|
248
|
+
r = self.r
|
|
249
|
+
out = ""
|
|
250
|
+
|
|
251
|
+
title = _join([p.name for p in parents], me.name, sep=" ")
|
|
252
|
+
out += r.h(len(parents) + 1, text=title)
|
|
253
|
+
return out
|
|
254
|
+
|
|
255
|
+
def r_command_info(self, me: BoundCommand) -> str:
|
|
256
|
+
r = self.r
|
|
257
|
+
out = ""
|
|
258
|
+
|
|
259
|
+
info = me.obj.info or me.obj.help
|
|
260
|
+
if info:
|
|
261
|
+
out += r.p(str(info))
|
|
262
|
+
return out
|
|
263
|
+
|
|
264
|
+
def r_command_usage(
|
|
265
|
+
self, me: BoundCommand, parents: Sequence[BoundGroup], options_sections: Iterable[Section]
|
|
266
|
+
) -> str:
|
|
267
|
+
r = self.r
|
|
268
|
+
out = ""
|
|
269
|
+
|
|
270
|
+
out += r.p(self.config.usage_title + ":")
|
|
271
|
+
out += r.codeblock(
|
|
272
|
+
_join(
|
|
273
|
+
[self._group_usage_partial(p) for p in parents],
|
|
274
|
+
me.name,
|
|
275
|
+
[f"[{section.title.lower()}]" for section in options_sections],
|
|
276
|
+
[self.r_metavar(arg.metavar, arg) for arg in self.get_arguments(me)],
|
|
277
|
+
sep=" ",
|
|
278
|
+
)
|
|
279
|
+
)
|
|
280
|
+
return out
|
|
281
|
+
|
|
282
|
+
def r_command(self, me: BoundCommand, parents: Sequence[BoundGroup]) -> str:
|
|
283
|
+
out = ""
|
|
284
|
+
out += self.r_command_title(me, parents)
|
|
285
|
+
out += self.r_command_info(me)
|
|
286
|
+
out += self.r_aliases(me)
|
|
287
|
+
|
|
288
|
+
opts_sections = self._opts_by_sections(me)
|
|
289
|
+
|
|
290
|
+
out += self.r_command_usage(me, parents, opts_sections)
|
|
291
|
+
out += self.r_arguments(self.get_arguments(me))
|
|
292
|
+
|
|
293
|
+
for section, opts in opts_sections.items():
|
|
294
|
+
out += self.r_options_section(section, opts)
|
|
295
|
+
|
|
296
|
+
return out
|
|
297
|
+
|
|
298
|
+
def r_group_legend(self, me: BoundGroup, parents: Sequence[BoundGroup]) -> str:
|
|
299
|
+
r = self.r
|
|
300
|
+
out = ""
|
|
301
|
+
|
|
302
|
+
if not me.obj.nodes:
|
|
303
|
+
return ""
|
|
304
|
+
|
|
305
|
+
out += r.p((me.obj.title or self.config.commands_title) + ":")
|
|
306
|
+
|
|
307
|
+
if me.obj.headline:
|
|
308
|
+
out += r.p(str(me.obj.headline))
|
|
309
|
+
|
|
310
|
+
dtdds: list[tuple[str, str]] = []
|
|
311
|
+
for names, node in me.obj.nodes.items():
|
|
312
|
+
name, _aliases = name_and_aliases(names)
|
|
313
|
+
anchor = _join([p.name for p in parents], me.name, name, sep="-").lower()
|
|
314
|
+
text = _join(name, "..." if isinstance(node, Group) else None, sep=" ")
|
|
315
|
+
dtdds.append((r.link("#" + anchor, text=text), str(node.help or "")))
|
|
316
|
+
|
|
317
|
+
out += r.dl(dtdds)
|
|
318
|
+
return out
|
|
319
|
+
|
|
320
|
+
def r_group_info(self, me: BoundGroup) -> str:
|
|
321
|
+
r = self.r
|
|
322
|
+
out = ""
|
|
323
|
+
|
|
324
|
+
info = me.obj.info or me.obj.help
|
|
325
|
+
if info:
|
|
326
|
+
out += r.p(str(info))
|
|
327
|
+
return out
|
|
328
|
+
|
|
329
|
+
def r_group_title(self, me: BoundGroup, parents: Sequence[BoundGroup]) -> str:
|
|
330
|
+
r = self.r
|
|
331
|
+
out = ""
|
|
332
|
+
|
|
333
|
+
title = _join([p.name for p in parents], me.name, "..." if parents else None, sep=" ")
|
|
334
|
+
out += r.h(len(parents) + 1, text=title)
|
|
335
|
+
return out
|
|
336
|
+
|
|
337
|
+
def r_group(self, me: BoundGroup, parents: Sequence[BoundGroup]) -> str:
|
|
338
|
+
out = ""
|
|
339
|
+
|
|
340
|
+
out = self.r_group_title(me, parents)
|
|
341
|
+
out += self.r_group_info(me)
|
|
342
|
+
out += self.r_aliases(me)
|
|
343
|
+
out += self.r_arguments(self.get_arguments(me))
|
|
344
|
+
|
|
345
|
+
opts_sections = self._opts_by_sections(me)
|
|
346
|
+
|
|
347
|
+
for section, opts in opts_sections.items():
|
|
348
|
+
out += self.r_options_section(section, opts)
|
|
349
|
+
|
|
350
|
+
out += self.r_group_legend(me, parents)
|
|
351
|
+
|
|
352
|
+
for item_name, node in me.obj.nodes.items():
|
|
353
|
+
if isinstance(node, Group):
|
|
354
|
+
out += self.r_group(BoundGroup(*name_and_aliases(item_name), node), [*parents, me])
|
|
355
|
+
else:
|
|
356
|
+
out += self.r_command(BoundCommand(*name_and_aliases(item_name), node), [*parents, me])
|
|
357
|
+
|
|
358
|
+
return out
|
|
359
|
+
|
|
360
|
+
def generate(self, node: Group | Command[...], *, prog: str) -> str:
|
|
361
|
+
r = self.r
|
|
362
|
+
|
|
363
|
+
if isinstance(node, Group):
|
|
364
|
+
content = self.r_group(BoundGroup(prog, (), node), [])
|
|
365
|
+
else:
|
|
366
|
+
content = self.r_command(BoundCommand(prog, (), node), [])
|
|
367
|
+
|
|
368
|
+
return r.postprocess(r.body(content))
|
paramspecli/fake.py
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import dataclasses
|
|
2
|
+
from typing import Any, Final, overload
|
|
3
|
+
|
|
4
|
+
from .cli import Arg, Cnst, Ctx, MixedOpts, Opt
|
|
5
|
+
|
|
6
|
+
# These specialized arguments/options classes are for the typechecher benefit
|
|
7
|
+
# These are really shouldn't be referenced anywhere else
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class LieMixin[T]:
|
|
11
|
+
# NOTE: arguments, options, etc are using this mixin.
|
|
12
|
+
# they all define __slots__ for extra savings, so we should define an empty __slots__ too
|
|
13
|
+
__slots__ = ()
|
|
14
|
+
|
|
15
|
+
@property
|
|
16
|
+
def t(self) -> T:
|
|
17
|
+
return self # type: ignore[return-value] # ty: ignore[invalid-return-type]
|
|
18
|
+
|
|
19
|
+
def __neg__(self) -> T:
|
|
20
|
+
return self # type: ignore[return-value] # ty: ignore[invalid-return-type]
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class Argument[T, D](Arg, LieMixin[T | D]):
|
|
24
|
+
"""Parameter-argument"""
|
|
25
|
+
|
|
26
|
+
__slots__ = ()
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class Option[T, D](Opt, LieMixin[T | D]):
|
|
30
|
+
"""Parameter-option"""
|
|
31
|
+
|
|
32
|
+
__slots__ = ()
|
|
33
|
+
|
|
34
|
+
def mixed_with[O](self, other: "Option[O, Any]") -> "MixedOptions[T | O, D]":
|
|
35
|
+
return MixedOptions(self, other)
|
|
36
|
+
|
|
37
|
+
__or__ = mixed_with
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class RepeatedOption[T](Opt, LieMixin[list[T]]):
|
|
41
|
+
"""Parameter-option which could be repeated multiple times on command line"""
|
|
42
|
+
|
|
43
|
+
__slots__ = ()
|
|
44
|
+
|
|
45
|
+
def mixed_with[O](self, other: "RepeatedOption[O]") -> "MixedRepeatedOptions[T | O]":
|
|
46
|
+
return MixedRepeatedOptions(self, other)
|
|
47
|
+
|
|
48
|
+
__add__ = mixed_with
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class MixedOptions[T, D](MixedOpts, LieMixin[T | D]):
|
|
52
|
+
"""Parameter-mix of several options targeting the same parameter"""
|
|
53
|
+
|
|
54
|
+
__slots__ = ()
|
|
55
|
+
|
|
56
|
+
def mixed_with[O](self, other: "Option[O, Any]") -> "MixedOptions[T | O, D]":
|
|
57
|
+
return MixedOptions(*self.options, other)
|
|
58
|
+
|
|
59
|
+
__or__ = mixed_with
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class MixedRepeatedOptions[T](MixedOpts, LieMixin[list[T]]):
|
|
63
|
+
"""Parameter-mix of several repeated options targeting the same parameter"""
|
|
64
|
+
|
|
65
|
+
__slots__ = ()
|
|
66
|
+
|
|
67
|
+
def mixed_with[O](self, other: RepeatedOption[O]) -> "MixedRepeatedOptions[T | O]":
|
|
68
|
+
# ty wants this specialization for some reason
|
|
69
|
+
return MixedRepeatedOptions[T | O](*self.options, other)
|
|
70
|
+
|
|
71
|
+
__add__ = mixed_with
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
class Const[T](Cnst, LieMixin[T]):
|
|
75
|
+
"""Parameter-constant value. Allows to remove parameter from the command line"""
|
|
76
|
+
|
|
77
|
+
__slots__ = ()
|
|
78
|
+
|
|
79
|
+
def __init__(self, value: T):
|
|
80
|
+
super().__init__(value)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
class Context[T](Ctx, LieMixin[T]):
|
|
84
|
+
"""Parameter-context. Marks parameter as accepting the shared context object"""
|
|
85
|
+
|
|
86
|
+
__slots__ = ()
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
class Lier:
|
|
90
|
+
"""Cast argument/option to its type. In other words, lie to the type checker"""
|
|
91
|
+
|
|
92
|
+
@overload
|
|
93
|
+
def __call__[T, D](self, obj: Option[T, D] | MixedOptions[T, D] | Argument[T, D]) -> T | D: ...
|
|
94
|
+
|
|
95
|
+
@overload
|
|
96
|
+
def __call__[T](self, obj: RepeatedOption[T] | MixedRepeatedOptions[T]) -> list[T]: ...
|
|
97
|
+
|
|
98
|
+
@overload
|
|
99
|
+
def __call__[T](self, obj: Const[T]) -> T: ...
|
|
100
|
+
|
|
101
|
+
def __call__(self, obj: Any) -> Any:
|
|
102
|
+
return obj
|
|
103
|
+
|
|
104
|
+
__getitem__ = __call__
|
|
105
|
+
__matmul__ = __call__
|
|
106
|
+
__rmatmul__ = __call__
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
@overload
|
|
110
|
+
def required[T](option: Option[T, Any]) -> Option[T, T]: ...
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
@overload
|
|
114
|
+
def required[T](option: RepeatedOption[T]) -> RepeatedOption[T]: ...
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def required(option: Opt) -> Opt:
|
|
118
|
+
"""Return a copy of option with the `required` flag set"""
|
|
119
|
+
return dataclasses.replace(option, required=True)
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def deprecated[T: Opt](option: T) -> T:
|
|
123
|
+
"""Return a copy of option with the `deprecated` flag set"""
|
|
124
|
+
return dataclasses.replace(option, deprecated=True)
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
t: Final = Lier()
|