diff --git a/README.rst b/README.rst index 60b9a9d..cdd81ca 100644 --- a/README.rst +++ b/README.rst @@ -433,22 +433,35 @@ Aliases ~~~~~~~ The configargparse.ArgumentParser API inherits its class and method -names from argparse and also provides the following shorter names for +names from argparse and also provides the following shorter alias names for convenience: -- p = configargparse.get_arg_parser() # get global singleton instance -- p = configargparse.get_parser() -- p = configargparse.ArgParser() # create a new instance -- p = configargparse.Parser() -- p.add_arg(..) -- p.add(..) -- options = p.parse(..) +**Class Aliases:** -HelpFormatters: +- ``ArgParser`` = ``ArgumentParser`` (create a new instance) +- ``Parser`` = ``ArgumentParser`` -- RawFormatter = RawDescriptionHelpFormatter -- DefaultsFormatter = ArgumentDefaultsHelpFormatter -- DefaultsRawFormatter = ArgumentDefaultsRawHelpFormatter +**Method Aliases:** + +- ``p.add(..)`` = ``p.add_argument(..)`` +- ``p.add_arg(..)`` = ``p.add_argument(..)`` +- ``p.parse(..)`` = ``p.parse_args(..)`` +- ``p.parse_known(..)`` = ``p.parse_known_args(..)`` + +**Function Aliases (for global singleton):** + +- ``configargparse.get_arg_parser()`` = ``configargparse.get_argument_parser()`` +- ``configargparse.get_parser()`` = ``configargparse.get_argument_parser()`` +- ``configargparse.getArgumentParser()`` = ``configargparse.get_argument_parser()`` +- ``configargparse.getArgParser()`` = ``configargparse.get_argument_parser()`` +- ``configargparse.getParser()`` = ``configargparse.get_argument_parser()`` +- ``configargparse.initArgumentParser()`` = ``configargparse.init_argument_parser()`` + +**HelpFormatter Aliases:** + +- ``RawFormatter`` = ``RawDescriptionHelpFormatter`` +- ``DefaultsFormatter`` = ``ArgumentDefaultsHelpFormatter`` +- ``DefaultsRawFormatter`` = ``ArgumentDefaultsRawHelpFormatter`` API Documentation ~~~~~~~~~~~~~~~~~ diff --git a/configargparse.py b/configargparse.py index db92aa6..7219e49 100644 --- a/configargparse.py +++ b/configargparse.py @@ -968,10 +968,18 @@ def parse_known_args( value = [elem.strip() for elem in value[1:-1].split(",")] env_var_args += self.convert_item_to_command_line_arg(action, key, value) - if nargs: - args = args + env_var_args + # Insert env var args before the first optional arg (starts with -) + # to preserve -- separator and positional args that come after it. + # If nargs is True, we still need to respect the -- separator. + insertion_index = 0 + for i, arg in enumerate(args): + if arg.startswith(tuple(self.prefix_chars)): + insertion_index = i + break else: - args = env_var_args + args + # No optional args found, append to end + insertion_index = len(args) + args = args[:insertion_index] + env_var_args + args[insertion_index:] if env_var_args: self._source_to_settings[_ENV_VAR_SOURCE_KEY] = OrderedDict( @@ -1054,10 +1062,18 @@ def parse_known_args( ): nargs = True - if nargs: - args = args + config_args + # Insert config args before the first optional arg (starts with -) + # to preserve -- separator and positional args that come after it. + # If nargs is True, we still need to respect the -- separator. + insertion_index = 0 + for i, arg in enumerate(args): + if arg.startswith(tuple(self.prefix_chars)): + insertion_index = i + break else: - args = config_args + args + # No optional args found, append to end + insertion_index = len(args) + args = args[:insertion_index] + config_args + args[insertion_index:] # save default settings for use by print_values() default_settings = OrderedDict() diff --git a/tests/test_configargparse.py b/tests/test_configargparse.py index d46fde6..f7479af 100644 --- a/tests/test_configargparse.py +++ b/tests/test_configargparse.py @@ -1344,6 +1344,56 @@ def error_func(path): args="-g file.txt", ) + def testDoubleDashSeparator(self): + """Test that -- separator correctly separates optional from positional args + when config file or env vars are used. Regression test for issue #298.""" + # Create a config file with list option + config_file = tempfile.NamedTemporaryFile( + mode="w", delete=False, suffix=".conf" + ) + config_file.write("list = [1, 2, 3]\n") + config_file.flush() + config_file.close() # Close file to avoid Windows file locking issues + + try: + # Test with config file + self.initParser(args_for_setting_config_path=["-c", "--config"]) + self.parser.add_argument("--list", action="append") + self.parser.add_argument("positional_arg", nargs="*") + + # Parse with -- separator + ns = self.parse(args=f"-c {config_file.name} -- foo bar") + + # Positional args should only contain 'foo' and 'bar' + # NOT the config file values '1', '2', '3' + self.assertEqual(ns.positional_arg, ["foo", "bar"]) + self.assertEqual(ns.list, ["1", "2", "3"]) + + # Test without -- separator (config file args should be inserted correctly) + ns = self.parse(args=f"-c {config_file.name} foo bar") + self.assertEqual(ns.positional_arg, ["foo", "bar"]) + self.assertEqual(ns.list, ["1", "2", "3"]) + + # Test with env var and -- separator + self.initParser() + self.parser.add_argument("--list", action="append", env_var="MY_LIST") + self.parser.add_argument("positional_arg", nargs="*") + + old_env = os.environ.get("MY_LIST") + try: + os.environ["MY_LIST"] = "[1,2,3]" + ns = self.parse(args="-- foo bar") + self.assertEqual(ns.positional_arg, ["foo", "bar"]) + self.assertEqual(ns.list, ["1", "2", "3"]) + finally: + if old_env is not None: + os.environ["MY_LIST"] = old_env + elif "MY_LIST" in os.environ: + del os.environ["MY_LIST"] + + finally: + os.unlink(config_file.name) + class TestConfigFileParsers(TestCase): """Test ConfigFileParser subclasses in isolation"""