diff --git a/setup.py b/setup.py index 88a12e1b..aff859ab 100644 --- a/setup.py +++ b/setup.py @@ -83,6 +83,7 @@ def launch_http_server(directory): tests_require = [ "black", "mock", + "toml", "PyYAML", "pytest", "pytest-cov", diff --git a/tests/test_configargparse.py b/tests/test_configargparse.py index 8ece2a65..156b6ffe 100644 --- a/tests/test_configargparse.py +++ b/tests/test_configargparse.py @@ -9,8 +9,9 @@ import types import unittest from unittest import mock +import textwrap -from io import StringIO +from io import BytesIO, StringIO if sys.version_info >= (3, 10): OPTIONAL_ARGS_STRING = "options" @@ -60,7 +61,6 @@ def captured_output(): class TestCase(unittest.TestCase): - def initParser(self, *args, **kwargs): p = configargparse.ArgParser(*args, **kwargs) self.parser = replace_error_method(p) @@ -1749,6 +1749,177 @@ def testYAMLConfigFileParser_w_ArgumentParser_parsed_values(self): assert args.level == 35 +class TestTomlConfigParser(unittest.TestCase): + def write_toml_file(self, content, obj=StringIO): + f = obj() + dedent = lambda x: x + if isinstance(content, str): + dedent = textwrap.dedent + f.write(dedent(content)) + f.seek(0) + return f + + def test_section(self): + f = self.write_toml_file( + """ + [section] + key1 = 'toml1' + key2 = 'toml2' + """ + ) + parser = configargparse.TomlConfigParser(["section"]) + self.assertEqual(parser.parse(f), {"key1": "toml1", "key2": "toml2"}) + + def test_no_sections(self): + f = self.write_toml_file( + """ + [section] + key1 = 'toml1' + key2 = 'toml2' + """ + ) + parser = configargparse.TomlConfigParser([]) + self.assertEqual(parser.parse(f), {}) + + def test_empty_section(self): + f = self.write_toml_file("[section]") + parser = configargparse.TomlConfigParser(["section"]) + self.assertEqual(parser.parse(f), {}) + + def test_empty_file(self): + f = self.write_toml_file("") + parser = configargparse.TomlConfigParser(["section"]) + self.assertEqual(parser.parse(f), {}) + + @unittest.expectedFailure # Ints should be strings + def test_advanced(self): + f = self.write_toml_file( + """ + [tool.section] + key1 = "toml1" + key2 = [1, 2, 3] + """ + ) + parser = configargparse.TomlConfigParser(["tool.section"]) + self.assertEqual(parser.parse(f), {"key1": "toml1", "key2": ["1", "2", "3"]}) + + def test_fails_binary_read(self): + f = self.write_toml_file( + b"""[tool.section]\nkey1 = "toml1" + """, + obj=BytesIO, + ) + parser = configargparse.TomlConfigParser(["tool.section"]) + with self.assertRaises(configargparse.ConfigFileParserException): + parser.parse(f) + + +class TestCompositeConfigParser(unittest.TestCase): + def setUp(self): + self.tmpdir = tempfile.TemporaryDirectory() + self.old_cwd = os.getcwd() + os.chdir(self.tmpdir.name) + self.parser = configargparse.ArgParser( + description="test", + default_config_files=["config.yaml", "config.toml", "config.ini"], + config_file_parser_class=configargparse.CompositeConfigParser( + [ + configargparse.IniConfigParser(["section"], False), + configargparse.TomlConfigParser(["section"]), + configargparse.YAMLConfigFileParser, + ] + ), + ) + self.parser.add_argument("--config", is_config_file=True) + self.parser.add_argument("--key1", type=str) + self.parser.add_argument("--key2", type=str) + + def tearDown(self): + os.chdir(self.old_cwd) + self.tmpdir.cleanup() + + def write_yaml_file(self, content="[section]\nkey1: yaml1\nkey2: yaml2"): + with open("config.yaml", "w") as f: + f.write(content) + + def write_ini_file( + self, content="[section]\nkey1=ini1\nkey2=ini2\n; this is an ini comment" + ): + with open("config.ini", "w") as f: + f.write(content) + + def write_toml_file( + self, + content="[section]\nkey1 = 'toml1'\nkey2 = 'toml2'\nspecial.key = { a = 1 }\n# this is a toml comment", + ): + with open("config.toml", "w") as f: + f.write(content) + + def write_toml_file_extra(self): + self.write_toml_file() + with open("config.toml", "a") as f: + f.write("\nkey3 = 'toml3'\nkey4 = 'toml4'") + + def test_plain(self): + self.assertEqual( + vars(self.parser.parse_args([])), + {"config": None, "key1": None, "key2": None}, + ) + + @unittest.expectedFailure # unknown fields should be ignored + def test_with_all_configs(self): + self.write_yaml_file() + self.write_ini_file() + self.write_toml_file() + self.assertEqual( + vars(self.parser.parse_args([])), + {"config": None, "key1": "ini1", "key2": "ini2"}, + ) + + @unittest.expectedFailure # unknown fields should be ignored + def test_with_all_configs_override(self): + self.write_yaml_file() + self.write_ini_file() + self.write_toml_file() + self.assertEqual( + vars(self.parser.parse_args(["--key1", "val1"])), + {"config": None, "key1": "val1", "key2": "ini2"}, + ) + + def test_missing_primary_config(self): + self.write_yaml_file() + self.assertEqual( + vars(self.parser.parse_args([])), + {"config": None, "key1": "yaml1", "key2": "yaml2"}, + ) + + @unittest.expectedFailure # unknown fields should be ignored + def test_toml(self): + self.write_yaml_file() + self.write_toml_file() + self.assertEqual( + vars(self.parser.parse_args([])), + {"config": None, "key1": "toml1", "key2": "toml2"}, + ) + + @unittest.expectedFailure # unknown fields should be ignored + def test_override_primary_config(self): + self.write_yaml_file() + self.write_ini_file() + self.write_toml_file() + self.assertEqual( + vars(self.parser.parse_args(["--config", "config.yaml"])), + {"config": "config.yaml", "key1": "yaml1", "key2": "yaml2"}, + ) + + def test_toml_extra(self): + self.write_yaml_file() + self.write_ini_file() + self.write_toml_file_extra() + with self.assertRaises(SystemExit): + self.parser.parse_args([]) + + ################################################################################ # since configargparse should work as a drop-in replacement for argparse # in all situations, run argparse unittests on configargparse by modifying