diff --git a/.travis.yml b/.travis.yml index 80ad5d0e..dd91fdfc 100644 --- a/.travis.yml +++ b/.travis.yml @@ -7,10 +7,14 @@ python: - "3.3" - "3.2" - "2.7" + - "2.6" - "pypy" # command to install dependencies: # install: pip install -r requirements.txt # command to run tests: -script: python setup.py test \ No newline at end of file +script: python setup.py test + +# migrate to container-based travis.ci: http://docs.travis-ci.com/user/migrating-from-legacy +sudo: false \ No newline at end of file diff --git a/LICENSE b/LICENSE index 242132de..c98670d9 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ The MIT License (MIT) -Copyright (c) +Copyright (c) 2015 bw2 Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.rst b/README.rst index 90570872..74533b94 100644 --- a/README.rst +++ b/README.rst @@ -1,3 +1,9 @@ +.. note:: + + Though this is stored under the python2.6-0.9.3 branch, this is now + actually a copy of ConfigArgParse 0.10.0. See + https://github.com/letsencrypt/letsencrypt/issues/2243 for more explanation. + Overview ~~~~~~~~ @@ -9,6 +15,8 @@ Python's command line parsing modules such as argparse have very limited support for config files and environment variables, so this module extends argparse to add these features. +|Travis CI Status for bw2/ConfigArgParse| + Features ~~~~~~~~ @@ -239,9 +247,6 @@ test.test_argparse module) but on configargparse in place of argparse. This ensures that configargparse will work as a drop in replacement for argparse in all usecases. -Are unittests still passing: |Travis CI Status for -zorro3/ConfigArgParse| |Analytics| - Previously existing modules (PyPI search keywords: config argparse): - argparse (built-in module python v2.7+ ) @@ -348,6 +353,5 @@ Relevant sites: - http://tricksntweaks.blogspot.com/2013_05_01_archive.html - http://www.youtube.com/watch?v=vvCwqHgZJc8#t=35 -.. |Travis CI Status for zorro3/ConfigArgParse| image:: https://api.travis-ci.org/zorro3/ConfigArgParse.svg?branch=master -.. |Analytics| image:: https://ga-beacon.appspot.com/UA-52264120-1/ConfigArgParse/ConfigArgParse - :target: https://github.com/igrigorik/ga-beacon + +.. |Travis CI Status for bw2/ConfigArgParse| image:: https://travis-ci.org/bw2/ConfigArgParse.svg?branch=master diff --git a/configargparse.py b/configargparse.py index 922dae4f..38311840 100644 --- a/configargparse.py +++ b/configargparse.py @@ -66,6 +66,13 @@ class ArgumentDefaultsRawHelpFormatter( pass +# used while parsing args to keep track of where they came from +_COMMAND_LINE_SOURCE_KEY = "command_line" +_ENV_VAR_SOURCE_KEY = "environment_variables" +_CONFIG_FILE_SOURCE_KEY = "config_file" +_DEFAULTS_SOURCE_KEY = "defaults" + + class ArgumentParser(argparse.ArgumentParser): """Drop-in replacement for argparse.ArgumentParser that adds support for environment variables and .ini or .yaml-style config files. @@ -88,12 +95,21 @@ def __init__(self, add_config_file_help=True, add_env_var_help=True, + auto_env_var_prefix=None, + + config_file_parser=None, default_config_files=[], - allow_unknown_config_file_keys=False, + ignore_unknown_config_file_keys=False, + allow_unknown_config_file_keys=False, # deprecated args_for_setting_config_path=[], config_arg_is_required=False, config_arg_help_message="config file path", + + args_for_writing_out_config_file=[], + write_out_config_file_arg_help_message="takes the current command line " + "args and writes them out to a config file at the given path, then " + "exits" ): """Supports all the same args as the argparse.ArgumentParser @@ -104,6 +120,14 @@ def __init__(self, syntax to the help message. add_env_var_help: Whether to add something to the help message for args that can be set through environment variables. + auto_env_var_prefix: If set to a string instead of None, all config- + file-settable options will become also settable via environment + variables whose names are this prefix followed by the config + file key, all in upper case. (eg. setting this to "foo_" will + allow an arg like "--my-arg" to also be set via the FOO_MY_ARG + environment variable) + config_file_parser: An instance of a parser to be used for parsing + config files. Default: ConfigFileParser() default_config_files: When specified, this list of config files will be parsed in order, with the values from each config file taking precedence over pervious ones. This allows an application @@ -112,37 +136,69 @@ def __init__(self, ["/app_config.ini", "~/.my_app_config.ini", "./app_config.txt"] - allow_unknown_config_file_keys: Whether unknown config file keys - should be ignored or whether it should be an error. + ignore_unknown_config_file_keys: If true, settings that are found + in a config file but don't correspond to any defined + configargparse args will be ignored. If false, they will be + processed and appended to the commandline like other args, and + can be retrieved using parse_known_args() instead of parse_args() + allow_unknown_config_file_keys: + @deprecated + Use ignore_unknown_config_file_keys instead. + + If true, settings that are found in a config file but don't + correspond to any defined configargparse args, will still be + processed and appended to the command line (eg. for + parsing with parse_known_args()). If false, they will be ignored. + args_for_setting_config_path: A list of one or more command line - args that would allow a user to provide a config file path + args to be used for specifying the config file path (eg. ["-c", "--config-file"]). Default: [] - config_arg_is_required: when args_for_setting_config_path is set, + config_arg_is_required: When args_for_setting_config_path is set, set this to True to always require users to provide a config path. - config_arg_help_message: when args_for_setting_config_path is set, - this will be the help message for the config_file_args. + config_arg_help_message: the help message to use for the + args listed in args_for_setting_config_path. + args_for_writing_out_config_file: A list of one or more command line + args to use for specifying a config file output path. If + provided, these args cause configargparse to write out a config + file with settings based on the other provided commandline args, + environment variants and defaults, and then to exit. + (eg. ["-w", "--write-out-config-file"]). Default: [] + write_out_config_file_arg_help_message: The help message to use for + the args in args_for_writing_out_config_file. """ self._add_config_file_help = add_config_file_help self._add_env_var_help = add_env_var_help + self._auto_env_var_prefix = auto_env_var_prefix # extract kwargs that can be passed to the super constructor - kwargs_for_super = dict([(k, v) for k, v in locals().items() if k in [ + kwargs_for_super = dict((k, v) for k, v in locals().items() if k in [ "prog", "usage", "description", "epilog", "version", "parents", "formatter_class", "prefix_chars", "fromfile_prefix_chars", - "argument_default", "conflict_handler", "add_help" ]]) + "argument_default", "conflict_handler", "add_help" ]) if sys.version_info >= (3, 3) and "version" in kwargs_for_super: del kwargs_for_super["version"] # version arg deprecated in v3.3 argparse.ArgumentParser.__init__(self, **kwargs_for_super) # parse the additionial args + if config_file_parser is None: + self._config_file_parser = ConfigFileParser() + else: + self._config_file_parser = config_file_parser self._default_config_files = default_config_files - self._allow_unknown_config_file_keys = allow_unknown_config_file_keys + self._ignore_unknown_config_file_keys = ignore_unknown_config_file_keys \ + or allow_unknown_config_file_keys if args_for_setting_config_path: self.add_argument(*args_for_setting_config_path, dest="config_file", required=config_arg_is_required, help=config_arg_help_message, - is_config_file=True) + is_config_file_arg=True) + if args_for_writing_out_config_file: + self.add_argument(*args_for_writing_out_config_file, + dest="write_out_config_file_to_this_path", + metavar="CONFIG_OUTPUT_PATH", + help=write_out_config_file_arg_help_message, + is_write_out_config_file_arg=True) def parse_args(self, args = None, namespace = None, config_file_contents = None, env_vars = os.environ): @@ -173,7 +229,6 @@ def parse_known_args(self, args = None, namespace = None, config_file_contents: String. Used for testing. env_vars: Dictionary. Used for testing. """ - if args is None: args = sys.argv[1:] elif type(args) == str: @@ -181,35 +236,51 @@ def parse_known_args(self, args = None, namespace = None, else: args = list(args) + for a in self._actions: + a.is_positional_arg = not a.option_strings + # maps string describing the source (eg. env var) to a settings dict # to keep track of where values came from (used by print_values()) self._source_to_settings = OrderedDict() - self._command_line_args_string = ' '.join(args) if args: - self._source_to_settings["Command Line Args: "] = { - '': self._command_line_args_string} - - # add env var settings to the command line that aren't there already + a_v_pair = (None, list(args)) # copy args list to isolate changes + self._source_to_settings[_COMMAND_LINE_SOURCE_KEY] = {'': a_v_pair} + + # handle auto_env_var_prefix __init__ arg by setting a.env_var as needed + if self._auto_env_var_prefix is not None: + for a in self._actions: + config_file_keys = self.get_possible_config_keys(a) + if config_file_keys and not (a.env_var or a.is_positional_arg + or a.is_config_file_arg or a.is_write_out_config_file_arg): + stripped_config_file_key = config_file_keys[0].strip( + self.prefix_chars) + a.env_var = (self._auto_env_var_prefix + + stripped_config_file_key).replace('-', '_').upper() + + # add env var settings to the commandline that aren't there already env_var_args = [] actions_with_env_var_values = [a for a in self._actions - if a.option_strings and a.env_var - and a.env_var in env_vars - and not any(opt in args for opt in a.option_strings)] - for a in actions_with_env_var_values: - key = a.env_var + if not a.is_positional_arg and a.env_var and a.env_var in env_vars + and not already_on_command_line(args, a.option_strings)] + for action in actions_with_env_var_values: + key = action.env_var value = env_vars[key] env_var_args += self.convert_setting_to_command_line_arg( - a, key, value) + action, key, value) - args = env_var_args + args + args += env_var_args if env_var_args: - self._source_to_settings["Environment Variables:\n"] = OrderedDict( - [(a.env_var, env_vars[a.env_var]) + self._source_to_settings[_ENV_VAR_SOURCE_KEY] = OrderedDict( + [(a.env_var, (a, env_vars[a.env_var])) for a in actions_with_env_var_values]) - # read settings from config file(s) + # prepare for reading config file(s) + known_config_keys = dict((config_key, action) for action in self._actions + for config_key in self.get_possible_config_keys(action)) + + # open the config file(s) if config_file_contents: stream = StringIO(config_file_contents) stream.name = "method arg" @@ -217,137 +288,207 @@ def parse_known_args(self, args = None, namespace = None, else: config_streams = self._open_config_files(args) - # add config file settings to the command line that aren't there already - - # for each action, add its possible config keys to a dict - possible_config_keys = dict([(config_key, action) for action in self._actions - for config_key in self.get_possible_config_keys(action)]) - # parse each config file for stream in config_streams[::-1]: try: - config_settings = self.parse_config_file(stream) + config_settings = self._config_file_parser.parse(stream) + except ConfigFileParserException as e: + self.error(e) finally: if hasattr(stream, "close"): stream.close() - # make sure config file doesn't use any unknown keys - if not self._allow_unknown_config_file_keys: - invalid_keys = list( - set(config_settings.keys()) - set(possible_config_keys.keys())) - if invalid_keys: - self.error(("%s contains unknown config key(s): %s") % ( - stream.name, ", ".join(invalid_keys))) - - # add config settings to the command line if they aren't there already + + # add each config setting to the commandline unless it's there already config_args = [] for key, value in config_settings.items(): - if key in possible_config_keys: - action = possible_config_keys[key] - already_on_command_line = any( - arg in args for arg in action.option_strings) - if already_on_command_line: - del config_settings[key] - else: - config_args += self.convert_setting_to_command_line_arg( - action, key, value) - - args = config_args + args - - if config_args: - self._source_to_settings[ - "Config File (%s):\n" %stream.name]=config_settings + if key in known_config_keys: + action = known_config_keys[key] + discard_this_key = already_on_command_line( + args, action.option_strings) + else: + action = None + discard_this_key = self._ignore_unknown_config_file_keys or \ + already_on_command_line( + args, + self.get_command_line_key_for_unknown_config_file_setting(key)) + + if not discard_this_key: + config_args += self.convert_setting_to_command_line_arg( + action, key, value) + source_key = "%s|%s" %(_CONFIG_FILE_SOURCE_KEY, stream.name) + if source_key not in self._source_to_settings: + self._source_to_settings[source_key] = OrderedDict() + self._source_to_settings[source_key][key] = (action, value) + + args += config_args + # save default settings for use by print_values() default_settings = OrderedDict() - for a in self._actions: - already_on_command_line = any(arg in args for arg in a.option_strings) - cares_about_default = a.option_strings or a.nargs in [ - argparse.OPTIONAL, argparse.ZERO_OR_MORE] - if (already_on_command_line or not cares_about_default or - a.default == None or a.default == argparse.SUPPRESS or - type(a) in ACTION_TYPES_THAT_DONT_NEED_A_VALUE): + for action in self._actions: + cares_about_default_value = (not action.is_positional_arg or + action.nargs in [OPTIONAL, ZERO_OR_MORE]) + if (already_on_command_line(args, action.option_strings) or + not cares_about_default_value or + action.default is None or + action.default == SUPPRESS or + type(action) in ACTION_TYPES_THAT_DONT_NEED_A_VALUE): continue else: - key = a.option_strings[-1] if a.option_strings else a.dest - default_settings[key] = str(a.default) + if action.option_strings: + key = action.option_strings[-1] + else: + key = action.dest + default_settings[key] = (action, str(action.default)) if default_settings: - self._source_to_settings["Defaults:\n"] = default_settings + self._source_to_settings[_DEFAULTS_SOURCE_KEY] = default_settings - # parse all args (including command-line, config file, and env var) - return argparse.ArgumentParser.parse_known_args( + # parse all args (including commandline, config file, and env var) + namespace, unknown_args = argparse.ArgumentParser.parse_known_args( self, args=args, namespace=namespace) - def parse_config_file(self, stream): - """Parses a config file and return a dictionary of settings""" + # handle any args that have is_write_out_config_file_arg set to true + user_write_out_config_file_arg_actions = [a for a in self._actions + if getattr(a, "is_write_out_config_file_arg", False)] + if user_write_out_config_file_arg_actions: + output_file_paths = [] + for action in user_write_out_config_file_arg_actions: + # check if the user specified this arg on the commandline + output_file_path = getattr(namespace, action.dest, None) + if output_file_path: + # validate the output file path + try: + with open(output_file_path, "w") as output_file: + output_file_paths.append(output_file_path) + except IOError as e: + raise ValueError("Couldn't open %s for writing: %s" % ( + output_file_path, e)) + + if output_file_paths: + # generate the config file contents + config_items = self.get_items_for_config_file_output( + self._source_to_settings, namespace) + contents = self._config_file_parser.serialize(config_items) + for output_file_path in output_file_paths: + with open(output_file_path, "w") as output_file: + output_file.write(contents) + if len(output_file_paths) == 1: + output_file_paths = output_file_paths[0] + self.exit(0, "Wrote config file to " + str(output_file_paths)) + return namespace, unknown_args + + def get_command_line_key_for_unknown_config_file_setting(self, key): + """Compute a commandline arg key to be used for a config file setting + that doesn't correspond to any defined configargparse arg (and so + doesn't have a user-specified commandline arg key). - settings = OrderedDict() - for i, line in enumerate(stream): - line = line.strip() - if not line or line[0] in ["#", ";", "["] or line.startswith("---"): - continue - white_space = "\\s*" - key = "(?P[^:=;#\s]+?)" - value1 = white_space+"[:=]"+white_space+"(?P[^;#]+?)" - value2 = white_space+"[\s]"+white_space+"(?P[^;#\s]+?)" - comment = white_space+"(?P\\s[;#].*)?" + Args: + key: The config file key that was being set. + """ + key_without_prefix_chars = key.strip(self.prefix_chars) + command_line_key = self.prefix_chars[0]*2 + key_without_prefix_chars - key_only_match = re.match("^" + key + comment +"$", line) - if key_only_match: - key = key_only_match.group("key") - settings[key] = "true" - continue + return command_line_key - key_value_match = re.match("^"+key+value1+comment+"$", line) or \ - re.match("^"+key+value2+comment+"$", line) - if key_value_match: - key = key_value_match.group("key") - value = key_value_match.group("value") - settings[key] = value - continue + def get_items_for_config_file_output(self, source_to_settings, + parsed_namespace): + """Does the inverse of config parsing by taking parsed values and + converting them back to a string representing config file contents. - self.error("Unexpected line %s in %s: %s" % (i, stream.name, line)) - return settings + Args: + source_to_settings: the dictionary created within parse_known_args() + parsed_namespace: namespace object created within parse_known_args() + Returns: + an OrderedDict with the items to be written to the config file + """ + config_file_items = OrderedDict() + for source, settings in source_to_settings.items(): + if source == _COMMAND_LINE_SOURCE_KEY: + _, existing_command_line_args = settings[''] + for action in self._actions: + config_file_keys = self.get_possible_config_keys(action) + if config_file_keys and not action.is_positional_arg and \ + already_on_command_line(existing_command_line_args, + action.option_strings): + value = getattr(parsed_namespace, action.dest, None) + if value is not None: + if type(value) is bool: + value = str(value).lower() + elif type(value) is list: + value = "["+", ".join(map(str, value))+"]" + config_file_items[config_file_keys[0]] = value + + elif source == _ENV_VAR_SOURCE_KEY: + for key, (action, value) in settings.items(): + config_file_keys = self.get_possible_config_keys(action) + if config_file_keys: + value = getattr(parsed_namespace, action.dest, None) + if value is not None: + config_file_items[config_file_keys[0]] = value + elif source.startswith(_CONFIG_FILE_SOURCE_KEY): + for key, (action, value) in settings.items(): + config_file_items[key] = value + elif source == _DEFAULTS_SOURCE_KEY: + for key, (action, value) in settings.items(): + config_file_keys = self.get_possible_config_keys(action) + if config_file_keys: + value = getattr(parsed_namespace, action.dest, None) + if value is not None: + config_file_items[config_file_keys[0]] = value + return config_file_items def convert_setting_to_command_line_arg(self, action, key, value): """Converts a config file or env var key/value to a list of - command line args to append to the command line. + commandline args to append to the commandline. Args: - action: The action corresponding to this setting - key: The config file key or env var name (used for error messages) + action: The action corresponding to this setting, or None if this + is a config file setting that doesn't correspond to any + defined configargparse arg. + key: The config file key or env var name value: The raw value string from the config file or env var """ - assert type(value) == str + if type(value) != str: + raise ValueError("type(value) != str: %s" % str(value)) args = [] + if action is None: + command_line_key = \ + self.get_command_line_key_for_unknown_config_file_setting(key) + else: + command_line_key = action.option_strings[-1] + if value.lower() == "true": - if type(action) not in ACTION_TYPES_THAT_DONT_NEED_A_VALUE: - self.error("%s set to 'True' rather than a value" % key) - args.append( action.option_strings[-1] ) + if action is not None: + if type(action) not in ACTION_TYPES_THAT_DONT_NEED_A_VALUE: + self.error("%s set to 'True' rather than a value" % key) + args.append( command_line_key ) elif value.startswith("[") and value.endswith("]"): - if type(action) != argparse._AppendAction: - self.error(("%s can't be set to a list '%s' unless its action " - "type is changed to 'append'") % (key, value)) + if action is not None: + if type(action) != argparse._AppendAction: + self.error(("%s can't be set to a list '%s' unless its " + "action type is changed to 'append'") % (key, value)) for list_elem in value[1:-1].split(","): - args.append( action.option_strings[-1] ) + args.append( command_line_key ) args.append( list_elem.strip() ) else: - if type(action) in ACTION_TYPES_THAT_DONT_NEED_A_VALUE: - self.error("%s is a flag but is being set to '%s'" % (key, - value)) - args.append( action.option_strings[-1] ) + if action is not None: + if type(action) in ACTION_TYPES_THAT_DONT_NEED_A_VALUE: + self.error("%s is a flag but is being set to '%s'" % ( + key, value)) + args.append( command_line_key ) args.append( value ) return args def get_possible_config_keys(self, action): """This method decides which actions can be set in a config file and - what their keys will be. It return a list of 0 or more config keys that + what their keys will be. It returns a list of 0 or more config keys that can be used to set the given action's value in a config file. """ keys = [] for arg in action.option_strings: - if arg.startswith(2*self.prefix_chars[0]): + if any([arg.startswith(2*c) for c in self.prefix_chars]): keys += [arg[2:], arg] # eg. for '--bla' return ['bla', '--bla'] return keys @@ -356,7 +497,7 @@ def get_possible_config_keys(self, action): def _open_config_files(self, command_line_args): """Tries to parse config file path(s) from within command_line_args. Returns a list of opened config files, including files specified on the - command line as well as any default_config_files specified in the + commandline as well as any default_config_files specified in the constructor that are present on disk. Args: @@ -369,10 +510,10 @@ def _open_config_files(self, command_line_args): if not command_line_args: return config_files - # list actions which had is_config_file=True set. Its possible there is - # more than one such arg (perhaps to have multiple aliases for the file) + # list actions with is_config_file_arg=True. Its possible there is more + # than one such arg. user_config_file_arg_actions = [ - a for a in self._actions if getattr(a, "is_config_file", False)] + a for a in self._actions if getattr(a, "is_config_file_arg", False)] if not user_config_file_arg_actions: return config_files @@ -412,17 +553,28 @@ def error_method(self, message): def format_values(self): """Returns a string with all args and settings and where they came from - (eg. command line, config file, enviroment variable or default) + (eg. commandline, config file, enviroment variable or default) """ + source_key_to_display_value_map = { + _COMMAND_LINE_SOURCE_KEY: "Command Line Args: ", + _ENV_VAR_SOURCE_KEY: "Environment Variables:\n", + _CONFIG_FILE_SOURCE_KEY: "Config File (%s):\n", + _DEFAULTS_SOURCE_KEY: "Defaults:\n" + } r = StringIO() for source, settings in self._source_to_settings.items(): + source = source.split("|") + source = source_key_to_display_value_map[source[0]] % tuple(source[1:]) r.write(source) - for key, value in settings.items(): + for key, (action, value) in settings.items(): if key: r.write(" %-19s%s\n" % (key+":", value)) else: - r.write(" %s\n" % value) + if type(value) is str: + r.write(" %s\n" % value) + elif type(value) is list: + r.write(" %s\n" % ' '.join(value)) return r.getvalue() @@ -437,36 +589,28 @@ def format_help(self): if self._add_config_file_help: default_config_files = self._default_config_files cc = 2*self.prefix_chars[0] # eg. -- - config_keys = [(arg, a) for a in self._actions for arg in - a.option_strings if arg.startswith(cc) and a.dest != "help"] + config_settable_args = [(arg, a) for a in self._actions for arg in + a.option_strings if self.get_possible_config_keys(a) and not + (a.dest == "help" or a.is_config_file_arg or + a.is_write_out_config_file_arg)] config_path_actions = [a for a in - self._actions if getattr(a, "is_config_file", False)] + self._actions if getattr(a, "is_config_file_arg", False)] - if (default_config_files or config_path_actions) and config_keys: + if config_settable_args and (default_config_files or + config_path_actions): self._add_config_file_help = False # prevent duplication added_config_file_help = True msg += ("Args that start with '%s' (eg. %s) can also be set in " - "a config file") % (cc, config_keys[0][0]) + "a config file") % (cc, config_settable_args[0][0]) config_arg_string = " or ".join(a.option_strings[0] for a in config_path_actions if a.option_strings) if config_arg_string: config_arg_string = "specified via " + config_arg_string if default_config_files or config_arg_string: - msg += " (%s)" % " or ".join(default_config_files + + msg += " (%s)." % " or ".join(default_config_files + [config_arg_string]) - msg += " by using .ini or .yaml-style syntax " - examples = [] - key_value_args = [arg for arg, a in config_keys - if a.type not in ACTION_TYPES_THAT_DONT_NEED_A_VALUE] - if key_value_args: - examples += ["%s=value" % key_value_args[0].strip(cc)] - flag_args = [arg for arg, a in config_keys - if a.type in ACTION_TYPES_THAT_DONT_NEED_A_VALUE] - if flag_args: - examples += ["%s=TRUE" % flag_args[0].strip(cc)] - if examples: - msg += "(eg. %s)." % " or ".join(examples) + msg += " " + self._config_file_parser.get_syntax_description() if self._add_env_var_help: env_var_actions = [(a.env_var, a) for a in self._actions @@ -487,7 +631,7 @@ def format_help(self): if added_env_var_help: value_sources = ["environment variables"] + value_sources msg += (" If an arg is specified in more than one place, then " - "command-line values override %s.") % ( + "commandline values override %s.") % ( " which override ".join(value_sources)) if msg: self.description = (self.description or "") + " " + msg @@ -495,44 +639,138 @@ def format_help(self): return argparse.ArgumentParser.format_help(self) +class ConfigFileParser(object): + + def parse(self, stream): + """Parses a config file and return a dictionary of settings""" + + settings = OrderedDict() + for i, line in enumerate(stream): + line = line.strip() + if not line or line[0] in ["#", ";", "["] or line.startswith("---"): + continue + white_space = "\\s*" + key = "(?P[^:=;#\s]+?)" + value1 = white_space+"[:=]"+white_space+"(?P[^;#]+?)" + value2 = white_space+"[\s]"+white_space+"(?P[^;#\s]+?)" + comment = white_space+"(?P\\s[;#].*)?" + + key_only_match = re.match("^" + key + comment + "$", line) + if key_only_match: + key = key_only_match.group("key") + settings[key] = "true" + continue + + key_value_match = re.match("^"+key+value1+comment+"$", line) or \ + re.match("^"+key+value2+comment+"$", line) + if key_value_match: + key = key_value_match.group("key") + value = key_value_match.group("value") + settings[key] = value + continue + + raise ConfigFileParserException("Unexpected line %s in %s: %s" % \ + (i, stream.name, line)) + return settings + + def serialize(self, items): + """Does the inverse of config parsing by taking parsed values and + converting them back to a string representing config file contents. + + Args: + items: an OrderedDict with items to be written to the config file + Returns: + contents of config file as a string + """ + r = StringIO() + for key, value in items.items(): + r.write("%s = %s\n" % (key, value)) + return r.getvalue() + + def get_syntax_description(self): + msg = ("The recognized syntax for setting (key, value) pairs is based " + "on the INI and YAML formats (e.g. key=value or foo=TRUE). " + "For full documentation of the differences from the standards " + "please refer to the ConfigArgParse documentation.") + return msg + +class ConfigFileParserException(Exception): + """Raised when config file parsing failed. + """ + pass + + def add_argument(self, *args, **kwargs): """ This method supports the same args as ArgumentParser.add_argument(..) as well as the additional args below. - All Additional Args: - env_var: The name of the environment variable to check. - is_config_file: If True, this arg is treated as a config file path + env_var: If set, the value of this environment variable will override + any config file or default values for this arg (but can itself + be overriden on the commandline). Also, if auto_env_var_prefix is + set in the constructor, this env var name will be used instead of + the automatic name. + is_config_file_arg: If True, this arg is treated as a config file path This provides an alternative way to specify config files in place of the ArgumentParser(fromfile_prefix_chars=..) mechanism. Default: False + is_write_out_config_file_arg: If True, this arg will be treated as a + config file path, and, when it is specified, will cause + configargparse to write all current commandline args to this file + as config options and then exit. + Default: False """ env_var = kwargs.pop("env_var", None) - is_config_file = kwargs.pop("is_config_file", None) - action = self.original_add_argument_method(*args, **kwargs) + is_config_file_arg = kwargs.pop( + "is_config_file_arg", None) or kwargs.pop( + "is_config_file", None) # for backward compat. - is_positional_arg = not action.option_strings - if is_positional_arg and env_var: - raise ValueError("env_var can't be set for a positional arg.") - if is_config_file and type(action) != argparse._StoreAction: - raise ValueError("arg with is_config_file=True must have action='store'") + is_write_out_config_file_arg = kwargs.pop( + "is_write_out_config_file_arg", None) + + action = self.original_add_argument_method(*args, **kwargs) + action.is_positional_arg = not action.option_strings action.env_var = env_var - action.is_config_file = is_config_file + action.is_config_file_arg = is_config_file_arg + action.is_write_out_config_file_arg = is_write_out_config_file_arg + + if action.is_positional_arg and env_var: + raise ValueError("env_var can't be set for a positional arg.") + if action.is_config_file_arg and type(action) != argparse._StoreAction: + raise ValueError("arg with is_config_file_arg=True must have " + "action='store'") + if action.is_write_out_config_file_arg: + error_prefix = "arg with is_write_out_config_file_arg=True " + if type(action) != argparse._StoreAction: + raise ValueError(error_prefix + "must have action='store'") + if is_config_file_arg: + raise ValueError(error_prefix + "can't also have " + "is_config_file_arg=True") return action + +def already_on_command_line(existing_args, potential_command_line_args): + """Utility method for checking if any of the existing_args is + already present in existing_args + """ + return any(potential_arg in existing_args + for potential_arg in potential_command_line_args) + + + # wrap ArgumentParser's add_argument(..) method with the one above argparse._ActionsContainer.original_add_argument_method = argparse._ActionsContainer.add_argument argparse._ActionsContainer.add_argument = add_argument -# add all public classes in argparse module's namespace to this namespace so -# that the 2 modules are truly interchangeable +# add all public classes and constants from argparse module's namespace to this +# module's namespace so that the 2 modules are truly interchangeable HelpFormatter = argparse.HelpFormatter RawDescriptionHelpFormatter = argparse.RawDescriptionHelpFormatter RawTextHelpFormatter = argparse.RawTextHelpFormatter @@ -542,7 +780,11 @@ def add_argument(self, *args, **kwargs): Action = argparse.Action FileType = argparse.FileType Namespace = argparse.Namespace - +ONE_OR_MORE = argparse.ONE_OR_MORE +OPTIONAL = argparse.OPTIONAL +REMAINDER = argparse.REMAINDER +SUPPRESS = argparse.SUPPRESS +ZERO_OR_MORE = argparse.ZERO_OR_MORE # create shorter aliases for the key methods and class names getArgParser = getArgumentParser diff --git a/setup.py b/setup.py index 31edcbd4..55d34e66 100644 --- a/setup.py +++ b/setup.py @@ -79,7 +79,7 @@ def launch_http_server(directory): setup( name='ConfigArgParse', - version="0.9.3", + version="0.10.0", description='A drop-in replacement for argparse that allows options to also be set via config files and/or environment variables.', long_description=long_description, author='Zorro', @@ -95,13 +95,14 @@ def launch_http_server(directory): 'License :: OSI Approved :: MIT License', 'Natural Language :: English', "Programming Language :: Python :: 2", + 'Programming Language :: Python :: 2.6', 'Programming Language :: Python :: 2.7', 'Programming Language :: Python :: 3', 'Programming Language :: Python :: 3.2', 'Programming Language :: Python :: 3.3', 'Programming Language :: Python :: 3.4', - 'Programming Language :: Python :: Implementation :: CPython', - 'Programming Language :: Python :: Implementation :: PyPy', + 'Programming Language :: Python :: Implementation :: CPython', + 'Programming Language :: Python :: Implementation :: PyPy', ], test_suite='tests', install_requires=install_requires, diff --git a/tests/test_configargparse.py b/tests/test_configargparse.py index 03faedb4..c060b2b5 100644 --- a/tests/test_configargparse.py +++ b/tests/test_configargparse.py @@ -12,11 +12,6 @@ else: import unittest -if sys.version_info >= (3, 0): - from io import StringIO -else: - from StringIO import StringIO - # enable logging to simplify debugging logger = logging.getLogger() @@ -31,7 +26,13 @@ def replace_error_method(arg_parser): def error_method(self, message): raise argparse.ArgumentError(None, message) + def exit_method(self, status, message): + self._exit_method_called = True + + arg_parser._exit_method_called = False arg_parser.error = types.MethodType(error_method, arg_parser) + arg_parser.exit = types.MethodType(exit_method, arg_parser) + return arg_parser @@ -42,15 +43,19 @@ def initParser(self, *args, **kwargs): self.parser = replace_error_method(p) self.add_arg = self.parser.add_argument self.parse = self.parser.parse_args + self.parse_known = self.parser.parse_known_args self.format_values = self.parser.format_values self.format_help = self.parser.format_help - self.assertAddArgRaises = functools.partial(self.assertRaisesRegexp, - Exception, callable_obj = self.add_arg) - self.assertParseArgsRaises = functools.partial(self.assertRaisesRegexp, - argparse.ArgumentError, callable_obj = self.parse) - return self.parser + if not hasattr(self, "assertRegex"): + self.assertRegex = self.assertRegexpMatches + if not hasattr(self, "assertRaisesRegex"): + self.assertRaisesRegex = self.assertRaisesRegexp + + self.assertParseArgsRaises = functools.partial(self.assertRaisesRegex, + argparse.ArgumentError, callable_obj = self.parse) + return self.parser class TestBasicUseCases(TestCase): @@ -81,11 +86,11 @@ def testBasicCase1(self): ns = self.parse(args="file1.txt --arg-x -y 3 --arg-z 10", config_file_contents="") self.assertListEqual(ns.filenames, ["file1.txt"]) - self.assertEquals(ns.arg_x, True) - self.assertEquals(ns.y1, 3) - self.assertEquals(ns.arg_z, [10]) + self.assertEqual(ns.arg_x, True) + self.assertEqual(ns.y1, 3) + self.assertEqual(ns.arg_z, [10]) - self.assertRegexpMatches(self.format_values(), + self.assertRegex(self.format_values(), 'Command Line Args: file1.txt --arg-x -y 3 --arg-z 10') # check values after setting args in config file @@ -97,11 +102,11 @@ def testBasicCase1(self): arg-z = 40 """) self.assertListEqual(ns.filenames, ["file1.txt", "file2.txt"]) - self.assertEquals(ns.arg_x, True) - self.assertEquals(ns.y1, 10) - self.assertEquals(ns.arg_z, [40]) + self.assertEqual(ns.arg_x, True) + self.assertEqual(ns.y1, 10) + self.assertEqual(ns.arg_z, [40]) - self.assertRegexpMatches(self.format_values(), + self.assertRegex(self.format_values(), 'Command Line Args: \s+ file1.txt file2.txt\n' 'Config File \(method arg\):\n' ' arg-x: \s+ True\n' @@ -116,11 +121,11 @@ def testBasicCase1(self): self.format_help() self.format_values() self.assertListEqual(ns.filenames, ["file1.txt", "file2.txt"]) - self.assertEquals(ns.arg_x, True) - self.assertEquals(ns.y1, 3) - self.assertEquals(ns.arg_z, [100]) + self.assertEqual(ns.arg_x, True) + self.assertEqual(ns.y1, 3) + self.assertEqual(ns.arg_z, [100]) - self.assertRegexpMatches(self.format_values(), + self.assertRegex(self.format_values(), "Command Line Args: file1.txt file2.txt --arg-x -y 3 --arg-z 100") def testBasicCase2(self, use_groups=False): @@ -167,13 +172,13 @@ def testBasicCase2(self, use_groups=False): config_file2.flush() ns = self.parse(args="--genome hg19 -g %s bla.vcf " % config_file2.name) - self.assertEquals(ns.genome, "hg19") - self.assertEquals(ns.verbose, False) - self.assertEquals(ns.dbsnp, None) - self.assertEquals(ns.fmt, "BED") + self.assertEqual(ns.genome, "hg19") + self.assertEqual(ns.verbose, False) + self.assertEqual(ns.dbsnp, None) + self.assertEqual(ns.fmt, "BED") self.assertListEqual(ns.vcf, ["bla.vcf"]) - self.assertRegexpMatches(self.format_values(), + self.assertRegex(self.format_values(), 'Command Line Args: --genome hg19 -g [^\s]+ bla.vcf\n' 'Defaults:\n' ' --format: \s+ BED\n') @@ -182,8 +187,8 @@ def testBasicCase2(self, use_groups=False): default_config_file.write("--format MAF") default_config_file.flush() ns = self.parse(args="--genome hg19 -g %s f.vcf " % config_file2.name) - self.assertEquals(ns.fmt, "MAF") - self.assertRegexpMatches(self.format_values(), + self.assertEqual(ns.fmt, "MAF") + self.assertRegex(self.format_values(), 'Command Line Args: --genome hg19 -g [^\s]+ f.vcf\n' 'Config File \([^\s]+\):\n' ' --format: \s+ MAF\n') @@ -191,17 +196,17 @@ def testBasicCase2(self, use_groups=False): config_file2.write("--format VCF") config_file2.flush() ns = self.parse(args="--genome hg19 -g %s f.vcf " % config_file2.name) - self.assertEquals(ns.fmt, "VCF") - self.assertRegexpMatches(self.format_values(), + self.assertEqual(ns.fmt, "VCF") + self.assertRegex(self.format_values(), 'Command Line Args: --genome hg19 -g [^\s]+ f.vcf\n' 'Config File \([^\s]+\):\n' ' --format: \s+ VCF\n') ns = self.parse(env_vars={"OUTPUT_FORMAT":"R", "DBSNP_PATH":"/a/b.vcf"}, args="--genome hg19 -g %s f.vcf " % config_file2.name) - self.assertEquals(ns.fmt, "R") - self.assertEquals(ns.dbsnp, "/a/b.vcf") - self.assertRegexpMatches(self.format_values(), + self.assertEqual(ns.fmt, "R") + self.assertEqual(ns.dbsnp, "/a/b.vcf") + self.assertRegex(self.format_values(), 'Command Line Args: --genome hg19 -g [^\s]+ f.vcf\n' 'Environment Variables:\n' ' DBSNP_PATH: \s+ /a/b.vcf\n' @@ -210,18 +215,18 @@ def testBasicCase2(self, use_groups=False): ns = self.parse(env_vars={"OUTPUT_FORMAT":"R", "DBSNP_PATH":"/a/b.vcf", "ANOTHER_VAR":"something"}, args="--genome hg19 -g %s --format WIG f.vcf" % config_file2.name) - self.assertEquals(ns.fmt, "WIG") - self.assertEquals(ns.dbsnp, "/a/b.vcf") - self.assertRegexpMatches(self.format_values(), + self.assertEqual(ns.fmt, "WIG") + self.assertEqual(ns.dbsnp, "/a/b.vcf") + self.assertRegex(self.format_values(), 'Command Line Args: --genome hg19 -g [^\s]+ --format WIG f.vcf\n' 'Environment Variables:\n' ' DBSNP_PATH: \s+ /a/b.vcf\n') if not use_groups: - self.assertRegexpMatches(self.format_help(), + self.assertRegex(self.format_help(), 'usage: .* \[-h\] --genome GENOME \[-v\] -g MY_CFG_FILE' ' \[-d DBSNP\]\s+\[-f FRMT\]\s+vcf \[vcf ...\]\n\n' + - 7*'(.+\s+)'+ # repeated 7 times because .+ matches atmost 1 line + 8*'(.+\s+)'+ # repeated 8 times because .+ matches atmost 1 line 'positional arguments:\n' ' vcf \s+ Variant file\(s\)\n\n' 'optional arguments:\n' @@ -232,10 +237,10 @@ def testBasicCase2(self, use_groups=False): ' -d DBSNP, --dbsnp DBSNP\s+\[env var: DBSNP_PATH\]\n' ' -f FRMT, --format FRMT\s+\[env var: OUTPUT_FORMAT\]\n') else: - self.assertRegexpMatches(self.format_help(), + self.assertRegex(self.format_help(), 'usage: .* \[-h\] --genome GENOME \[-v\] -g MY_CFG_FILE' ' \[-d DBSNP\]\s+\[-f FRMT\]\s+vcf \[vcf ...\]\n\n'+ - 7*'.+\s+'+ # repeated 7 times because .+ matches atmost 1 line + 8*'.+\s+'+ # repeated 8 times because .+ matches atmost 1 line 'positional arguments:\n' ' vcf \s+ Variant file\(s\)\n\n' 'optional arguments:\n' @@ -253,11 +258,14 @@ def testBasicCase2(self, use_groups=False): self.assertParseArgsRaises("unrecognized arguments: --bla", args="--bla --genome hg19 -g %s f.vcf" % config_file2.name) + default_config_file.close() + config_file2.close() + + def testBasicCase2_WithGroups(self): self.testBasicCase2(use_groups=True) - def testMutuallyExclusiveArgs(self): config_file = tempfile.NamedTemporaryFile(mode="w", delete=True) @@ -278,26 +286,26 @@ def testMutuallyExclusiveArgs(self): env_var='BAM_FORMAT') ns = self.parse(args="--genome hg19 -f1 %s --bam" % config_file.name) - self.assertEquals(ns.genome, "hg19") - self.assertEquals(ns.verbose, False) - self.assertEquals(ns.fmt, "BAM") + self.assertEqual(ns.genome, "hg19") + self.assertEqual(ns.verbose, False) + self.assertEqual(ns.fmt, "BAM") ns = self.parse(env_vars={"BAM_FORMAT" : "true"}, args="--genome hg19 -f1 %s" % config_file.name) - self.assertEquals(ns.genome, "hg19") - self.assertEquals(ns.verbose, False) - self.assertEquals(ns.fmt, "BAM") - self.assertRegexpMatches(self.format_values(), + self.assertEqual(ns.genome, "hg19") + self.assertEqual(ns.verbose, False) + self.assertEqual(ns.fmt, "BAM") + self.assertRegex(self.format_values(), 'Command Line Args: --genome hg19 -f1 [^\s]+\n' 'Environment Variables:\n' ' BAM_FORMAT: \s+ true\n' 'Defaults:\n' ' --format: \s+ BED\n') - self.assertRegexpMatches(self.format_help(), + self.assertRegex(self.format_help(), 'usage: .* \[-h\] --genome GENOME \[-v\]\s+ \(-f1 TYPE1_CFG_FILE \|' ' \s*-f2 TYPE2_CFG_FILE\)\s+\(-f FRMT \| -b\)\n\n' + - 5*'.+\s+'+ # repeated 5 times because .+ matches atmost 1 line + 7*'.+\s+'+ # repeated 7 times because .+ matches atmost 1 line 'optional arguments:\n' ' -h, --help show this help message and exit\n' ' -f1 TYPE1_CFG_FILE, --type1-cfg-file TYPE1_CFG_FILE\n' @@ -307,6 +315,7 @@ def testMutuallyExclusiveArgs(self): 'group1:\n' ' --genome GENOME Path to genome file\n' ' -v\n') + config_file.close() def testSubParsers(self): config_file1 = tempfile.NamedTemporaryFile(mode="w", delete=True) @@ -341,6 +350,18 @@ def testSubParsers(self): ns = parser.parse_args(args = "update -config2 " + config_file2.name) self.assertEqual(ns.p, 10) + config_file1.close() + config_file2.close() + + def testAddArgsErrors(self): + self.assertRaisesRegex(ValueError, "arg with " + "is_write_out_config_file_arg=True can't also have " + "is_config_file_arg=True", self.add_arg, "-x", "--X", + is_config_file=True, is_write_out_config_file_arg=True) + self.assertRaisesRegex(ValueError, "arg with " + "is_write_out_config_file_arg=True must have action='store'", + self.add_arg, "-y", "--Y", action="append", + is_write_out_config_file_arg=True) def testConfigFileSyntax(self): @@ -389,7 +410,7 @@ def testConfigFileSyntax(self): self.assertEqual(ns.z, None) self.assertEqual(ns.b, True) self.assertEqual(ns.a, [33]) - self.assertRegexpMatches(self.format_values(), + self.assertRegex(self.format_values(), 'Command Line Args: \s+ -x 1\n' 'Config File \(method arg\):\n' ' y: \s+ 12.1\n' @@ -397,10 +418,10 @@ def testConfigFileSyntax(self): ' a: \s+ 33\n') # -x is not a long arg so can't be set via config file - self.assertParseArgsRaises("contains unknown config key\(s\): -x", + self.assertParseArgsRaises("argument -x is required" + if sys.version_info < (3,3) else + "the following arguments are required: -x, --y", config_file_contents="-x 3") - self.assertParseArgsRaises("contains unknown config key\(s\): bla", - config_file_contents="bla: 3") self.assertParseArgsRaises("invalid float value: 'abc'", args="-x 5", config_file_contents="y: abc") @@ -411,10 +432,21 @@ def testConfigFileSyntax(self): config_file_contents="z: 1") self.assertParseArgsRaises("Unexpected line 0", config_file_contents="z z 1") - self.assertParseArgsRaises("unknown config key\(s\): --bla", - config_file_contents="--bla") + # test unknown config file args + self.assertParseArgsRaises("bla", + args="-x 1 --y 2.3", + config_file_contents="bla=3") + + ns, args = self.parse_known("-x 10 --y 3.8", + config_file_contents="bla=3", + env_vars={"bla": "2"}) + self.assertListEqual(args, ["--bla", "3"]) + self.initParser(allow_unknown_config_file_keys=False) + ns, args = self.parse_known(args="-x 1", config_file_contents="bla=3", + env_vars={"bla": "2"}) + self.assertListEqual(args, ["-x", "1", "--bla", "3"]) def testConfigOrEnvValueErrors(self): # error should occur when a non-flag arg is set to True @@ -458,6 +490,28 @@ def testConfigOrEnvValueErrors(self): ns = self.parse("", config_file_contents="file=[1,2,3, 5]") self.assertEqual(ns.file, [1,2,3,5]) + def testAutoEnvVarPrefix(self): + self.initParser(auto_env_var_prefix="TEST_") + self.add_arg("-a", "--arg0", is_config_file_arg=True) + self.add_arg("-b", "--arg1", is_write_out_config_file_arg=True) + self.add_arg("-x", "--arg2", env_var="TEST2", type=int) + self.add_arg("-y", "--arg3", action="append", type=int) + self.add_arg("-z", "--arg4", required=True) + self.add_arg("-w", "--arg4-more", required=True) + ns = self.parse("", env_vars = { + "TEST_ARG0": "0", + "TEST_ARG1": "1", + "TEST_ARG2": "2", + "TEST2": "22", + "TEST_ARG3": "[1,2,3]", + "TEST_ARG4": "arg4_value", + "TEST_ARG4_MORE": "magic"}) + self.assertEqual(ns.arg0, None) + self.assertEqual(ns.arg1, None) + self.assertEqual(ns.arg2, 22) + self.assertListEqual(ns.arg3, [1,2,3]) + self.assertEqual(ns.arg4, "arg4_value") + self.assertEqual(ns.arg4_more, "magic") class TestMisc(TestCase): # TODO test different action types with config file, env var @@ -470,7 +524,7 @@ def testGlobalInstances(self, name=None): p = configargparse.getArgumentParser(name, prog="prog", usage="test") self.assertEqual(p.usage, "test") self.assertEqual(p.prog, "prog") - self.assertRaisesRegexp(ValueError, "kwargs besides 'name' can only be " + self.assertRaisesRegex(ValueError, "kwargs besides 'name' can only be " "passed in the first time", configargparse.getArgumentParser, name, prog="prog") @@ -507,15 +561,11 @@ def testConstructor_ConfigFileArgs(self): # args_for_setting_config_path # allow_unknown_config_file_keys # config_arg_is_required + # config_arg_help_message temp_cfg = tempfile.NamedTemporaryFile(mode="w", delete=True) temp_cfg.write("genome=hg19") temp_cfg.flush() - # Test constructor args: - # args_for_setting_config_path - # allow_unknown_config_file_keys - # config_arg_is_required - # config_arg_help_message self.initParser(args_for_setting_config_path=["-c", "--config"], config_arg_is_required = True, config_arg_help_message = "my config file", @@ -535,14 +585,144 @@ def testConstructor_ConfigFileArgs(self): ns = self.parse("-c " + temp_cfg2.name) self.assertEqual(ns.genome, "hg20") - self.assertRegexpMatches(self.format_help(), + self.assertRegex(self.format_help(), 'usage: .* \[-h\] -c CONFIG_FILE --genome GENOME\n\n'+ - 5*'.+\s+'+ # repeated 5 times because .+ matches atmost 1 line + 7*'.+\s+'+ # repeated 7 times because .+ matches atmost 1 line 'optional arguments:\n' ' -h, --help\s+ show this help message and exit\n' ' -c CONFIG_FILE, --config CONFIG_FILE\s+ my config file\n' ' --genome GENOME\s+ Path to genome file\n') + # just run print_values() to make sure it completes and returns None + self.assertEqual(self.parser.print_values(file=sys.stderr), None) + + # test ignore_unknown_config_file_keys=False + self.initParser(ignore_unknown_config_file_keys=False) + self.assertRaisesRegex(argparse.ArgumentError, "unrecognized arguments", + self.parse, config_file_contents="arg1 = 3") + ns, args = self.parse_known(config_file_contents="arg1 = 3") + self.assertEqual(getattr(ns, "arg1", ""), "") + + # test ignore_unknown_config_file_keys=True + self.initParser(ignore_unknown_config_file_keys=True) + ns = self.parse(args="", config_file_contents="arg1 = 3") + self.assertEqual(getattr(ns, "arg1", ""), "") + ns, args = self.parse_known(config_file_contents="arg1 = 3") + self.assertEqual(getattr(ns, "arg1", ""), "") + + + def test_FormatHelp(self): + self.initParser(args_for_setting_config_path=["-c", "--config"], + config_arg_is_required = True, + config_arg_help_message = "my config file", + default_config_files=["~/.myconfig"], + args_for_writing_out_config_file=["-w", "--write-config"], + ) + self.add_arg('--arg1', help='Arg1 help text', required=True) + self.add_arg('--flag', help='Flag help text', action="store_true") + + self.assertRegex(self.format_help(), + 'usage: .* \[-h\] -c CONFIG_FILE\s+' + '\[-w CONFIG_OUTPUT_PATH\]\s* --arg1 ARG1\s* \[--flag\]\s*' + 'Args that start with \'--\' \(eg. --arg1\) can also be set in a ' + 'config file\s*\(~/.myconfig or specified via -c\).\s*' + 'The recognized syntax for setting \(key,\s*value\) pairs is based on ' + 'the INI and YAML formats \(e.g. key=value or\s*foo=TRUE\). For full ' + 'documentation of the differences from the standards please\s*' + 'refer to the ConfigArgParse documentation.\s*' + 'If an arg is specified in more than\s*one place, then ' + 'commandline values override config file values which override\s*' + 'defaults.\s*' + 'optional arguments:\s*' + '-h, --help \s* show this help message and exit\n\s*' + '-c CONFIG_FILE, --config CONFIG_FILE\s+my config file\s*' + '-w CONFIG_OUTPUT_PATH, --write-config CONFIG_OUTPUT_PATH\s*takes\s*' + 'the current command line args and writes them\s*' + 'out to a config file at the given path, then exits\s*' + '--arg1 ARG1\s*Arg1 help text\s*' + '--flag \s*Flag help text' + ) + + def testConstructor_WriteOutConfigFileArgs(self): + # Test constructor args: + # args_for_writing_out_config_file + # write_out_config_file_arg_help_message + cfg_f = tempfile.NamedTemporaryFile(mode="w+", delete=True) + self.initParser(args_for_writing_out_config_file=["-w"], + write_out_config_file_arg_help_message="write config") + + + self.add_arg("-not-config-file-settable") + self.add_arg("--config-file-settable-arg", type=int) + self.add_arg("--config-file-settable-arg2", type=int, default=3) + self.add_arg("--config-file-settable-flag", action="store_true") + self.add_arg("-l", "--config-file-settable-list", action="append") + + # write out a config file + command_line_args = "-w %s " % cfg_f.name + command_line_args += "--config-file-settable-arg 1 " + command_line_args += "--config-file-settable-flag " + command_line_args += "-l a -l b -l c -l d " + + self.assertFalse(self.parser._exit_method_called) + + ns = self.parse(command_line_args) + self.assertTrue(self.parser._exit_method_called) + + cfg_f.seek(0) + expected_config_file_contents = "config-file-settable-arg = 1\n" + expected_config_file_contents += "config-file-settable-flag = true\n" + expected_config_file_contents += "config-file-settable-list = [a, b, c, d]\n" + expected_config_file_contents += "config-file-settable-arg2 = 3\n" + + self.assertEqual(cfg_f.read().strip(), + expected_config_file_contents.strip()) + self.assertRaisesRegex(ValueError, "Couldn't open / for writing:", + self.parse, args = command_line_args + " -w /") + + def testConstructor_WriteOutConfigFileArgs2(self): + # Test constructor args: + # args_for_writing_out_config_file + # write_out_config_file_arg_help_message + cfg_f = tempfile.NamedTemporaryFile(mode="w+", delete=True) + self.initParser(args_for_writing_out_config_file=["-w"], + write_out_config_file_arg_help_message="write config") + + + self.add_arg("-not-config-file-settable") + self.add_arg("-a", "--arg1", type=int, env_var="ARG1") + self.add_arg("-b", "--arg2", type=int, default=3) + self.add_arg("-c", "--arg3") + self.add_arg("-d", "--arg4") + self.add_arg("-e", "--arg5") + self.add_arg("--config-file-settable-flag", action="store_true", + env_var="FLAG_ARG") + self.add_arg("-l", "--config-file-settable-list", action="append") + + # write out a config file + command_line_args = "-w %s " % cfg_f.name + command_line_args += "-l a -l b -l c -l d " + + self.assertFalse(self.parser._exit_method_called) + + ns = self.parse(command_line_args, + env_vars={"ARG1": "10", "FLAG_ARG": "true", + "SOME_OTHER_ENV_VAR": "2"}, + config_file_contents="arg3 = bla3\narg4 = bla4") + self.assertTrue(self.parser._exit_method_called) + + cfg_f.seek(0) + expected_config_file_contents = "config-file-settable-list = [a, b, c, d]\n" + expected_config_file_contents += "arg1 = 10\n" + expected_config_file_contents += "config-file-settable-flag = True\n" + expected_config_file_contents += "arg3 = bla3\n" + expected_config_file_contents += "arg4 = bla4\n" + expected_config_file_contents += "arg2 = 3\n" + + self.assertEqual(cfg_f.read().strip(), + expected_config_file_contents.strip()) + self.assertRaisesRegex(ValueError, "Couldn't open / for writing:", + self.parse, args = command_line_args + " -w /") def testMethodAliases(self): p = self.parser