import logging
import os
import pprint
-import re
import sys
from typing import Any, Dict, List, Optional
# Note: at this point in time, logging hasn't been configured and
# anything we log will come out the root logger.
+logger = logging.getLogger(__name__)
+# Defer logging messages until later when logging has been initialized.
+saved_messages: List[str] = []
-class LoadFromFile(argparse.Action):
- """Helper to load a config file into argparse."""
- def __call__ (self, parser, namespace, values, option_string = None):
- with values as f:
- buf = f.read()
- argv = []
- for line in buf.split(','):
- line = line.strip()
- line = line.strip('{')
- line = line.strip('}')
- m = re.match(r"^'([a-zA-Z_\-]+)'\s*:\s*(.*)$", line)
- if m:
- key = m.group(1)
- value = m.group(2)
- value = value.strip("'")
- if value not in ('None', 'True', 'False'):
- argv.append(f'--{key}')
- argv.append(value)
- parser.parse_args(argv, namespace)
-
+# Make a copy of the original program arguments.
+program_name = os.path.basename(sys.argv[0])
+original_argv = [arg for arg in sys.argv]
# A global parser that we will collect arguments into.
-prog = os.path.basename(sys.argv[0])
args = argparse.ArgumentParser(
description=None,
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
fromfile_prefix_chars="@",
- epilog=f'-----------------------------------------------------------------------------\n{prog} uses config.py ({__file__}) for global, cross-module configuration setup and parsing.\n-----------------------------------------------------------------------------'
+ epilog=f'-----------------------------------------------------------------------------\n{program_name} uses config.py ({__file__}) for global, cross-module configuration setup and parsing.\n-----------------------------------------------------------------------------'
)
+
+# Keep track of if we've been called and prevent being called more
+# than once.
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
+# 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: Dict[str, Any] = {}
-# Defer logging messages until later when logging has been initialized.
-saved_messages: List[str] = []
-
def add_commandline_args(title: str, description: str = ""):
"""Create a new context for arguments and return a handle."""
)
group.add_argument(
'--config_loadfile',
- type=open,
- action=LoadFromFile,
metavar='FILENAME',
default=None,
help='Config file from which to read args in lieu or in addition to commandline.',
"""Main program should call this early in main()"""
global config_parse_called
if config_parse_called:
+ logger.warning('config.parse has already been called; ignoring spurious invocation')
return config
- config_parse_called = True
+
global saved_messages
# If we're about to do the usage message dump, put the main module's
- # argument group first in the list (if possible), please.
+ # 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.
reordered_action_groups = []
- prog = sys.argv[0]
-
+ global prog
for arg in sys.argv:
if arg == '--help' or arg == '-h':
for group in args._action_groups:
if entry_module is not None and entry_module in group.title:
- reordered_action_groups.insert(0, group) # prepend
- elif prog in group.title:
- reordered_action_groups.insert(0, group) # prepend
+ reordered_action_groups.append(group)
+ elif program_name in group.title:
+ reordered_action_groups.append(group)
else:
- reordered_action_groups.append(group) # append
+ reordered_action_groups.insert(0, group)
args._action_groups = reordered_action_groups
- # Examine the environment variables to settings that match
- # known flags. For a flag called --example_flag the corresponding
- # environment variable would be called EXAMPLE_FLAG.
+ # Examine the environment variables that match known flags. For a
+ # flag called --example_flag the corresponding environment
+ # variable would be called EXAMPLE_FLAG.
usage_message = args.format_usage()
optional = False
var = ''
else:
next
- # Parse (possibly augmented) commandline args with argparse normally.
+ # Look for loadfile and read/parse it if present.
+ loadfile = None
+ saw_other_args = False
+ grab_next_arg = False
+ for arg in sys.argv[1:]:
+ if 'config_loadfile' in arg:
+ pieces = arg.split('=')
+ if len(pieces) > 1:
+ loadfile = pieces[1]
+ else:
+ grab_next_arg = True
+ elif grab_next_arg:
+ loadfile = arg
+ else:
+ saw_other_args = True
+
+ if loadfile is not None:
+ if saw_other_args:
+ print(
+ 'WARNING: ignoring some commandline arguments; only args in --config_loadfile be parsed.',
+ file=sys.stderr
+ )
+ if not os.path.exists(loadfile):
+ print(f'--config_loadfile argument must be a file, {loadfile} not found.',
+ file=sys.stderr)
+ sys.exit(-1)
+ with open(loadfile, 'r') as rf:
+ newargs = rf.readlines()
+ newargs = [arg.strip('\n') for arg in newargs if 'config_savefile' not in arg]
+ sys.argv = sys.argv[:1] + newargs
+
+ # Parse (possibly augmented, possibly completely overwritten)
+ # commandline args with argparse normally and populate config.
known, unknown = args.parse_known_args()
config.update(vars(known))
# someone else will.
sys.argv = sys.argv[:1] + unknown
- if config['config_savefile']:
- with open(config['config_savefile'], 'w') as wf:
- wf.write("\n".join(sys.argv[1:]))
+ # 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:])
+ )
+ # Also dump the config on stderr if requested.
if config['config_dump']:
dump_config()
+
+ config_parse_called = True
return config