Skip to content

Instantly share code, notes, and snippets.

@four43
Last active January 10, 2019 21:37
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save four43/52e72702fee922870ee2f4c1ffb11264 to your computer and use it in GitHub Desktop.
Save four43/52e72702fee922870ee2f4c1ffb11264 to your computer and use it in GitHub Desktop.
In Python, set default values for ArugmentParser, optionally read in a config file or override with CLI arguments.
[defaults]
option2:Overridden #2 in config
option3:Overridden #3 in config
flag: 0
number_int: 6
number_float: 0.2
#!/usr/bin/env python3
# Thanks to: http://blog.vwelch.com/2011/04/combining-configparser-and-argparse.html
# Features:
# * Cascading overrides (defaults, then config file, then command line arguments)
# * bool/int/float parse correctly as long as we have a default set.
import argparse
import configparser
from typing import TypeVar, Callable, Generic, Dict
T = TypeVar('T')
class _ConfigType(Generic[T]):
def __init__(self, name: str, parse_func: Callable[[str, str], T]):
self.name = name
self.parse_func = parse_func
def parse_config_and_args(arg_parser: argparse.ArgumentParser):
# Add option for our config file path
arg_parser.add_argument("-c", "--config", help="Specify config file", metavar="FILE")
args, remaining_argv = arg_parser.parse_known_args() # Parse for config file path
if args.config:
config = configparser.ConfigParser()
config.read([args.config])
first_section = config.sections()[0] # Just assume first section for this example.
config_values = dict(config.items(first_section))
defaults = vars(arg_parser.parse_args([]))
merged_config_file = {
**defaults,
**config_values
}
# Try and parse out config with defaults
type_mapping: Dict[type, _ConfigType] = {
bool: _ConfigType[bool]("boolean", config.getboolean),
int: _ConfigType[int]("integer", config.getint),
float: _ConfigType[float]("float", config.getfloat)
}
for key, value in defaults.items():
type_config = None
try:
type_config = type_mapping[type(value)]
merged_config_file[key] = type_config.parse_func(first_section, key)
except ValueError:
if isinstance(type_config, _ConfigType):
raise ValueError("\"%s\" in config wasn't set to a %s. Either omit or set to a valid "
"%s." % (key, type_config.name, type_config.name))
else:
raise ValueError(
"\"%s\" wasn't the correct type. Please double check the type in the config." % key)
except KeyError:
pass
except configparser.NoOptionError:
pass
arg_parser.set_defaults(**merged_config_file)
return arg_parser.parse_args() # Parse our full list of CLI opts now.
if __name__ == '__main__':
parser = argparse.ArgumentParser(
description="Testing app to quickly check layering of config file + cli arguments."
)
parser.add_argument("--option1", default="opt1 default", help="some option help text")
parser.add_argument("--option2", default="opt2 default", help="some other option help text")
parser.add_argument("--option3", default="opt3 default", help="some other option")
parser.add_argument("--no-default", help="No default")
parser.add_argument("--flag", default=False, action="store_true") # A boolean, True if flag present.
parser.add_argument("--number-int", dest="number_int", type=int, default=42, help="Ensure we have an int")
parser.add_argument("--number-float", dest="number_float", type=float, default=0.5,
help="Ensure we have a float")
args = parse_config_and_args(parser)
print(args)
# Tests:
# $ ./arg_test.py
# > Namespace(config=None, flag=False, no_default=None, number_float=0.5, number_int=42, option1='opt1 default', option2='opt2 default', option3='opt3 default')
# $ ./arg_test.py -c ./app.ini --option3 "Hello World"
# > Namespace(config='./app.ini', flag=False, no_default=None, number_float=0.2, number_int=6, option1='opt1 default', option2='Overridden #2 in config', option3='Hello World')
# $ ./arg_test.py -c ./app.ini --option3 "Hello World" --flag --number-int 7
# > Namespace(config='./app.ini', flag=True, no_default=None, number_float=0.2, number_int=7, option1='opt1 default', option2='Overridden #2 in config', option3='Hello World')
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment