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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: log21
3
- Version: 3.6.0
3
+ Version: 3.7.0
4
4
  Summary: A simple logging package
5
5
  Keywords: python,log,colorize,color,logging,Python3,CodeWriter21
6
6
  Author: CodeWriter21(Mehrad Pooryoussof)
@@ -23,7 +23,7 @@ dependencies = [
23
23
  "webcolors",
24
24
  "docstring-parser"
25
25
  ]
26
- version = "3.6.0"
26
+ version = "3.7.0"
27
27
 
28
28
  [build-system]
29
29
  requires = ["uv_build>=0.8.15,<0.9.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.6.0'
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 info.arguments.values():
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
- _add_arguments(parser, info, exclusive_groups=groups)
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 nested `add_subparsers`
1066
- level of their own, so `prog group --help` lists the group's commands.
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(entry_point, mutually_exclusive, env_prefix, secret_prompt)
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