summaryrefslogtreecommitdiff
path: root/config.py
diff options
context:
space:
mode:
Diffstat (limited to 'config.py')
-rw-r--r--config.py164
1 files changed, 164 insertions, 0 deletions
diff --git a/config.py b/config.py
new file mode 100644
index 0000000..bc0dcdf
--- /dev/null
+++ b/config.py
@@ -0,0 +1,164 @@
+#!/usr/bin/env python3
+
+"""Global configuration driven by commandline arguments (even across
+different modules). Usage:
+
+ module.py:
+ ----------
+ import config
+
+ parser = config.add_commandline_args(
+ "Module",
+ "Args related to module doing the thing.",
+ )
+ parser.add_argument(
+ "--module_do_the_thing",
+ type=bool,
+ default=True,
+ help="Should the module do the thing?"
+ )
+
+ main.py:
+ --------
+ import config
+
+ def main() -> None:
+ parser = config.add_commandline_args(
+ "Main",
+ "A program that does the thing.",
+ )
+ parser.add_argument(
+ "--dry_run",
+ type=bool,
+ default=False,
+ help="Should we really do the thing?"
+ )
+ config.parse() # Very important, this must be invoked!
+
+ If you set this up and remember to invoke config.parse(), all commandline
+ arguments will play nicely together:
+
+ % main.py -h
+ usage: main.py [-h]
+ [--module_do_the_thing MODULE_DO_THE_THING]
+ [--dry_run DRY_RUN]
+
+ Module:
+ Args related to module doing the thing.
+
+ --module_do_the_thing MODULE_DO_THE_THING
+ Should the module do the thing?
+
+ Main:
+ A program that does the thing
+
+ --dry_run
+ Should we really do the thing?
+
+ Arguments themselves should be accessed via config.config['arg_name']. e.g.
+
+ if not config.config['dry_run']:
+ module.do_the_thing()
+"""
+
+import argparse
+import pprint
+import re
+import sys
+from typing import Dict, Any
+
+# Note: at this point in time, logging hasn't been configured and
+# anything we log will come out the root logger.
+
+
+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)
+
+
+# A global parser that we will collect arguments into.
+args = argparse.ArgumentParser(
+ description=f"This program uses config.py ({__file__}) for global, cross-module configuration.",
+ formatter_class=argparse.ArgumentDefaultsHelpFormatter,
+ fromfile_prefix_chars="@"
+)
+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
+config: Dict[str, Any] = {}
+
+
+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)
+
+
+group = add_commandline_args(
+ f'Global Config ({__file__})',
+ 'Args that control the global config itself; how meta!',
+)
+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.',
+)
+group.add_argument(
+ '--config_dump',
+ default=False,
+ action='store_true',
+ help='Display the global configuration on STDERR at program startup.',
+)
+group.add_argument(
+ '--config_savefile',
+ type=str,
+ metavar='FILENAME',
+ default=None,
+ help='Populate config file compatible --config_loadfile to save config for later use.',
+)
+
+
+def parse() -> Dict[str, Any]:
+ """Main program should call this early in main()"""
+ global config_parse_called
+ config_parse_called = True
+ config.update(vars(args.parse_args()))
+
+ if config['config_savefile']:
+ with open(config['config_savefile'], 'w') as wf:
+ wf.write("\n".join(sys.argv[1:]))
+
+ if config['config_dump']:
+ dump_config()
+
+ return config
+
+
+def has_been_parsed() -> bool:
+ global config_parse_called
+ return config_parse_called
+
+
+def dump_config():
+ """Print the current config to stdout."""
+ print("Global Configuration:", file=sys.stderr)
+ pprint.pprint(config, stream=sys.stderr)