log21 3.3.3__tar.gz → 3.4.0__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.
- {log21-3.3.3 → log21-3.4.0}/PKG-INFO +1 -1
- {log21-3.3.3 → log21-3.4.0}/pyproject.toml +1 -1
- {log21-3.3.3 → log21-3.4.0}/src/log21/__init__.py +1 -1
- {log21-3.3.3 → log21-3.4.0}/src/log21/argparse.py +15 -2
- {log21-3.3.3 → log21-3.4.0}/src/log21/argumentify.py +312 -10
- {log21-3.3.3 → log21-3.4.0}/README.md +0 -0
- {log21-3.3.3 → log21-3.4.0}/src/log21/_argparse.py +0 -0
- {log21-3.3.3 → log21-3.4.0}/src/log21/_module_helper.py +0 -0
- {log21-3.3.3 → log21-3.4.0}/src/log21/colors.py +0 -0
- {log21-3.3.3 → log21-3.4.0}/src/log21/crash_reporter/__init__.py +0 -0
- {log21-3.3.3 → log21-3.4.0}/src/log21/crash_reporter/formatters.py +0 -0
- {log21-3.3.3 → log21-3.4.0}/src/log21/crash_reporter/reporters.py +0 -0
- {log21-3.3.3 → log21-3.4.0}/src/log21/file_handler.py +0 -0
- {log21-3.3.3 → log21-3.4.0}/src/log21/formatters.py +0 -0
- {log21-3.3.3 → log21-3.4.0}/src/log21/helper_types.py +0 -0
- {log21-3.3.3 → log21-3.4.0}/src/log21/levels.py +0 -0
- {log21-3.3.3 → log21-3.4.0}/src/log21/logger.py +0 -0
- {log21-3.3.3 → log21-3.4.0}/src/log21/logging_window.py +0 -0
- {log21-3.3.3 → log21-3.4.0}/src/log21/manager.py +0 -0
- {log21-3.3.3 → log21-3.4.0}/src/log21/pprint.py +0 -0
- {log21-3.3.3 → log21-3.4.0}/src/log21/progress_bar.py +0 -0
- {log21-3.3.3 → log21-3.4.0}/src/log21/stream_handler.py +0 -0
- {log21-3.3.3 → log21-3.4.0}/src/log21/tree_print.py +0 -0
|
@@ -33,7 +33,7 @@ from .stream_handler import StreamHandler, ColorizingStreamHandler
|
|
|
33
33
|
# yapf: enable
|
|
34
34
|
|
|
35
35
|
__author__ = 'CodeWriter21 (Mehrad Pooryoussof)'
|
|
36
|
-
__version__ = '3.
|
|
36
|
+
__version__ = '3.4.0'
|
|
37
37
|
__github__ = 'https://GitHub.com/MPCodeWriter21/log21'
|
|
38
38
|
__all__ = [
|
|
39
39
|
'ColorizingStreamHandler', 'DecolorizingFileHandler', 'ColorizingFormatter',
|
|
@@ -612,13 +612,17 @@ class _ActionsContainer(_argparse._ActionsContainer): # novm
|
|
|
612
612
|
else:
|
|
613
613
|
func_type = func_type.__args__ # type: ignore
|
|
614
614
|
|
|
615
|
-
# Handle `List` as a type (e.g. `List[int]`)
|
|
615
|
+
# Handle `List` as a type (e.g. `List[int]` and builtin `list[int]`)
|
|
616
616
|
elif (hasattr(_typing, '_GenericAlias')
|
|
617
617
|
and isinstance(func_type, _typing._GenericAlias) # type: ignore
|
|
618
618
|
and func_type.__origin__ is list) or (
|
|
619
619
|
hasattr(_typing, '_GenericAlias')
|
|
620
620
|
and isinstance(func_type, _typing._GenericAlias) # type: ignore
|
|
621
|
-
and func_type.__origin__ is collections.abc.Sequence)
|
|
621
|
+
and func_type.__origin__ is collections.abc.Sequence) or (
|
|
622
|
+
hasattr(_types, 'GenericAlias')
|
|
623
|
+
and isinstance(func_type, _types.GenericAlias)
|
|
624
|
+
and getattr(func_type, '__origin__', None) in
|
|
625
|
+
(list, collections.abc.Sequence)):
|
|
622
626
|
func_type = func_type.__args__[0]
|
|
623
627
|
if kwargs.get('nargs') is None:
|
|
624
628
|
action.nargs = '+'
|
|
@@ -668,6 +672,15 @@ class _ActionsContainer(_argparse._ActionsContainer): # novm
|
|
|
668
672
|
_types.UnionType,
|
|
669
673
|
))):
|
|
670
674
|
func_type = self._validate_func_type(action, func_type, kwargs, level + 1)
|
|
675
|
+
elif (hasattr(_types, 'GenericAlias')
|
|
676
|
+
and isinstance(func_type, _types.GenericAlias)
|
|
677
|
+
and getattr(func_type, '__origin__', None) in
|
|
678
|
+
(list, collections.abc.Sequence)):
|
|
679
|
+
# Builtin `list[T]` unwrapped from a PEP 604 optional (e.g.
|
|
680
|
+
# `list[str] | None`): route it through the `List` branch above.
|
|
681
|
+
# Restricted to list/Sequence origins so other generics (e.g.
|
|
682
|
+
# `dict[str, int]`) cannot recurse forever.
|
|
683
|
+
func_type = self._validate_func_type(action, func_type, kwargs, level + 1)
|
|
671
684
|
else:
|
|
672
685
|
func_type = (func_type, )
|
|
673
686
|
|
|
@@ -7,6 +7,8 @@ import re as _re
|
|
|
7
7
|
import string as _string
|
|
8
8
|
import asyncio as _asyncio
|
|
9
9
|
import inspect as _inspect
|
|
10
|
+
import types as _stdlib_types
|
|
11
|
+
import collections.abc as _collections_abc
|
|
10
12
|
from typing import (Any as _Any, Set as _Set, Dict as _Dict, List as _List,
|
|
11
13
|
Tuple as _Tuple, Union as _Union, Callable as _Callable,
|
|
12
14
|
Optional as _Optional, Awaitable as _Awaitable,
|
|
@@ -331,16 +333,231 @@ def generate_flag( # pylint: disable=too-many-branches
|
|
|
331
333
|
return flags
|
|
332
334
|
|
|
333
335
|
|
|
336
|
+
def _list_element_type(annotation: _Any) -> _Tuple[bool, _Any]:
|
|
337
|
+
"""Check whether an annotation is a `list[T]` and return its element type.
|
|
338
|
+
|
|
339
|
+
Handles `list[T]`, `typing.List[T]`, `Sequence[T]` as well as
|
|
340
|
+
`Optional[...]` / `X | None` wrapped variants of those. Bare `list`
|
|
341
|
+
(without a subscript) returns `(True, None)`, meaning no per-item
|
|
342
|
+
conversion.
|
|
343
|
+
|
|
344
|
+
:param annotation: The parameter annotation to inspect.
|
|
345
|
+
:return: A `(is_list, element_type)` tuple.
|
|
346
|
+
"""
|
|
347
|
+
if annotation is None:
|
|
348
|
+
return False, None
|
|
349
|
+
# Unwrap `Optional[X]` / `X | None` (both `typing` and PEP 604 spellings).
|
|
350
|
+
args = getattr(annotation, '__args__', None)
|
|
351
|
+
none_type = getattr(_stdlib_types, 'NoneType', None)
|
|
352
|
+
if (args is not None and none_type is not None and len(args) == 2
|
|
353
|
+
and (args[0] is none_type or args[1] is none_type)):
|
|
354
|
+
annotation = args[1] if args[0] is none_type else args[0]
|
|
355
|
+
args = getattr(annotation, '__args__', None)
|
|
356
|
+
if annotation is list:
|
|
357
|
+
return True, None
|
|
358
|
+
origin = getattr(annotation, '__origin__', None)
|
|
359
|
+
if origin is list or origin is _collections_abc.Sequence:
|
|
360
|
+
if args:
|
|
361
|
+
return True, args[0]
|
|
362
|
+
return True, None
|
|
363
|
+
if (hasattr(_stdlib_types, 'GenericAlias')
|
|
364
|
+
and isinstance(annotation, _stdlib_types.GenericAlias)
|
|
365
|
+
and getattr(annotation, '__origin__', None) in
|
|
366
|
+
(list, _collections_abc.Sequence)):
|
|
367
|
+
args = getattr(annotation, '__args__', None) or ()
|
|
368
|
+
return True, args[0] if args else None
|
|
369
|
+
return False, None
|
|
370
|
+
|
|
371
|
+
|
|
372
|
+
def _is_repeatable_flag(argument: Argument) -> bool:
|
|
373
|
+
"""Check whether an argument maps to a repeatable `--flag` option.
|
|
374
|
+
|
|
375
|
+
Only non-positional parameters annotated with `list[T]` are repeatable
|
|
376
|
+
flags. Positional-only `list[T]` parameters become multi-value
|
|
377
|
+
positionals instead, and `*args` is handled separately.
|
|
378
|
+
"""
|
|
379
|
+
if argument.kind in (_inspect._ParameterKind.POSITIONAL_ONLY,
|
|
380
|
+
_inspect._ParameterKind.VAR_POSITIONAL,
|
|
381
|
+
_inspect._ParameterKind.VAR_KEYWORD):
|
|
382
|
+
return False
|
|
383
|
+
is_list, _ = _list_element_type(argument.annotation)
|
|
384
|
+
return is_list
|
|
385
|
+
|
|
386
|
+
|
|
387
|
+
def _normalize_exclusive_groups(
|
|
388
|
+
mutually_exclusive: _Any
|
|
389
|
+
) -> _List[_Tuple[_List[str], bool]]:
|
|
390
|
+
"""Normalize the `mutually_exclusive` argument into `(names, required)` groups.
|
|
391
|
+
|
|
392
|
+
Each group is either a list/tuple of parameter names (an optional group)
|
|
393
|
+
or a `{"names": [...], "required": bool}` mapping. Groups must name at
|
|
394
|
+
least two parameters.
|
|
395
|
+
|
|
396
|
+
:param mutually_exclusive: The raw `mutually_exclusive` value.
|
|
397
|
+
:raises ValueError: If a group is malformed.
|
|
398
|
+
:return: A list of `(names, required)` tuples.
|
|
399
|
+
"""
|
|
400
|
+
if mutually_exclusive is None:
|
|
401
|
+
return []
|
|
402
|
+
if isinstance(mutually_exclusive, _Dict):
|
|
403
|
+
if 'names' in mutually_exclusive:
|
|
404
|
+
# A single group passed without a wrapping list.
|
|
405
|
+
mutually_exclusive = [mutually_exclusive]
|
|
406
|
+
else:
|
|
407
|
+
raise ValueError(
|
|
408
|
+
'mutually_exclusive must be a list of groups, not a mapping. '
|
|
409
|
+
'To target groups at specific commands, pass a '
|
|
410
|
+
'{command_name: groups} mapping as documented in `argumentify`.'
|
|
411
|
+
)
|
|
412
|
+
groups: _List[_Tuple[_List[str], bool]] = []
|
|
413
|
+
if all(isinstance(item, str) for item in mutually_exclusive):
|
|
414
|
+
raise ValueError(
|
|
415
|
+
'mutually_exclusive must be a list of groups, e.g. '
|
|
416
|
+
f'[{list(mutually_exclusive)!r}], not a bare list of names.'
|
|
417
|
+
)
|
|
418
|
+
for group in mutually_exclusive:
|
|
419
|
+
required = False
|
|
420
|
+
if isinstance(group, _Dict):
|
|
421
|
+
names = group.get('names', [])
|
|
422
|
+
required = bool(group.get('required', False))
|
|
423
|
+
elif isinstance(group, (list, tuple)):
|
|
424
|
+
names = list(group)
|
|
425
|
+
else:
|
|
426
|
+
raise ValueError(
|
|
427
|
+
'Each mutually exclusive group must be a list of parameter '
|
|
428
|
+
f'names or a {{"names": [...], "required": bool}} mapping, '
|
|
429
|
+
f'got: {group!r}'
|
|
430
|
+
)
|
|
431
|
+
names = list(names)
|
|
432
|
+
if any(not isinstance(name, str) for name in names):
|
|
433
|
+
raise ValueError(
|
|
434
|
+
'Mutually exclusive group members must be parameter names '
|
|
435
|
+
f'(strings), got: {names!r}'
|
|
436
|
+
)
|
|
437
|
+
if len(names) < 2:
|
|
438
|
+
raise ValueError(
|
|
439
|
+
'Each mutually exclusive group must name at least two '
|
|
440
|
+
f'parameters, got: {names!r}'
|
|
441
|
+
)
|
|
442
|
+
if len(set(names)) != len(names):
|
|
443
|
+
raise ValueError(
|
|
444
|
+
'Duplicate parameter names in mutually exclusive group: '
|
|
445
|
+
f'{names!r}'
|
|
446
|
+
)
|
|
447
|
+
groups.append((names, required))
|
|
448
|
+
return groups
|
|
449
|
+
|
|
450
|
+
|
|
451
|
+
def _resolve_command_groups(
|
|
452
|
+
mutually_exclusive: _Any, command: _Optional[str],
|
|
453
|
+
argument_names: _Set[str]
|
|
454
|
+
) -> _List[_Tuple[_List[str], bool]]:
|
|
455
|
+
"""Resolve which exclusive groups apply to a single command parser.
|
|
456
|
+
|
|
457
|
+
`mutually_exclusive` may be a plain list of groups (broadcast to every
|
|
458
|
+
command that defines all named parameters) or a `{command_name: groups}`
|
|
459
|
+
mapping for targeted groups. Unknown parameter names and groups that
|
|
460
|
+
match no command raise `ValueError`.
|
|
461
|
+
|
|
462
|
+
:param mutually_exclusive: The raw `mutually_exclusive` value.
|
|
463
|
+
:param command: The command name, or `None` for a single-function parser.
|
|
464
|
+
:param argument_names: The parameter names of the command.
|
|
465
|
+
:raises ValueError: If a group references unknown parameters.
|
|
466
|
+
:return: The groups applying to this command.
|
|
467
|
+
"""
|
|
468
|
+
if mutually_exclusive is None:
|
|
469
|
+
return []
|
|
470
|
+
if isinstance(mutually_exclusive, _Dict):
|
|
471
|
+
if command is None:
|
|
472
|
+
raise ValueError(
|
|
473
|
+
'mutually_exclusive as a {command_name: groups} mapping is '
|
|
474
|
+
'only supported for multiple entry points; pass a plain list '
|
|
475
|
+
'of groups for a single function.'
|
|
476
|
+
)
|
|
477
|
+
if command not in mutually_exclusive:
|
|
478
|
+
return []
|
|
479
|
+
groups = _normalize_exclusive_groups(mutually_exclusive[command])
|
|
480
|
+
for names, _ in groups:
|
|
481
|
+
unknown = [name for name in names if name not in argument_names]
|
|
482
|
+
if unknown:
|
|
483
|
+
raise ValueError(
|
|
484
|
+
f'Mutually exclusive group {names!r} references unknown '
|
|
485
|
+
f'parameters of command {command!r}: {unknown!r}'
|
|
486
|
+
)
|
|
487
|
+
_check_group_overlap(groups, command)
|
|
488
|
+
return groups
|
|
489
|
+
groups = _normalize_exclusive_groups(mutually_exclusive)
|
|
490
|
+
applicable = [
|
|
491
|
+
group for group in groups
|
|
492
|
+
if all(name in argument_names for name in group[0])
|
|
493
|
+
]
|
|
494
|
+
if groups and not applicable:
|
|
495
|
+
# A broadcast list that matches nowhere is almost certainly a typo:
|
|
496
|
+
# fail fast instead of silently ignoring it.
|
|
497
|
+
missing = [
|
|
498
|
+
name for names, _ in groups for name in names
|
|
499
|
+
if name not in argument_names
|
|
500
|
+
]
|
|
501
|
+
label = f'command {command!r} ' if command is not None else ''
|
|
502
|
+
raise ValueError(
|
|
503
|
+
f'Mutually exclusive groups reference unknown parameters for '
|
|
504
|
+
f'{label}: {sorted(set(missing))!r}'
|
|
505
|
+
)
|
|
506
|
+
_check_group_overlap(applicable, command)
|
|
507
|
+
return applicable
|
|
508
|
+
|
|
509
|
+
|
|
510
|
+
def _check_group_overlap(
|
|
511
|
+
groups: _List[_Tuple[_List[str], bool]], command: _Optional[str]
|
|
512
|
+
) -> None:
|
|
513
|
+
"""Reject parameters shared between two groups of the same parser."""
|
|
514
|
+
seen: _Dict[str, _List[str]] = {}
|
|
515
|
+
for names, _ in groups:
|
|
516
|
+
for name in names:
|
|
517
|
+
seen.setdefault(name, []).append(names)
|
|
518
|
+
for name, owners in seen.items():
|
|
519
|
+
if len(owners) > 1:
|
|
520
|
+
label = f'command {command!r} ' if command is not None else ''
|
|
521
|
+
raise ValueError(
|
|
522
|
+
f'Parameter {name!r} for {label}is in more than one mutually '
|
|
523
|
+
'exclusive group.'
|
|
524
|
+
)
|
|
525
|
+
|
|
526
|
+
|
|
527
|
+
def _apply_repeatable_defaults(cli_args: _Any, info: FunctionInfo) -> None:
|
|
528
|
+
"""Restore declared `list[T]` defaults for absent repeatable flags.
|
|
529
|
+
|
|
530
|
+
Repeatable flags are registered with an internal `None` default (so
|
|
531
|
+
`argparse` never mutates the function signature's default list via
|
|
532
|
+
`action="append"`). When the flag was not used and the parameter declares
|
|
533
|
+
a default, substitute it back here.
|
|
534
|
+
"""
|
|
535
|
+
for argument in info.arguments.values():
|
|
536
|
+
if _is_repeatable_flag(argument) and getattr(
|
|
537
|
+
cli_args, argument.name, None
|
|
538
|
+
) is None and argument.default is not None:
|
|
539
|
+
setattr(cli_args, argument.name, argument.default)
|
|
540
|
+
|
|
541
|
+
|
|
334
542
|
def _add_arguments(
|
|
335
543
|
parser: _Union[_argparse.ColorizingArgumentParser, _argparse._ArgumentGroup],
|
|
336
544
|
info: FunctionInfo,
|
|
337
|
-
reserved_flags: _Optional[_Set[str]] = None
|
|
545
|
+
reserved_flags: _Optional[_Set[str]] = None,
|
|
546
|
+
exclusive_groups: _Optional[_List[_Tuple[_List[str], bool]]] = None
|
|
338
547
|
) -> None:
|
|
339
548
|
"""Add the arguments to the parser.
|
|
340
549
|
|
|
550
|
+
`list[T]` parameters become multi-value arguments: positional-only ones
|
|
551
|
+
accept several values (`nargs='+'`, or `'*'` when a default is declared),
|
|
552
|
+
while other parameters become repeatable `--flag` options
|
|
553
|
+
(`action="append"`, each occurrence converted to `T`). Parameters listed
|
|
554
|
+
in `exclusive_groups` are added to `argparse` mutually exclusive groups.
|
|
555
|
+
|
|
341
556
|
:param parser: The parser to add the arguments to.
|
|
342
557
|
:param info: The function info.
|
|
343
558
|
:param reserved_flags: The reserved flags.
|
|
559
|
+
:param exclusive_groups: Normalized `(names, required)` groups applying
|
|
560
|
+
to this parser.
|
|
344
561
|
"""
|
|
345
562
|
if reserved_flags is None:
|
|
346
563
|
reserved_flags = RESERVED_FLAGS.copy()
|
|
@@ -367,6 +584,9 @@ def _add_arguments(
|
|
|
367
584
|
)
|
|
368
585
|
|
|
369
586
|
# Add the arguments
|
|
587
|
+
containers: _Dict[int, _Any] = {}
|
|
588
|
+
for index, (_, required) in enumerate(exclusive_groups or []):
|
|
589
|
+
containers[index] = parser.add_mutually_exclusive_group(required=required)
|
|
370
590
|
for argument in info.arguments.values():
|
|
371
591
|
config: _Dict[str, _Any] = {
|
|
372
592
|
'action': 'store',
|
|
@@ -376,32 +596,69 @@ def _add_arguments(
|
|
|
376
596
|
flags = generate_flag(argument, reserved_flags=reserved_flags)
|
|
377
597
|
if argument.annotation is bool:
|
|
378
598
|
config['action'] = 'store_true'
|
|
599
|
+
elif _is_repeatable_flag(argument):
|
|
600
|
+
_, element = _list_element_type(argument.annotation)
|
|
601
|
+
config['action'] = 'append'
|
|
602
|
+
if element is not None:
|
|
603
|
+
config['type'] = element
|
|
604
|
+
# Registered with an internal `None` default so `argparse` never
|
|
605
|
+
# mutates the signature's default list; the declared default is
|
|
606
|
+
# restored by `_apply_repeatable_defaults` after parsing.
|
|
607
|
+
config['default'] = None
|
|
379
608
|
elif argument.annotation:
|
|
380
609
|
config['type'] = argument.annotation
|
|
381
610
|
if argument.kind == _inspect._ParameterKind.POSITIONAL_ONLY:
|
|
382
611
|
flags = [config.pop('dest')]
|
|
612
|
+
is_list, _ = _list_element_type(argument.annotation)
|
|
613
|
+
if is_list and config.get('nargs') is None and argument.default is not None:
|
|
614
|
+
# A declared default (e.g. `= []`) makes the multi-value
|
|
615
|
+
# positional optional instead of requiring 1+ values.
|
|
616
|
+
config['nargs'] = '*'
|
|
617
|
+
if any(argument.name in names for names, _ in exclusive_groups or []):
|
|
618
|
+
# `argparse` only accepts optional actions in mutually
|
|
619
|
+
# exclusive groups: single-value positionals become `nargs='?'`
|
|
620
|
+
# and multi-value ones `nargs='*'` (group-level `required`
|
|
621
|
+
# still enforces "exactly one" when requested).
|
|
622
|
+
if config.get('nargs') is None:
|
|
623
|
+
config['nargs'] = '?'
|
|
624
|
+
elif config.get('nargs') == '+':
|
|
625
|
+
config['nargs'] = '*'
|
|
383
626
|
if argument.kind == _inspect._ParameterKind.VAR_POSITIONAL:
|
|
384
627
|
config['nargs'] = '*'
|
|
385
628
|
flags = [config.pop('dest')]
|
|
386
629
|
if argument.kind == _inspect._ParameterKind.POSITIONAL_OR_KEYWORD and keyword_only_exists:
|
|
387
630
|
config['required'] = True
|
|
388
|
-
if argument.default is not None:
|
|
631
|
+
if argument.default is not None and config.get('action') != 'append':
|
|
389
632
|
config['default'] = argument.default
|
|
390
|
-
parser
|
|
633
|
+
target = parser
|
|
634
|
+
for index, (names, _) in enumerate(exclusive_groups or []):
|
|
635
|
+
if argument.name in names:
|
|
636
|
+
target = containers[index]
|
|
637
|
+
break
|
|
638
|
+
target.add_argument(*flags, **config)
|
|
391
639
|
|
|
392
640
|
|
|
393
|
-
def _argumentify_one(
|
|
641
|
+
def _argumentify_one(
|
|
642
|
+
func: Callable, mutually_exclusive: _Any = None
|
|
643
|
+
) -> None:
|
|
394
644
|
"""This function argumentifies one function as the entry point of the script.
|
|
395
645
|
|
|
396
646
|
:param function: The function to argumentify.
|
|
647
|
+
:param mutually_exclusive: A list of mutually exclusive groups. Each
|
|
648
|
+
group is a list of parameter names or a
|
|
649
|
+
`{"names": [...], "required": bool}` mapping.
|
|
397
650
|
"""
|
|
398
651
|
info = FunctionInfo(func)
|
|
399
652
|
|
|
400
653
|
# Create the parser
|
|
401
654
|
parser = _argparse.ColorizingArgumentParser(description=info.docstring.description)
|
|
402
655
|
# Add the arguments
|
|
403
|
-
|
|
656
|
+
groups = _resolve_command_groups(
|
|
657
|
+
mutually_exclusive, None, set(info.arguments)
|
|
658
|
+
)
|
|
659
|
+
_add_arguments(parser, info, exclusive_groups=groups)
|
|
404
660
|
cli_args = parser.parse_args()
|
|
661
|
+
_apply_repeatable_defaults(cli_args, info)
|
|
405
662
|
args = []
|
|
406
663
|
kwargs = {}
|
|
407
664
|
for argument in info.arguments.values():
|
|
@@ -421,11 +678,18 @@ def _argumentify_one(func: Callable) -> None:
|
|
|
421
678
|
parser.error(error.message)
|
|
422
679
|
|
|
423
680
|
|
|
424
|
-
def _argumentify(
|
|
681
|
+
def _argumentify(
|
|
682
|
+
functions: _Dict[str, Callable], mutually_exclusive: _Any = None
|
|
683
|
+
) -> None:
|
|
425
684
|
"""This function argumentifies one or more functions as the entry point of the
|
|
426
685
|
script.
|
|
427
686
|
|
|
428
687
|
:param functions: A dictionary of functions to argumentify.
|
|
688
|
+
:param mutually_exclusive: A list of mutually exclusive groups broadcast
|
|
689
|
+
to every command defining all named parameters, or a
|
|
690
|
+
`{command_name: groups}` mapping for targeted groups. Each group is a
|
|
691
|
+
list of parameter names or a `{"names": [...], "required": bool}`
|
|
692
|
+
mapping.
|
|
429
693
|
:raises RuntimeError:
|
|
430
694
|
"""
|
|
431
695
|
functions_info: _Dict[str, _Tuple[Callable, FunctionInfo]] = {}
|
|
@@ -445,7 +709,10 @@ def _argumentify(functions: _Dict[str, Callable]) -> None:
|
|
|
445
709
|
subparsers = parser.add_subparsers(required=True)
|
|
446
710
|
for name, (_, info) in functions_info.items():
|
|
447
711
|
subparser = subparsers.add_parser(name, help=info.docstring.description)
|
|
448
|
-
|
|
712
|
+
groups = _resolve_command_groups(
|
|
713
|
+
mutually_exclusive, name, set(info.arguments)
|
|
714
|
+
)
|
|
715
|
+
_add_arguments(subparser, info, exclusive_groups=groups)
|
|
449
716
|
subparser.set_defaults(func=info.function)
|
|
450
717
|
cli_args = parser.parse_args()
|
|
451
718
|
args = []
|
|
@@ -456,6 +723,7 @@ def _argumentify(functions: _Dict[str, Callable]) -> None:
|
|
|
456
723
|
break
|
|
457
724
|
else:
|
|
458
725
|
raise RuntimeError('No function found for the given arguments.')
|
|
726
|
+
_apply_repeatable_defaults(cli_args, info)
|
|
459
727
|
for argument in info.arguments.values():
|
|
460
728
|
if argument.kind in (_inspect._ParameterKind.POSITIONAL_ONLY,
|
|
461
729
|
_inspect._ParameterKind.POSITIONAL_OR_KEYWORD):
|
|
@@ -474,7 +742,8 @@ def _argumentify(functions: _Dict[str, Callable]) -> None:
|
|
|
474
742
|
|
|
475
743
|
|
|
476
744
|
def argumentify(
|
|
477
|
-
entry_point: _Union[Callable, _List[Callable], _Dict[str, Callable]]
|
|
745
|
+
entry_point: _Union[Callable, _List[Callable], _Dict[str, Callable]],
|
|
746
|
+
mutually_exclusive: _Optional[_Any] = None
|
|
478
747
|
) -> _Union[Callable, _List[Callable], _Dict[str, Callable]]:
|
|
479
748
|
"""This function argumentifies one or more functions as the entry point of the
|
|
480
749
|
script.
|
|
@@ -498,15 +767,48 @@ def argumentify(
|
|
|
498
767
|
$ python argumentified.py Mehrad Pooryoussof
|
|
499
768
|
Mehrad Pooryoussof is not yet born.
|
|
500
769
|
|
|
770
|
+
Parameters annotated with `list[T]` accept multiple values:
|
|
771
|
+
positional-only ones take several values (`items: list[str], /` called as
|
|
772
|
+
`prog a b c`), while other parameters become repeatable `--flag` options
|
|
773
|
+
(`defines: list[str]` called as `prog --defines a=1 --defines b=2`, each
|
|
774
|
+
occurrence converted to `T`). A declared default (e.g. `= []`) is used
|
|
775
|
+
when the argument is omitted. As with other parameters, if the function
|
|
776
|
+
also defines keyword-only parameters, positional-or-keyword `list[T]`
|
|
777
|
+
parameters become required flags; declare them keyword-only (after `*`)
|
|
778
|
+
to keep them optional.
|
|
779
|
+
|
|
780
|
+
Mutually exclusive parameters can be declared with `mutually_exclusive`.
|
|
781
|
+
Each group is a list of parameter names or a
|
|
782
|
+
`{"names": [...], "required": bool}` mapping (`required=True` means
|
|
783
|
+
exactly one of them must be given):
|
|
784
|
+
|
|
785
|
+
def run(recipe_path: str = None, /, *, builtin: str = None) -> None: ...
|
|
786
|
+
argumentify(run, mutually_exclusive=[["recipe_path", "builtin"]])
|
|
787
|
+
|
|
788
|
+
For multiple entry points, pass a `{command_name: groups}` mapping, or a
|
|
789
|
+
plain list of groups which applies to every command defining all named
|
|
790
|
+
parameters. Positional parameters in a group accept zero values (single
|
|
791
|
+
ones behave like `nargs='?'`, multi-value ones like `nargs='*'`) and
|
|
792
|
+
receive their default (or `None`) when omitted; pass
|
|
793
|
+
`{"names": [...], "required": True}` when exactly one of them must be
|
|
794
|
+
given:
|
|
795
|
+
|
|
796
|
+
argumentify(
|
|
797
|
+
{"run-recipe": run_recipe},
|
|
798
|
+
mutually_exclusive={"run-recipe": [["recipe_path", "builtin"]]},
|
|
799
|
+
)
|
|
800
|
+
|
|
501
801
|
:param entry_point: The function(s) to argumentify.
|
|
802
|
+
:param mutually_exclusive: Mutually exclusive parameter groups.
|
|
502
803
|
:raises TypeError: A function must be a function or a list of functions or a
|
|
503
804
|
dictionary of functions.
|
|
805
|
+
:raises ValueError: A group is malformed or references unknown parameters.
|
|
504
806
|
"""
|
|
505
807
|
|
|
506
808
|
functions = {}
|
|
507
809
|
# Check the types
|
|
508
810
|
if callable(entry_point):
|
|
509
|
-
_argumentify_one(entry_point)
|
|
811
|
+
_argumentify_one(entry_point, mutually_exclusive)
|
|
510
812
|
return entry_point
|
|
511
813
|
if isinstance(entry_point, _List):
|
|
512
814
|
for func in entry_point:
|
|
@@ -530,5 +832,5 @@ def argumentify(
|
|
|
530
832
|
"dictionary of functions."
|
|
531
833
|
)
|
|
532
834
|
|
|
533
|
-
_argumentify(functions)
|
|
835
|
+
_argumentify(functions, mutually_exclusive)
|
|
534
836
|
return entry_point
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|