log21 3.6.0__tar.gz → 3.7.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.6.0 → log21-3.7.0}/PKG-INFO +1 -1
- {log21-3.6.0 → log21-3.7.0}/pyproject.toml +1 -1
- {log21-3.6.0 → log21-3.7.0}/src/log21/__init__.py +1 -1
- {log21-3.6.0 → log21-3.7.0}/src/log21/argumentify.py +177 -18
- {log21-3.6.0 → log21-3.7.0}/README.md +0 -0
- {log21-3.6.0 → log21-3.7.0}/src/log21/_argparse.py +0 -0
- {log21-3.6.0 → log21-3.7.0}/src/log21/_module_helper.py +0 -0
- {log21-3.6.0 → log21-3.7.0}/src/log21/argparse.py +0 -0
- {log21-3.6.0 → log21-3.7.0}/src/log21/colors.py +0 -0
- {log21-3.6.0 → log21-3.7.0}/src/log21/crash_reporter/__init__.py +0 -0
- {log21-3.6.0 → log21-3.7.0}/src/log21/crash_reporter/formatters.py +0 -0
- {log21-3.6.0 → log21-3.7.0}/src/log21/crash_reporter/reporters.py +0 -0
- {log21-3.6.0 → log21-3.7.0}/src/log21/file_handler.py +0 -0
- {log21-3.6.0 → log21-3.7.0}/src/log21/formatters.py +0 -0
- {log21-3.6.0 → log21-3.7.0}/src/log21/helper_types.py +0 -0
- {log21-3.6.0 → log21-3.7.0}/src/log21/levels.py +0 -0
- {log21-3.6.0 → log21-3.7.0}/src/log21/logger.py +0 -0
- {log21-3.6.0 → log21-3.7.0}/src/log21/logging_window.py +0 -0
- {log21-3.6.0 → log21-3.7.0}/src/log21/manager.py +0 -0
- {log21-3.6.0 → log21-3.7.0}/src/log21/pprint.py +0 -0
- {log21-3.6.0 → log21-3.7.0}/src/log21/progress_bar.py +0 -0
- {log21-3.6.0 → log21-3.7.0}/src/log21/stream_handler.py +0 -0
- {log21-3.6.0 → log21-3.7.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.7.0'
|
|
37
37
|
__github__ = 'https://GitHub.com/MPCodeWriter21/log21'
|
|
38
38
|
__all__ = [
|
|
39
39
|
'ColorizingStreamHandler', 'DecolorizingFileHandler', 'ColorizingFormatter',
|
|
@@ -720,11 +720,114 @@ def _apply_repeatable_defaults(cli_args: _Any, info: FunctionInfo) -> None:
|
|
|
720
720
|
setattr(cli_args, argument.name, argument.default)
|
|
721
721
|
|
|
722
722
|
|
|
723
|
+
def _normalize_common(common: _Any) -> _Dict[str, Argument]:
|
|
724
|
+
"""Normalize the `common` mapping into synthetic flag arguments.
|
|
725
|
+
|
|
726
|
+
Each entry is either a plain default (`{"force": False}`, type inferred
|
|
727
|
+
as `bool`/`str`/`int`/`float`, `None` for untyped) or a spec mapping with
|
|
728
|
+
`default`, `annotation` (`type` accepted as an alias), and `help` keys.
|
|
729
|
+
Every entry becomes an optional `--flag` on leaves that don't declare the
|
|
730
|
+
name themselves.
|
|
731
|
+
|
|
732
|
+
:param common: The raw `common` value.
|
|
733
|
+
:raises TypeError: If `common` is not a mapping.
|
|
734
|
+
:raises ValueError: If a name or spec is malformed, or a spec is
|
|
735
|
+
`Secret`-typed (declare `Secret[...]` in the entry-point signatures
|
|
736
|
+
that need it; `env_prefix` already applies globally).
|
|
737
|
+
:return: `{name: Argument}` shared flag definitions.
|
|
738
|
+
"""
|
|
739
|
+
if common is None:
|
|
740
|
+
return {}
|
|
741
|
+
if not isinstance(common, _Dict):
|
|
742
|
+
raise TypeError(
|
|
743
|
+
'common must be a mapping of parameter name to default or spec '
|
|
744
|
+
f'mapping, not {type(common).__name__}.'
|
|
745
|
+
)
|
|
746
|
+
normalized: _Dict[str, Argument] = {}
|
|
747
|
+
for name, spec in common.items():
|
|
748
|
+
if not isinstance(name, str) or not name.isidentifier():
|
|
749
|
+
raise ValueError(
|
|
750
|
+
'Common parameter names must be valid identifiers, '
|
|
751
|
+
f'got: {name!r}'
|
|
752
|
+
)
|
|
753
|
+
if isinstance(spec, _Dict):
|
|
754
|
+
unknown = set(spec) - {'default', 'annotation', 'type', 'help'}
|
|
755
|
+
if unknown:
|
|
756
|
+
raise ValueError(
|
|
757
|
+
f'Unknown keys for common parameter {name!r}: '
|
|
758
|
+
f'{sorted(unknown)!r} (allowed: default, annotation, help).'
|
|
759
|
+
)
|
|
760
|
+
annotation = spec.get('annotation', spec.get('type'))
|
|
761
|
+
if 'annotation' in spec and 'type' in spec and spec['annotation'] is not spec['type']:
|
|
762
|
+
raise ValueError(
|
|
763
|
+
f"Common parameter {name!r} sets both 'annotation' and "
|
|
764
|
+
"'type' to different values."
|
|
765
|
+
)
|
|
766
|
+
default = spec.get('default')
|
|
767
|
+
has_default = 'default' in spec
|
|
768
|
+
help_text = spec.get('help')
|
|
769
|
+
else:
|
|
770
|
+
if isinstance(spec, list):
|
|
771
|
+
raise ValueError(
|
|
772
|
+
f'Common parameter {name!r} needs a dict spec with an '
|
|
773
|
+
"'annotation' for list values, e.g. "
|
|
774
|
+
f'{{"{name}": {{"annotation": list[str], "default": []}}}}.'
|
|
775
|
+
)
|
|
776
|
+
if spec is not None and not isinstance(spec, (bool, str, int, float)):
|
|
777
|
+
raise ValueError(
|
|
778
|
+
f'Unsupported default for common parameter {name!r}: '
|
|
779
|
+
f'{type(spec).__name__} (use a bool/str/int/float/None '
|
|
780
|
+
'default or a dict spec).'
|
|
781
|
+
)
|
|
782
|
+
annotation = None if spec is None else type(spec)
|
|
783
|
+
default, has_default, help_text = spec, True, None
|
|
784
|
+
is_secret, _, _, _, _ = _secret_params(annotation)
|
|
785
|
+
if not is_secret:
|
|
786
|
+
_, element = _list_element_type(annotation)
|
|
787
|
+
is_secret = _secret_params(element)[0] if element is not None else False
|
|
788
|
+
if is_secret:
|
|
789
|
+
raise ValueError(
|
|
790
|
+
f'Common parameter {name!r} must not be Secret-typed: declare '
|
|
791
|
+
'Secret[...] in the entry-point signatures that need it '
|
|
792
|
+
'(they take precedence, and env_prefix applies globally).'
|
|
793
|
+
)
|
|
794
|
+
normalized[name] = Argument(
|
|
795
|
+
name=name,
|
|
796
|
+
kind=_inspect._ParameterKind.POSITIONAL_OR_KEYWORD,
|
|
797
|
+
annotation=annotation,
|
|
798
|
+
default=default,
|
|
799
|
+
help=help_text,
|
|
800
|
+
has_default=has_default,
|
|
801
|
+
)
|
|
802
|
+
return normalized
|
|
803
|
+
|
|
804
|
+
|
|
805
|
+
def _validate_common_names(
|
|
806
|
+
common: _Dict[str, Argument], leaf_infos: _Dict[str, FunctionInfo]
|
|
807
|
+
) -> None:
|
|
808
|
+
"""Reject common parameters declared by no leaf command at all.
|
|
809
|
+
|
|
810
|
+
Without this, a typo'd (or not-yet-adopted) name would silently do
|
|
811
|
+
nothing everywhere.
|
|
812
|
+
|
|
813
|
+
:param common: Normalized common flag definitions.
|
|
814
|
+
:param leaf_infos: `{dotted_path: FunctionInfo}` for every leaf.
|
|
815
|
+
:raises ValueError: If a name matches no command.
|
|
816
|
+
"""
|
|
817
|
+
for name in common:
|
|
818
|
+
if not any(name in info.arguments for info in leaf_infos.values()):
|
|
819
|
+
raise ValueError(
|
|
820
|
+
f'Common parameter {name!r} matches no command: no entry '
|
|
821
|
+
'point declares it.'
|
|
822
|
+
)
|
|
823
|
+
|
|
824
|
+
|
|
723
825
|
def _add_arguments(
|
|
724
826
|
parser: _Union[_argparse.ColorizingArgumentParser, _argparse._ArgumentGroup],
|
|
725
827
|
info: FunctionInfo,
|
|
726
828
|
reserved_flags: _Optional[_Set[str]] = None,
|
|
727
|
-
exclusive_groups: _Optional[_List[_Tuple[_List[str], bool]]] = None
|
|
829
|
+
exclusive_groups: _Optional[_List[_Tuple[_List[str], bool]]] = None,
|
|
830
|
+
common: _Optional[_Dict[str, Argument]] = None
|
|
728
831
|
) -> None:
|
|
729
832
|
"""Add the arguments to the parser.
|
|
730
833
|
|
|
@@ -739,6 +842,14 @@ def _add_arguments(
|
|
|
739
842
|
:param reserved_flags: The reserved flags.
|
|
740
843
|
:param exclusive_groups: Normalized `(names, required)` groups applying
|
|
741
844
|
to this parser.
|
|
845
|
+
:param common: Normalized shared flag definitions. Entries whose name the
|
|
846
|
+
function declares itself are skipped (the local declaration wins);
|
|
847
|
+
the rest are added first, in mapping order, so shared flags claim
|
|
848
|
+
stable short forms across commands and per-command parameters fall
|
|
849
|
+
back deterministically. Common flags are always optional and never
|
|
850
|
+
join mutually exclusive groups; values for undeclared names are
|
|
851
|
+
accepted but not delivered (functions only receive declared keyword
|
|
852
|
+
arguments).
|
|
742
853
|
"""
|
|
743
854
|
if reserved_flags is None:
|
|
744
855
|
reserved_flags = RESERVED_FLAGS.copy()
|
|
@@ -764,11 +875,29 @@ def _add_arguments(
|
|
|
764
875
|
unsupported_arg=argument.name
|
|
765
876
|
)
|
|
766
877
|
|
|
767
|
-
# Add the arguments
|
|
878
|
+
# Add the arguments. Shared flags go first, in mapping order, so they
|
|
879
|
+
# claim stable short forms across commands; explicitly declared
|
|
880
|
+
# parameters with the same names keep precedence and follow in mapping
|
|
881
|
+
# order too. Positional parameters always stay in signature order
|
|
882
|
+
# (`argparse` positionals are order-sensitive). The rest follows in
|
|
883
|
+
# signature order. Without `common`, this is exactly signature order.
|
|
884
|
+
common = common or {}
|
|
885
|
+
declared = [
|
|
886
|
+
info.arguments[name] for name in common if name in info.arguments
|
|
887
|
+
and info.arguments[name].kind
|
|
888
|
+
not in (_inspect._ParameterKind.POSITIONAL_ONLY,
|
|
889
|
+
_inspect._ParameterKind.VAR_POSITIONAL)
|
|
890
|
+
]
|
|
891
|
+
missing = [spec for name, spec in common.items() if name not in info.arguments]
|
|
892
|
+
common_names = {spec.name for spec in missing}
|
|
893
|
+
rest = [
|
|
894
|
+
argument for argument in info.arguments.values()
|
|
895
|
+
if argument not in declared
|
|
896
|
+
]
|
|
768
897
|
containers: _Dict[int, _Any] = {}
|
|
769
898
|
for index, (_, required) in enumerate(exclusive_groups or []):
|
|
770
899
|
containers[index] = parser.add_mutually_exclusive_group(required=required)
|
|
771
|
-
for argument in
|
|
900
|
+
for argument in declared + missing + rest:
|
|
772
901
|
config: _Dict[str, _Any] = {
|
|
773
902
|
'action': 'store',
|
|
774
903
|
'dest': argument.name,
|
|
@@ -837,7 +966,9 @@ def _add_arguments(
|
|
|
837
966
|
if is_secret:
|
|
838
967
|
config['type'] = _secret_converter(secret_type)
|
|
839
968
|
if (argument.kind == _inspect._ParameterKind.POSITIONAL_OR_KEYWORD
|
|
840
|
-
and keyword_only_exists and not is_secret
|
|
969
|
+
and keyword_only_exists and not is_secret
|
|
970
|
+
and argument.name not in common_names):
|
|
971
|
+
# Shared flags are always optional (non-breaking adoption).
|
|
841
972
|
config['required'] = True
|
|
842
973
|
if (argument.default is not None and config.get('action') != 'append'
|
|
843
974
|
and not is_secret):
|
|
@@ -854,7 +985,8 @@ def _argumentify_one(
|
|
|
854
985
|
func: Callable,
|
|
855
986
|
mutually_exclusive: _Any = None,
|
|
856
987
|
env_prefix: _Optional[str] = None,
|
|
857
|
-
secret_prompt: bool = True
|
|
988
|
+
secret_prompt: bool = True,
|
|
989
|
+
common: _Optional[_Dict[str, _Any]] = None
|
|
858
990
|
) -> None:
|
|
859
991
|
"""This function argumentifies one function as the entry point of the script.
|
|
860
992
|
|
|
@@ -864,6 +996,8 @@ def _argumentify_one(
|
|
|
864
996
|
`{"names": [...], "required": bool}` mapping.
|
|
865
997
|
:param env_prefix: App-wide environment prefix for `Secret` parameters.
|
|
866
998
|
:param secret_prompt: Whether missing secrets may prompt securely.
|
|
999
|
+
:param common: Shared `{name: default-or-spec}` flags added unless the
|
|
1000
|
+
function declares the name itself.
|
|
867
1001
|
"""
|
|
868
1002
|
info = FunctionInfo(func)
|
|
869
1003
|
|
|
@@ -873,7 +1007,9 @@ def _argumentify_one(
|
|
|
873
1007
|
groups = _resolve_command_groups(
|
|
874
1008
|
mutually_exclusive, None, set(info.arguments)
|
|
875
1009
|
)
|
|
876
|
-
|
|
1010
|
+
shared = _normalize_common(common)
|
|
1011
|
+
_validate_common_names(shared, {'': info})
|
|
1012
|
+
_add_arguments(parser, info, exclusive_groups=groups, common=shared)
|
|
877
1013
|
cli_args = parser.parse_args()
|
|
878
1014
|
_apply_repeatable_defaults(cli_args, info)
|
|
879
1015
|
_resolve_secrets(cli_args, info, parser, env_prefix, secret_prompt)
|
|
@@ -1057,13 +1193,15 @@ def _add_command_level(
|
|
|
1057
1193
|
path: _Tuple[str, ...],
|
|
1058
1194
|
leaf_infos: _Dict[str, FunctionInfo],
|
|
1059
1195
|
mutually_exclusive: _Any = None,
|
|
1060
|
-
leaf_paths: _Optional[_List[str]] = None
|
|
1196
|
+
leaf_paths: _Optional[_List[str]] = None,
|
|
1197
|
+
common: _Optional[_Dict[str, Argument]] = None
|
|
1061
1198
|
) -> None:
|
|
1062
1199
|
"""Add one command-tree level to a subparsers action (recursively).
|
|
1063
1200
|
|
|
1064
1201
|
Leaf commands get annotation-driven arguments (plus any applicable
|
|
1065
|
-
mutually exclusive groups); group nodes get a
|
|
1066
|
-
level of their own, so `prog group --help` lists
|
|
1202
|
+
mutually exclusive groups and shared `common` flags); group nodes get a
|
|
1203
|
+
nested `add_subparsers` level of their own, so `prog group --help` lists
|
|
1204
|
+
the group's commands.
|
|
1067
1205
|
"""
|
|
1068
1206
|
for name, child in node.items():
|
|
1069
1207
|
if callable(child):
|
|
@@ -1074,14 +1212,14 @@ def _add_command_level(
|
|
|
1074
1212
|
mutually_exclusive, dotted, name, set(info.arguments),
|
|
1075
1213
|
leaf_paths or [dotted]
|
|
1076
1214
|
)
|
|
1077
|
-
_add_arguments(subparser, info, exclusive_groups=groups)
|
|
1215
|
+
_add_arguments(subparser, info, exclusive_groups=groups, common=common)
|
|
1078
1216
|
subparser.set_defaults(func=info.function)
|
|
1079
1217
|
else:
|
|
1080
1218
|
group_parser = subparsers.add_parser(name)
|
|
1081
1219
|
nested = group_parser.add_subparsers(required=True)
|
|
1082
1220
|
_add_command_level(
|
|
1083
1221
|
nested, child, (*path, name), leaf_infos, mutually_exclusive,
|
|
1084
|
-
leaf_paths
|
|
1222
|
+
leaf_paths, common
|
|
1085
1223
|
)
|
|
1086
1224
|
|
|
1087
1225
|
|
|
@@ -1089,7 +1227,8 @@ def _argumentify(
|
|
|
1089
1227
|
functions: _Dict[str, _Any],
|
|
1090
1228
|
mutually_exclusive: _Any = None,
|
|
1091
1229
|
env_prefix: _Optional[str] = None,
|
|
1092
|
-
secret_prompt: bool = True
|
|
1230
|
+
secret_prompt: bool = True,
|
|
1231
|
+
common: _Optional[_Dict[str, _Any]] = None
|
|
1093
1232
|
) -> None:
|
|
1094
1233
|
"""This function argumentifies one or more functions as the entry point of the
|
|
1095
1234
|
script.
|
|
@@ -1107,6 +1246,8 @@ def _argumentify(
|
|
|
1107
1246
|
e.g. `"PDF_HELPER"` derives `PDF_HELPER_PASSWORD` from `--password`.
|
|
1108
1247
|
:param secret_prompt: Whether missing secrets may fall back to a secure
|
|
1109
1248
|
prompt (per-parameter `Secret[..., prompt]` overrides this).
|
|
1249
|
+
:param common: Shared `{name: default-or-spec}` flags added to every leaf
|
|
1250
|
+
that doesn't declare the name itself.
|
|
1110
1251
|
:raises RuntimeError:
|
|
1111
1252
|
"""
|
|
1112
1253
|
tree = _normalize_command_node(functions)
|
|
@@ -1134,8 +1275,11 @@ def _argumentify(
|
|
|
1134
1275
|
leaf_paths = [dotted for dotted, _, _ in leaves]
|
|
1135
1276
|
_validate_mutex_keys(mutually_exclusive, leaf_paths)
|
|
1136
1277
|
_validate_broadcast_groups(mutually_exclusive, leaf_infos)
|
|
1278
|
+
shared = _normalize_common(common)
|
|
1279
|
+
_validate_common_names(shared, leaf_infos)
|
|
1137
1280
|
_add_command_level(
|
|
1138
|
-
subparsers, tree, (), leaf_infos, mutually_exclusive, leaf_paths
|
|
1281
|
+
subparsers, tree, (), leaf_infos, mutually_exclusive, leaf_paths,
|
|
1282
|
+
shared
|
|
1139
1283
|
)
|
|
1140
1284
|
cli_args = parser.parse_args()
|
|
1141
1285
|
args = []
|
|
@@ -1169,7 +1313,8 @@ def argumentify(
|
|
|
1169
1313
|
entry_point: _Union[Callable, _List[Callable], _Dict[str, _Any]],
|
|
1170
1314
|
mutually_exclusive: _Optional[_Any] = None,
|
|
1171
1315
|
env_prefix: _Optional[str] = None,
|
|
1172
|
-
secret_prompt: bool = True
|
|
1316
|
+
secret_prompt: bool = True,
|
|
1317
|
+
common: _Optional[_Dict[str, _Any]] = None
|
|
1173
1318
|
) -> _Union[Callable, _List[Callable], _Dict[str, _Any]]:
|
|
1174
1319
|
"""This function argumentifies one or more functions as the entry point of the
|
|
1175
1320
|
script.
|
|
@@ -1260,19 +1405,33 @@ def argumentify(
|
|
|
1260
1405
|
def encrypt(in_path, out_path, /, password: Secret[str] | None = None): ...
|
|
1261
1406
|
argumentify(encrypt, env_prefix="PDF_HELPER")
|
|
1262
1407
|
|
|
1408
|
+
Flags shared by every command can be declared once with `common`
|
|
1409
|
+
(`{name: default}` or `{name: {"default": ..., "annotation": ...,
|
|
1410
|
+
"help": ...}}`). Each flag is added to every leaf that doesn't declare
|
|
1411
|
+
the name itself (an explicit local declaration always wins), claims a
|
|
1412
|
+
stable short form across commands, and is always optional. Values reach
|
|
1413
|
+
only functions declaring a matching parameter; the rest accept the flag
|
|
1414
|
+
but ignore it, so adopting `common` never breaks a command:
|
|
1415
|
+
|
|
1416
|
+
argumentify(tree, common={"force": False, "verbose": False})
|
|
1417
|
+
|
|
1263
1418
|
:param entry_point: The function(s) to argumentify.
|
|
1264
1419
|
:param mutually_exclusive: Mutually exclusive parameter groups.
|
|
1265
1420
|
:param env_prefix: App-wide environment prefix for `Secret` parameters.
|
|
1266
1421
|
:param secret_prompt: Whether missing secrets may prompt securely.
|
|
1422
|
+
:param common: Shared `{name: default-or-spec}` flags.
|
|
1267
1423
|
:raises TypeError: A function must be a function or a list of functions or a
|
|
1268
|
-
dictionary of functions.
|
|
1269
|
-
:raises ValueError: A group is malformed or references unknown parameters
|
|
1424
|
+
dictionary of functions, or `common` is not a mapping.
|
|
1425
|
+
:raises ValueError: A group is malformed or references unknown parameters,
|
|
1426
|
+
or a `common` name/spec is malformed or matches no command.
|
|
1270
1427
|
"""
|
|
1271
1428
|
|
|
1272
1429
|
functions = {}
|
|
1273
1430
|
# Check the types
|
|
1274
1431
|
if callable(entry_point):
|
|
1275
|
-
_argumentify_one(
|
|
1432
|
+
_argumentify_one(
|
|
1433
|
+
entry_point, mutually_exclusive, env_prefix, secret_prompt, common
|
|
1434
|
+
)
|
|
1276
1435
|
return entry_point
|
|
1277
1436
|
if isinstance(entry_point, _List):
|
|
1278
1437
|
for func in entry_point:
|
|
@@ -1290,5 +1449,5 @@ def argumentify(
|
|
|
1290
1449
|
"dictionary of functions."
|
|
1291
1450
|
)
|
|
1292
1451
|
|
|
1293
|
-
_argumentify(functions, mutually_exclusive, env_prefix, secret_prompt)
|
|
1452
|
+
_argumentify(functions, mutually_exclusive, env_prefix, secret_prompt, common)
|
|
1294
1453
|
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
|
|
File without changes
|