argsui 0.2.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- argsui/__init__.py +19 -0
- argsui/args.py +689 -0
- argsui/bulk_args.py +232 -0
- argsui/config_args.py +59 -0
- argsui/dataclass_construct.py +86 -0
- argsui/extra_types.py +58 -0
- argsui/interactive_args.py +627 -0
- argsui/py.typed +0 -0
- argsui-0.2.0.dist-info/METADATA +48 -0
- argsui-0.2.0.dist-info/RECORD +12 -0
- argsui-0.2.0.dist-info/WHEEL +4 -0
- argsui-0.2.0.dist-info/licenses/LICENSES/Apache-2.0.txt +73 -0
argsui/__init__.py
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: 2026 Duncan McDougall <duncan.mcdougall@rfi.ac.uk>
|
|
2
|
+
#
|
|
3
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
|
|
5
|
+
from .args import (
|
|
6
|
+
ArgType,
|
|
7
|
+
Action,
|
|
8
|
+
no_arg_field,
|
|
9
|
+
arg_field,
|
|
10
|
+
from_arguments,
|
|
11
|
+
add_arguments,
|
|
12
|
+
from_dataclass,
|
|
13
|
+
from_field,
|
|
14
|
+
)
|
|
15
|
+
from .config_args import ConfigFileArgs
|
|
16
|
+
from .extra_types import DirPathType, FilePathType
|
|
17
|
+
from .bulk_args import FileDetails, process_bulk
|
|
18
|
+
|
|
19
|
+
from .interactive_args import InteractiveArgs, NoInteractiveArgs
|
argsui/args.py
ADDED
|
@@ -0,0 +1,689 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: 2026 Duncan McDougall <duncan.mcdougall@rfi.ac.uk>
|
|
2
|
+
#
|
|
3
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
import itertools
|
|
5
|
+
|
|
6
|
+
import copy
|
|
7
|
+
from typing import Any, get_args, NamedTuple, Optional, Literal, Self, Type, TypeVar
|
|
8
|
+
import argparse
|
|
9
|
+
from dataclasses import field, Field, MISSING, fields, dataclass, is_dataclass
|
|
10
|
+
from enum import EnumType, Enum
|
|
11
|
+
import sys
|
|
12
|
+
|
|
13
|
+
from .extra_types import BaseType
|
|
14
|
+
from .dataclass_construct import build_dataclass_from_dict
|
|
15
|
+
|
|
16
|
+
from icecream import ic
|
|
17
|
+
|
|
18
|
+
T = TypeVar("T")
|
|
19
|
+
|
|
20
|
+
MISSING_TYPE = type(MISSING)
|
|
21
|
+
|
|
22
|
+
PossibleActions = Literal["store", "store_true", "store_false", "append"]
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class ArgType(Enum):
|
|
26
|
+
NOT_AN_ARG = 0
|
|
27
|
+
AUTOMATIC = 1
|
|
28
|
+
POSITIONAL = 2
|
|
29
|
+
EXPLICIT_ONLY = 3
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class PartialParsedArgs(NamedTuple):
|
|
33
|
+
remaining_args: list[str]
|
|
34
|
+
config: dict[str, Any]
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def no_arg_field(**kw_args):
|
|
38
|
+
kw_args.update(dict(metadata=dict(arg_type=ArgType.NOT_AN_ARG)))
|
|
39
|
+
return field(**kw_args)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def arg_field(*args, arg_type: ArgType = ArgType.AUTOMATIC, defer=False, **kw_args):
|
|
43
|
+
|
|
44
|
+
field_keys = (
|
|
45
|
+
"default",
|
|
46
|
+
"default_factory",
|
|
47
|
+
"init",
|
|
48
|
+
"repr",
|
|
49
|
+
"hash",
|
|
50
|
+
"compare",
|
|
51
|
+
"metadata",
|
|
52
|
+
"kw_only",
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
field_kw_args = {}
|
|
56
|
+
|
|
57
|
+
for key in field_keys:
|
|
58
|
+
if key in kw_args:
|
|
59
|
+
field_kw_args[key] = kw_args[key]
|
|
60
|
+
del kw_args[key]
|
|
61
|
+
|
|
62
|
+
if "metadata" not in field_kw_args:
|
|
63
|
+
field_kw_args["metadata"] = dict()
|
|
64
|
+
|
|
65
|
+
if "doc" in kw_args:
|
|
66
|
+
assert "help" not in kw_args
|
|
67
|
+
if sys.version_info.minor < 14:
|
|
68
|
+
field_kw_args["metadata"]["doc"] = kw_args["doc"]
|
|
69
|
+
del kw_args["doc"]
|
|
70
|
+
elif "help" in kw_args:
|
|
71
|
+
assert "doc" not in kw_args
|
|
72
|
+
if sys.version_info.minor < 14:
|
|
73
|
+
field_kw_args["metadata"]["doc"] = kw_args["help"]
|
|
74
|
+
else:
|
|
75
|
+
kw_args["doc"] = kw_args["help"]
|
|
76
|
+
del kw_args["help"]
|
|
77
|
+
else:
|
|
78
|
+
if sys.version_info.minor < 14:
|
|
79
|
+
field_kw_args["metadata"]["doc"] = ""
|
|
80
|
+
else:
|
|
81
|
+
kw_args["doc"] = ""
|
|
82
|
+
|
|
83
|
+
# field_kw_args.update(kw_args)
|
|
84
|
+
# else:
|
|
85
|
+
# field_kw_args["metadata"] = kw_args
|
|
86
|
+
|
|
87
|
+
field_kw_args["metadata"]["args"] = args
|
|
88
|
+
field_kw_args["metadata"]["arg_type"] = arg_type
|
|
89
|
+
field_kw_args["metadata"]["defer"] = defer
|
|
90
|
+
field_kw_args["metadata"]["kw_args"] = kw_args
|
|
91
|
+
|
|
92
|
+
if "action" in kw_args:
|
|
93
|
+
action = kw_args["action"]
|
|
94
|
+
if action == "store_true":
|
|
95
|
+
field_kw_args["default"] = False
|
|
96
|
+
elif action == "store_false":
|
|
97
|
+
field_kw_args["default"] = True
|
|
98
|
+
|
|
99
|
+
return field(**field_kw_args)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
class Action:
|
|
103
|
+
def __init__(self):
|
|
104
|
+
self.aliases: list[str] = []
|
|
105
|
+
|
|
106
|
+
self.arg_type: ArgType = ArgType.AUTOMATIC
|
|
107
|
+
self.defer: bool = False
|
|
108
|
+
|
|
109
|
+
self.action: PossibleActions = "store"
|
|
110
|
+
self.value_type: Any | MISSING_TYPE = MISSING
|
|
111
|
+
self.help: str | MISSING_TYPE = MISSING
|
|
112
|
+
self.default: Any | MISSING_TYPE = MISSING
|
|
113
|
+
self.const: Any | MISSING_TYPE = MISSING
|
|
114
|
+
self.nargs: None | str | int = None
|
|
115
|
+
self.choices: list[Any] | None = None
|
|
116
|
+
self.required: bool = False
|
|
117
|
+
self.dest: str = ""
|
|
118
|
+
self.metavar: str | tuple[str] | None = None
|
|
119
|
+
self.extra_kw_args: dict[str, Any] = dict()
|
|
120
|
+
|
|
121
|
+
self.is_parent: bool = False
|
|
122
|
+
self.children: list["Action"] = []
|
|
123
|
+
|
|
124
|
+
self.has_appending_parent: bool = False
|
|
125
|
+
|
|
126
|
+
def __getitem__(self, index):
|
|
127
|
+
match index:
|
|
128
|
+
case "aliases":
|
|
129
|
+
return self.aliases
|
|
130
|
+
case "arg_type":
|
|
131
|
+
return self.arg_type
|
|
132
|
+
case "defer":
|
|
133
|
+
return self.defer
|
|
134
|
+
case "action":
|
|
135
|
+
return self.action
|
|
136
|
+
case "value_type":
|
|
137
|
+
return self.value_type
|
|
138
|
+
case "help":
|
|
139
|
+
return self.help
|
|
140
|
+
case "default":
|
|
141
|
+
return self.default
|
|
142
|
+
case "const":
|
|
143
|
+
return self.const
|
|
144
|
+
case "nargs":
|
|
145
|
+
return self.nargs
|
|
146
|
+
case "choices":
|
|
147
|
+
return self.choices
|
|
148
|
+
case "required":
|
|
149
|
+
return self.required
|
|
150
|
+
case "dest":
|
|
151
|
+
return self.dest
|
|
152
|
+
case "metavar":
|
|
153
|
+
return self.metavar
|
|
154
|
+
case "extra_kw_args":
|
|
155
|
+
return self.extra_kw_args
|
|
156
|
+
case _:
|
|
157
|
+
raise ValueError(f"Unknown value for [{index}]")
|
|
158
|
+
|
|
159
|
+
def fields(self) -> tuple[str, ...]:
|
|
160
|
+
return (
|
|
161
|
+
"aliases",
|
|
162
|
+
"arg_type",
|
|
163
|
+
"defer",
|
|
164
|
+
"action",
|
|
165
|
+
"value_type",
|
|
166
|
+
"help",
|
|
167
|
+
"default",
|
|
168
|
+
"const",
|
|
169
|
+
"nargs",
|
|
170
|
+
"choices",
|
|
171
|
+
"required",
|
|
172
|
+
"dest",
|
|
173
|
+
"metavar",
|
|
174
|
+
"extra_kw_args",
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
def __repr__(self) -> str:
|
|
178
|
+
parts = []
|
|
179
|
+
for k in self.fields():
|
|
180
|
+
parts.append(f"{k}={self[k]}")
|
|
181
|
+
return f"Action( {', '.join(parts)})"
|
|
182
|
+
|
|
183
|
+
def _to_append_argument_kwargs(self) -> dict[str, Any]:
|
|
184
|
+
result: dict[str, Any] = {}
|
|
185
|
+
|
|
186
|
+
def add_if_not(key, filter_value):
|
|
187
|
+
if self[key] != filter_value:
|
|
188
|
+
result[key] = self[key]
|
|
189
|
+
|
|
190
|
+
result["dest"] = self.dest
|
|
191
|
+
|
|
192
|
+
match self.action:
|
|
193
|
+
case "store_true" | "store_false":
|
|
194
|
+
result["const"] = self.action == "store_true"
|
|
195
|
+
result["action"] = "append_const"
|
|
196
|
+
case "append" | "extend" | "append_const":
|
|
197
|
+
raise RuntimeError("We should not get here.")
|
|
198
|
+
case "store":
|
|
199
|
+
if self.value_type != MISSING:
|
|
200
|
+
result["type"] = self.value_type
|
|
201
|
+
result["action"] = "append"
|
|
202
|
+
case "store_const":
|
|
203
|
+
result["const"] = self.const
|
|
204
|
+
result["action"] = "append_const"
|
|
205
|
+
case _:
|
|
206
|
+
raise NotImplementedError(
|
|
207
|
+
f"{self.action} not implemented for subfieds of an appending parent."
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
add_if_not("help", MISSING)
|
|
211
|
+
add_if_not("nargs", None)
|
|
212
|
+
add_if_not("choices", None)
|
|
213
|
+
add_if_not("metavar", None)
|
|
214
|
+
for k, v in self.extra_kw_args.items():
|
|
215
|
+
result[k] = v
|
|
216
|
+
|
|
217
|
+
return result
|
|
218
|
+
|
|
219
|
+
def _to_argument_kwargs(self) -> dict[str, Any]:
|
|
220
|
+
result: dict[str, Any] = dict(action=self.action)
|
|
221
|
+
|
|
222
|
+
def add_if_not(key, filter_value):
|
|
223
|
+
if self[key] != filter_value:
|
|
224
|
+
result[key] = self[key]
|
|
225
|
+
|
|
226
|
+
if self.value_type != MISSING:
|
|
227
|
+
result["type"] = self.value_type
|
|
228
|
+
|
|
229
|
+
if self.arg_type != ArgType.POSITIONAL:
|
|
230
|
+
result["dest"] = self.dest
|
|
231
|
+
result["required"] = self.required
|
|
232
|
+
|
|
233
|
+
if self.help != MISSING and self.default != MISSING:
|
|
234
|
+
result["help"] = f"{self.help} Default '{self.default}'."
|
|
235
|
+
else:
|
|
236
|
+
add_if_not("help", MISSING)
|
|
237
|
+
|
|
238
|
+
add_if_not("default", MISSING)
|
|
239
|
+
add_if_not("const", MISSING)
|
|
240
|
+
|
|
241
|
+
add_if_not("nargs", None)
|
|
242
|
+
add_if_not("choices", None)
|
|
243
|
+
add_if_not("metavar", None)
|
|
244
|
+
for k, v in self.extra_kw_args.items():
|
|
245
|
+
result[k] = v
|
|
246
|
+
return result
|
|
247
|
+
|
|
248
|
+
def get_default_aliase(self) -> str:
|
|
249
|
+
return self.aliases[-1]
|
|
250
|
+
|
|
251
|
+
def get_display_name(self) -> str:
|
|
252
|
+
return self.get_default_aliase().strip("-").replace("-", " ")
|
|
253
|
+
|
|
254
|
+
# def is_boolean(self) -> bool:
|
|
255
|
+
# return self.action in ["store_true", "store_false"]
|
|
256
|
+
|
|
257
|
+
def is_default(self, value) -> bool:
|
|
258
|
+
"""
|
|
259
|
+
Is the given value the default value.
|
|
260
|
+
"""
|
|
261
|
+
return self.default == value
|
|
262
|
+
|
|
263
|
+
def get_default(self) -> Any:
|
|
264
|
+
if self.default == MISSING:
|
|
265
|
+
if self.action in ["store_true", "store_false"]:
|
|
266
|
+
return self.action == "store_false"
|
|
267
|
+
else:
|
|
268
|
+
raise ValueError("Missing default requested.")
|
|
269
|
+
return self.default
|
|
270
|
+
|
|
271
|
+
def _value_to_str(self, value):
|
|
272
|
+
match type(value):
|
|
273
|
+
case EnumType():
|
|
274
|
+
return str(value.value)
|
|
275
|
+
case str():
|
|
276
|
+
return value
|
|
277
|
+
case _:
|
|
278
|
+
return str(value)
|
|
279
|
+
|
|
280
|
+
def to_cli(self, value) -> list[str]:
|
|
281
|
+
match self.action:
|
|
282
|
+
case "store_true":
|
|
283
|
+
assert isinstance(value, bool)
|
|
284
|
+
return [self.get_default_aliase()] if value else []
|
|
285
|
+
case "store_false":
|
|
286
|
+
assert isinstance(value, bool)
|
|
287
|
+
return [self.get_default_aliase()] if not value else []
|
|
288
|
+
case "append":
|
|
289
|
+
results = []
|
|
290
|
+
for inner_value in value:
|
|
291
|
+
if self.nargs is not None:
|
|
292
|
+
results.extend(
|
|
293
|
+
[
|
|
294
|
+
self.get_default_aliase(),
|
|
295
|
+
*[self._value_to_str(v) for v in inner_value],
|
|
296
|
+
]
|
|
297
|
+
)
|
|
298
|
+
else:
|
|
299
|
+
results.extend(
|
|
300
|
+
[self.get_default_aliase(), self._value_to_str(inner_value)]
|
|
301
|
+
)
|
|
302
|
+
return results
|
|
303
|
+
case _:
|
|
304
|
+
if self.nargs is not None:
|
|
305
|
+
return [
|
|
306
|
+
self.get_default_aliase(),
|
|
307
|
+
*[self._value_to_str(v) for v in value],
|
|
308
|
+
]
|
|
309
|
+
else:
|
|
310
|
+
return [self.get_default_aliase(), self._value_to_str(value)]
|
|
311
|
+
|
|
312
|
+
def _child_alaises(
|
|
313
|
+
self, short_aliases: list[str], long_aliases: list[str], child: "Action"
|
|
314
|
+
) -> list[str]:
|
|
315
|
+
child_short_aliases = [
|
|
316
|
+
a.strip("-") for a in child.aliases if a[0] == "-" and a[1] != "-"
|
|
317
|
+
]
|
|
318
|
+
child_long_aliases = [a.strip("-") for a in child.aliases if a[0:2] == "--"]
|
|
319
|
+
|
|
320
|
+
aliases = [
|
|
321
|
+
f"{outer}.{inner}"
|
|
322
|
+
for outer, inner in itertools.product(short_aliases, child_short_aliases)
|
|
323
|
+
]
|
|
324
|
+
aliases.extend(
|
|
325
|
+
[
|
|
326
|
+
f"{outer}.{inner}"
|
|
327
|
+
for outer, inner in itertools.product(long_aliases, child_long_aliases)
|
|
328
|
+
]
|
|
329
|
+
)
|
|
330
|
+
return aliases
|
|
331
|
+
|
|
332
|
+
def all_aliases(self) -> list[str]:
|
|
333
|
+
if not self.is_parent:
|
|
334
|
+
return self.aliases
|
|
335
|
+
else:
|
|
336
|
+
short_aliases = [a for a in self.aliases if a[0] == "-" and a[1] != "-"]
|
|
337
|
+
long_aliases = [a for a in self.aliases if a[0:2] == "--"]
|
|
338
|
+
aliases = []
|
|
339
|
+
for child in self.children:
|
|
340
|
+
aliases.extend(self._child_alaises(short_aliases, long_aliases, child))
|
|
341
|
+
return aliases
|
|
342
|
+
|
|
343
|
+
def add_to_parser(self, parser, override_alaises: list[str] | None = None) -> list:
|
|
344
|
+
results = []
|
|
345
|
+
aliases = self.aliases if override_alaises is None else override_alaises
|
|
346
|
+
if not self.is_parent:
|
|
347
|
+
if self.has_appending_parent:
|
|
348
|
+
results.append(
|
|
349
|
+
parser.add_argument(*aliases, **self._to_append_argument_kwargs())
|
|
350
|
+
)
|
|
351
|
+
else:
|
|
352
|
+
results.append(
|
|
353
|
+
parser.add_argument(*aliases, **self._to_argument_kwargs())
|
|
354
|
+
)
|
|
355
|
+
else:
|
|
356
|
+
short_aliases = [a for a in aliases if a[0] == "-" and a[1] != "-"]
|
|
357
|
+
long_aliases = [a for a in aliases if a[0:2] == "--"]
|
|
358
|
+
for child in self.children:
|
|
359
|
+
child_aliases = self._child_alaises(short_aliases, long_aliases, child)
|
|
360
|
+
results.extend(child.add_to_parser(parser, child_aliases))
|
|
361
|
+
return results
|
|
362
|
+
|
|
363
|
+
def dist_dict(self) -> dict[str, Any]:
|
|
364
|
+
if not self.is_parent:
|
|
365
|
+
return {self.dest: self}
|
|
366
|
+
else:
|
|
367
|
+
result = {}
|
|
368
|
+
for child in self.children:
|
|
369
|
+
result.update(child.dist_dict())
|
|
370
|
+
return result
|
|
371
|
+
|
|
372
|
+
|
|
373
|
+
class ActionList(list[Action]):
|
|
374
|
+
def aliase_dict(self, include_short: bool = False) -> dict[str, Action]:
|
|
375
|
+
result: dict[str, Action] = {}
|
|
376
|
+
for a in self:
|
|
377
|
+
a_dict = {
|
|
378
|
+
name.strip("-"): a
|
|
379
|
+
for name in a.all_aliases()
|
|
380
|
+
if include_short or name.startswith("--")
|
|
381
|
+
}
|
|
382
|
+
result.update(a_dict)
|
|
383
|
+
return result
|
|
384
|
+
|
|
385
|
+
def dist_dict(self, include_short: bool = False) -> dict[str, Action]:
|
|
386
|
+
result: dict[str, Action] = {}
|
|
387
|
+
for a in self:
|
|
388
|
+
a_dict = a.dist_dict()
|
|
389
|
+
result.update(a_dict)
|
|
390
|
+
return result
|
|
391
|
+
|
|
392
|
+
|
|
393
|
+
def _process_root_action(action, kw_args) -> Action:
|
|
394
|
+
match action.action:
|
|
395
|
+
case "store_true" | "store_false":
|
|
396
|
+
action.default = MISSING
|
|
397
|
+
action.value_type = MISSING
|
|
398
|
+
if "nargs" in kw_args:
|
|
399
|
+
raise TypeError(
|
|
400
|
+
"nargs cannot be specified with action=store_true | store_false"
|
|
401
|
+
)
|
|
402
|
+
if "const" in kw_args:
|
|
403
|
+
raise TypeError(
|
|
404
|
+
"const cannot be specified with action=store_true | store_false"
|
|
405
|
+
)
|
|
406
|
+
case "store_const":
|
|
407
|
+
if "nargs" in kw_args:
|
|
408
|
+
raise TypeError(
|
|
409
|
+
"nargs cannot be specified with action=store_true | store_false"
|
|
410
|
+
)
|
|
411
|
+
action.value_type = MISSING
|
|
412
|
+
case "append":
|
|
413
|
+
inner_type = get_args(action.value_type)
|
|
414
|
+
if len(inner_type) != 1:
|
|
415
|
+
raise TypeError(
|
|
416
|
+
f"Expected {action.dest} to have a single nested type, but found {len(inner_type)} for type {action.value_type}"
|
|
417
|
+
)
|
|
418
|
+
action.value_type = inner_type[0]
|
|
419
|
+
case _:
|
|
420
|
+
pass
|
|
421
|
+
return action
|
|
422
|
+
|
|
423
|
+
|
|
424
|
+
def _check_child_action(action, kw_args):
|
|
425
|
+
if isinstance(action.nargs, str) and (action.nargs == "*" or action.nargs == "+"):
|
|
426
|
+
raise TypeError("variable nargs cannot be specified on a child property.")
|
|
427
|
+
|
|
428
|
+
|
|
429
|
+
def _check_nested_append_action(parent, action, kw_args):
|
|
430
|
+
if action.required:
|
|
431
|
+
raise NotImplementedError("Required nested fields are not supported.")
|
|
432
|
+
|
|
433
|
+
match action.action:
|
|
434
|
+
case "append" | "extend" | "append_const":
|
|
435
|
+
raise NotImplementedError(
|
|
436
|
+
f"{action.action} on nested fields is not supported."
|
|
437
|
+
)
|
|
438
|
+
case _:
|
|
439
|
+
pass
|
|
440
|
+
|
|
441
|
+
|
|
442
|
+
def _root(
|
|
443
|
+
fld: Field, parent: Action | None, appending_parent: bool
|
|
444
|
+
) -> Optional[Action]:
|
|
445
|
+
try:
|
|
446
|
+
if "arg_type" not in fld.metadata:
|
|
447
|
+
return None
|
|
448
|
+
elif fld.metadata["arg_type"] == ArgType.NOT_AN_ARG:
|
|
449
|
+
return None
|
|
450
|
+
|
|
451
|
+
action = Action()
|
|
452
|
+
|
|
453
|
+
# default
|
|
454
|
+
# default_factory
|
|
455
|
+
if fld.default != MISSING:
|
|
456
|
+
assert fld.default_factory == MISSING
|
|
457
|
+
action.default = fld.default
|
|
458
|
+
elif fld.default_factory != MISSING:
|
|
459
|
+
assert fld.default == MISSING
|
|
460
|
+
action.default = fld.default_factory()
|
|
461
|
+
else:
|
|
462
|
+
action.default = MISSING
|
|
463
|
+
|
|
464
|
+
# init
|
|
465
|
+
# repr
|
|
466
|
+
# hash
|
|
467
|
+
# compare
|
|
468
|
+
# kw_only
|
|
469
|
+
# -> ignore
|
|
470
|
+
# doc
|
|
471
|
+
assert sys.version_info.major == 3
|
|
472
|
+
if sys.version_info.minor < 14:
|
|
473
|
+
action.help = fld.metadata["doc"]
|
|
474
|
+
else:
|
|
475
|
+
action.help = fld.doc
|
|
476
|
+
|
|
477
|
+
# metadata
|
|
478
|
+
action.aliases = list(fld.metadata["args"])
|
|
479
|
+
action.arg_type = fld.metadata["arg_type"]
|
|
480
|
+
action.defer = fld.metadata["defer"]
|
|
481
|
+
|
|
482
|
+
if parent is not None and action.arg_type != ArgType.AUTOMATIC:
|
|
483
|
+
raise TypeError("arg_type must be AUTOMATIC fro nested fields.")
|
|
484
|
+
|
|
485
|
+
match action.arg_type:
|
|
486
|
+
case ArgType.POSITIONAL:
|
|
487
|
+
assert len(action.aliases) == 0
|
|
488
|
+
action.aliases.append(f"{fld.name.replace('_', '-')}")
|
|
489
|
+
case ArgType.AUTOMATIC:
|
|
490
|
+
action.aliases.append(f"--{fld.name.replace('_', '-')}")
|
|
491
|
+
case ArgType.EXPLICIT_ONLY:
|
|
492
|
+
pass
|
|
493
|
+
|
|
494
|
+
if parent is not None:
|
|
495
|
+
action.dest = f"{parent.dest}.{fld.name}"
|
|
496
|
+
else:
|
|
497
|
+
action.dest = fld.name
|
|
498
|
+
|
|
499
|
+
kw_args = copy.copy(fld.metadata["kw_args"])
|
|
500
|
+
action.action = kw_args.get("action", "store")
|
|
501
|
+
|
|
502
|
+
if "type" in kw_args:
|
|
503
|
+
action.value_type = kw_args["type"]
|
|
504
|
+
del kw_args["type"]
|
|
505
|
+
|
|
506
|
+
else:
|
|
507
|
+
action.value_type = fld.type
|
|
508
|
+
|
|
509
|
+
action = _process_root_action(action, kw_args)
|
|
510
|
+
|
|
511
|
+
action.nargs = kw_args.get("nargs", None)
|
|
512
|
+
if isinstance(action.nargs, int):
|
|
513
|
+
nargs_more_than_one = action.nargs > 1
|
|
514
|
+
elif isinstance(action.nargs, str):
|
|
515
|
+
nargs_more_than_one = action.nargs == "*" or action.nargs == "+"
|
|
516
|
+
else:
|
|
517
|
+
nargs_more_than_one = False
|
|
518
|
+
|
|
519
|
+
if nargs_more_than_one:
|
|
520
|
+
inner_type = get_args(action.value_type)
|
|
521
|
+
if len(inner_type) != 1:
|
|
522
|
+
raise TypeError(
|
|
523
|
+
f"Expected {action.dest} to have a single nested type, but found {len(inner_type)} for type {action.value_type}"
|
|
524
|
+
)
|
|
525
|
+
action.value_type = inner_type[0]
|
|
526
|
+
|
|
527
|
+
if parent is not None:
|
|
528
|
+
_check_child_action(action, kw_args)
|
|
529
|
+
|
|
530
|
+
if appending_parent:
|
|
531
|
+
_check_nested_append_action(parent, action, kw_args)
|
|
532
|
+
action.has_appending_parent = True
|
|
533
|
+
|
|
534
|
+
if is_dataclass(action.value_type):
|
|
535
|
+
appending_parent = appending_parent or (action.action == "append")
|
|
536
|
+
action.children = _children(fld, action, appending_parent, kw_args)
|
|
537
|
+
action.is_parent = True
|
|
538
|
+
return action
|
|
539
|
+
else:
|
|
540
|
+
return _single(fld, action, kw_args)
|
|
541
|
+
|
|
542
|
+
except BaseException as e:
|
|
543
|
+
raise e
|
|
544
|
+
raise RuntimeError(f"Could not process field '{fld.name}'") from e
|
|
545
|
+
|
|
546
|
+
|
|
547
|
+
def _children(
|
|
548
|
+
fld: Field, action: Action, appending_parent: bool, kw_args
|
|
549
|
+
) -> list[Action]:
|
|
550
|
+
if action.action not in ("store", "append"):
|
|
551
|
+
raise TypeError("Dataclasses can only be stored or appended")
|
|
552
|
+
invalid_nargs = False
|
|
553
|
+
if isinstance(action.nargs, int):
|
|
554
|
+
invalid_nargs = action.nargs > 1
|
|
555
|
+
elif action.nargs is not None:
|
|
556
|
+
invalid_nargs = True
|
|
557
|
+
if invalid_nargs:
|
|
558
|
+
raise TypeError("multiple nargs cannot be specified with a nested class.")
|
|
559
|
+
if "choices" in kw_args:
|
|
560
|
+
raise TypeError("choices cannot be specified with a nested class.")
|
|
561
|
+
if action.arg_type != ArgType.AUTOMATIC:
|
|
562
|
+
raise TypeError("arg_type must be AUTOMATIC for nested classes.")
|
|
563
|
+
|
|
564
|
+
return [
|
|
565
|
+
a
|
|
566
|
+
for a in [_root(f, action, appending_parent) for f in fields(action.value_type)]
|
|
567
|
+
if a is not None
|
|
568
|
+
]
|
|
569
|
+
|
|
570
|
+
|
|
571
|
+
def _single(fld: Field, action: Action, kw_args) -> Action:
|
|
572
|
+
|
|
573
|
+
if isinstance(action.value_type, BaseType):
|
|
574
|
+
if action.value_type.get_type() is not fld.type:
|
|
575
|
+
raise TypeError(
|
|
576
|
+
f'Expected "{fld.name}" of type {action.value_type} to be {action.value_type.get_type()} but found {fld.type}'
|
|
577
|
+
)
|
|
578
|
+
if isinstance(action.value_type, EnumType):
|
|
579
|
+
members = set(list(action.value_type))
|
|
580
|
+
if "choices" in kw_args:
|
|
581
|
+
if not set(kw_args["choices"]) <= members:
|
|
582
|
+
raise TypeError(
|
|
583
|
+
f"Expected choices of '{action.dest}' to be a subset of the Enum {action.value_type}"
|
|
584
|
+
)
|
|
585
|
+
action.choices = kw_args["choices"]
|
|
586
|
+
del kw_args["choices"]
|
|
587
|
+
else:
|
|
588
|
+
action.choices = list(action.value_type)
|
|
589
|
+
else:
|
|
590
|
+
action.choices = kw_args.get("choices", None)
|
|
591
|
+
|
|
592
|
+
if (
|
|
593
|
+
action.choices is not None
|
|
594
|
+
and action.default != MISSING
|
|
595
|
+
and action.default not in action.choices
|
|
596
|
+
):
|
|
597
|
+
raise TypeError(
|
|
598
|
+
f"Expected '{action.default}' to be a valid choice for '{action.dest}'"
|
|
599
|
+
)
|
|
600
|
+
|
|
601
|
+
if action.arg_type == ArgType.POSITIONAL:
|
|
602
|
+
assert kw_args.get("required", False) is False
|
|
603
|
+
action.required = kw_args.get("required", False)
|
|
604
|
+
action.metavar = kw_args.get("metavar", None)
|
|
605
|
+
action.const = kw_args.get("const", MISSING)
|
|
606
|
+
|
|
607
|
+
for name in action.fields():
|
|
608
|
+
if name in kw_args:
|
|
609
|
+
del kw_args[name]
|
|
610
|
+
action.extra_kw_args = kw_args
|
|
611
|
+
|
|
612
|
+
return action
|
|
613
|
+
|
|
614
|
+
|
|
615
|
+
def from_field(fld: Field) -> Optional[Action]:
|
|
616
|
+
return _root(fld, parent=None, appending_parent=False)
|
|
617
|
+
|
|
618
|
+
|
|
619
|
+
def from_dataclass(dcls) -> ActionList:
|
|
620
|
+
return ActionList(
|
|
621
|
+
[f for f in [from_field(f) for f in fields(dcls)] if f is not None]
|
|
622
|
+
)
|
|
623
|
+
|
|
624
|
+
|
|
625
|
+
def add_arguments(parser: argparse.ArgumentParser, dcls: type | list[Action]):
|
|
626
|
+
if isinstance(dcls, type):
|
|
627
|
+
actions = from_dataclass(dcls)
|
|
628
|
+
else:
|
|
629
|
+
actions = dcls
|
|
630
|
+
defered = []
|
|
631
|
+
for a in actions:
|
|
632
|
+
if a.defer:
|
|
633
|
+
defered.append(a)
|
|
634
|
+
else:
|
|
635
|
+
a.add_to_parser(parser)
|
|
636
|
+
for a in defered:
|
|
637
|
+
a.add_to_parser(parser)
|
|
638
|
+
|
|
639
|
+
|
|
640
|
+
def _recursively_group_and_invert(evaluation_dict, actions, prefix):
|
|
641
|
+
dest_to_action = {a.dest[len(prefix) :]: a for a in actions}
|
|
642
|
+
dest_to_sub_dict = {}
|
|
643
|
+
keys_to_reevaluate = set()
|
|
644
|
+
|
|
645
|
+
for k, v in evaluation_dict.items():
|
|
646
|
+
parts = k.split(sep=".", maxsplit=1)
|
|
647
|
+
action = dest_to_action[parts[0]]
|
|
648
|
+
if len(parts) > 1 and not action.is_parent:
|
|
649
|
+
raise RuntimeError("We should never get here.")
|
|
650
|
+
if action.is_parent:
|
|
651
|
+
if len(parts) != 2:
|
|
652
|
+
raise RuntimeError("We should never get here.")
|
|
653
|
+
|
|
654
|
+
if parts[0] not in dest_to_sub_dict:
|
|
655
|
+
dest_to_sub_dict[parts[0]] = {}
|
|
656
|
+
dest_to_sub_dict[parts[0]][parts[1]] = v
|
|
657
|
+
keys_to_reevaluate.add(parts[0])
|
|
658
|
+
else:
|
|
659
|
+
dest_to_sub_dict[parts[0]] = v
|
|
660
|
+
|
|
661
|
+
for k in keys_to_reevaluate:
|
|
662
|
+
action = dest_to_action[k]
|
|
663
|
+
sub_dict = dest_to_sub_dict[k]
|
|
664
|
+
if action.action == "append":
|
|
665
|
+
lengths = [len(v) for v in sub_dict.values()]
|
|
666
|
+
assert len(lengths) > 0
|
|
667
|
+
max_len = max(lengths)
|
|
668
|
+
inverted_dict: list[dict[str, Any]] = [{} for _ in range(max_len)]
|
|
669
|
+
for sub_k, v in sub_dict.items():
|
|
670
|
+
for ii, vv in enumerate(v):
|
|
671
|
+
inverted_dict[ii][sub_k] = vv
|
|
672
|
+
|
|
673
|
+
dest_to_sub_dict[k] = [
|
|
674
|
+
_recursively_group_and_invert(inv, action.children, f"{prefix}{k}.")
|
|
675
|
+
for inv in inverted_dict
|
|
676
|
+
]
|
|
677
|
+
else:
|
|
678
|
+
dest_to_sub_dict[k] = _recursively_group_and_invert(
|
|
679
|
+
sub_dict, action.children, f"{prefix}{k}."
|
|
680
|
+
)
|
|
681
|
+
return dest_to_sub_dict
|
|
682
|
+
|
|
683
|
+
|
|
684
|
+
def from_arguments(args: argparse.Namespace, dcls: Type[T]) -> T:
|
|
685
|
+
actions = from_dataclass(dcls)
|
|
686
|
+
|
|
687
|
+
data_dict = _recursively_group_and_invert(vars(args), actions, "")
|
|
688
|
+
|
|
689
|
+
return build_dataclass_from_dict(dcls, data_dict)
|