#!/usr/bin/env python3
+# © Copyright 2021-2022, Scott Gasch
+
"""Global configuration driven by commandline arguments, environment variables
and saved configuration files. This works across several modules.
# taking any unnecessary dependencies back on them.
# Defer logging messages until later when logging has been initialized.
-saved_messages: List[str] = []
+SAVED_MESSAGES: List[str] = []
# Make a copy of the original program arguments.
-program_name = os.path.basename(sys.argv[0])
-original_argv = [arg for arg in sys.argv]
+PROGRAM_NAME: str = os.path.basename(sys.argv[0])
+ORIG_ARGV: List[str] = sys.argv.copy()
+
+
+class OptionalRawFormatter(argparse.HelpFormatter):
+ """This formatter has the same bahavior as the normal argparse text
+ formatter except when the help text of an argument begins with
+ "RAW|". In that case, the line breaks are preserved and the text
+ is not wrapped.
+
+ """
+
+ def _split_lines(self, text, width):
+ if text.startswith('RAW|'):
+ return text[4:].splitlines()
+ return argparse.HelpFormatter._split_lines(self, text, width)
# A global parser that we will collect arguments into.
-args = argparse.ArgumentParser(
+ARGS = argparse.ArgumentParser(
description=None,
- formatter_class=argparse.ArgumentDefaultsHelpFormatter,
+ formatter_class=OptionalRawFormatter,
fromfile_prefix_chars="@",
- epilog=f'{program_name} uses config.py ({__file__}) for global, cross-module configuration setup and parsing.',
+ epilog=f'{PROGRAM_NAME} uses config.py ({__file__}) for global, cross-module configuration setup and parsing.',
)
# Keep track of if we've been called and prevent being called more
# than once.
-config_parse_called = False
+CONFIG_PARSE_CALLED = False
# A global configuration dictionary that will contain parsed arguments.
# It is also this variable that modules use to access parsed arguments.
# This is the data that is most interesting to our callers; it will hold
# the configuration result.
-config = {}
+config: Dict[str, Any] = {}
+
# It would be really nice if this shit worked from interactive python
def add_commandline_args(title: str, description: str = ""):
"""Create a new context for arguments and return a handle."""
- return args.add_argument_group(title, description)
+ return ARGS.add_argument_group(title, description)
group = add_commandline_args(
)
+def overwrite_argparse_epilog(msg: str) -> None:
+ ARGS.epilog = msg
+
+
def is_flag_already_in_argv(var: str):
"""Is a particular flag passed on the commandline?"""
for _ in sys.argv:
return False
-def reorder_arg_action_groups(entry_module: Optional[str]):
+def reorder_arg_action_groups_before_help(entry_module: Optional[str]):
reordered_action_groups = []
- for group in args._action_groups:
- if entry_module is not None and entry_module in group.title:
- reordered_action_groups.append(group)
- elif program_name in group.title:
- reordered_action_groups.append(group)
+ for grp in ARGS._action_groups:
+ if entry_module is not None and entry_module in grp.title: # type: ignore
+ reordered_action_groups.append(grp)
+ elif PROGRAM_NAME in group.title: # type: ignore
+ reordered_action_groups.append(grp)
else:
- reordered_action_groups.insert(0, group)
+ reordered_action_groups.insert(0, grp)
return reordered_action_groups
def augment_sys_argv_from_environment_variables():
- global saved_messages
- usage_message = args.format_usage()
+ usage_message = ARGS.format_usage()
optional = False
var = ''
for x in usage_message.split():
if env in os.environ:
if not is_flag_already_in_argv(var):
value = os.environ[env]
- saved_messages.append(
- f'Initialized from environment: {var} = {value}'
- )
+ SAVED_MESSAGES.append(f'Initialized from environment: {var} = {value}')
from string_utils import to_bool
if len(chunks) == 1 and to_bool(value):
def augment_sys_argv_from_loadfile():
- global saved_messages
loadfile = None
saw_other_args = False
grab_next_arg = False
else:
msg = f'Reading commandline arguments from {loadfile}.'
print(msg, file=sys.stderr)
- saved_messages.append(msg)
+ SAVED_MESSAGES.append(msg)
with open(loadfile, 'r') as rf:
newargs = rf.readlines()
bootstrap.initialize wrapper takes care of this automatically.
"""
- global config_parse_called
- if config_parse_called:
+ global CONFIG_PARSE_CALLED
+ if CONFIG_PARSE_CALLED:
return config
- global saved_messages
# If we're about to do the usage message dump, put the main
# module's argument group last in the list (if possible) so that
# when the user passes -h or --help, it will be visible on the
# screen w/o scrolling.
for arg in sys.argv:
- if arg == '--help' or arg == '-h':
- args._action_groups = reorder_arg_action_groups(entry_module)
+ if arg in ('--help', '-h'):
+ if entry_module is not None:
+ entry_module = os.path.basename(entry_module)
+ ARGS._action_groups = reorder_arg_action_groups_before_help(entry_module)
# Examine the environment for variables that match known flags.
# For a flag called --example_flag the corresponding environment
# Parse (possibly augmented, possibly completely overwritten)
# commandline args with argparse normally and populate config.
- known, unknown = args.parse_known_args()
+ known, unknown = ARGS.parse_known_args()
config.update(vars(known))
# Reconstruct the argv with unrecognized flags for the benefit of
raise Exception(
f'Encountered unrecognized config argument(s) {unknown} with --config_rejects_unrecognized_arguments enabled; halting.'
)
- saved_messages.append(
- f'Config encountered unrecognized commandline arguments: {unknown}'
- )
+ SAVED_MESSAGES.append(f'Config encountered unrecognized commandline arguments: {unknown}')
sys.argv = sys.argv[:1] + unknown
# Check for savefile and populate it if requested.
savefile = config['config_savefile']
if savefile and len(savefile) > 0:
with open(savefile, 'w') as wf:
- wf.write("\n".join(original_argv[1:]))
+ wf.write("\n".join(ORIG_ARGV[1:]))
# Also dump the config on stderr if requested.
if config['config_dump']:
dump_config()
- config_parse_called = True
+ CONFIG_PARSE_CALLED = True
return config
def has_been_parsed() -> bool:
"""Has the global config been parsed yet?"""
- global config_parse_called
- return config_parse_called
+ return CONFIG_PARSE_CALLED
def dump_config():
def late_logging():
"""Log messages saved earlier now that logging has been initialized."""
logger = logging.getLogger(__name__)
- global saved_messages
- for _ in saved_messages:
+ logger.debug('Original commandline was: %s', ORIG_ARGV)
+ for _ in SAVED_MESSAGES:
logger.debug(_)