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 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)