From 4071e0833061063711f1e5a61e0487ba6431de05 Mon Sep 17 00:00:00 2001 From: BatuhanSA Date: Fri, 3 Oct 2025 17:44:33 -0700 Subject: [PATCH 01/19] Added set functionality, needs testing. --- edq/cli/config/set.py | 95 ++++++++++++++++++++++++++++ edq/core/config.py | 19 ++++-- edq/core/config_test.py | 134 ++++++++++++++++++++++++++++++++++++++-- 3 files changed, 238 insertions(+), 10 deletions(-) create mode 100644 edq/cli/config/set.py diff --git a/edq/cli/config/set.py b/edq/cli/config/set.py new file mode 100644 index 0000000..db4f15d --- /dev/null +++ b/edq/cli/config/set.py @@ -0,0 +1,95 @@ +""" +Set a configuration option. +""" + +import argparse +import sys + +import edq.core.argparser +import edq.core.config +import edq.util.dirent +import edq.util.json + +def write_configs_to_file(file_path: str, configs_to_write: list[str]) -> None: + """ Write configs to a specified file path. Create the path if it do not exist. """ + + if (not (edq.util.dirent.exists(file_path))): + edq.util.json.dump_path({}, file_path) + + config = edq.util.json.load_path(file_path) + + for config_option in configs_to_write: + if ("=" not in config_option): + raise ValueError( + f"Invalid configuration option '{config_option}'." + + " Configuration options must be provided in the format `=` when passed via the CLI.") + + (key, value) = config_option.split("=", maxsplit = 1) + + key = key.strip() + if (key == ""): + raise ValueError(f"Found an empty configuration option key associated with the value '{value}'.") + + config[key] = value + + edq.util.json.dump_path(config, file_path, indent = 4) + +def run_cli(args: argparse.Namespace) -> int: + """ Run the CLI. """ + + if (not (args.write_to_local or args.write_to_global or (args.file_to_write is not None))): + args.write_to_local = True + + if (args.write_to_local): + local_config_path = args._config_params.get(edq.core.config.LOCAL_CONFIG_KEY) + if (local_config_path is None): + local_config_path = args._config_params.get(edq.core.config.FILENAME_KEY) + write_configs_to_file(local_config_path, args.config_to_set) + elif (args.write_to_global): + global_config_path = args._config_params.get(edq.core.config.GLOBAL_CONFIG_KEY) + write_configs_to_file(global_config_path, args.config_to_set) + elif (args.file_to_write is not None): + write_configs_to_file(args.file_to_write, args.config_to_set) + + return 0 + +def main() -> int: + """ Get a parser, parse the args, and call run. """ + + return run_cli(_get_parser().parse_args()) + +def _get_parser() -> argparse.ArgumentParser: + """ Get a parser and add addition flags. """ + + parser = edq.core.argparser.get_default_parser(__doc__.strip()) + modify_parser(parser) + + return parser + +def modify_parser(parser: argparse.ArgumentParser) -> None: + """ Add this CLI's flags to the given parser. """ + + parser.add_argument('config_to_set', metavar = "=", + action = 'store', nargs = '+', type = str, + help = 'Configuration option to be set. Expected config format is =.', + ) + + group = parser.add_mutually_exclusive_group() + + group.add_argument('--local', + action = 'store_true', dest = 'write_to_local', + help = 'Sets the variable in local config file.', + ) + + group.add_argument('--global', + action = 'store_true', dest = 'write_to_global', + help = 'Sets the variable in global config file.', + ) + + group.add_argument('--file', + action = 'store', type = str, default = None, dest = 'file_to_write', + help = 'Sets the variable in a specified config file.', + ) + +if (__name__ == '__main__'): + sys.exit(main()) diff --git a/edq/core/config.py b/edq/core/config.py index fae78ff..2ad9387 100644 --- a/edq/core/config.py +++ b/edq/core/config.py @@ -15,6 +15,8 @@ CONFIG_PATHS_KEY: str = 'config_paths' CONFIGS_KEY: str = 'configs' GLOBAL_CONFIG_KEY: str = 'global_config_path' +LOCAL_CONFIG_KEY: str = 'local_config_path' +FILENAME_KEY: str = 'config_filename' IGNORE_CONFIGS_KEY: str = 'ignore_configs' DEFAULT_CONFIG_FILENAME: str = "edq-config.json" @@ -47,7 +49,7 @@ def get_tiered_config( legacy_config_filename: typing.Union[str, None] = None, cli_arguments: typing.Union[dict, argparse.Namespace, None] = None, local_config_root_cutoff: typing.Union[str, None] = None, - ) -> typing.Tuple[typing.Dict[str, str], typing.Dict[str, ConfigSource]]: + ) -> typing.Tuple[typing.Dict[str, str], typing.Dict[str, ConfigSource], typing.Dict[str, typing.Union[str, None]]]: """ Load all configuration options from files and command-line arguments. Returns a configuration dictionary with the values based on tiering rules and a source dictionary mapping each key to its origin. @@ -58,12 +60,16 @@ def get_tiered_config( config: typing.Dict[str, str] = {} sources: typing.Dict[str, ConfigSource] = {} + config_params: typing.Dict[str, typing.Union[str, None]] = {} + + config_params["config_filename"] = config_filename # Ensure CLI arguments are always a dict, even if provided as argparse.Namespace. if (isinstance(cli_arguments, argparse.Namespace)): cli_arguments = vars(cli_arguments) global_config_path = cli_arguments.get(GLOBAL_CONFIG_KEY, get_global_config_path(config_filename)) + config_params[GLOBAL_CONFIG_KEY] = global_config_path # Check the global user config file. if (os.path.isfile(global_config_path)): @@ -76,6 +82,8 @@ def get_tiered_config( local_config_root_cutoff = local_config_root_cutoff, ) + config_params[LOCAL_CONFIG_KEY] = local_config_path + if (local_config_path is not None): _load_config_file(local_config_path, config, sources, CONFIG_SOURCE_LOCAL) @@ -108,7 +116,7 @@ def get_tiered_config( config.pop(ignore_config, None) sources.pop(ignore_config, None) - return config, sources + return config, sources, config_params def _load_config_file( config_path: str, @@ -215,7 +223,7 @@ def set_cli_args(parser: argparse.ArgumentParser, extra_state: typing.Dict[str, + ' Will override options form both global and local config files.') ) - parser.add_argument('--config', dest = CONFIGS_KEY, + parser.add_argument('--config', dest = CONFIGS_KEY, metavar = "=", action = 'append', type = str, default = [], help = ('Set a configuration option from the command-line.' + ' Specify options as = pairs.' @@ -237,6 +245,7 @@ def load_config_into_args( args: argparse.Namespace, extra_state: typing.Dict[str, typing.Any], config_filename: str = DEFAULT_CONFIG_FILENAME, + legacy_config_filename: typing.Union[str, None] = None, cli_arg_config_map: typing.Union[typing.Dict[str, str], None] = None, **kwargs: typing.Any, ) -> None: @@ -259,10 +268,12 @@ def load_config_into_args( if (value is not None): getattr(args, CONFIGS_KEY).append(f"{config_key}={value}") - (config_dict, sources_dict) = get_tiered_config( + (config_dict, sources_dict, config_params_dict) = get_tiered_config( cli_arguments = args, config_filename = config_filename, + legacy_config_filename = legacy_config_filename, ) setattr(args, "_config", config_dict) setattr(args, "_config_sources", sources_dict) + setattr(args, "_config_params", config_params_dict) diff --git a/edq/core/config_test.py b/edq/core/config_test.py index 5a87a2f..855765a 100644 --- a/edq/core/config_test.py +++ b/edq/core/config_test.py @@ -5,9 +5,9 @@ import edq.util.dirent import edq.util.json -def creat_test_dir(temp_dir_prefix: str) -> str: +def create_test_dir(temp_dir_prefix: str) -> str: """ - Creat a temp dir and populate it with dirents for testing. + Create a temp dir and populate it with dirents for testing. This test data directory is laid out as: . @@ -32,6 +32,7 @@ def creat_test_dir(temp_dir_prefix: str) -> str: │   └── nest1 │   ├── nest2a │   └── nest2b + │   ├── config.json │   └── edq-config.json ├── old-name │   ├── config.json @@ -83,6 +84,10 @@ def creat_test_dir(temp_dir_prefix: str) -> str: {"user": "user@test.edulinq.org"}, os.path.join(nested_dir_path, "nest1", "nest2b", edq.core.config.DEFAULT_CONFIG_FILENAME), ) + edq.util.json.dump_path( + {"user": "user@test.edulinq.org"}, + os.path.join(nested_dir_path, "nest1", "nest2b", "config.json"), + ) simple_config_dir_path = os.path.join(temp_dir, "simple") edq.util.dirent.mkdir(simple_config_dir_path) @@ -108,7 +113,7 @@ def test_get_tiered_config_base(self): Test that configuration files are loaded correctly from the file system with the expected tier. """ - temp_dir = creat_test_dir(temp_dir_prefix = "edq-test-config-get-tiered-config-") + temp_dir = create_test_dir(temp_dir_prefix = "edq-test-config-get-tiered-config-") # [(work directory, extra arguments, expected config, expected source, error substring), ...] test_cases = [ @@ -118,6 +123,7 @@ def test_get_tiered_config_base(self): {}, {}, {}, + {}, None, ), @@ -140,6 +146,9 @@ def test_get_tiered_config_base(self): path = os.path.join(temp_dir, "global", edq.core.config.DEFAULT_CONFIG_FILENAME), ), }, + { + edq.core.config.GLOBAL_CONFIG_KEY: os.path.join(temp_dir, "global", edq.core.config.DEFAULT_CONFIG_FILENAME), + }, None, ), @@ -153,6 +162,9 @@ def test_get_tiered_config_base(self): }, {}, {}, + { + edq.core.config.GLOBAL_CONFIG_KEY: os.path.join(temp_dir, "empty", edq.core.config.DEFAULT_CONFIG_FILENAME), + }, None, ), @@ -166,6 +178,7 @@ def test_get_tiered_config_base(self): }, {}, {}, + {}, "Found an empty configuration option key associated with the value 'user@test.edulinq.org'.", ), @@ -179,6 +192,9 @@ def test_get_tiered_config_base(self): }, {}, {}, + { + edq.core.config.GLOBAL_CONFIG_KEY: os.path.join(temp_dir, "dir-config", edq.core.config.DEFAULT_CONFIG_FILENAME), + }, None, ), @@ -192,6 +208,9 @@ def test_get_tiered_config_base(self): }, {}, {}, + { + edq.core.config.GLOBAL_CONFIG_KEY: os.path.join(temp_dir, "empty-dir", "non-existent-config.json"), + }, None, ), @@ -205,6 +224,7 @@ def test_get_tiered_config_base(self): }, {}, {}, + {}, "Failed to read JSON file", ), @@ -228,6 +248,10 @@ def test_get_tiered_config_base(self): path = os.path.join(temp_dir, "multiple-options", edq.core.config.DEFAULT_CONFIG_FILENAME), ), }, + { + + edq.core.config.GLOBAL_CONFIG_KEY: os.path.join(temp_dir, "multiple-options", edq.core.config.DEFAULT_CONFIG_FILENAME), + }, None, ), @@ -251,6 +275,9 @@ def test_get_tiered_config_base(self): path = os.path.join(temp_dir, "simple", edq.core.config.DEFAULT_CONFIG_FILENAME), ), }, + { + edq.core.config.GLOBAL_CONFIG_KEY: os.path.join(temp_dir, "simple", edq.core.config.DEFAULT_CONFIG_FILENAME), + }, None, ), @@ -269,6 +296,9 @@ def test_get_tiered_config_base(self): path = os.path.join(temp_dir, "simple", edq.core.config.DEFAULT_CONFIG_FILENAME), ), }, + { + edq.core.config.LOCAL_CONFIG_KEY: os.path.join(temp_dir, "simple", edq.core.config.DEFAULT_CONFIG_FILENAME), + }, None, ), @@ -276,7 +306,7 @@ def test_get_tiered_config_base(self): ( "custom-name", { - "config_filename": "custom-edq-config.json", + edq.core.config.FILENAME_KEY: "custom-edq-config.json", }, { "user": "user@test.edulinq.org", @@ -287,6 +317,10 @@ def test_get_tiered_config_base(self): path = os.path.join(temp_dir, "custom-name", "custom-edq-config.json"), ), }, + { + edq.core.config.FILENAME_KEY: "custom-edq-config.json", + edq.core.config.LOCAL_CONFIG_KEY: os.path.join(temp_dir, "custom-name","custom-edq-config.json"), + }, None, ), @@ -305,6 +339,9 @@ def test_get_tiered_config_base(self): path = os.path.join(temp_dir, "old-name", "config.json"), ), }, + { + edq.core.config.LOCAL_CONFIG_KEY: os.path.join(temp_dir, "old-name", "config.json"), + }, None, ), @@ -321,6 +358,9 @@ def test_get_tiered_config_base(self): path = os.path.join(temp_dir, "nested", edq.core.config.DEFAULT_CONFIG_FILENAME), ), }, + { + edq.core.config.LOCAL_CONFIG_KEY: os.path.join(temp_dir, "nested", edq.core.config.DEFAULT_CONFIG_FILENAME), + }, None, ), @@ -332,6 +372,7 @@ def test_get_tiered_config_base(self): }, {}, {}, + {}, None, ), @@ -341,6 +382,9 @@ def test_get_tiered_config_base(self): {}, {}, {}, + { + edq.core.config.LOCAL_CONFIG_KEY: os.path.join(temp_dir, "empty", edq.core.config.DEFAULT_CONFIG_FILENAME), + }, None, ), @@ -350,6 +394,7 @@ def test_get_tiered_config_base(self): {}, {}, {}, + {}, "Found an empty configuration option key associated with the value 'user@test.edulinq.org'.", ), @@ -359,6 +404,7 @@ def test_get_tiered_config_base(self): {}, {}, {}, + {}, None, ), @@ -368,6 +414,7 @@ def test_get_tiered_config_base(self): {}, {}, {}, + {}, "Failed to read JSON file", ), @@ -390,6 +437,9 @@ def test_get_tiered_config_base(self): path = os.path.join(temp_dir, "multiple-options", edq.core.config.DEFAULT_CONFIG_FILENAME), ), }, + { + edq.core.config.LOCAL_CONFIG_KEY: os.path.join(temp_dir, "multiple-options", edq.core.config.DEFAULT_CONFIG_FILENAME), + }, None, ), @@ -412,6 +462,9 @@ def test_get_tiered_config_base(self): path = os.path.join(temp_dir, "simple", edq.core.config.DEFAULT_CONFIG_FILENAME), ), }, + { + edq.core.config.LOCAL_CONFIG_KEY: os.path.join(temp_dir, "simple", edq.core.config.DEFAULT_CONFIG_FILENAME) + }, None, ), @@ -431,6 +484,9 @@ def test_get_tiered_config_base(self): path = os.path.join(temp_dir, "nested", "nest1", "nest2b", edq.core.config.DEFAULT_CONFIG_FILENAME), ), }, + { + edq.core.config.LOCAL_CONFIG_KEY: os.path.join(temp_dir, "nested", "nest1", "nest2b", edq.core.config.DEFAULT_CONFIG_FILENAME), + }, None, ), @@ -461,6 +517,7 @@ def test_get_tiered_config_base(self): path = os.path.join(temp_dir, "nested", edq.core.config.DEFAULT_CONFIG_FILENAME), ), }, + {}, None, ), @@ -484,6 +541,7 @@ def test_get_tiered_config_base(self): path = os.path.join(temp_dir, "simple", edq.core.config.DEFAULT_CONFIG_FILENAME), ), }, + {}, None, ), @@ -499,6 +557,7 @@ def test_get_tiered_config_base(self): }, {}, {}, + {}, None, ), @@ -514,6 +573,7 @@ def test_get_tiered_config_base(self): }, {}, {}, + {}, "Found an empty configuration option key associated with the value 'user@test.edulinq.org'.", ), @@ -529,6 +589,7 @@ def test_get_tiered_config_base(self): }, {}, {}, + {}, "IsADirectoryError", ), @@ -544,6 +605,7 @@ def test_get_tiered_config_base(self): }, {}, {}, + {}, "FileNotFoundError", ), @@ -559,6 +621,7 @@ def test_get_tiered_config_base(self): }, {}, {}, + {}, "Failed to read JSON file", ), @@ -584,6 +647,7 @@ def test_get_tiered_config_base(self): path = os.path.join(temp_dir, "multiple-options", edq.core.config.DEFAULT_CONFIG_FILENAME), ), }, + {}, None, ), @@ -609,6 +673,7 @@ def test_get_tiered_config_base(self): path = os.path.join(temp_dir, "simple", edq.core.config.DEFAULT_CONFIG_FILENAME), ), }, + {}, None, ), @@ -631,6 +696,7 @@ def test_get_tiered_config_base(self): { "user": edq.core.config.ConfigSource(label = edq.core.config.CONFIG_SOURCE_CLI), }, + {}, None, ), @@ -646,6 +712,7 @@ def test_get_tiered_config_base(self): }, {}, {}, + {}, "Found an empty configuration option key associated with the value 'user@test.edulinq.org'.", ), @@ -665,6 +732,7 @@ def test_get_tiered_config_base(self): { "user": edq.core.config.ConfigSource(label = edq.core.config.CONFIG_SOURCE_CLI), }, + {}, None, ), @@ -684,6 +752,7 @@ def test_get_tiered_config_base(self): { "pass": edq.core.config.ConfigSource(label = edq.core.config.CONFIG_SOURCE_CLI), }, + {}, None, ), @@ -699,6 +768,7 @@ def test_get_tiered_config_base(self): }, {}, {}, + {}, "Invalid configuration option 'useruser@test.edulinq.org'.", ), @@ -722,6 +792,7 @@ def test_get_tiered_config_base(self): { "user": edq.core.config.ConfigSource(label = edq.core.config.CONFIG_SOURCE_CLI), }, + {}, None, ), @@ -744,6 +815,7 @@ def test_get_tiered_config_base(self): { "user": edq.core.config.ConfigSource(label = edq.core.config.CONFIG_SOURCE_CLI), }, + {}, None, ), @@ -766,6 +838,10 @@ def test_get_tiered_config_base(self): path = os.path.join(temp_dir, "simple", edq.core.config.DEFAULT_CONFIG_FILENAME), ), }, + { + edq.core.config.GLOBAL_CONFIG_KEY: os.path.join(temp_dir, "global", edq.core.config.DEFAULT_CONFIG_FILENAME), + edq.core.config.LOCAL_CONFIG_KEY: os.path.join(temp_dir, "simple", edq.core.config.DEFAULT_CONFIG_FILENAME), + }, None, ), @@ -789,6 +865,9 @@ def test_get_tiered_config_base(self): path = os.path.join(temp_dir, "simple", edq.core.config.DEFAULT_CONFIG_FILENAME), ), }, + { + edq.core.config.GLOBAL_CONFIG_KEY: os.path.join(temp_dir, "global", edq.core.config.DEFAULT_CONFIG_FILENAME), + }, None, ), @@ -809,6 +888,9 @@ def test_get_tiered_config_base(self): { "user": edq.core.config.ConfigSource(label = edq.core.config.CONFIG_SOURCE_CLI), }, + { + edq.core.config.GLOBAL_CONFIG_KEY: os.path.join(temp_dir, "global", edq.core.config.DEFAULT_CONFIG_FILENAME), + }, None, ), @@ -831,6 +913,9 @@ def test_get_tiered_config_base(self): path = os.path.join(temp_dir, "custom-name", "custom-edq-config.json"), ), }, + { + edq.core.config.LOCAL_CONFIG_KEY: os.path.join(temp_dir, "simple", edq.core.config.DEFAULT_CONFIG_FILENAME), + }, None, ), @@ -850,6 +935,9 @@ def test_get_tiered_config_base(self): { "user": edq.core.config.ConfigSource(label = edq.core.config.CONFIG_SOURCE_CLI), }, + { + edq.core.config.LOCAL_CONFIG_KEY: os.path.join(temp_dir, "simple", edq.core.config.DEFAULT_CONFIG_FILENAME), + }, None, ), @@ -872,6 +960,7 @@ def test_get_tiered_config_base(self): { "user": edq.core.config.ConfigSource(label = edq.core.config.CONFIG_SOURCE_CLI), }, + {}, None, ), @@ -895,6 +984,9 @@ def test_get_tiered_config_base(self): { "user": edq.core.config.ConfigSource(label = edq.core.config.CONFIG_SOURCE_CLI), }, + { + edq.core.config.GLOBAL_CONFIG_KEY: os.path.join(temp_dir, "global", edq.core.config.DEFAULT_CONFIG_FILENAME), + }, None, ), @@ -915,6 +1007,10 @@ def test_get_tiered_config_base(self): { "user": edq.core.config.ConfigSource(label = edq.core.config.CONFIG_SOURCE_CLI), }, + { + edq.core.config.GLOBAL_CONFIG_KEY: os.path.join(temp_dir, "global", edq.core.config.DEFAULT_CONFIG_FILENAME), + edq.core.config.LOCAL_CONFIG_KEY: os.path.join(temp_dir, "simple", edq.core.config.DEFAULT_CONFIG_FILENAME), + }, None, ), @@ -938,6 +1034,10 @@ def test_get_tiered_config_base(self): path = os.path.join(temp_dir, "custom-name", "custom-edq-config.json"), ), }, + { + edq.core.config.GLOBAL_CONFIG_KEY: os.path.join(temp_dir, "global", edq.core.config.DEFAULT_CONFIG_FILENAME), + edq.core.config.LOCAL_CONFIG_KEY: os.path.join(temp_dir, "simple", edq.core.config.DEFAULT_CONFIG_FILENAME), + }, None, ), @@ -960,6 +1060,9 @@ def test_get_tiered_config_base(self): { "user": edq.core.config.ConfigSource(label = edq.core.config.CONFIG_SOURCE_CLI), }, + { + edq.core.config.LOCAL_CONFIG_KEY: os.path.join(temp_dir, "simple", edq.core.config.DEFAULT_CONFIG_FILENAME), + }, None, ), @@ -988,12 +1091,16 @@ def test_get_tiered_config_base(self): ), "pass": edq.core.config.ConfigSource(label = edq.core.config.CONFIG_SOURCE_CLI), }, + { + edq.core.config.GLOBAL_CONFIG_KEY: os.path.join(temp_dir, "global", edq.core.config.DEFAULT_CONFIG_FILENAME), + edq.core.config.LOCAL_CONFIG_KEY: os.path.join(temp_dir, "simple", edq.core.config.DEFAULT_CONFIG_FILENAME), + }, None, ), ] for (i, test_case) in enumerate(test_cases): - (test_work_dir, extra_args, expected_config, expected_source, error_substring) = test_case + (test_work_dir, extra_args, expected_config, expected_source, expected_config_params, error_substring) = test_case with self.subTest(msg = f"Case {i} ('{test_work_dir}'):"): cli_args = extra_args.get("cli_arguments", None) @@ -1012,12 +1119,26 @@ def test_get_tiered_config_base(self): if (cutoff is None): extra_args["local_config_root_cutoff"] = temp_dir + global_file_used = expected_config_params.get(edq.core.config.GLOBAL_CONFIG_KEY, None) + if (global_file_used is None): + expected_config_params[edq.core.config.GLOBAL_CONFIG_KEY] = os.path.join( + temp_dir, "empty", edq.core.config.DEFAULT_CONFIG_FILENAME + ) + + local_file_used = expected_config_params.get(edq.core.config.LOCAL_CONFIG_KEY, None) + if (local_file_used is None): + expected_config_params[edq.core.config.LOCAL_CONFIG_KEY] = None + + file_name_used = expected_config_params.get(edq.core.config.FILENAME_KEY, None) + if (file_name_used is None): + expected_config_params[edq.core.config.FILENAME_KEY] = edq.core.config.DEFAULT_CONFIG_FILENAME + previous_work_directory = os.getcwd() initial_work_directory = os.path.join(temp_dir, test_work_dir) os.chdir(initial_work_directory) try: - (actual_config, actual_sources) = edq.core.config.get_tiered_config(**extra_args) + (actual_config, actual_sources, actual_config_params) = edq.core.config.get_tiered_config(**extra_args) except Exception as ex: error_string = self.format_error_string(ex) @@ -1036,3 +1157,4 @@ def test_get_tiered_config_base(self): self.assertJSONDictEqual(expected_config, actual_config) self.assertJSONDictEqual(expected_source, actual_sources) + self.assertJSONDictEqual(expected_config_params, actual_config_params) From aea1be8e9e53523edaf5fba1c7330cf11c723157 Mon Sep 17 00:00:00 2001 From: BatuhanSA Date: Fri, 3 Oct 2025 17:50:04 -0700 Subject: [PATCH 02/19] Updated a type hint. --- edq/cli/config/set.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/edq/cli/config/set.py b/edq/cli/config/set.py index db4f15d..b645367 100644 --- a/edq/cli/config/set.py +++ b/edq/cli/config/set.py @@ -4,13 +4,14 @@ import argparse import sys +import typing import edq.core.argparser import edq.core.config import edq.util.dirent import edq.util.json -def write_configs_to_file(file_path: str, configs_to_write: list[str]) -> None: +def write_configs_to_file(file_path: str, configs_to_write: typing.List[str]) -> None: """ Write configs to a specified file path. Create the path if it do not exist. """ if (not (edq.util.dirent.exists(file_path))): From df080f74bf75a1b61c59717f9b8859416ae083ec Mon Sep 17 00:00:00 2001 From: BatuhanSA Date: Tue, 7 Oct 2025 19:15:25 -0700 Subject: [PATCH 03/19] Added set config cli and basic tests. --- edq/cli/config/set.py | 4 +- edq/cli/config/set_test.py | 123 +++++++++++++++++++++++++++++++++++++ edq/core/config.py | 12 ++-- edq/core/config_test.py | 103 ++++++++++++++++--------------- 4 files changed, 184 insertions(+), 58 deletions(-) create mode 100644 edq/cli/config/set_test.py diff --git a/edq/cli/config/set.py b/edq/cli/config/set.py index b645367..236e196 100644 --- a/edq/cli/config/set.py +++ b/edq/cli/config/set.py @@ -42,12 +42,12 @@ def run_cli(args: argparse.Namespace) -> int: args.write_to_local = True if (args.write_to_local): - local_config_path = args._config_params.get(edq.core.config.LOCAL_CONFIG_KEY) + local_config_path = args._config_params.get(edq.core.config.LOCAL_CONFIG_PATH_KEY) if (local_config_path is None): local_config_path = args._config_params.get(edq.core.config.FILENAME_KEY) write_configs_to_file(local_config_path, args.config_to_set) elif (args.write_to_global): - global_config_path = args._config_params.get(edq.core.config.GLOBAL_CONFIG_KEY) + global_config_path = args._config_params.get(edq.core.config.GLOBAL_CONFIG_PATH_KEY) write_configs_to_file(global_config_path, args.config_to_set) elif (args.file_to_write is not None): write_configs_to_file(args.file_to_write, args.config_to_set) diff --git a/edq/cli/config/set_test.py b/edq/cli/config/set_test.py new file mode 100644 index 0000000..3967e0f --- /dev/null +++ b/edq/cli/config/set_test.py @@ -0,0 +1,123 @@ +import argparse +import os + +import edq.testing.unittest +import edq.cli.config.set +import edq.core.config +import edq.util.dirent +import edq.util.json + +GLOBAL_DIR: str = 'global' +LOCAL_DIR: str = 'local' + +def create_test_dir(temp_dir_prefix: str) -> str: + """ + Create a temp dir and populate it with dirents for testing. + + This test data directory is laid out as: + + . + ├── empty-config + │   └── edq-config.json + ├── global + └── non-empty-config + └── edq-config.json + """ + + temp_dir = edq.util.dirent.get_temp_dir(prefix = temp_dir_prefix, rm = False) + + global_config_path = os.path.join(temp_dir, GLOBAL_DIR) + edq.util.dirent.mkdir(global_config_path) + + path_empty_config = os.path.join(temp_dir, "empty-config") + edq.util.dirent.mkdir(path_empty_config) + edq.util.json.dump_path( + {}, + os.path.join(path_empty_config, edq.core.config.DEFAULT_CONFIG_FILENAME), + ) + + path_non_empty_config = os.path.join(temp_dir, "non-empty-config") + edq.util.dirent.mkdir(path_non_empty_config) + edq.util.json.dump_path( + {"user": "user@test.edulinq.org"}, + os.path.join(path_non_empty_config, edq.core.config.DEFAULT_CONFIG_FILENAME), + ) + + return temp_dir + +class TestSetConfig(edq.testing.unittest.BaseTest): + """ Test basic functionality of set config. """ + + def test_set_base(self): + """ + Test that the set command creates configuration files and writes the specified configuration correctly. + """ + + # [(set cli arguments, expected result, error substring), ...] + test_cases = [ + ( + { + "config_to_set": ["user=user@test.edulinq.org"], + }, + ( + { + "path": os.path.join("TEMP_DIR", edq.core.config.DEFAULT_CONFIG_FILENAME), + "data": {"user": "user@test.edulinq.org"}, + }, + ), + None, + ) + + ] + + for test_case in test_cases: + temp_dir = create_test_dir(temp_dir_prefix = "edq-test-config-set-") + + cli_arguments, expected_result, error_substring = test_case + + set_args = argparse.Namespace( + write_to_local = cli_arguments.get("write_to_local", False), + write_to_global = cli_arguments.get("write_to_global", False), + file_to_write = cli_arguments.get("file_to_write", None), + config_to_set = cli_arguments.get("config_to_set"), + _config_params = { + edq.core.config.LOCAL_CONFIG_PATH_KEY: cli_arguments.get(edq.core.config.LOCAL_CONFIG_PATH_KEY, None), + edq.core.config.FILENAME_KEY: cli_arguments.get(edq.core.config.FILENAME_KEY, edq.core.config.DEFAULT_CONFIG_FILENAME), + edq.core.config.GLOBAL_CONFIG_PATH_KEY: cli_arguments.get( + edq.core.config.GLOBAL_CONFIG_PATH_KEY, + os.path.join(temp_dir, GLOBAL_DIR) + ) + } + ) + + error_substring = None + + previous_work_directory = os.getcwd() + os.chdir(temp_dir) + + try: + edq.cli.config.set.run_cli(set_args) + except Exception as ex: + error_string = self.format_error_string(ex) + + if (error_substring is None): + self.fail(f"Unexpected error: '{error_string}'.") + + self.assertIn(error_substring, error_string, 'Error is not as expected.') + + finally: + os.chdir(previous_work_directory) + + if (error_substring is not None): + self.fail(f"Did not get expected error: '{error_substring}'.") + + for file in expected_result: + file_path = file.get("path") + file_path = file_path.replace('TEMP_DIR', temp_dir) + + if (not edq.util.dirent.exists(file_path)): + self.fail("Expected file doesn't exist.") + + data_actual = edq.util.json.load_path(file_path) + + self.assertJSONDictEqual(data_actual, file.get("data")) diff --git a/edq/core/config.py b/edq/core/config.py index 2ad9387..479e091 100644 --- a/edq/core/config.py +++ b/edq/core/config.py @@ -14,8 +14,8 @@ CONFIG_PATHS_KEY: str = 'config_paths' CONFIGS_KEY: str = 'configs' -GLOBAL_CONFIG_KEY: str = 'global_config_path' -LOCAL_CONFIG_KEY: str = 'local_config_path' +GLOBAL_CONFIG_PATH_KEY: str = 'global_config_path' +LOCAL_CONFIG_PATH_KEY: str = 'local_config_path' FILENAME_KEY: str = 'config_filename' IGNORE_CONFIGS_KEY: str = 'ignore_configs' DEFAULT_CONFIG_FILENAME: str = "edq-config.json" @@ -68,8 +68,8 @@ def get_tiered_config( if (isinstance(cli_arguments, argparse.Namespace)): cli_arguments = vars(cli_arguments) - global_config_path = cli_arguments.get(GLOBAL_CONFIG_KEY, get_global_config_path(config_filename)) - config_params[GLOBAL_CONFIG_KEY] = global_config_path + global_config_path = cli_arguments.get(GLOBAL_CONFIG_PATH_KEY, get_global_config_path(config_filename)) + config_params[GLOBAL_CONFIG_PATH_KEY] = global_config_path # Check the global user config file. if (os.path.isfile(global_config_path)): @@ -82,7 +82,7 @@ def get_tiered_config( local_config_root_cutoff = local_config_root_cutoff, ) - config_params[LOCAL_CONFIG_KEY] = local_config_path + config_params[LOCAL_CONFIG_PATH_KEY] = local_config_path if (local_config_path is not None): _load_config_file(local_config_path, config, sources, CONFIG_SOURCE_LOCAL) @@ -210,7 +210,7 @@ def set_cli_args(parser: argparse.ArgumentParser, extra_state: typing.Dict[str, Set common CLI arguments for configuration. """ - parser.add_argument('--config-global', dest = GLOBAL_CONFIG_KEY, + parser.add_argument('--config-global', dest = GLOBAL_CONFIG_PATH_KEY, action = 'store', type = str, default = get_global_config_path(config_filename), help = 'Set the default global config file path (default: %(default)s).', ) diff --git a/edq/core/config_test.py b/edq/core/config_test.py index 855765a..58b8992 100644 --- a/edq/core/config_test.py +++ b/edq/core/config_test.py @@ -134,7 +134,7 @@ def test_get_tiered_config_base(self): "empty-dir", { "cli_arguments": { - edq.core.config.GLOBAL_CONFIG_KEY: os.path.join(temp_dir, "global", edq.core.config.DEFAULT_CONFIG_FILENAME), + edq.core.config.GLOBAL_CONFIG_PATH_KEY: os.path.join(temp_dir, "global", edq.core.config.DEFAULT_CONFIG_FILENAME), }, }, { @@ -147,7 +147,7 @@ def test_get_tiered_config_base(self): ), }, { - edq.core.config.GLOBAL_CONFIG_KEY: os.path.join(temp_dir, "global", edq.core.config.DEFAULT_CONFIG_FILENAME), + edq.core.config.GLOBAL_CONFIG_PATH_KEY: os.path.join(temp_dir, "global", edq.core.config.DEFAULT_CONFIG_FILENAME), }, None, ), @@ -157,13 +157,13 @@ def test_get_tiered_config_base(self): "empty-dir", { "cli_arguments": { - edq.core.config.GLOBAL_CONFIG_KEY: os.path.join(temp_dir, "empty", edq.core.config.DEFAULT_CONFIG_FILENAME), + edq.core.config.GLOBAL_CONFIG_PATH_KEY: os.path.join(temp_dir, "empty", edq.core.config.DEFAULT_CONFIG_FILENAME), }, }, {}, {}, { - edq.core.config.GLOBAL_CONFIG_KEY: os.path.join(temp_dir, "empty", edq.core.config.DEFAULT_CONFIG_FILENAME), + edq.core.config.GLOBAL_CONFIG_PATH_KEY: os.path.join(temp_dir, "empty", edq.core.config.DEFAULT_CONFIG_FILENAME), }, None, ), @@ -173,7 +173,7 @@ def test_get_tiered_config_base(self): "empty-dir", { "cli_arguments": { - edq.core.config.GLOBAL_CONFIG_KEY: os.path.join(temp_dir, "empty-key", edq.core.config.DEFAULT_CONFIG_FILENAME), + edq.core.config.GLOBAL_CONFIG_PATH_KEY: os.path.join(temp_dir, "empty-key", edq.core.config.DEFAULT_CONFIG_FILENAME), }, }, {}, @@ -187,13 +187,13 @@ def test_get_tiered_config_base(self): "empty-dir", { "cli_arguments": { - edq.core.config.GLOBAL_CONFIG_KEY: os.path.join(temp_dir, "dir-config", edq.core.config.DEFAULT_CONFIG_FILENAME), + edq.core.config.GLOBAL_CONFIG_PATH_KEY: os.path.join(temp_dir, "dir-config", edq.core.config.DEFAULT_CONFIG_FILENAME), }, }, {}, {}, { - edq.core.config.GLOBAL_CONFIG_KEY: os.path.join(temp_dir, "dir-config", edq.core.config.DEFAULT_CONFIG_FILENAME), + edq.core.config.GLOBAL_CONFIG_PATH_KEY: os.path.join(temp_dir, "dir-config", edq.core.config.DEFAULT_CONFIG_FILENAME), }, None, ), @@ -203,13 +203,13 @@ def test_get_tiered_config_base(self): "empty-dir", { "cli_arguments": { - edq.core.config.GLOBAL_CONFIG_KEY: os.path.join(temp_dir, "empty-dir", "non-existent-config.json"), + edq.core.config.GLOBAL_CONFIG_PATH_KEY: os.path.join(temp_dir, "empty-dir", "non-existent-config.json"), }, }, {}, {}, { - edq.core.config.GLOBAL_CONFIG_KEY: os.path.join(temp_dir, "empty-dir", "non-existent-config.json"), + edq.core.config.GLOBAL_CONFIG_PATH_KEY: os.path.join(temp_dir, "empty-dir", "non-existent-config.json"), }, None, ), @@ -219,7 +219,7 @@ def test_get_tiered_config_base(self): "empty-dir", { "cli_arguments": { - edq.core.config.GLOBAL_CONFIG_KEY: os.path.join(temp_dir, "malformed", edq.core.config.DEFAULT_CONFIG_FILENAME), + edq.core.config.GLOBAL_CONFIG_PATH_KEY: os.path.join(temp_dir, "malformed", edq.core.config.DEFAULT_CONFIG_FILENAME), }, }, {}, @@ -233,7 +233,7 @@ def test_get_tiered_config_base(self): "empty-dir", { "cli_arguments": { - edq.core.config.GLOBAL_CONFIG_KEY: os.path.join(temp_dir, "multiple-options", edq.core.config.DEFAULT_CONFIG_FILENAME), + edq.core.config.GLOBAL_CONFIG_PATH_KEY: os.path.join(temp_dir, "multiple-options", edq.core.config.DEFAULT_CONFIG_FILENAME), edq.core.config.IGNORE_CONFIGS_KEY: [ "pass", ], @@ -250,7 +250,7 @@ def test_get_tiered_config_base(self): }, { - edq.core.config.GLOBAL_CONFIG_KEY: os.path.join(temp_dir, "multiple-options", edq.core.config.DEFAULT_CONFIG_FILENAME), + edq.core.config.GLOBAL_CONFIG_PATH_KEY: os.path.join(temp_dir, "multiple-options", edq.core.config.DEFAULT_CONFIG_FILENAME), }, None, ), @@ -260,7 +260,7 @@ def test_get_tiered_config_base(self): "empty-dir", { "cli_arguments": { - edq.core.config.GLOBAL_CONFIG_KEY: os.path.join(temp_dir, "simple", edq.core.config.DEFAULT_CONFIG_FILENAME), + edq.core.config.GLOBAL_CONFIG_PATH_KEY: os.path.join(temp_dir, "simple", edq.core.config.DEFAULT_CONFIG_FILENAME), edq.core.config.IGNORE_CONFIGS_KEY: [ "non-existing-option", ], @@ -276,7 +276,7 @@ def test_get_tiered_config_base(self): ), }, { - edq.core.config.GLOBAL_CONFIG_KEY: os.path.join(temp_dir, "simple", edq.core.config.DEFAULT_CONFIG_FILENAME), + edq.core.config.GLOBAL_CONFIG_PATH_KEY: os.path.join(temp_dir, "simple", edq.core.config.DEFAULT_CONFIG_FILENAME), }, None, ), @@ -297,7 +297,7 @@ def test_get_tiered_config_base(self): ), }, { - edq.core.config.LOCAL_CONFIG_KEY: os.path.join(temp_dir, "simple", edq.core.config.DEFAULT_CONFIG_FILENAME), + edq.core.config.LOCAL_CONFIG_PATH_KEY: os.path.join(temp_dir, "simple", edq.core.config.DEFAULT_CONFIG_FILENAME), }, None, ), @@ -319,7 +319,7 @@ def test_get_tiered_config_base(self): }, { edq.core.config.FILENAME_KEY: "custom-edq-config.json", - edq.core.config.LOCAL_CONFIG_KEY: os.path.join(temp_dir, "custom-name","custom-edq-config.json"), + edq.core.config.LOCAL_CONFIG_PATH_KEY: os.path.join(temp_dir, "custom-name","custom-edq-config.json"), }, None, ), @@ -340,7 +340,7 @@ def test_get_tiered_config_base(self): ), }, { - edq.core.config.LOCAL_CONFIG_KEY: os.path.join(temp_dir, "old-name", "config.json"), + edq.core.config.LOCAL_CONFIG_PATH_KEY: os.path.join(temp_dir, "old-name", "config.json"), }, None, ), @@ -359,7 +359,7 @@ def test_get_tiered_config_base(self): ), }, { - edq.core.config.LOCAL_CONFIG_KEY: os.path.join(temp_dir, "nested", edq.core.config.DEFAULT_CONFIG_FILENAME), + edq.core.config.LOCAL_CONFIG_PATH_KEY: os.path.join(temp_dir, "nested", edq.core.config.DEFAULT_CONFIG_FILENAME), }, None, ), @@ -383,7 +383,7 @@ def test_get_tiered_config_base(self): {}, {}, { - edq.core.config.LOCAL_CONFIG_KEY: os.path.join(temp_dir, "empty", edq.core.config.DEFAULT_CONFIG_FILENAME), + edq.core.config.LOCAL_CONFIG_PATH_KEY: os.path.join(temp_dir, "empty", edq.core.config.DEFAULT_CONFIG_FILENAME), }, None, ), @@ -438,7 +438,7 @@ def test_get_tiered_config_base(self): ), }, { - edq.core.config.LOCAL_CONFIG_KEY: os.path.join(temp_dir, "multiple-options", edq.core.config.DEFAULT_CONFIG_FILENAME), + edq.core.config.LOCAL_CONFIG_PATH_KEY: os.path.join(temp_dir, "multiple-options", edq.core.config.DEFAULT_CONFIG_FILENAME), }, None, ), @@ -463,7 +463,7 @@ def test_get_tiered_config_base(self): ), }, { - edq.core.config.LOCAL_CONFIG_KEY: os.path.join(temp_dir, "simple", edq.core.config.DEFAULT_CONFIG_FILENAME) + edq.core.config.LOCAL_CONFIG_PATH_KEY: os.path.join(temp_dir, "simple", edq.core.config.DEFAULT_CONFIG_FILENAME) }, None, ), @@ -485,7 +485,10 @@ def test_get_tiered_config_base(self): ), }, { - edq.core.config.LOCAL_CONFIG_KEY: os.path.join(temp_dir, "nested", "nest1", "nest2b", edq.core.config.DEFAULT_CONFIG_FILENAME), + edq.core.config.LOCAL_CONFIG_PATH_KEY: os.path.join( + temp_dir, "nested", "nest1", "nest2b", + edq.core.config.DEFAULT_CONFIG_FILENAME, + ), }, None, ), @@ -826,7 +829,7 @@ def test_get_tiered_config_base(self): "simple", { "cli_arguments": { - edq.core.config.GLOBAL_CONFIG_KEY: os.path.join(temp_dir, "global", edq.core.config.DEFAULT_CONFIG_FILENAME), + edq.core.config.GLOBAL_CONFIG_PATH_KEY: os.path.join(temp_dir, "global", edq.core.config.DEFAULT_CONFIG_FILENAME), }, }, { @@ -839,8 +842,8 @@ def test_get_tiered_config_base(self): ), }, { - edq.core.config.GLOBAL_CONFIG_KEY: os.path.join(temp_dir, "global", edq.core.config.DEFAULT_CONFIG_FILENAME), - edq.core.config.LOCAL_CONFIG_KEY: os.path.join(temp_dir, "simple", edq.core.config.DEFAULT_CONFIG_FILENAME), + edq.core.config.GLOBAL_CONFIG_PATH_KEY: os.path.join(temp_dir, "global", edq.core.config.DEFAULT_CONFIG_FILENAME), + edq.core.config.LOCAL_CONFIG_PATH_KEY: os.path.join(temp_dir, "simple", edq.core.config.DEFAULT_CONFIG_FILENAME), }, None, ), @@ -853,7 +856,7 @@ def test_get_tiered_config_base(self): edq.core.config.CONFIG_PATHS_KEY: [ os.path.join(temp_dir, "simple", edq.core.config.DEFAULT_CONFIG_FILENAME), ], - edq.core.config.GLOBAL_CONFIG_KEY: os.path.join(temp_dir, "global", edq.core.config.DEFAULT_CONFIG_FILENAME), + edq.core.config.GLOBAL_CONFIG_PATH_KEY: os.path.join(temp_dir, "global", edq.core.config.DEFAULT_CONFIG_FILENAME), }, }, { @@ -866,7 +869,7 @@ def test_get_tiered_config_base(self): ), }, { - edq.core.config.GLOBAL_CONFIG_KEY: os.path.join(temp_dir, "global", edq.core.config.DEFAULT_CONFIG_FILENAME), + edq.core.config.GLOBAL_CONFIG_PATH_KEY: os.path.join(temp_dir, "global", edq.core.config.DEFAULT_CONFIG_FILENAME), }, None, ), @@ -879,7 +882,7 @@ def test_get_tiered_config_base(self): edq.core.config.CONFIGS_KEY: [ "user=user@test.edulinq.org", ], - edq.core.config.GLOBAL_CONFIG_KEY: os.path.join(temp_dir, "global", edq.core.config.DEFAULT_CONFIG_FILENAME), + edq.core.config.GLOBAL_CONFIG_PATH_KEY: os.path.join(temp_dir, "global", edq.core.config.DEFAULT_CONFIG_FILENAME), }, }, { @@ -889,7 +892,7 @@ def test_get_tiered_config_base(self): "user": edq.core.config.ConfigSource(label = edq.core.config.CONFIG_SOURCE_CLI), }, { - edq.core.config.GLOBAL_CONFIG_KEY: os.path.join(temp_dir, "global", edq.core.config.DEFAULT_CONFIG_FILENAME), + edq.core.config.GLOBAL_CONFIG_PATH_KEY: os.path.join(temp_dir, "global", edq.core.config.DEFAULT_CONFIG_FILENAME), }, None, ), @@ -914,7 +917,7 @@ def test_get_tiered_config_base(self): ), }, { - edq.core.config.LOCAL_CONFIG_KEY: os.path.join(temp_dir, "simple", edq.core.config.DEFAULT_CONFIG_FILENAME), + edq.core.config.LOCAL_CONFIG_PATH_KEY: os.path.join(temp_dir, "simple", edq.core.config.DEFAULT_CONFIG_FILENAME), }, None, ), @@ -936,7 +939,7 @@ def test_get_tiered_config_base(self): "user": edq.core.config.ConfigSource(label = edq.core.config.CONFIG_SOURCE_CLI), }, { - edq.core.config.LOCAL_CONFIG_KEY: os.path.join(temp_dir, "simple", edq.core.config.DEFAULT_CONFIG_FILENAME), + edq.core.config.LOCAL_CONFIG_PATH_KEY: os.path.join(temp_dir, "simple", edq.core.config.DEFAULT_CONFIG_FILENAME), }, None, ), @@ -975,7 +978,7 @@ def test_get_tiered_config_base(self): edq.core.config.CONFIG_PATHS_KEY: [ os.path.join(temp_dir, "simple", edq.core.config.DEFAULT_CONFIG_FILENAME), ], - edq.core.config.GLOBAL_CONFIG_KEY: os.path.join(temp_dir, "global", edq.core.config.DEFAULT_CONFIG_FILENAME), + edq.core.config.GLOBAL_CONFIG_PATH_KEY: os.path.join(temp_dir, "global", edq.core.config.DEFAULT_CONFIG_FILENAME), }, }, { @@ -985,7 +988,7 @@ def test_get_tiered_config_base(self): "user": edq.core.config.ConfigSource(label = edq.core.config.CONFIG_SOURCE_CLI), }, { - edq.core.config.GLOBAL_CONFIG_KEY: os.path.join(temp_dir, "global", edq.core.config.DEFAULT_CONFIG_FILENAME), + edq.core.config.GLOBAL_CONFIG_PATH_KEY: os.path.join(temp_dir, "global", edq.core.config.DEFAULT_CONFIG_FILENAME), }, None, ), @@ -998,7 +1001,7 @@ def test_get_tiered_config_base(self): edq.core.config.CONFIGS_KEY: [ "user=user@test.edulinq.org", ], - edq.core.config.GLOBAL_CONFIG_KEY: os.path.join(temp_dir, "global", edq.core.config.DEFAULT_CONFIG_FILENAME), + edq.core.config.GLOBAL_CONFIG_PATH_KEY: os.path.join(temp_dir, "global", edq.core.config.DEFAULT_CONFIG_FILENAME), }, }, { @@ -1008,8 +1011,8 @@ def test_get_tiered_config_base(self): "user": edq.core.config.ConfigSource(label = edq.core.config.CONFIG_SOURCE_CLI), }, { - edq.core.config.GLOBAL_CONFIG_KEY: os.path.join(temp_dir, "global", edq.core.config.DEFAULT_CONFIG_FILENAME), - edq.core.config.LOCAL_CONFIG_KEY: os.path.join(temp_dir, "simple", edq.core.config.DEFAULT_CONFIG_FILENAME), + edq.core.config.GLOBAL_CONFIG_PATH_KEY: os.path.join(temp_dir, "global", edq.core.config.DEFAULT_CONFIG_FILENAME), + edq.core.config.LOCAL_CONFIG_PATH_KEY: os.path.join(temp_dir, "simple", edq.core.config.DEFAULT_CONFIG_FILENAME), }, None, ), @@ -1022,7 +1025,7 @@ def test_get_tiered_config_base(self): edq.core.config.CONFIG_PATHS_KEY: [ os.path.join(temp_dir, "custom-name", "custom-edq-config.json"), ], - edq.core.config.GLOBAL_CONFIG_KEY: os.path.join(temp_dir, "global", edq.core.config.DEFAULT_CONFIG_FILENAME), + edq.core.config.GLOBAL_CONFIG_PATH_KEY: os.path.join(temp_dir, "global", edq.core.config.DEFAULT_CONFIG_FILENAME), }, }, { @@ -1035,8 +1038,8 @@ def test_get_tiered_config_base(self): ), }, { - edq.core.config.GLOBAL_CONFIG_KEY: os.path.join(temp_dir, "global", edq.core.config.DEFAULT_CONFIG_FILENAME), - edq.core.config.LOCAL_CONFIG_KEY: os.path.join(temp_dir, "simple", edq.core.config.DEFAULT_CONFIG_FILENAME), + edq.core.config.GLOBAL_CONFIG_PATH_KEY: os.path.join(temp_dir, "global", edq.core.config.DEFAULT_CONFIG_FILENAME), + edq.core.config.LOCAL_CONFIG_PATH_KEY: os.path.join(temp_dir, "simple", edq.core.config.DEFAULT_CONFIG_FILENAME), }, None, ), @@ -1061,7 +1064,7 @@ def test_get_tiered_config_base(self): "user": edq.core.config.ConfigSource(label = edq.core.config.CONFIG_SOURCE_CLI), }, { - edq.core.config.LOCAL_CONFIG_KEY: os.path.join(temp_dir, "simple", edq.core.config.DEFAULT_CONFIG_FILENAME), + edq.core.config.LOCAL_CONFIG_PATH_KEY: os.path.join(temp_dir, "simple", edq.core.config.DEFAULT_CONFIG_FILENAME), }, None, ), @@ -1077,7 +1080,7 @@ def test_get_tiered_config_base(self): edq.core.config.CONFIGS_KEY: [ "pass=password1234", ], - edq.core.config.GLOBAL_CONFIG_KEY: os.path.join(temp_dir, "global", edq.core.config.DEFAULT_CONFIG_FILENAME), + edq.core.config.GLOBAL_CONFIG_PATH_KEY: os.path.join(temp_dir, "global", edq.core.config.DEFAULT_CONFIG_FILENAME), }, }, { @@ -1092,8 +1095,8 @@ def test_get_tiered_config_base(self): "pass": edq.core.config.ConfigSource(label = edq.core.config.CONFIG_SOURCE_CLI), }, { - edq.core.config.GLOBAL_CONFIG_KEY: os.path.join(temp_dir, "global", edq.core.config.DEFAULT_CONFIG_FILENAME), - edq.core.config.LOCAL_CONFIG_KEY: os.path.join(temp_dir, "simple", edq.core.config.DEFAULT_CONFIG_FILENAME), + edq.core.config.GLOBAL_CONFIG_PATH_KEY: os.path.join(temp_dir, "global", edq.core.config.DEFAULT_CONFIG_FILENAME), + edq.core.config.LOCAL_CONFIG_PATH_KEY: os.path.join(temp_dir, "simple", edq.core.config.DEFAULT_CONFIG_FILENAME), }, None, ), @@ -1106,12 +1109,12 @@ def test_get_tiered_config_base(self): cli_args = extra_args.get("cli_arguments", None) if cli_args is None: extra_args["cli_arguments"] = { - edq.core.config.GLOBAL_CONFIG_KEY: os.path.join(temp_dir, "empty", edq.core.config.DEFAULT_CONFIG_FILENAME) + edq.core.config.GLOBAL_CONFIG_PATH_KEY: os.path.join(temp_dir, "empty", edq.core.config.DEFAULT_CONFIG_FILENAME) } else: - cli_global_config_path = cli_args.get(edq.core.config.GLOBAL_CONFIG_KEY, None) + cli_global_config_path = cli_args.get(edq.core.config.GLOBAL_CONFIG_PATH_KEY, None) if cli_global_config_path is None: - extra_args["cli_arguments"][edq.core.config.GLOBAL_CONFIG_KEY] = os.path.join( + extra_args["cli_arguments"][edq.core.config.GLOBAL_CONFIG_PATH_KEY] = os.path.join( temp_dir, "empty", edq.core.config.DEFAULT_CONFIG_FILENAME ) @@ -1119,15 +1122,15 @@ def test_get_tiered_config_base(self): if (cutoff is None): extra_args["local_config_root_cutoff"] = temp_dir - global_file_used = expected_config_params.get(edq.core.config.GLOBAL_CONFIG_KEY, None) + global_file_used = expected_config_params.get(edq.core.config.GLOBAL_CONFIG_PATH_KEY, None) if (global_file_used is None): - expected_config_params[edq.core.config.GLOBAL_CONFIG_KEY] = os.path.join( + expected_config_params[edq.core.config.GLOBAL_CONFIG_PATH_KEY] = os.path.join( temp_dir, "empty", edq.core.config.DEFAULT_CONFIG_FILENAME ) - local_file_used = expected_config_params.get(edq.core.config.LOCAL_CONFIG_KEY, None) + local_file_used = expected_config_params.get(edq.core.config.LOCAL_CONFIG_PATH_KEY, None) if (local_file_used is None): - expected_config_params[edq.core.config.LOCAL_CONFIG_KEY] = None + expected_config_params[edq.core.config.LOCAL_CONFIG_PATH_KEY] = None file_name_used = expected_config_params.get(edq.core.config.FILENAME_KEY, None) if (file_name_used is None): From cb95745cf9faaff130e76e9797c6fa87499a69a2 Mon Sep 17 00:00:00 2001 From: BatuhanSA Date: Thu, 9 Oct 2025 10:00:16 -0700 Subject: [PATCH 04/19] Revised the set testing structure and edited some help messages. --- edq/cli/config/set.py | 40 ++++--- edq/cli/config/set_test.py | 229 ++++++++++++++++++++++++++++++------- 2 files changed, 209 insertions(+), 60 deletions(-) diff --git a/edq/cli/config/set.py b/edq/cli/config/set.py index 236e196..5e85ffd 100644 --- a/edq/cli/config/set.py +++ b/edq/cli/config/set.py @@ -3,6 +3,7 @@ """ import argparse +import os import sys import typing @@ -11,10 +12,11 @@ import edq.util.dirent import edq.util.json -def write_configs_to_file(file_path: str, configs_to_write: typing.List[str]) -> None: +def write_config_to_file(file_path: str, configs_to_write: typing.List[str]) -> None: """ Write configs to a specified file path. Create the path if it do not exist. """ if (not (edq.util.dirent.exists(file_path))): + edq.util.dirent.mkdir(os.path.dirname(file_path)) edq.util.json.dump_path({}, file_path) config = edq.util.json.load_path(file_path) @@ -38,19 +40,19 @@ def write_configs_to_file(file_path: str, configs_to_write: typing.List[str]) -> def run_cli(args: argparse.Namespace) -> int: """ Run the CLI. """ - if (not (args.write_to_local or args.write_to_global or (args.file_to_write is not None))): - args.write_to_local = True + if (not (args.set_is_local or args.set_is_global or (args.set_to_file_path is not None))): + args.set_is_local = True - if (args.write_to_local): + if (args.set_is_local): local_config_path = args._config_params.get(edq.core.config.LOCAL_CONFIG_PATH_KEY) if (local_config_path is None): local_config_path = args._config_params.get(edq.core.config.FILENAME_KEY) - write_configs_to_file(local_config_path, args.config_to_set) - elif (args.write_to_global): + write_config_to_file(local_config_path, args.config_to_set) + elif (args.set_is_global): global_config_path = args._config_params.get(edq.core.config.GLOBAL_CONFIG_PATH_KEY) - write_configs_to_file(global_config_path, args.config_to_set) - elif (args.file_to_write is not None): - write_configs_to_file(args.file_to_write, args.config_to_set) + write_config_to_file(global_config_path, args.config_to_set) + elif (args.set_to_file_path is not None): + write_config_to_file(args.set_to_file_path, args.config_to_set) return 0 @@ -72,24 +74,28 @@ def modify_parser(parser: argparse.ArgumentParser) -> None: parser.add_argument('config_to_set', metavar = "=", action = 'store', nargs = '+', type = str, - help = 'Configuration option to be set. Expected config format is =.', + help = ('Configuration option to be set. Expected config format is =.'), ) group = parser.add_mutually_exclusive_group() group.add_argument('--local', - action = 'store_true', dest = 'write_to_local', - help = 'Sets the variable in local config file.', + action = 'store_true', dest = 'set_is_local', + help = ('Set the configuration option in a local config file if one exists. ' + + ' If no local config file is found, a new one will be created in the current directory.'), ) group.add_argument('--global', - action = 'store_true', dest = 'write_to_global', - help = 'Sets the variable in global config file.', + action = 'store_true', dest = 'set_is_global', + help = ('Set the configuration option in the global config file if it exists.' + + " If it doesn't exist, it will be created." + + " Use '--config-global' to view or change the global config file location."), ) - group.add_argument('--file', - action = 'store', type = str, default = None, dest = 'file_to_write', - help = 'Sets the variable in a specified config file.', + group.add_argument('--file', metavar = "", + action = 'store', type = str, default = None, dest = 'set_to_file_path', + help = ('Set the config option in a specified config file.' + + " If the given file path doesn't exist, it will be created.") ) if (__name__ == '__main__'): diff --git a/edq/cli/config/set_test.py b/edq/cli/config/set_test.py index 3967e0f..662f0b2 100644 --- a/edq/cli/config/set_test.py +++ b/edq/cli/config/set_test.py @@ -1,5 +1,6 @@ import argparse import os +import typing import edq.testing.unittest import edq.cli.config.set @@ -45,79 +46,221 @@ def create_test_dir(temp_dir_prefix: str) -> str: return temp_dir +def replace_placeholders_str(unresolved_object: str, placeholder: str, replacement: str) -> str: + """ + In-place replacement of a placeholder in a string with a given replacement. + If the input is not a string, no changes are made and the original object is returned. + """ + + if(isinstance(unresolved_object, str)): + unresolved_object = unresolved_object.replace(placeholder, replacement) + + return unresolved_object + +def replace_placeholders_dict(unresolved_object: dict, placeholder: str, replacement: str) -> dict: + """ Performs an in-place, recursive replacement of placeholder with replacement across all levels of a nested dictionary. """ + + if(not isinstance(unresolved_object, dict)): + raise TypeError(f"Expected unresolved object to be dict, got {type(unresolved_object).__name__}") + + for element in unresolved_object: + if (isinstance(unresolved_object[element], dict)): + unresolved_object[element] = replace_placeholders_dict(unresolved_object[element], placeholder = placeholder, replacement = replacement) + if (isinstance(unresolved_object[element], str)): + unresolved_object[element] = replace_placeholders_str(unresolved_object[element], placeholder = placeholder, replacement = replacement) + + return unresolved_object + +def replace_tempdir_placeholder( + cli_args: typing.Dict[str, typing.Union[str, typing.List, bool]], + expected_result: typing.List, + temp_dir_path: str, + ) -> typing.Tuple[typing.Dict[str, typing.Union[str, typing.List]], typing.List[typing.Dict[str, typing.Union[str, typing.Dict[str, str]]]]]: + """ Replaces all occurrences of 'TEMP_DIR' in CLI arguments and expected results with the actual temporary directory path. """ + + + resolved_expected_result = [] + for file in expected_result: + set_to_file_path = file.get("path") + data = file.get("data") + set_to_file_path = replace_placeholders_str(set_to_file_path, 'TEMP_DIR', temp_dir_path) + resolved_expected_result.append({ + "path": set_to_file_path, + "data": data + }) + + resolved_cli_args = replace_placeholders_dict(cli_args, 'TEMP_DIR', temp_dir_path) + return resolved_cli_args, resolved_expected_result + class TestSetConfig(edq.testing.unittest.BaseTest): """ Test basic functionality of set config. """ def test_set_base(self): """ - Test that the set command creates configuration files and writes the specified configuration correctly. + Test that the 'set' command creates configuration files and writes the specified configuration correctly. + 'TEMP_DIR' gets replaced with actual temp dir path when testing. """ # [(set cli arguments, expected result, error substring), ...] test_cases = [ + # Invalid Option + + # Empty Key + ( + { + "config_to_set": ["=user@test.edulinq.org"], + }, + [], + "Found an empty configuration option key associated with the value 'user@test.edulinq.org'.", + ), + + # Missing '=' + ( + { + "config_to_set": ["useruser@test.edulinq.org"], + }, + [], + "Configuration options must be provided in the format `=` when passed via the CLI.", + ), + + # Local Config + + # No Config ( { "config_to_set": ["user=user@test.edulinq.org"], }, - ( + [ { - "path": os.path.join("TEMP_DIR", edq.core.config.DEFAULT_CONFIG_FILENAME), + "path": os.path.join('TEMP_DIR', edq.core.config.DEFAULT_CONFIG_FILENAME), "data": {"user": "user@test.edulinq.org"}, }, - ), + ], None, - ) + ), - ] + # Empty Config + ( + { + "config_to_set": ["user=user@test.edulinq.org"], + "_config_params": { + edq.core.config.LOCAL_CONFIG_PATH_KEY: os.path.join("TEMP_DIR", "empty-config", edq.core.config.DEFAULT_CONFIG_FILENAME), + }, + }, + [ + { + "path": os.path.join("TEMP_DIR", "empty-config", edq.core.config.DEFAULT_CONFIG_FILENAME), + "data": {"user": "user@test.edulinq.org"}, + }, + ], + None, + ), + + # Non Empty Config + ( + { + "config_to_set": ["pass=password123"], + "_config_params": { + edq.core.config.LOCAL_CONFIG_PATH_KEY: os.path.join("TEMP_DIR", "non-empty-config", edq.core.config.DEFAULT_CONFIG_FILENAME), + }, + }, + [ + { + "path": os.path.join("TEMP_DIR", "non-empty-config", edq.core.config.DEFAULT_CONFIG_FILENAME), + "data": {"user": "user@test.edulinq.org", "pass": "password123"}, + }, + ], + None, + ), + + # Global Config + + # No Config + ( + { + "config_to_set": ["user=user@test.edulinq.org"], + "set_is_global": True, + }, + [ + { + "path": os.path.join("TEMP_DIR", GLOBAL_DIR, edq.core.config.DEFAULT_CONFIG_FILENAME), + "data": {"user": "user@test.edulinq.org"}, + }, + ], + None, + ), + + # File Config - for test_case in test_cases: - temp_dir = create_test_dir(temp_dir_prefix = "edq-test-config-set-") + # Non Existent File + ( + { + "config_to_set": ["user=user@test.edulinq.org"], + "set_to_file_path": os.path.join("TEMP_DIR", "non-existent", "path", edq.core.config.DEFAULT_CONFIG_FILENAME), + }, + [ + { + "path": os.path.join("TEMP_DIR", "non-existent", "path", edq.core.config.DEFAULT_CONFIG_FILENAME), + "data": {"user": "user@test.edulinq.org"}, + }, + ], + None, + ), + ] + for (i, test_case) in enumerate(test_cases): cli_arguments, expected_result, error_substring = test_case - set_args = argparse.Namespace( - write_to_local = cli_arguments.get("write_to_local", False), - write_to_global = cli_arguments.get("write_to_global", False), - file_to_write = cli_arguments.get("file_to_write", None), - config_to_set = cli_arguments.get("config_to_set"), - _config_params = { - edq.core.config.LOCAL_CONFIG_PATH_KEY: cli_arguments.get(edq.core.config.LOCAL_CONFIG_PATH_KEY, None), - edq.core.config.FILENAME_KEY: cli_arguments.get(edq.core.config.FILENAME_KEY, edq.core.config.DEFAULT_CONFIG_FILENAME), - edq.core.config.GLOBAL_CONFIG_PATH_KEY: cli_arguments.get( - edq.core.config.GLOBAL_CONFIG_PATH_KEY, - os.path.join(temp_dir, GLOBAL_DIR) - ) - } - ) + with self.subTest(msg = f"Case {i}"): + temp_dir = create_test_dir(temp_dir_prefix = "edq-test-config-set-") + + (cli_arguments, expected_result) = replace_tempdir_placeholder(cli_arguments, expected_result, temp_dir) + + config_params = cli_arguments.get("_config_params", {}) + filename = config_params.get(edq.core.config.FILENAME_KEY, edq.core.config.DEFAULT_CONFIG_FILENAME) + + set_args = argparse.Namespace( + set_is_local = cli_arguments.get("set_is_local", False), + set_is_global = cli_arguments.get("set_is_global", False), + set_to_file_path = cli_arguments.get("set_to_file_path", None), + config_to_set = cli_arguments.get("config_to_set"), + _config_params = { + edq.core.config.LOCAL_CONFIG_PATH_KEY: config_params.get(edq.core.config.LOCAL_CONFIG_PATH_KEY, None), + edq.core.config.GLOBAL_CONFIG_PATH_KEY: config_params.get( + edq.core.config.GLOBAL_CONFIG_PATH_KEY, + os.path.join(temp_dir, GLOBAL_DIR, filename), + ), + edq.core.config.FILENAME_KEY: filename + } + ) - error_substring = None + previous_work_directory = os.getcwd() + os.chdir(temp_dir) - previous_work_directory = os.getcwd() - os.chdir(temp_dir) + try: + edq.cli.config.set.run_cli(set_args) + except Exception as ex: + error_string = self.format_error_string(ex) - try: - edq.cli.config.set.run_cli(set_args) - except Exception as ex: - error_string = self.format_error_string(ex) + if (error_substring is None): + self.fail(f"Unexpected error: '{error_string}'.") - if (error_substring is None): - self.fail(f"Unexpected error: '{error_string}'.") + self.assertIn(error_substring, error_string, 'Error is not as expected.') - self.assertIn(error_substring, error_string, 'Error is not as expected.') + continue - finally: - os.chdir(previous_work_directory) + finally: + os.chdir(previous_work_directory) - if (error_substring is not None): - self.fail(f"Did not get expected error: '{error_substring}'.") + if (error_substring is not None): + self.fail(f"Did not get expected error: '{error_substring}'.") - for file in expected_result: - file_path = file.get("path") - file_path = file_path.replace('TEMP_DIR', temp_dir) + for file in expected_result: + set_to_file_path = file.get("path") - if (not edq.util.dirent.exists(file_path)): - self.fail("Expected file doesn't exist.") + if (not edq.util.dirent.exists(set_to_file_path)): + self.fail(f"Expected file does not exist at path: {set_to_file_path}") - data_actual = edq.util.json.load_path(file_path) + data_actual = edq.util.json.load_path(set_to_file_path) - self.assertJSONDictEqual(data_actual, file.get("data")) + self.assertJSONDictEqual(data_actual, file.get("data")) From 9b9d72c7f1a9d8da92e8e40458b94b82fdbf2f8e Mon Sep 17 00:00:00 2001 From: BatuhanSA Date: Fri, 10 Oct 2025 23:33:46 -0700 Subject: [PATCH 05/19] Added cwd to cli testing and changed the output separator. --- edq/cli/config/set_test.py | 102 ++++++++---------- edq/testing/cli.py | 19 +++- .../config/set/config_set_base_global.txt | 9 ++ .../config/set/config_set_base_local.txt | 7 ++ .../config_set_mutually_exclusive_flags.txt | 13 +++ 5 files changed, 91 insertions(+), 59 deletions(-) create mode 100644 edq/testing/testdata/cli/tests/config/set/config_set_base_global.txt create mode 100644 edq/testing/testdata/cli/tests/config/set/config_set_base_local.txt create mode 100644 edq/testing/testdata/cli/tests/config/set/config_set_mutually_exclusive_flags.txt diff --git a/edq/cli/config/set_test.py b/edq/cli/config/set_test.py index 662f0b2..40e9703 100644 --- a/edq/cli/config/set_test.py +++ b/edq/cli/config/set_test.py @@ -1,6 +1,5 @@ import argparse import os -import typing import edq.testing.unittest import edq.cli.config.set @@ -46,52 +45,6 @@ def create_test_dir(temp_dir_prefix: str) -> str: return temp_dir -def replace_placeholders_str(unresolved_object: str, placeholder: str, replacement: str) -> str: - """ - In-place replacement of a placeholder in a string with a given replacement. - If the input is not a string, no changes are made and the original object is returned. - """ - - if(isinstance(unresolved_object, str)): - unresolved_object = unresolved_object.replace(placeholder, replacement) - - return unresolved_object - -def replace_placeholders_dict(unresolved_object: dict, placeholder: str, replacement: str) -> dict: - """ Performs an in-place, recursive replacement of placeholder with replacement across all levels of a nested dictionary. """ - - if(not isinstance(unresolved_object, dict)): - raise TypeError(f"Expected unresolved object to be dict, got {type(unresolved_object).__name__}") - - for element in unresolved_object: - if (isinstance(unresolved_object[element], dict)): - unresolved_object[element] = replace_placeholders_dict(unresolved_object[element], placeholder = placeholder, replacement = replacement) - if (isinstance(unresolved_object[element], str)): - unresolved_object[element] = replace_placeholders_str(unresolved_object[element], placeholder = placeholder, replacement = replacement) - - return unresolved_object - -def replace_tempdir_placeholder( - cli_args: typing.Dict[str, typing.Union[str, typing.List, bool]], - expected_result: typing.List, - temp_dir_path: str, - ) -> typing.Tuple[typing.Dict[str, typing.Union[str, typing.List]], typing.List[typing.Dict[str, typing.Union[str, typing.Dict[str, str]]]]]: - """ Replaces all occurrences of 'TEMP_DIR' in CLI arguments and expected results with the actual temporary directory path. """ - - - resolved_expected_result = [] - for file in expected_result: - set_to_file_path = file.get("path") - data = file.get("data") - set_to_file_path = replace_placeholders_str(set_to_file_path, 'TEMP_DIR', temp_dir_path) - resolved_expected_result.append({ - "path": set_to_file_path, - "data": data - }) - - resolved_cli_args = replace_placeholders_dict(cli_args, 'TEMP_DIR', temp_dir_path) - return resolved_cli_args, resolved_expected_result - class TestSetConfig(edq.testing.unittest.BaseTest): """ Test basic functionality of set config. """ @@ -132,7 +85,7 @@ def test_set_base(self): }, [ { - "path": os.path.join('TEMP_DIR', edq.core.config.DEFAULT_CONFIG_FILENAME), + "path": edq.core.config.DEFAULT_CONFIG_FILENAME, "data": {"user": "user@test.edulinq.org"}, }, ], @@ -144,12 +97,12 @@ def test_set_base(self): { "config_to_set": ["user=user@test.edulinq.org"], "_config_params": { - edq.core.config.LOCAL_CONFIG_PATH_KEY: os.path.join("TEMP_DIR", "empty-config", edq.core.config.DEFAULT_CONFIG_FILENAME), + edq.core.config.LOCAL_CONFIG_PATH_KEY: os.path.join("empty-config", edq.core.config.DEFAULT_CONFIG_FILENAME), }, }, [ { - "path": os.path.join("TEMP_DIR", "empty-config", edq.core.config.DEFAULT_CONFIG_FILENAME), + "path": os.path.join("empty-config", edq.core.config.DEFAULT_CONFIG_FILENAME), "data": {"user": "user@test.edulinq.org"}, }, ], @@ -161,12 +114,12 @@ def test_set_base(self): { "config_to_set": ["pass=password123"], "_config_params": { - edq.core.config.LOCAL_CONFIG_PATH_KEY: os.path.join("TEMP_DIR", "non-empty-config", edq.core.config.DEFAULT_CONFIG_FILENAME), + edq.core.config.LOCAL_CONFIG_PATH_KEY: os.path.join("non-empty-config", edq.core.config.DEFAULT_CONFIG_FILENAME), }, }, [ { - "path": os.path.join("TEMP_DIR", "non-empty-config", edq.core.config.DEFAULT_CONFIG_FILENAME), + "path": os.path.join("non-empty-config", edq.core.config.DEFAULT_CONFIG_FILENAME), "data": {"user": "user@test.edulinq.org", "pass": "password123"}, }, ], @@ -183,24 +136,60 @@ def test_set_base(self): }, [ { - "path": os.path.join("TEMP_DIR", GLOBAL_DIR, edq.core.config.DEFAULT_CONFIG_FILENAME), + "path": os.path.join(GLOBAL_DIR, edq.core.config.DEFAULT_CONFIG_FILENAME), + "data": {"user": "user@test.edulinq.org"}, + }, + ], + None, + ), + + # Empty Config + ( + { + "config_to_set": ["user=user@test.edulinq.org"], + "set_is_global": True, + "_config_params": { + edq.core.config.GLOBAL_CONFIG_PATH_KEY: os.path.join("empty-config", edq.core.config.DEFAULT_CONFIG_FILENAME) + } + }, + [ + { + "path": os.path.join("empty-config", edq.core.config.DEFAULT_CONFIG_FILENAME), "data": {"user": "user@test.edulinq.org"}, }, ], None, ), + # Non Empty Config + ( + { + "config_to_set": ["pass=password123"], + "_config_params": { + edq.core.config.GLOBAL_CONFIG_PATH_KEY: os.path.join("non-empty-config", edq.core.config.DEFAULT_CONFIG_FILENAME), + }, + "set_is_global": True, + }, + [ + { + "path": os.path.join("non-empty-config", edq.core.config.DEFAULT_CONFIG_FILENAME), + "data": {"user": "user@test.edulinq.org", "pass": "password123"}, + }, + ], + None, + ), + # File Config # Non Existent File ( { "config_to_set": ["user=user@test.edulinq.org"], - "set_to_file_path": os.path.join("TEMP_DIR", "non-existent", "path", edq.core.config.DEFAULT_CONFIG_FILENAME), + "set_to_file_path": os.path.join("non-existent", "path", edq.core.config.DEFAULT_CONFIG_FILENAME), }, [ { - "path": os.path.join("TEMP_DIR", "non-existent", "path", edq.core.config.DEFAULT_CONFIG_FILENAME), + "path": os.path.join("non-existent", "path", edq.core.config.DEFAULT_CONFIG_FILENAME), "data": {"user": "user@test.edulinq.org"}, }, ], @@ -214,8 +203,6 @@ def test_set_base(self): with self.subTest(msg = f"Case {i}"): temp_dir = create_test_dir(temp_dir_prefix = "edq-test-config-set-") - (cli_arguments, expected_result) = replace_tempdir_placeholder(cli_arguments, expected_result, temp_dir) - config_params = cli_arguments.get("_config_params", {}) filename = config_params.get(edq.core.config.FILENAME_KEY, edq.core.config.DEFAULT_CONFIG_FILENAME) @@ -257,6 +244,7 @@ def test_set_base(self): for file in expected_result: set_to_file_path = file.get("path") + set_to_file_path = os.path.join(temp_dir, set_to_file_path) if (not edq.util.dirent.exists(set_to_file_path)): self.fail(f"Expected file does not exist at path: {set_to_file_path}") diff --git a/edq/testing/cli.py b/edq/testing/cli.py index 62125c8..7146613 100644 --- a/edq/testing/cli.py +++ b/edq/testing/cli.py @@ -35,6 +35,7 @@ import edq.util.pyimport TEST_CASE_SEP: str = '---' +OUTPUT_SEP: str = '+++' DATA_DIR_ID: str = '__DATA_DIR__' ABS_DATA_DIR_ID: str = '__ABS_DATA_DIR__' TEMP_DIR_ID: str = '__TEMP_DIR__' @@ -52,6 +53,7 @@ def __init__(self, base_dir: str, data_dir: str, temp_dir: str, + cwd: typing.Union[str, None] = None, cli: typing.Union[str, None] = None, arguments: typing.Union[typing.List[str], None] = None, error: bool = False, @@ -100,6 +102,15 @@ def __init__(self, This is the expansion for `__TEMP_DIR__` paths. """ + self.cwd: str = self.temp_dir + """ + The directory that test runs from + """ + + if (cwd is not None): + self.cwd = self._expand_paths(cwd) + + edq.util.dirent.mkdir(temp_dir) if (cli is None): @@ -149,7 +160,7 @@ def __init__(self, """ Split stdout and stderr into different strings for testing. By default, these two will be combined. - If both are non-empty, then they will be joined like: f"{stdout}\n{TEST_CASE_SEP}\n{stderr}". + If both are non-empty, then they will be joined like: f"{stdout}\n{OUTPUT_SEP}\n{stderr}". Otherwise, only the non-empty one will be present with no separator. Any stdout assertions will be applied to the combined text. """ @@ -269,6 +280,9 @@ def __method(self: edq.testing.unittest.BaseTest) -> None: old_args = sys.argv sys.argv = [test_info.module.__file__] + test_info.arguments + previous_work_directory = os.getcwd() + os.chdir(test_info.cwd) + try: with contextlib.redirect_stdout(io.StringIO()) as stdout_output: with contextlib.redirect_stderr(io.StringIO()) as stderr_output: @@ -289,11 +303,12 @@ def __method(self: edq.testing.unittest.BaseTest) -> None: if (isinstance(ex, SystemExit) and (ex.__context__ is not None)): stderr_text = self.format_error_string(ex.__context__) finally: + os.chdir(previous_work_directory) sys.argv = old_args if (not test_info.split_stdout_stderr): if ((len(stdout_text) > 0) and (len(stderr_text) > 0)): - stdout_text = f"{stdout_text}\n{TEST_CASE_SEP}\n{stderr_text}" + stdout_text = f"{stdout_text}\n{OUTPUT_SEP}\n{stderr_text}" elif (len(stderr_text) > 0): stdout_text = stderr_text diff --git a/edq/testing/testdata/cli/tests/config/set/config_set_base_global.txt b/edq/testing/testdata/cli/tests/config/set/config_set_base_global.txt new file mode 100644 index 0000000..33b69c5 --- /dev/null +++ b/edq/testing/testdata/cli/tests/config/set/config_set_base_global.txt @@ -0,0 +1,9 @@ +{ + "cli": "edq.cli.config.set", + "arguments": [ + "user=user@test.edulinq.org", + "--global", + "--config-global", "__TEMP_DIR__(edq-config.json)", + ], +} +--- \ No newline at end of file diff --git a/edq/testing/testdata/cli/tests/config/set/config_set_base_local.txt b/edq/testing/testdata/cli/tests/config/set/config_set_base_local.txt new file mode 100644 index 0000000..9f67b8f --- /dev/null +++ b/edq/testing/testdata/cli/tests/config/set/config_set_base_local.txt @@ -0,0 +1,7 @@ +{ + "cli": "edq.cli.config.set", + "arguments": [ + "user=user@test.edulinq.org", + ], +} +--- \ No newline at end of file diff --git a/edq/testing/testdata/cli/tests/config/set/config_set_mutually_exclusive_flags.txt b/edq/testing/testdata/cli/tests/config/set/config_set_mutually_exclusive_flags.txt new file mode 100644 index 0000000..36c7978 --- /dev/null +++ b/edq/testing/testdata/cli/tests/config/set/config_set_mutually_exclusive_flags.txt @@ -0,0 +1,13 @@ +{ + "cli": "edq.cli.config.set", + "arguments": [ + "user=user@test.edulinq.org", + "--global", + "--local", + ], + "error": true +} +--- +builtins.SystemExit: 2 ++++ +argparse.ArgumentError: argument --local: not allowed with argument --global From 71b6054ad343e2c5cb0ef40201ca714ff9965491 Mon Sep 17 00:00:00 2001 From: BatuhanSA Date: Sat, 11 Oct 2025 07:26:42 -0700 Subject: [PATCH 06/19] Added more tests. --- edq-config.json | 1 + edq/cli/config/set.py | 5 +-- edq/cli/config/set_test.py | 35 +++++++++++++++++++ edq/core/config.py | 10 +++--- .../tests/config/set/config_set_base_file.txt | 8 +++++ .../config/set/config_set_base_local.txt | 1 + 6 files changed, 54 insertions(+), 6 deletions(-) create mode 100644 edq-config.json create mode 100644 edq/testing/testdata/cli/tests/config/set/config_set_base_file.txt diff --git a/edq-config.json b/edq-config.json new file mode 100644 index 0000000..9e26dfe --- /dev/null +++ b/edq-config.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/edq/cli/config/set.py b/edq/cli/config/set.py index 5e85ffd..2faa767 100644 --- a/edq/cli/config/set.py +++ b/edq/cli/config/set.py @@ -77,11 +77,12 @@ def modify_parser(parser: argparse.ArgumentParser) -> None: help = ('Configuration option to be set. Expected config format is =.'), ) - group = parser.add_mutually_exclusive_group() + config_file_locations = parser.add_argument_group("set config options") + group = config_file_locations.add_mutually_exclusive_group() group.add_argument('--local', action = 'store_true', dest = 'set_is_local', - help = ('Set the configuration option in a local config file if one exists. ' + help = ('Set the configuration option in a local config file if one exists.' + ' If no local config file is found, a new one will be created in the current directory.'), ) diff --git a/edq/cli/config/set_test.py b/edq/cli/config/set_test.py index 40e9703..d8fc1e6 100644 --- a/edq/cli/config/set_test.py +++ b/edq/cli/config/set_test.py @@ -133,6 +133,9 @@ def test_set_base(self): { "config_to_set": ["user=user@test.edulinq.org"], "set_is_global": True, + "_config_params": { + edq.core.config.GLOBAL_CONFIG_PATH_KEY: os.path.join(GLOBAL_DIR, edq.core.config.DEFAULT_CONFIG_FILENAME) + } }, [ { @@ -195,6 +198,38 @@ def test_set_base(self): ], None, ), + + # Empty Config + ( + { + "config_to_set": ["user=user@test.edulinq.org"], + "set_to_file_path": os.path.join("empty-config", edq.core.config.DEFAULT_CONFIG_FILENAME), + }, + [ + { + "path": os.path.join("empty-config", edq.core.config.DEFAULT_CONFIG_FILENAME), + "data": {"user": "user@test.edulinq.org"}, + }, + ], + None, + ), + + # Non Empty Config + ( + { + "config_to_set": ["pass=password123"], + "set_to_file_path": os.path.join("non-empty-config", edq.core.config.DEFAULT_CONFIG_FILENAME), + }, + [ + { + "path": os.path.join("non-empty-config", edq.core.config.DEFAULT_CONFIG_FILENAME), + "data": {"user": "user@test.edulinq.org", "pass": "password123"}, + }, + ], + None, + ), + + ] for (i, test_case) in enumerate(test_cases): diff --git a/edq/core/config.py b/edq/core/config.py index 479e091..358b5d4 100644 --- a/edq/core/config.py +++ b/edq/core/config.py @@ -210,12 +210,14 @@ def set_cli_args(parser: argparse.ArgumentParser, extra_state: typing.Dict[str, Set common CLI arguments for configuration. """ - parser.add_argument('--config-global', dest = GLOBAL_CONFIG_PATH_KEY, + group = parser.add_argument_group("load config options") + + group.add_argument('--config-global', dest = GLOBAL_CONFIG_PATH_KEY, action = 'store', type = str, default = get_global_config_path(config_filename), help = 'Set the default global config file path (default: %(default)s).', ) - parser.add_argument('--config-file', dest = CONFIG_PATHS_KEY, + group.add_argument('--config-file', dest = CONFIG_PATHS_KEY, action = 'append', type = str, default = [], help = ('Load config options from a JSON file.' + ' This flag can be specified multiple times.' @@ -223,7 +225,7 @@ def set_cli_args(parser: argparse.ArgumentParser, extra_state: typing.Dict[str, + ' Will override options form both global and local config files.') ) - parser.add_argument('--config', dest = CONFIGS_KEY, metavar = "=", + group.add_argument('--config', dest = CONFIGS_KEY, metavar = "=", action = 'append', type = str, default = [], help = ('Set a configuration option from the command-line.' + ' Specify options as = pairs.' @@ -232,7 +234,7 @@ def set_cli_args(parser: argparse.ArgumentParser, extra_state: typing.Dict[str, + ' Will override options form all config files.') ) - parser.add_argument('--ignore-config-option', dest = IGNORE_CONFIGS_KEY, + group.add_argument('--ignore-config-option', dest = IGNORE_CONFIGS_KEY, action = 'append', type = str, default = [], help = ('Ignore any config option with the specified key.' + ' The system-provided default value will be used for that option if one exists.' diff --git a/edq/testing/testdata/cli/tests/config/set/config_set_base_file.txt b/edq/testing/testdata/cli/tests/config/set/config_set_base_file.txt new file mode 100644 index 0000000..96523ec --- /dev/null +++ b/edq/testing/testdata/cli/tests/config/set/config_set_base_file.txt @@ -0,0 +1,8 @@ +{ + "cli": "edq.cli.config.set", + "arguments": [ + "user=user@test.edulinq.org", + "--file", "__TEMP_DIR__(edq-config.json)", + ], +} +--- \ No newline at end of file diff --git a/edq/testing/testdata/cli/tests/config/set/config_set_base_local.txt b/edq/testing/testdata/cli/tests/config/set/config_set_base_local.txt index 9f67b8f..7056025 100644 --- a/edq/testing/testdata/cli/tests/config/set/config_set_base_local.txt +++ b/edq/testing/testdata/cli/tests/config/set/config_set_base_local.txt @@ -2,6 +2,7 @@ "cli": "edq.cli.config.set", "arguments": [ "user=user@test.edulinq.org", + "--local" ], } --- \ No newline at end of file From cf9db9392ff6b983bba4cbf5d178b34b5409cf5c Mon Sep 17 00:00:00 2001 From: BatuhanSA Date: Sat, 11 Oct 2025 07:32:04 -0700 Subject: [PATCH 07/19] Deleting test artifact. --- edq-config.json | 1 - 1 file changed, 1 deletion(-) delete mode 100644 edq-config.json diff --git a/edq-config.json b/edq-config.json deleted file mode 100644 index 9e26dfe..0000000 --- a/edq-config.json +++ /dev/null @@ -1 +0,0 @@ -{} \ No newline at end of file From 1808972f9d6e9cabbe8caec4e1d98e989654d25f Mon Sep 17 00:00:00 2001 From: BatuhanSA Date: Sat, 11 Oct 2025 07:40:37 -0700 Subject: [PATCH 08/19] Changed the name of a testing dir. --- edq/cli/config/set_test.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/edq/cli/config/set_test.py b/edq/cli/config/set_test.py index d8fc1e6..fc18b00 100644 --- a/edq/cli/config/set_test.py +++ b/edq/cli/config/set_test.py @@ -7,8 +7,7 @@ import edq.util.dirent import edq.util.json -GLOBAL_DIR: str = 'global' -LOCAL_DIR: str = 'local' +EMPTY_DIR: str = 'empty-dir' def create_test_dir(temp_dir_prefix: str) -> str: """ @@ -19,14 +18,14 @@ def create_test_dir(temp_dir_prefix: str) -> str: . ├── empty-config │   └── edq-config.json - ├── global + ├── empty-dir └── non-empty-config └── edq-config.json """ - temp_dir = edq.util.dirent.get_temp_dir(prefix = temp_dir_prefix, rm = False) + temp_dir = edq.util.dirent.get_temp_dir(prefix = temp_dir_prefix) - global_config_path = os.path.join(temp_dir, GLOBAL_DIR) + global_config_path = os.path.join(temp_dir, EMPTY_DIR) edq.util.dirent.mkdir(global_config_path) path_empty_config = os.path.join(temp_dir, "empty-config") @@ -116,6 +115,7 @@ def test_set_base(self): "_config_params": { edq.core.config.LOCAL_CONFIG_PATH_KEY: os.path.join("non-empty-config", edq.core.config.DEFAULT_CONFIG_FILENAME), }, + "set_is_local": True, }, [ { @@ -134,12 +134,12 @@ def test_set_base(self): "config_to_set": ["user=user@test.edulinq.org"], "set_is_global": True, "_config_params": { - edq.core.config.GLOBAL_CONFIG_PATH_KEY: os.path.join(GLOBAL_DIR, edq.core.config.DEFAULT_CONFIG_FILENAME) + edq.core.config.GLOBAL_CONFIG_PATH_KEY: os.path.join(EMPTY_DIR, edq.core.config.DEFAULT_CONFIG_FILENAME) } }, [ { - "path": os.path.join(GLOBAL_DIR, edq.core.config.DEFAULT_CONFIG_FILENAME), + "path": os.path.join(EMPTY_DIR, edq.core.config.DEFAULT_CONFIG_FILENAME), "data": {"user": "user@test.edulinq.org"}, }, ], @@ -250,7 +250,7 @@ def test_set_base(self): edq.core.config.LOCAL_CONFIG_PATH_KEY: config_params.get(edq.core.config.LOCAL_CONFIG_PATH_KEY, None), edq.core.config.GLOBAL_CONFIG_PATH_KEY: config_params.get( edq.core.config.GLOBAL_CONFIG_PATH_KEY, - os.path.join(temp_dir, GLOBAL_DIR, filename), + os.path.join(temp_dir, EMPTY_DIR, filename), ), edq.core.config.FILENAME_KEY: filename } From fc129630d07a3cc5f9d982b2c89a021dfaf847f8 Mon Sep 17 00:00:00 2001 From: BatuhanSA Date: Sat, 11 Oct 2025 10:29:04 -0700 Subject: [PATCH 09/19] Made '=' to a constant variable. --- edq/cli/config/set.py | 11 +++--- edq/cli/config/set_test.py | 78 ++++++++++++++++++++++++++++++++------ edq/core/config.py | 12 +++--- edq/testing/cli.py | 2 +- 4 files changed, 80 insertions(+), 23 deletions(-) diff --git a/edq/cli/config/set.py b/edq/cli/config/set.py index 2faa767..ad0a2ba 100644 --- a/edq/cli/config/set.py +++ b/edq/cli/config/set.py @@ -22,12 +22,12 @@ def write_config_to_file(file_path: str, configs_to_write: typing.List[str]) -> config = edq.util.json.load_path(file_path) for config_option in configs_to_write: - if ("=" not in config_option): + if (edq.core.config.CLI_CONFIG_KEY_VALUE_SEPARATOR not in config_option): raise ValueError( f"Invalid configuration option '{config_option}'." + " Configuration options must be provided in the format `=` when passed via the CLI.") - (key, value) = config_option.split("=", maxsplit = 1) + (key, value) = config_option.split(edq.core.config.CLI_CONFIG_KEY_VALUE_SEPARATOR, maxsplit = 1) key = key.strip() if (key == ""): @@ -62,7 +62,7 @@ def main() -> int: return run_cli(_get_parser().parse_args()) def _get_parser() -> argparse.ArgumentParser: - """ Get a parser and add addition flags. """ + """ Get a parser and add additional flags. """ parser = edq.core.argparser.get_default_parser(__doc__.strip()) modify_parser(parser) @@ -72,9 +72,10 @@ def _get_parser() -> argparse.ArgumentParser: def modify_parser(parser: argparse.ArgumentParser) -> None: """ Add this CLI's flags to the given parser. """ - parser.add_argument('config_to_set', metavar = "=", + parser.add_argument('config_to_set', metavar = f"{edq.core.config.CLI_CONFIG_KEY_VALUE_SEPARATOR}", action = 'store', nargs = '+', type = str, - help = ('Configuration option to be set. Expected config format is =.'), + help = ('Configuration option to be set.' + + f" Expected config format is {edq.core.config.CLI_CONFIG_KEY_VALUE_SEPARATOR}."), ) config_file_locations = parser.add_argument_group("set config options") diff --git a/edq/cli/config/set_test.py b/edq/cli/config/set_test.py index fc18b00..7076d04 100644 --- a/edq/cli/config/set_test.py +++ b/edq/cli/config/set_test.py @@ -7,10 +7,8 @@ import edq.util.dirent import edq.util.json -EMPTY_DIR: str = 'empty-dir' - def create_test_dir(temp_dir_prefix: str) -> str: - """ + """ Create a temp dir and populate it with dirents for testing. This test data directory is laid out as: @@ -25,17 +23,17 @@ def create_test_dir(temp_dir_prefix: str) -> str: temp_dir = edq.util.dirent.get_temp_dir(prefix = temp_dir_prefix) - global_config_path = os.path.join(temp_dir, EMPTY_DIR) + global_config_path = os.path.join(temp_dir, 'empty-dir') edq.util.dirent.mkdir(global_config_path) - path_empty_config = os.path.join(temp_dir, "empty-config") + path_empty_config = os.path.join(temp_dir, 'empty-config') edq.util.dirent.mkdir(path_empty_config) edq.util.json.dump_path( {}, os.path.join(path_empty_config, edq.core.config.DEFAULT_CONFIG_FILENAME), ) - path_non_empty_config = os.path.join(temp_dir, "non-empty-config") + path_non_empty_config = os.path.join(temp_dir, 'non-empty-config') edq.util.dirent.mkdir(path_non_empty_config) edq.util.json.dump_path( {"user": "user@test.edulinq.org"}, @@ -50,7 +48,6 @@ class TestSetConfig(edq.testing.unittest.BaseTest): def test_set_base(self): """ Test that the 'set' command creates configuration files and writes the specified configuration correctly. - 'TEMP_DIR' gets replaced with actual temp dir path when testing. """ # [(set cli arguments, expected result, error substring), ...] @@ -108,6 +105,26 @@ def test_set_base(self): None, ), + # Multiple Configs + ( + { + "config_to_set": ["user=user@test.edulinq.org", "pass=password123"], + "_config_params": { + edq.core.config.LOCAL_CONFIG_PATH_KEY: os.path.join("empty-config", edq.core.config.DEFAULT_CONFIG_FILENAME), + }, + }, + [ + { + "path": os.path.join("empty-config", edq.core.config.DEFAULT_CONFIG_FILENAME), + "data": { + "user": "user@test.edulinq.org", + "pass": "password123", + }, + }, + ], + None, + ), + # Non Empty Config ( { @@ -134,12 +151,12 @@ def test_set_base(self): "config_to_set": ["user=user@test.edulinq.org"], "set_is_global": True, "_config_params": { - edq.core.config.GLOBAL_CONFIG_PATH_KEY: os.path.join(EMPTY_DIR, edq.core.config.DEFAULT_CONFIG_FILENAME) + edq.core.config.GLOBAL_CONFIG_PATH_KEY: os.path.join('empty-dir', edq.core.config.DEFAULT_CONFIG_FILENAME) } }, [ { - "path": os.path.join(EMPTY_DIR, edq.core.config.DEFAULT_CONFIG_FILENAME), + "path": os.path.join('empty-dir', edq.core.config.DEFAULT_CONFIG_FILENAME), "data": {"user": "user@test.edulinq.org"}, }, ], @@ -164,6 +181,27 @@ def test_set_base(self): None, ), + # Multiple Configs + ( + { + "config_to_set": ["user=user@test.edulinq.org", "pass=password123"], + "set_is_global": True, + "_config_params": { + edq.core.config.GLOBAL_CONFIG_PATH_KEY: os.path.join("empty-config", edq.core.config.DEFAULT_CONFIG_FILENAME) + } + }, + [ + { + "path": os.path.join("empty-config", edq.core.config.DEFAULT_CONFIG_FILENAME), + "data": { + "user": "user@test.edulinq.org", + "pass": "password123", + }, + }, + ], + None, + ), + # Non Empty Config ( { @@ -214,6 +252,24 @@ def test_set_base(self): None, ), + # Multiple Configs + ( + { + "config_to_set": ["user=user@test.edulinq.org", "pass=password123"], + "set_to_file_path": os.path.join("empty-config", edq.core.config.DEFAULT_CONFIG_FILENAME), + }, + [ + { + "path": os.path.join("empty-config", edq.core.config.DEFAULT_CONFIG_FILENAME), + "data": { + "user": "user@test.edulinq.org", + "pass": "password123", + }, + }, + ], + None, + ), + # Non Empty Config ( { @@ -228,8 +284,6 @@ def test_set_base(self): ], None, ), - - ] for (i, test_case) in enumerate(test_cases): @@ -250,7 +304,7 @@ def test_set_base(self): edq.core.config.LOCAL_CONFIG_PATH_KEY: config_params.get(edq.core.config.LOCAL_CONFIG_PATH_KEY, None), edq.core.config.GLOBAL_CONFIG_PATH_KEY: config_params.get( edq.core.config.GLOBAL_CONFIG_PATH_KEY, - os.path.join(temp_dir, EMPTY_DIR, filename), + os.path.join(temp_dir, 'empty-dir', filename), ), edq.core.config.FILENAME_KEY: filename } diff --git a/edq/core/config.py b/edq/core/config.py index 358b5d4..f48156b 100644 --- a/edq/core/config.py +++ b/edq/core/config.py @@ -14,12 +14,14 @@ CONFIG_PATHS_KEY: str = 'config_paths' CONFIGS_KEY: str = 'configs' +CLI_CONFIG_KEY_VALUE_SEPARATOR: str = "=" GLOBAL_CONFIG_PATH_KEY: str = 'global_config_path' LOCAL_CONFIG_PATH_KEY: str = 'local_config_path' FILENAME_KEY: str = 'config_filename' IGNORE_CONFIGS_KEY: str = 'ignore_configs' DEFAULT_CONFIG_FILENAME: str = "edq-config.json" + class ConfigSource: """ A class for storing config source information. """ @@ -95,13 +97,13 @@ def get_tiered_config( # Check the command-line config options. cli_configs = cli_arguments.get(CONFIGS_KEY, []) for cli_config in cli_configs: - if ("=" not in cli_config): + if (CLI_CONFIG_KEY_VALUE_SEPARATOR not in cli_config): raise ValueError( f"Invalid configuration option '{cli_config}'." + " Configuration options must be provided in the format `=` when passed via the CLI." ) - (key, value) = cli_config.split("=", maxsplit = 1) + (key, value) = cli_config.split(CLI_CONFIG_KEY_VALUE_SEPARATOR, maxsplit = 1) key = key.strip() if (key == ""): @@ -225,7 +227,7 @@ def set_cli_args(parser: argparse.ArgumentParser, extra_state: typing.Dict[str, + ' Will override options form both global and local config files.') ) - group.add_argument('--config', dest = CONFIGS_KEY, metavar = "=", + group.add_argument('--config', dest = CONFIGS_KEY, metavar = f"{CLI_CONFIG_KEY_VALUE_SEPARATOR}", action = 'append', type = str, default = [], help = ('Set a configuration option from the command-line.' + ' Specify options as = pairs.' @@ -259,7 +261,7 @@ def load_config_into_args( The keys of `cli_arg_config_map` represent attributes in the CLI arguments (`args`), while the values represent the desired config name this argument should be set as. For example, a `cli_arg_config_map` of `{'foo': 'baz'}` will make the CLI argument `--foo bar` - be equivalent to `--config baz=bar`. + be equivalent to f"--config baz{CLI_CONFIG_KEY_VALUE_SEPARATOR}bar". """ if (cli_arg_config_map is None): @@ -268,7 +270,7 @@ def load_config_into_args( for (cli_key, config_key) in cli_arg_config_map.items(): value = getattr(args, cli_key, None) if (value is not None): - getattr(args, CONFIGS_KEY).append(f"{config_key}={value}") + getattr(args, CONFIGS_KEY).append(f"{config_key}{CLI_CONFIG_KEY_VALUE_SEPARATOR}{value}") (config_dict, sources_dict, config_params_dict) = get_tiered_config( cli_arguments = args, diff --git a/edq/testing/cli.py b/edq/testing/cli.py index 7146613..003cf43 100644 --- a/edq/testing/cli.py +++ b/edq/testing/cli.py @@ -104,7 +104,7 @@ def __init__(self, self.cwd: str = self.temp_dir """ - The directory that test runs from + The directory the test runs from. """ if (cwd is not None): From e01ad616b012d83d354533a134f60bf1c1b844f5 Mon Sep 17 00:00:00 2001 From: BatuhanSA Date: Sat, 11 Oct 2025 10:47:53 -0700 Subject: [PATCH 10/19] Got rid of CLI_KEY_VALE_SEPERATOR --- edq/cli/config/set.py | 8 ++++---- edq/core/config.py | 11 +++++------ 2 files changed, 9 insertions(+), 10 deletions(-) diff --git a/edq/cli/config/set.py b/edq/cli/config/set.py index ad0a2ba..10b5bbf 100644 --- a/edq/cli/config/set.py +++ b/edq/cli/config/set.py @@ -22,12 +22,12 @@ def write_config_to_file(file_path: str, configs_to_write: typing.List[str]) -> config = edq.util.json.load_path(file_path) for config_option in configs_to_write: - if (edq.core.config.CLI_CONFIG_KEY_VALUE_SEPARATOR not in config_option): + if ("=" not in config_option): raise ValueError( f"Invalid configuration option '{config_option}'." + " Configuration options must be provided in the format `=` when passed via the CLI.") - (key, value) = config_option.split(edq.core.config.CLI_CONFIG_KEY_VALUE_SEPARATOR, maxsplit = 1) + (key, value) = config_option.split("=", maxsplit = 1) key = key.strip() if (key == ""): @@ -72,10 +72,10 @@ def _get_parser() -> argparse.ArgumentParser: def modify_parser(parser: argparse.ArgumentParser) -> None: """ Add this CLI's flags to the given parser. """ - parser.add_argument('config_to_set', metavar = f"{edq.core.config.CLI_CONFIG_KEY_VALUE_SEPARATOR}", + parser.add_argument('config_to_set', metavar = "=", action = 'store', nargs = '+', type = str, help = ('Configuration option to be set.' - + f" Expected config format is {edq.core.config.CLI_CONFIG_KEY_VALUE_SEPARATOR}."), + + " Expected config format is =."), ) config_file_locations = parser.add_argument_group("set config options") diff --git a/edq/core/config.py b/edq/core/config.py index f48156b..07e2e74 100644 --- a/edq/core/config.py +++ b/edq/core/config.py @@ -14,7 +14,6 @@ CONFIG_PATHS_KEY: str = 'config_paths' CONFIGS_KEY: str = 'configs' -CLI_CONFIG_KEY_VALUE_SEPARATOR: str = "=" GLOBAL_CONFIG_PATH_KEY: str = 'global_config_path' LOCAL_CONFIG_PATH_KEY: str = 'local_config_path' FILENAME_KEY: str = 'config_filename' @@ -97,13 +96,13 @@ def get_tiered_config( # Check the command-line config options. cli_configs = cli_arguments.get(CONFIGS_KEY, []) for cli_config in cli_configs: - if (CLI_CONFIG_KEY_VALUE_SEPARATOR not in cli_config): + if ( "=" not in cli_config): raise ValueError( f"Invalid configuration option '{cli_config}'." + " Configuration options must be provided in the format `=` when passed via the CLI." ) - (key, value) = cli_config.split(CLI_CONFIG_KEY_VALUE_SEPARATOR, maxsplit = 1) + (key, value) = cli_config.split("=", maxsplit = 1) key = key.strip() if (key == ""): @@ -227,7 +226,7 @@ def set_cli_args(parser: argparse.ArgumentParser, extra_state: typing.Dict[str, + ' Will override options form both global and local config files.') ) - group.add_argument('--config', dest = CONFIGS_KEY, metavar = f"{CLI_CONFIG_KEY_VALUE_SEPARATOR}", + group.add_argument('--config', dest = CONFIGS_KEY, metavar = "=", action = 'append', type = str, default = [], help = ('Set a configuration option from the command-line.' + ' Specify options as = pairs.' @@ -261,7 +260,7 @@ def load_config_into_args( The keys of `cli_arg_config_map` represent attributes in the CLI arguments (`args`), while the values represent the desired config name this argument should be set as. For example, a `cli_arg_config_map` of `{'foo': 'baz'}` will make the CLI argument `--foo bar` - be equivalent to f"--config baz{CLI_CONFIG_KEY_VALUE_SEPARATOR}bar". + be equivalent to f"--config baz=bar". """ if (cli_arg_config_map is None): @@ -270,7 +269,7 @@ def load_config_into_args( for (cli_key, config_key) in cli_arg_config_map.items(): value = getattr(args, cli_key, None) if (value is not None): - getattr(args, CONFIGS_KEY).append(f"{config_key}{CLI_CONFIG_KEY_VALUE_SEPARATOR}{value}") + getattr(args, CONFIGS_KEY).append(f"{config_key}={value}") (config_dict, sources_dict, config_params_dict) = get_tiered_config( cli_arguments = args, From d796b7d9dd658494f9c69e960709ad0495ed5d8d Mon Sep 17 00:00:00 2001 From: BatuhanSA Date: Sat, 11 Oct 2025 10:56:28 -0700 Subject: [PATCH 11/19] Went over the code one last time before PR. --- edq/cli/config/set_test.py | 2 +- edq/core/config.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/edq/cli/config/set_test.py b/edq/cli/config/set_test.py index 7076d04..82c0a0c 100644 --- a/edq/cli/config/set_test.py +++ b/edq/cli/config/set_test.py @@ -1,9 +1,9 @@ import argparse import os -import edq.testing.unittest import edq.cli.config.set import edq.core.config +import edq.testing.unittest import edq.util.dirent import edq.util.json diff --git a/edq/core/config.py b/edq/core/config.py index 07e2e74..4492a94 100644 --- a/edq/core/config.py +++ b/edq/core/config.py @@ -96,7 +96,7 @@ def get_tiered_config( # Check the command-line config options. cli_configs = cli_arguments.get(CONFIGS_KEY, []) for cli_config in cli_configs: - if ( "=" not in cli_config): + if ("=" not in cli_config): raise ValueError( f"Invalid configuration option '{cli_config}'." + " Configuration options must be provided in the format `=` when passed via the CLI." @@ -260,7 +260,7 @@ def load_config_into_args( The keys of `cli_arg_config_map` represent attributes in the CLI arguments (`args`), while the values represent the desired config name this argument should be set as. For example, a `cli_arg_config_map` of `{'foo': 'baz'}` will make the CLI argument `--foo bar` - be equivalent to f"--config baz=bar". + be equivalent to "--config baz=bar". """ if (cli_arg_config_map is None): From d5067d90837cebc4454cf1d9e551b896c4a031ad Mon Sep 17 00:00:00 2001 From: BatuhanSA Date: Fri, 17 Oct 2025 11:13:32 -0700 Subject: [PATCH 12/19] Added support for specifying a target direcotry when running a CLI test. --- edq/testing/cli.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/edq/testing/cli.py b/edq/testing/cli.py index e2b18c4..19755e0 100644 --- a/edq/testing/cli.py +++ b/edq/testing/cli.py @@ -53,6 +53,7 @@ def __init__(self, base_dir: str, data_dir: str, temp_dir: str, + work_dir: typing.Union[str, None] = None, cli: typing.Union[str, None] = None, arguments: typing.Union[typing.List[str], None] = None, error: bool = False, @@ -103,6 +104,13 @@ def __init__(self, edq.util.dirent.mkdir(temp_dir) + self.work_dir: str = os.getcwd() + """ The directory the test runs from. """ + + if (work_dir is not None): + work_dir_expanded = self._expand_paths(work_dir) + self.work_dir = work_dir_expanded + if (cli is None): raise ValueError("Missing CLI module.") @@ -270,6 +278,9 @@ def __method(self: edq.testing.unittest.BaseTest) -> None: old_args = sys.argv sys.argv = [test_info.module.__file__] + test_info.arguments + previous_work_directory = os.getcwd() + os.chdir(test_info.work_dir) + try: with contextlib.redirect_stdout(io.StringIO()) as stdout_output: with contextlib.redirect_stderr(io.StringIO()) as stderr_output: @@ -290,6 +301,8 @@ def __method(self: edq.testing.unittest.BaseTest) -> None: if (isinstance(ex, SystemExit) and (ex.__context__ is not None)): stderr_text = self.format_error_string(ex.__context__) finally: + os.chdir(previous_work_directory) + sys.argv = old_args if (not test_info.split_stdout_stderr): From c40e1622067af43c1a002a9edba0747b8fbcf943 Mon Sep 17 00:00:00 2001 From: BatuhanSA Date: Fri, 24 Oct 2025 14:10:59 -0700 Subject: [PATCH 13/19] Relocated write config to core from cli set. --- edq/cli/config/set.py | 31 +++---------------------------- edq/core/config.py | 25 +++++++++++++++++++++++++ 2 files changed, 28 insertions(+), 28 deletions(-) diff --git a/edq/cli/config/set.py b/edq/cli/config/set.py index 10b5bbf..69b5ffb 100644 --- a/edq/cli/config/set.py +++ b/edq/cli/config/set.py @@ -12,31 +12,6 @@ import edq.util.dirent import edq.util.json -def write_config_to_file(file_path: str, configs_to_write: typing.List[str]) -> None: - """ Write configs to a specified file path. Create the path if it do not exist. """ - - if (not (edq.util.dirent.exists(file_path))): - edq.util.dirent.mkdir(os.path.dirname(file_path)) - edq.util.json.dump_path({}, file_path) - - config = edq.util.json.load_path(file_path) - - for config_option in configs_to_write: - if ("=" not in config_option): - raise ValueError( - f"Invalid configuration option '{config_option}'." - + " Configuration options must be provided in the format `=` when passed via the CLI.") - - (key, value) = config_option.split("=", maxsplit = 1) - - key = key.strip() - if (key == ""): - raise ValueError(f"Found an empty configuration option key associated with the value '{value}'.") - - config[key] = value - - edq.util.json.dump_path(config, file_path, indent = 4) - def run_cli(args: argparse.Namespace) -> int: """ Run the CLI. """ @@ -47,12 +22,12 @@ def run_cli(args: argparse.Namespace) -> int: local_config_path = args._config_params.get(edq.core.config.LOCAL_CONFIG_PATH_KEY) if (local_config_path is None): local_config_path = args._config_params.get(edq.core.config.FILENAME_KEY) - write_config_to_file(local_config_path, args.config_to_set) + edq.core.config.write_config_to_file(local_config_path, args.config_to_set) elif (args.set_is_global): global_config_path = args._config_params.get(edq.core.config.GLOBAL_CONFIG_PATH_KEY) - write_config_to_file(global_config_path, args.config_to_set) + edq.core.config.write_config_to_file(global_config_path, args.config_to_set) elif (args.set_to_file_path is not None): - write_config_to_file(args.set_to_file_path, args.config_to_set) + edq.core.config.write_config_to_file(args.set_to_file_path, args.config_to_set) return 0 diff --git a/edq/core/config.py b/edq/core/config.py index 4492a94..0ad2bfe 100644 --- a/edq/core/config.py +++ b/edq/core/config.py @@ -40,6 +40,31 @@ def __eq__(self, other: object) -> bool: def __str__(self) -> str: return f"({self.label}, {self.path})" +def write_config_to_file(file_path: str, configs_to_write: typing.List[str]) -> None: + """ Write configs to a specified file path. Create the path if it do not exist. """ + + if (not (edq.util.dirent.exists(file_path))): + edq.util.dirent.mkdir(os.path.dirname(file_path)) + edq.util.json.dump_path({}, file_path) + + config = edq.util.json.load_path(file_path) + + for config_option in configs_to_write: + if ("=" not in config_option): + raise ValueError( + f"Invalid configuration option '{config_option}'." + + " Configuration options must be provided in the format `=` when passed via the CLI.") + + (key, value) = config_option.split("=", maxsplit = 1) + + key = key.strip() + if (key == ""): + raise ValueError(f"Found an empty configuration option key associated with the value '{value}'.") + + config[key] = value + + edq.util.json.dump_path(config, file_path, indent = 4) + def get_global_config_path(config_filename: str) -> str: """ Get the path for the global config file. """ From 817f50206604733bb21c0e97cbca4e5c65186066 Mon Sep 17 00:00:00 2001 From: BatuhanSA Date: Fri, 24 Oct 2025 14:51:52 -0700 Subject: [PATCH 14/19] Revised half of the PR. --- edq/cli/config/set.py | 34 ++++++++++++------------ edq/cli/config/set_test.py | 35 ++++++++++++------------- edq/core/config.py | 53 ++++++++++++++++++-------------------- 3 files changed, 59 insertions(+), 63 deletions(-) diff --git a/edq/cli/config/set.py b/edq/cli/config/set.py index 69b5ffb..98171a7 100644 --- a/edq/cli/config/set.py +++ b/edq/cli/config/set.py @@ -3,31 +3,31 @@ """ import argparse -import os import sys import typing import edq.core.argparser import edq.core.config -import edq.util.dirent -import edq.util.json def run_cli(args: argparse.Namespace) -> int: """ Run the CLI. """ - if (not (args.set_is_local or args.set_is_global or (args.set_to_file_path is not None))): - args.set_is_local = True + cli_config_dict: typing.Dict[str, str] = {} + edq.core.config._load_cli_config(args.config_to_set, cli_config_dict) - if (args.set_is_local): - local_config_path = args._config_params.get(edq.core.config.LOCAL_CONFIG_PATH_KEY) + if (not (args.write_local or args.write_global or (args.write_file_path is not None))): + args.write_local = True + + if (args.write_local): + local_config_path = args._config_params[edq.core.config.LOCAL_CONFIG_PATH_KEY] if (local_config_path is None): - local_config_path = args._config_params.get(edq.core.config.FILENAME_KEY) - edq.core.config.write_config_to_file(local_config_path, args.config_to_set) - elif (args.set_is_global): - global_config_path = args._config_params.get(edq.core.config.GLOBAL_CONFIG_PATH_KEY) - edq.core.config.write_config_to_file(global_config_path, args.config_to_set) - elif (args.set_to_file_path is not None): - edq.core.config.write_config_to_file(args.set_to_file_path, args.config_to_set) + local_config_path = args._config_params[edq.core.config.FILENAME_KEY] + edq.core.config.write_config_to_file(local_config_path, cli_config_dict) + elif (args.write_global): + global_config_path = args._config_params[edq.core.config.GLOBAL_CONFIG_PATH_KEY] + edq.core.config.write_config_to_file(global_config_path, cli_config_dict) + elif (args.write_file_path is not None): + edq.core.config.write_config_to_file(args.write_file_path, cli_config_dict) return 0 @@ -57,20 +57,20 @@ def modify_parser(parser: argparse.ArgumentParser) -> None: group = config_file_locations.add_mutually_exclusive_group() group.add_argument('--local', - action = 'store_true', dest = 'set_is_local', + action = 'store_true', dest = 'write_local', help = ('Set the configuration option in a local config file if one exists.' + ' If no local config file is found, a new one will be created in the current directory.'), ) group.add_argument('--global', - action = 'store_true', dest = 'set_is_global', + action = 'store_true', dest = 'write_global', help = ('Set the configuration option in the global config file if it exists.' + " If it doesn't exist, it will be created." + " Use '--config-global' to view or change the global config file location."), ) group.add_argument('--file', metavar = "", - action = 'store', type = str, default = None, dest = 'set_to_file_path', + action = 'store', type = str, default = None, dest = 'write_file_path', help = ('Set the config option in a specified config file.' + " If the given file path doesn't exist, it will be created.") ) diff --git a/edq/cli/config/set_test.py b/edq/cli/config/set_test.py index 82c0a0c..f9f19d3 100644 --- a/edq/cli/config/set_test.py +++ b/edq/cli/config/set_test.py @@ -12,7 +12,6 @@ def create_test_dir(temp_dir_prefix: str) -> str: Create a temp dir and populate it with dirents for testing. This test data directory is laid out as: - . ├── empty-config │   └── edq-config.json @@ -132,7 +131,7 @@ def test_set_base(self): "_config_params": { edq.core.config.LOCAL_CONFIG_PATH_KEY: os.path.join("non-empty-config", edq.core.config.DEFAULT_CONFIG_FILENAME), }, - "set_is_local": True, + "write_local": True, }, [ { @@ -149,7 +148,7 @@ def test_set_base(self): ( { "config_to_set": ["user=user@test.edulinq.org"], - "set_is_global": True, + "write_global": True, "_config_params": { edq.core.config.GLOBAL_CONFIG_PATH_KEY: os.path.join('empty-dir', edq.core.config.DEFAULT_CONFIG_FILENAME) } @@ -167,7 +166,7 @@ def test_set_base(self): ( { "config_to_set": ["user=user@test.edulinq.org"], - "set_is_global": True, + "write_global": True, "_config_params": { edq.core.config.GLOBAL_CONFIG_PATH_KEY: os.path.join("empty-config", edq.core.config.DEFAULT_CONFIG_FILENAME) } @@ -185,7 +184,7 @@ def test_set_base(self): ( { "config_to_set": ["user=user@test.edulinq.org", "pass=password123"], - "set_is_global": True, + "write_global": True, "_config_params": { edq.core.config.GLOBAL_CONFIG_PATH_KEY: os.path.join("empty-config", edq.core.config.DEFAULT_CONFIG_FILENAME) } @@ -209,7 +208,7 @@ def test_set_base(self): "_config_params": { edq.core.config.GLOBAL_CONFIG_PATH_KEY: os.path.join("non-empty-config", edq.core.config.DEFAULT_CONFIG_FILENAME), }, - "set_is_global": True, + "write_global": True, }, [ { @@ -226,7 +225,7 @@ def test_set_base(self): ( { "config_to_set": ["user=user@test.edulinq.org"], - "set_to_file_path": os.path.join("non-existent", "path", edq.core.config.DEFAULT_CONFIG_FILENAME), + "write_file_path": os.path.join("non-existent", "path", edq.core.config.DEFAULT_CONFIG_FILENAME), }, [ { @@ -241,7 +240,7 @@ def test_set_base(self): ( { "config_to_set": ["user=user@test.edulinq.org"], - "set_to_file_path": os.path.join("empty-config", edq.core.config.DEFAULT_CONFIG_FILENAME), + "write_file_path": os.path.join("empty-config", edq.core.config.DEFAULT_CONFIG_FILENAME), }, [ { @@ -256,7 +255,7 @@ def test_set_base(self): ( { "config_to_set": ["user=user@test.edulinq.org", "pass=password123"], - "set_to_file_path": os.path.join("empty-config", edq.core.config.DEFAULT_CONFIG_FILENAME), + "write_file_path": os.path.join("empty-config", edq.core.config.DEFAULT_CONFIG_FILENAME), }, [ { @@ -274,7 +273,7 @@ def test_set_base(self): ( { "config_to_set": ["pass=password123"], - "set_to_file_path": os.path.join("non-empty-config", edq.core.config.DEFAULT_CONFIG_FILENAME), + "write_file_path": os.path.join("non-empty-config", edq.core.config.DEFAULT_CONFIG_FILENAME), }, [ { @@ -296,9 +295,9 @@ def test_set_base(self): filename = config_params.get(edq.core.config.FILENAME_KEY, edq.core.config.DEFAULT_CONFIG_FILENAME) set_args = argparse.Namespace( - set_is_local = cli_arguments.get("set_is_local", False), - set_is_global = cli_arguments.get("set_is_global", False), - set_to_file_path = cli_arguments.get("set_to_file_path", None), + write_local = cli_arguments.get("write_local", False), + write_global = cli_arguments.get("write_global", False), + write_file_path = cli_arguments.get("write_file_path", None), config_to_set = cli_arguments.get("config_to_set"), _config_params = { edq.core.config.LOCAL_CONFIG_PATH_KEY: config_params.get(edq.core.config.LOCAL_CONFIG_PATH_KEY, None), @@ -332,12 +331,12 @@ def test_set_base(self): self.fail(f"Did not get expected error: '{error_substring}'.") for file in expected_result: - set_to_file_path = file.get("path") - set_to_file_path = os.path.join(temp_dir, set_to_file_path) + write_file_path = file.get("path") + write_file_path = os.path.join(temp_dir, write_file_path) - if (not edq.util.dirent.exists(set_to_file_path)): - self.fail(f"Expected file does not exist at path: {set_to_file_path}") + if (not edq.util.dirent.exists(write_file_path)): + self.fail(f"Expected file does not exist at path: {write_file_path}") - data_actual = edq.util.json.load_path(set_to_file_path) + data_actual = edq.util.json.load_path(write_file_path) self.assertJSONDictEqual(data_actual, file.get("data")) diff --git a/edq/core/config.py b/edq/core/config.py index 0ad2bfe..34cf094 100644 --- a/edq/core/config.py +++ b/edq/core/config.py @@ -40,30 +40,41 @@ def __eq__(self, other: object) -> bool: def __str__(self) -> str: return f"({self.label}, {self.path})" -def write_config_to_file(file_path: str, configs_to_write: typing.List[str]) -> None: +def write_config_to_file(file_path: str, configs_to_write: typing.Dict[str, str]) -> None: """ Write configs to a specified file path. Create the path if it do not exist. """ - if (not (edq.util.dirent.exists(file_path))): - edq.util.dirent.mkdir(os.path.dirname(file_path)) - edq.util.json.dump_path({}, file_path) + config = {} + if (edq.util.dirent.exists(file_path)): + config = edq.util.json.load_path(file_path) - config = edq.util.json.load_path(file_path) + config.update(configs_to_write) - for config_option in configs_to_write: + edq.util.dirent.mkdir(os.path.dirname(file_path)) + edq.util.json.dump_path(config, file_path, indent = 4) + +def _load_cli_config( + cli_configs_to_load: typing.List[str], + config_dict: typing.Dict[str, str], + sources_dict: typing.Union[typing.Dict[str, ConfigSource], None] = None + ) -> None: + + if (sources_dict is None): + sources_dict = {} + + for config_option in cli_configs_to_load: if ("=" not in config_option): raise ValueError( f"Invalid configuration option '{config_option}'." + " Configuration options must be provided in the format `=` when passed via the CLI.") - (key, value) = config_option.split("=", maxsplit = 1) + (key, value) = config_option.split('=', maxsplit = 1) key = key.strip() - if (key == ""): + if (key == ''): raise ValueError(f"Found an empty configuration option key associated with the value '{value}'.") - config[key] = value - - edq.util.json.dump_path(config, file_path, indent = 4) + config_dict[key] = value + sources_dict[key] = ConfigSource(label = CONFIG_SOURCE_CLI) def get_global_config_path(config_filename: str) -> str: """ Get the path for the global config file. """ @@ -88,7 +99,7 @@ def get_tiered_config( sources: typing.Dict[str, ConfigSource] = {} config_params: typing.Dict[str, typing.Union[str, None]] = {} - config_params["config_filename"] = config_filename + config_params[FILENAME_KEY] = config_filename # Ensure CLI arguments are always a dict, even if provided as argparse.Namespace. if (isinstance(cli_arguments, argparse.Namespace)): @@ -120,21 +131,7 @@ def get_tiered_config( # Check the command-line config options. cli_configs = cli_arguments.get(CONFIGS_KEY, []) - for cli_config in cli_configs: - if ("=" not in cli_config): - raise ValueError( - f"Invalid configuration option '{cli_config}'." - + " Configuration options must be provided in the format `=` when passed via the CLI." - ) - - (key, value) = cli_config.split("=", maxsplit = 1) - - key = key.strip() - if (key == ""): - raise ValueError(f"Found an empty configuration option key associated with the value '{value}'.") - - config[key] = value - sources[key] = ConfigSource(label = CONFIG_SOURCE_CLI) + _load_cli_config(cli_configs, config, sources) # Finally, ignore any configs that is specified from CLI command. cli_ignore_configs = cli_arguments.get(IGNORE_CONFIGS_KEY, []) @@ -155,7 +152,7 @@ def _load_config_file( config_path = os.path.abspath(config_path) for (key, value) in edq.util.json.load_path(config_path).items(): key = key.strip() - if (key == ""): + if (key == ''): raise ValueError(f"Found an empty configuration option key associated with the value '{value}'.") config[key] = value From 4e7ca2db489ac8f9b55f88aa5cdb713e937bed06 Mon Sep 17 00:00:00 2001 From: BatuhanSA Date: Fri, 24 Oct 2025 15:58:49 -0700 Subject: [PATCH 15/19] Mostly done with the PR needs testing rework. --- edq/cli/config/set.py | 7 ++++--- edq/core/config.py | 47 ++++++++++++++++++++++--------------------- 2 files changed, 28 insertions(+), 26 deletions(-) diff --git a/edq/cli/config/set.py b/edq/cli/config/set.py index 98171a7..a324b7e 100644 --- a/edq/cli/config/set.py +++ b/edq/cli/config/set.py @@ -15,6 +15,7 @@ def run_cli(args: argparse.Namespace) -> int: cli_config_dict: typing.Dict[str, str] = {} edq.core.config._load_cli_config(args.config_to_set, cli_config_dict) + # Defaults to the local configuration if no configuration type is specified. if (not (args.write_local or args.write_global or (args.write_file_path is not None))): args.write_local = True @@ -58,20 +59,20 @@ def modify_parser(parser: argparse.ArgumentParser) -> None: group.add_argument('--local', action = 'store_true', dest = 'write_local', - help = ('Set the configuration option in a local config file if one exists.' + help = ('Write config option(s) to the local config file if one exists.' + ' If no local config file is found, a new one will be created in the current directory.'), ) group.add_argument('--global', action = 'store_true', dest = 'write_global', - help = ('Set the configuration option in the global config file if it exists.' + help = ('Write config option(s) to the global config file if it exists.' + " If it doesn't exist, it will be created." + " Use '--config-global' to view or change the global config file location."), ) group.add_argument('--file', metavar = "", action = 'store', type = str, default = None, dest = 'write_file_path', - help = ('Set the config option in a specified config file.' + help = ('Write config option(s) to the specified config file.' + " If the given file path doesn't exist, it will be created.") ) diff --git a/edq/core/config.py b/edq/core/config.py index 34cf094..1477026 100644 --- a/edq/core/config.py +++ b/edq/core/config.py @@ -52,29 +52,6 @@ def write_config_to_file(file_path: str, configs_to_write: typing.Dict[str, str] edq.util.dirent.mkdir(os.path.dirname(file_path)) edq.util.json.dump_path(config, file_path, indent = 4) -def _load_cli_config( - cli_configs_to_load: typing.List[str], - config_dict: typing.Dict[str, str], - sources_dict: typing.Union[typing.Dict[str, ConfigSource], None] = None - ) -> None: - - if (sources_dict is None): - sources_dict = {} - - for config_option in cli_configs_to_load: - if ("=" not in config_option): - raise ValueError( - f"Invalid configuration option '{config_option}'." - + " Configuration options must be provided in the format `=` when passed via the CLI.") - - (key, value) = config_option.split('=', maxsplit = 1) - - key = key.strip() - if (key == ''): - raise ValueError(f"Found an empty configuration option key associated with the value '{value}'.") - - config_dict[key] = value - sources_dict[key] = ConfigSource(label = CONFIG_SOURCE_CLI) def get_global_config_path(config_filename: str) -> str: """ Get the path for the global config file. """ @@ -141,6 +118,30 @@ def get_tiered_config( return config, sources, config_params +def _load_cli_config( + cli_configs_to_load: typing.List[str], + config_dict: typing.Dict[str, str], + sources_dict: typing.Union[typing.Dict[str, ConfigSource], None] = None + ) -> None: + + if (sources_dict is None): + sources_dict = {} + + for config_option in cli_configs_to_load: + if ("=" not in config_option): + raise ValueError( + f"Invalid configuration option '{config_option}'." + + " Configuration options must be provided in the format `=` when passed via the CLI.") + + (key, value) = config_option.split('=', maxsplit = 1) + + key = key.strip() + if (key == ''): + raise ValueError(f"Found an empty configuration option key associated with the value '{value}'.") + + config_dict[key] = value + sources_dict[key] = ConfigSource(label = CONFIG_SOURCE_CLI) + def _load_config_file( config_path: str, config: typing.Dict[str, str], From e43c26e1139e504b69dc39f37965b57b9fb7d6be Mon Sep 17 00:00:00 2001 From: BatuhanSA Date: Thu, 30 Oct 2025 11:05:49 -0700 Subject: [PATCH 16/19] Abstracted just parsing/validation instead of the whole loop. --- edq/cli/config/set.py | 12 +- edq/cli/config/set_test.py | 342 ------------------------------------- edq/core/config.py | 38 ++--- 3 files changed, 25 insertions(+), 367 deletions(-) delete mode 100644 edq/cli/config/set_test.py diff --git a/edq/cli/config/set.py b/edq/cli/config/set.py index a324b7e..818c25c 100644 --- a/edq/cli/config/set.py +++ b/edq/cli/config/set.py @@ -12,8 +12,10 @@ def run_cli(args: argparse.Namespace) -> int: """ Run the CLI. """ - cli_config_dict: typing.Dict[str, str] = {} - edq.core.config._load_cli_config(args.config_to_set, cli_config_dict) + config: typing.Dict[str, str] = {} + for config_option in args.config_to_set: + (key, value) = edq.core.config._parse_cli_config_option(config_option) + config[key] = value # Defaults to the local configuration if no configuration type is specified. if (not (args.write_local or args.write_global or (args.write_file_path is not None))): @@ -23,12 +25,12 @@ def run_cli(args: argparse.Namespace) -> int: local_config_path = args._config_params[edq.core.config.LOCAL_CONFIG_PATH_KEY] if (local_config_path is None): local_config_path = args._config_params[edq.core.config.FILENAME_KEY] - edq.core.config.write_config_to_file(local_config_path, cli_config_dict) + edq.core.config.write_config_to_file(local_config_path, config) elif (args.write_global): global_config_path = args._config_params[edq.core.config.GLOBAL_CONFIG_PATH_KEY] - edq.core.config.write_config_to_file(global_config_path, cli_config_dict) + edq.core.config.write_config_to_file(global_config_path, config) elif (args.write_file_path is not None): - edq.core.config.write_config_to_file(args.write_file_path, cli_config_dict) + edq.core.config.write_config_to_file(args.write_file_path, config) return 0 diff --git a/edq/cli/config/set_test.py b/edq/cli/config/set_test.py deleted file mode 100644 index f9f19d3..0000000 --- a/edq/cli/config/set_test.py +++ /dev/null @@ -1,342 +0,0 @@ -import argparse -import os - -import edq.cli.config.set -import edq.core.config -import edq.testing.unittest -import edq.util.dirent -import edq.util.json - -def create_test_dir(temp_dir_prefix: str) -> str: - """ - Create a temp dir and populate it with dirents for testing. - - This test data directory is laid out as: - . - ├── empty-config - │   └── edq-config.json - ├── empty-dir - └── non-empty-config - └── edq-config.json - """ - - temp_dir = edq.util.dirent.get_temp_dir(prefix = temp_dir_prefix) - - global_config_path = os.path.join(temp_dir, 'empty-dir') - edq.util.dirent.mkdir(global_config_path) - - path_empty_config = os.path.join(temp_dir, 'empty-config') - edq.util.dirent.mkdir(path_empty_config) - edq.util.json.dump_path( - {}, - os.path.join(path_empty_config, edq.core.config.DEFAULT_CONFIG_FILENAME), - ) - - path_non_empty_config = os.path.join(temp_dir, 'non-empty-config') - edq.util.dirent.mkdir(path_non_empty_config) - edq.util.json.dump_path( - {"user": "user@test.edulinq.org"}, - os.path.join(path_non_empty_config, edq.core.config.DEFAULT_CONFIG_FILENAME), - ) - - return temp_dir - -class TestSetConfig(edq.testing.unittest.BaseTest): - """ Test basic functionality of set config. """ - - def test_set_base(self): - """ - Test that the 'set' command creates configuration files and writes the specified configuration correctly. - """ - - # [(set cli arguments, expected result, error substring), ...] - test_cases = [ - # Invalid Option - - # Empty Key - ( - { - "config_to_set": ["=user@test.edulinq.org"], - }, - [], - "Found an empty configuration option key associated with the value 'user@test.edulinq.org'.", - ), - - # Missing '=' - ( - { - "config_to_set": ["useruser@test.edulinq.org"], - }, - [], - "Configuration options must be provided in the format `=` when passed via the CLI.", - ), - - # Local Config - - # No Config - ( - { - "config_to_set": ["user=user@test.edulinq.org"], - }, - [ - { - "path": edq.core.config.DEFAULT_CONFIG_FILENAME, - "data": {"user": "user@test.edulinq.org"}, - }, - ], - None, - ), - - # Empty Config - ( - { - "config_to_set": ["user=user@test.edulinq.org"], - "_config_params": { - edq.core.config.LOCAL_CONFIG_PATH_KEY: os.path.join("empty-config", edq.core.config.DEFAULT_CONFIG_FILENAME), - }, - }, - [ - { - "path": os.path.join("empty-config", edq.core.config.DEFAULT_CONFIG_FILENAME), - "data": {"user": "user@test.edulinq.org"}, - }, - ], - None, - ), - - # Multiple Configs - ( - { - "config_to_set": ["user=user@test.edulinq.org", "pass=password123"], - "_config_params": { - edq.core.config.LOCAL_CONFIG_PATH_KEY: os.path.join("empty-config", edq.core.config.DEFAULT_CONFIG_FILENAME), - }, - }, - [ - { - "path": os.path.join("empty-config", edq.core.config.DEFAULT_CONFIG_FILENAME), - "data": { - "user": "user@test.edulinq.org", - "pass": "password123", - }, - }, - ], - None, - ), - - # Non Empty Config - ( - { - "config_to_set": ["pass=password123"], - "_config_params": { - edq.core.config.LOCAL_CONFIG_PATH_KEY: os.path.join("non-empty-config", edq.core.config.DEFAULT_CONFIG_FILENAME), - }, - "write_local": True, - }, - [ - { - "path": os.path.join("non-empty-config", edq.core.config.DEFAULT_CONFIG_FILENAME), - "data": {"user": "user@test.edulinq.org", "pass": "password123"}, - }, - ], - None, - ), - - # Global Config - - # No Config - ( - { - "config_to_set": ["user=user@test.edulinq.org"], - "write_global": True, - "_config_params": { - edq.core.config.GLOBAL_CONFIG_PATH_KEY: os.path.join('empty-dir', edq.core.config.DEFAULT_CONFIG_FILENAME) - } - }, - [ - { - "path": os.path.join('empty-dir', edq.core.config.DEFAULT_CONFIG_FILENAME), - "data": {"user": "user@test.edulinq.org"}, - }, - ], - None, - ), - - # Empty Config - ( - { - "config_to_set": ["user=user@test.edulinq.org"], - "write_global": True, - "_config_params": { - edq.core.config.GLOBAL_CONFIG_PATH_KEY: os.path.join("empty-config", edq.core.config.DEFAULT_CONFIG_FILENAME) - } - }, - [ - { - "path": os.path.join("empty-config", edq.core.config.DEFAULT_CONFIG_FILENAME), - "data": {"user": "user@test.edulinq.org"}, - }, - ], - None, - ), - - # Multiple Configs - ( - { - "config_to_set": ["user=user@test.edulinq.org", "pass=password123"], - "write_global": True, - "_config_params": { - edq.core.config.GLOBAL_CONFIG_PATH_KEY: os.path.join("empty-config", edq.core.config.DEFAULT_CONFIG_FILENAME) - } - }, - [ - { - "path": os.path.join("empty-config", edq.core.config.DEFAULT_CONFIG_FILENAME), - "data": { - "user": "user@test.edulinq.org", - "pass": "password123", - }, - }, - ], - None, - ), - - # Non Empty Config - ( - { - "config_to_set": ["pass=password123"], - "_config_params": { - edq.core.config.GLOBAL_CONFIG_PATH_KEY: os.path.join("non-empty-config", edq.core.config.DEFAULT_CONFIG_FILENAME), - }, - "write_global": True, - }, - [ - { - "path": os.path.join("non-empty-config", edq.core.config.DEFAULT_CONFIG_FILENAME), - "data": {"user": "user@test.edulinq.org", "pass": "password123"}, - }, - ], - None, - ), - - # File Config - - # Non Existent File - ( - { - "config_to_set": ["user=user@test.edulinq.org"], - "write_file_path": os.path.join("non-existent", "path", edq.core.config.DEFAULT_CONFIG_FILENAME), - }, - [ - { - "path": os.path.join("non-existent", "path", edq.core.config.DEFAULT_CONFIG_FILENAME), - "data": {"user": "user@test.edulinq.org"}, - }, - ], - None, - ), - - # Empty Config - ( - { - "config_to_set": ["user=user@test.edulinq.org"], - "write_file_path": os.path.join("empty-config", edq.core.config.DEFAULT_CONFIG_FILENAME), - }, - [ - { - "path": os.path.join("empty-config", edq.core.config.DEFAULT_CONFIG_FILENAME), - "data": {"user": "user@test.edulinq.org"}, - }, - ], - None, - ), - - # Multiple Configs - ( - { - "config_to_set": ["user=user@test.edulinq.org", "pass=password123"], - "write_file_path": os.path.join("empty-config", edq.core.config.DEFAULT_CONFIG_FILENAME), - }, - [ - { - "path": os.path.join("empty-config", edq.core.config.DEFAULT_CONFIG_FILENAME), - "data": { - "user": "user@test.edulinq.org", - "pass": "password123", - }, - }, - ], - None, - ), - - # Non Empty Config - ( - { - "config_to_set": ["pass=password123"], - "write_file_path": os.path.join("non-empty-config", edq.core.config.DEFAULT_CONFIG_FILENAME), - }, - [ - { - "path": os.path.join("non-empty-config", edq.core.config.DEFAULT_CONFIG_FILENAME), - "data": {"user": "user@test.edulinq.org", "pass": "password123"}, - }, - ], - None, - ), - ] - - for (i, test_case) in enumerate(test_cases): - cli_arguments, expected_result, error_substring = test_case - - with self.subTest(msg = f"Case {i}"): - temp_dir = create_test_dir(temp_dir_prefix = "edq-test-config-set-") - - config_params = cli_arguments.get("_config_params", {}) - filename = config_params.get(edq.core.config.FILENAME_KEY, edq.core.config.DEFAULT_CONFIG_FILENAME) - - set_args = argparse.Namespace( - write_local = cli_arguments.get("write_local", False), - write_global = cli_arguments.get("write_global", False), - write_file_path = cli_arguments.get("write_file_path", None), - config_to_set = cli_arguments.get("config_to_set"), - _config_params = { - edq.core.config.LOCAL_CONFIG_PATH_KEY: config_params.get(edq.core.config.LOCAL_CONFIG_PATH_KEY, None), - edq.core.config.GLOBAL_CONFIG_PATH_KEY: config_params.get( - edq.core.config.GLOBAL_CONFIG_PATH_KEY, - os.path.join(temp_dir, 'empty-dir', filename), - ), - edq.core.config.FILENAME_KEY: filename - } - ) - - previous_work_directory = os.getcwd() - os.chdir(temp_dir) - - try: - edq.cli.config.set.run_cli(set_args) - except Exception as ex: - error_string = self.format_error_string(ex) - - if (error_substring is None): - self.fail(f"Unexpected error: '{error_string}'.") - - self.assertIn(error_substring, error_string, 'Error is not as expected.') - - continue - - finally: - os.chdir(previous_work_directory) - - if (error_substring is not None): - self.fail(f"Did not get expected error: '{error_substring}'.") - - for file in expected_result: - write_file_path = file.get("path") - write_file_path = os.path.join(temp_dir, write_file_path) - - if (not edq.util.dirent.exists(write_file_path)): - self.fail(f"Expected file does not exist at path: {write_file_path}") - - data_actual = edq.util.json.load_path(write_file_path) - - self.assertJSONDictEqual(data_actual, file.get("data")) diff --git a/edq/core/config.py b/edq/core/config.py index 1477026..63963b0 100644 --- a/edq/core/config.py +++ b/edq/core/config.py @@ -108,7 +108,11 @@ def get_tiered_config( # Check the command-line config options. cli_configs = cli_arguments.get(CONFIGS_KEY, []) - _load_cli_config(cli_configs, config, sources) + for cli_config_option in cli_configs: + (key, value) = _parse_cli_config_option(cli_config_option) + + config[key] = value + sources[key] = ConfigSource(label = CONFIG_SOURCE_CLI) # Finally, ignore any configs that is specified from CLI command. cli_ignore_configs = cli_arguments.get(IGNORE_CONFIGS_KEY, []) @@ -118,29 +122,23 @@ def get_tiered_config( return config, sources, config_params -def _load_cli_config( - cli_configs_to_load: typing.List[str], - config_dict: typing.Dict[str, str], - sources_dict: typing.Union[typing.Dict[str, ConfigSource], None] = None - ) -> None: - - if (sources_dict is None): - sources_dict = {} +def _parse_cli_config_option( + config_option: str, + ) -> typing.Tuple[str, str]: + """ Parse and validate a CLI configuration option string, returs the resulting config option as a key value pair. """ - for config_option in cli_configs_to_load: - if ("=" not in config_option): - raise ValueError( - f"Invalid configuration option '{config_option}'." - + " Configuration options must be provided in the format `=` when passed via the CLI.") + if ("=" not in config_option): + raise ValueError( + f"Invalid configuration option '{config_option}'." + + " Configuration options must be provided in the format `=` when passed via the CLI.") - (key, value) = config_option.split('=', maxsplit = 1) + (key, value) = config_option.split('=', maxsplit = 1) - key = key.strip() - if (key == ''): - raise ValueError(f"Found an empty configuration option key associated with the value '{value}'.") + key = key.strip() + if (key == ''): + raise ValueError(f"Found an empty configuration option key associated with the value '{value}'.") - config_dict[key] = value - sources_dict[key] = ConfigSource(label = CONFIG_SOURCE_CLI) + return key, value def _load_config_file( config_path: str, From 9c638b573d58c9b559c5f0f5929109a8fc329ad2 Mon Sep 17 00:00:00 2001 From: BatuhanSA Date: Thu, 30 Oct 2025 11:58:39 -0700 Subject: [PATCH 17/19] Added the tests, need to go over the PR. --- edq/core/config.py | 4 +- edq/core/write_config_test.py | 167 ++++++++++++++++++++++++++++++++++ 2 files changed, 169 insertions(+), 2 deletions(-) create mode 100644 edq/core/write_config_test.py diff --git a/edq/core/config.py b/edq/core/config.py index 63963b0..5425167 100644 --- a/edq/core/config.py +++ b/edq/core/config.py @@ -40,14 +40,14 @@ def __eq__(self, other: object) -> bool: def __str__(self) -> str: return f"({self.label}, {self.path})" -def write_config_to_file(file_path: str, configs_to_write: typing.Dict[str, str]) -> None: +def write_config_to_file(file_path: str, config_to_write: typing.Dict[str, str]) -> None: """ Write configs to a specified file path. Create the path if it do not exist. """ config = {} if (edq.util.dirent.exists(file_path)): config = edq.util.json.load_path(file_path) - config.update(configs_to_write) + config.update(config_to_write) edq.util.dirent.mkdir(os.path.dirname(file_path)) edq.util.json.dump_path(config, file_path, indent = 4) diff --git a/edq/core/write_config_test.py b/edq/core/write_config_test.py new file mode 100644 index 0000000..a688c82 --- /dev/null +++ b/edq/core/write_config_test.py @@ -0,0 +1,167 @@ +import os + +import edq.cli.config.set +import edq.core.config +import edq.testing.unittest +import edq.util.dirent +import edq.util.json + +def create_test_dir(temp_dir_prefix: str) -> str: + """ + Create a temp dir and populate it with dirents for testing. + + This test data directory is laid out as: + . + ├── empty-config + │   └── edq-config.json + ├── empty-dir + └── non-empty-config + └── edq-config.json + """ + + temp_dir = edq.util.dirent.get_temp_dir(prefix = temp_dir_prefix) + + edq.util.dirent.mkdir(os.path.join(temp_dir, 'empty-dir')) + + empty_config_path = os.path.join(temp_dir, 'empty-config') + edq.util.dirent.mkdir(empty_config_path) + edq.util.json.dump_path( + {}, + os.path.join(empty_config_path, edq.core.config.DEFAULT_CONFIG_FILENAME), + ) + + non_empty_config_path = os.path.join(temp_dir, 'non-empty-config') + edq.util.dirent.mkdir(non_empty_config_path) + edq.util.json.dump_path( + {'user': 'user@test.edulinq.org'}, + os.path.join(non_empty_config_path, edq.core.config.DEFAULT_CONFIG_FILENAME), + ) + + return temp_dir + +class TestWriteConfig(edq.testing.unittest.BaseTest): + """ Test basic functionality of write config. """ + + def test_write_config_base(self): + """ + Test that the given config is written correctly and required paths are created. + """ + + # [(write config arguments, expected result, error substring), ...] + test_cases = [ + # Non-exisitng Config + ( + { + 'file_path': os.path.join('empty-dir', edq.core.config.DEFAULT_CONFIG_FILENAME), + 'config_to_write': {'user': 'user@test.edulinq.org'}, + }, + { + 'path': os.path.join('empty-dir', edq.core.config.DEFAULT_CONFIG_FILENAME), + 'data': {'user': 'user@test.edulinq.org'}, + }, + None, + ), + + # Non-exisiting Path + ( + { + 'file_path': os.path.join('non-exisiting-path', edq.core.config.DEFAULT_CONFIG_FILENAME), + 'config_to_write': {'user': 'user@test.edulinq.org'}, + }, + { + 'path': os.path.join('non-exisiting-path', edq.core.config.DEFAULT_CONFIG_FILENAME), + 'data': {'user': 'user@test.edulinq.org'}, + }, + None, + ), + + # Exisiting Directory Config + ( + { + 'file_path': 'empty-dir', + 'config_to_write': {'user': 'user@test.edulinq.org'}, + }, + {}, + "Cannot open JSON file, expected a file but got a directory", + ), + + # Empty Config + ( + { + 'file_path': os.path.join('empty-config', edq.core.config.DEFAULT_CONFIG_FILENAME), + 'config_to_write': {'user': 'user@test.edulinq.org'}, + }, + { + 'path': os.path.join('empty-config', edq.core.config.DEFAULT_CONFIG_FILENAME), + 'data': {'user': 'user@test.edulinq.org'}, + }, + None, + ), + + # Non-empty Config + ( + { + 'file_path': os.path.join('non-empty-config', edq.core.config.DEFAULT_CONFIG_FILENAME), + 'config_to_write': {'pass': 'password1234'}, + }, + { + 'path': os.path.join('non-empty-config', edq.core.config.DEFAULT_CONFIG_FILENAME), + 'data': {'user': 'user@test.edulinq.org', 'pass': 'password1234'}, + }, + None, + ), + + # Config Overwrite + ( + { + 'file_path': os.path.join('non-empty-config', edq.core.config.DEFAULT_CONFIG_FILENAME), + 'config_to_write': {'user': 'admin@test.edulinq.org'}, + }, + { + 'path': os.path.join('non-empty-config', edq.core.config.DEFAULT_CONFIG_FILENAME), + 'data': {'user': 'admin@test.edulinq.org'}, + }, + None, + ), + + ] + + for (i, test_case) in enumerate(test_cases): + arguments, expected_result, error_substring = test_case + + with self.subTest(msg = f"Case {i}"): + temp_dir = create_test_dir(temp_dir_prefix = "edq-test-config-set-") + + arguments['file_path'] = os.path.join(temp_dir, arguments['file_path']) + + previous_work_directory = os.getcwd() + os.chdir(temp_dir) + + try: + edq.core.config.write_config_to_file(**arguments) + except Exception as ex: + error_string = self.format_error_string(ex) + + if (error_substring is None): + self.fail(f"Unexpected error: '{error_string}'.") + + self.assertIn(error_substring, error_string, 'Error is not as expected.') + + continue + finally: + os.chdir(previous_work_directory) + + if (error_substring is not None): + self.fail(f"Did not get expected error: '{error_substring}'.") + + + write_file_path = expected_result["path"] + write_file_path = os.path.join(temp_dir, write_file_path) + + if (not edq.util.dirent.exists(write_file_path)): + self.fail(f"Expected file does not exist at path: {write_file_path}") + + data_actual = edq.util.json.load_path(write_file_path) + data_expected = expected_result.get('data') + + self.assertJSONDictEqual(data_actual, data_expected) From 657de358dc63dd4dd69b091e84a505566dd9a70b Mon Sep 17 00:00:00 2001 From: BatuhanSA Date: Sat, 1 Nov 2025 17:53:09 -0700 Subject: [PATCH 18/19] Revised the PR one last time. --- edq/cli/config/set.py | 3 +- edq/core/config.py | 17 +- edq/core/config_test.py | 110 +++++++++++- edq/core/write_config_test.py | 167 ------------------ .../config/set/config_set_invalid_config.txt | 10 ++ .../config/set/config_set_invalid_key.txt | 10 ++ 6 files changed, 140 insertions(+), 177 deletions(-) delete mode 100644 edq/core/write_config_test.py create mode 100644 edq/testing/testdata/cli/tests/config/set/config_set_invalid_config.txt create mode 100644 edq/testing/testdata/cli/tests/config/set/config_set_invalid_key.txt diff --git a/edq/cli/config/set.py b/edq/cli/config/set.py index 818c25c..98b2ec8 100644 --- a/edq/cli/config/set.py +++ b/edq/cli/config/set.py @@ -56,8 +56,7 @@ def modify_parser(parser: argparse.ArgumentParser) -> None: + " Expected config format is =."), ) - config_file_locations = parser.add_argument_group("set config options") - group = config_file_locations.add_mutually_exclusive_group() + group = parser.add_argument_group("set config options").add_mutually_exclusive_group() group.add_argument('--local', action = 'store_true', dest = 'write_local', diff --git a/edq/core/config.py b/edq/core/config.py index 5425167..14d5379 100644 --- a/edq/core/config.py +++ b/edq/core/config.py @@ -20,7 +20,6 @@ IGNORE_CONFIGS_KEY: str = 'ignore_configs' DEFAULT_CONFIG_FILENAME: str = "edq-config.json" - class ConfigSource: """ A class for storing config source information. """ @@ -133,12 +132,18 @@ def _parse_cli_config_option( + " Configuration options must be provided in the format `=` when passed via the CLI.") (key, value) = config_option.split('=', maxsplit = 1) + key = _validate_config_key(key, value) + + return key, value - key = key.strip() +def _validate_config_key(config_key: str, config_value: str) -> str: + """ Validate a configuration key and return its stripped version. """ + + key = config_key.strip() if (key == ''): - raise ValueError(f"Found an empty configuration option key associated with the value '{value}'.") + raise ValueError(f"Found an empty configuration option key associated with the value '{config_value}'.") - return key, value + return key def _load_config_file( config_path: str, @@ -150,9 +155,7 @@ def _load_config_file( config_path = os.path.abspath(config_path) for (key, value) in edq.util.json.load_path(config_path).items(): - key = key.strip() - if (key == ''): - raise ValueError(f"Found an empty configuration option key associated with the value '{value}'.") + key = _validate_config_key(key, value) config[key] = value sources[key] = ConfigSource(label = source_label, path = config_path) diff --git a/edq/core/config_test.py b/edq/core/config_test.py index 58b8992..5314f3d 100644 --- a/edq/core/config_test.py +++ b/edq/core/config_test.py @@ -1151,7 +1151,6 @@ def test_get_tiered_config_base(self): self.assertIn(error_substring, error_string, 'Error is not as expected.') continue - finally: os.chdir(previous_work_directory) @@ -1161,3 +1160,112 @@ def test_get_tiered_config_base(self): self.assertJSONDictEqual(expected_config, actual_config) self.assertJSONDictEqual(expected_source, actual_sources) self.assertJSONDictEqual(expected_config_params, actual_config_params) + + def test_write_config_base(self): + """ + Test that the given config is written correctly and paths are created correctly. + """ + + # [(write config arguments, expected result, error substring), ...] + test_cases = [ + # Non-exisiting Path + ( + { + 'file_path': os.path.join('non-exisiting-path', edq.core.config.DEFAULT_CONFIG_FILENAME), + 'config_to_write': {'user': 'user@test.edulinq.org'}, + }, + { + 'path': os.path.join('non-exisiting-path', edq.core.config.DEFAULT_CONFIG_FILENAME), + 'data': {'user': 'user@test.edulinq.org'}, + }, + None, + ), + + # Directory Path + ( + { + 'file_path': os.path.join("dir-config", edq.core.config.DEFAULT_CONFIG_FILENAME), + 'config_to_write': {'user': 'user@test.edulinq.org'}, + }, + {}, + "Cannot open JSON file, expected a file but got a directory", + ), + + # Empty Config + ( + { + 'file_path': os.path.join('empty', edq.core.config.DEFAULT_CONFIG_FILENAME), + 'config_to_write': {'user': 'user@test.edulinq.org'}, + }, + { + 'path': os.path.join('empty', edq.core.config.DEFAULT_CONFIG_FILENAME), + 'data': {'user': 'user@test.edulinq.org'}, + }, + None, + ), + + # Non-empty Config + ( + { + 'file_path': os.path.join('simple', edq.core.config.DEFAULT_CONFIG_FILENAME), + 'config_to_write': {'pass': 'password1234'}, + }, + { + 'path': os.path.join('simple', edq.core.config.DEFAULT_CONFIG_FILENAME), + 'data': {'user': 'user@test.edulinq.org', 'pass': 'password1234'}, + }, + None, + ), + + # Non-empty Config (Overwrite) + ( + { + 'file_path': os.path.join('simple', edq.core.config.DEFAULT_CONFIG_FILENAME), + 'config_to_write': {'user': 'admin@test.edulinq.org'}, + }, + { + 'path': os.path.join('simple', edq.core.config.DEFAULT_CONFIG_FILENAME), + 'data': {'user': 'admin@test.edulinq.org'}, + }, + None, + ), + ] + + for (i, test_case) in enumerate(test_cases): + arguments, expected_result, error_substring = test_case + + with self.subTest(msg = f"Case {i}"): + temp_dir = create_test_dir(temp_dir_prefix = "edq-test-write-config-") + + arguments['file_path'] = os.path.join(temp_dir, arguments['file_path']) + + previous_work_directory = os.getcwd() + os.chdir(temp_dir) + + try: + edq.core.config.write_config_to_file(**arguments) + except Exception as ex: + error_string = self.format_error_string(ex) + + if (error_substring is None): + self.fail(f"Unexpected error: '{error_string}'.") + + self.assertIn(error_substring, error_string, 'Error is not as expected.') + + continue + finally: + os.chdir(previous_work_directory) + + if (error_substring is not None): + self.fail(f"Did not get expected error: '{error_substring}'.") + + write_file_path = expected_result["path"] + write_file_path = os.path.join(temp_dir, write_file_path) + + if (not edq.util.dirent.exists(write_file_path)): + self.fail(f"Expected file does not exist at path: {write_file_path}") + + data_actual = edq.util.json.load_path(write_file_path) + data_expected = expected_result['data'] + + self.assertJSONDictEqual(data_actual, data_expected) diff --git a/edq/core/write_config_test.py b/edq/core/write_config_test.py deleted file mode 100644 index a688c82..0000000 --- a/edq/core/write_config_test.py +++ /dev/null @@ -1,167 +0,0 @@ -import os - -import edq.cli.config.set -import edq.core.config -import edq.testing.unittest -import edq.util.dirent -import edq.util.json - -def create_test_dir(temp_dir_prefix: str) -> str: - """ - Create a temp dir and populate it with dirents for testing. - - This test data directory is laid out as: - . - ├── empty-config - │   └── edq-config.json - ├── empty-dir - └── non-empty-config - └── edq-config.json - """ - - temp_dir = edq.util.dirent.get_temp_dir(prefix = temp_dir_prefix) - - edq.util.dirent.mkdir(os.path.join(temp_dir, 'empty-dir')) - - empty_config_path = os.path.join(temp_dir, 'empty-config') - edq.util.dirent.mkdir(empty_config_path) - edq.util.json.dump_path( - {}, - os.path.join(empty_config_path, edq.core.config.DEFAULT_CONFIG_FILENAME), - ) - - non_empty_config_path = os.path.join(temp_dir, 'non-empty-config') - edq.util.dirent.mkdir(non_empty_config_path) - edq.util.json.dump_path( - {'user': 'user@test.edulinq.org'}, - os.path.join(non_empty_config_path, edq.core.config.DEFAULT_CONFIG_FILENAME), - ) - - return temp_dir - -class TestWriteConfig(edq.testing.unittest.BaseTest): - """ Test basic functionality of write config. """ - - def test_write_config_base(self): - """ - Test that the given config is written correctly and required paths are created. - """ - - # [(write config arguments, expected result, error substring), ...] - test_cases = [ - # Non-exisitng Config - ( - { - 'file_path': os.path.join('empty-dir', edq.core.config.DEFAULT_CONFIG_FILENAME), - 'config_to_write': {'user': 'user@test.edulinq.org'}, - }, - { - 'path': os.path.join('empty-dir', edq.core.config.DEFAULT_CONFIG_FILENAME), - 'data': {'user': 'user@test.edulinq.org'}, - }, - None, - ), - - # Non-exisiting Path - ( - { - 'file_path': os.path.join('non-exisiting-path', edq.core.config.DEFAULT_CONFIG_FILENAME), - 'config_to_write': {'user': 'user@test.edulinq.org'}, - }, - { - 'path': os.path.join('non-exisiting-path', edq.core.config.DEFAULT_CONFIG_FILENAME), - 'data': {'user': 'user@test.edulinq.org'}, - }, - None, - ), - - # Exisiting Directory Config - ( - { - 'file_path': 'empty-dir', - 'config_to_write': {'user': 'user@test.edulinq.org'}, - }, - {}, - "Cannot open JSON file, expected a file but got a directory", - ), - - # Empty Config - ( - { - 'file_path': os.path.join('empty-config', edq.core.config.DEFAULT_CONFIG_FILENAME), - 'config_to_write': {'user': 'user@test.edulinq.org'}, - }, - { - 'path': os.path.join('empty-config', edq.core.config.DEFAULT_CONFIG_FILENAME), - 'data': {'user': 'user@test.edulinq.org'}, - }, - None, - ), - - # Non-empty Config - ( - { - 'file_path': os.path.join('non-empty-config', edq.core.config.DEFAULT_CONFIG_FILENAME), - 'config_to_write': {'pass': 'password1234'}, - }, - { - 'path': os.path.join('non-empty-config', edq.core.config.DEFAULT_CONFIG_FILENAME), - 'data': {'user': 'user@test.edulinq.org', 'pass': 'password1234'}, - }, - None, - ), - - # Config Overwrite - ( - { - 'file_path': os.path.join('non-empty-config', edq.core.config.DEFAULT_CONFIG_FILENAME), - 'config_to_write': {'user': 'admin@test.edulinq.org'}, - }, - { - 'path': os.path.join('non-empty-config', edq.core.config.DEFAULT_CONFIG_FILENAME), - 'data': {'user': 'admin@test.edulinq.org'}, - }, - None, - ), - - ] - - for (i, test_case) in enumerate(test_cases): - arguments, expected_result, error_substring = test_case - - with self.subTest(msg = f"Case {i}"): - temp_dir = create_test_dir(temp_dir_prefix = "edq-test-config-set-") - - arguments['file_path'] = os.path.join(temp_dir, arguments['file_path']) - - previous_work_directory = os.getcwd() - os.chdir(temp_dir) - - try: - edq.core.config.write_config_to_file(**arguments) - except Exception as ex: - error_string = self.format_error_string(ex) - - if (error_substring is None): - self.fail(f"Unexpected error: '{error_string}'.") - - self.assertIn(error_substring, error_string, 'Error is not as expected.') - - continue - finally: - os.chdir(previous_work_directory) - - if (error_substring is not None): - self.fail(f"Did not get expected error: '{error_substring}'.") - - - write_file_path = expected_result["path"] - write_file_path = os.path.join(temp_dir, write_file_path) - - if (not edq.util.dirent.exists(write_file_path)): - self.fail(f"Expected file does not exist at path: {write_file_path}") - - data_actual = edq.util.json.load_path(write_file_path) - data_expected = expected_result.get('data') - - self.assertJSONDictEqual(data_actual, data_expected) diff --git a/edq/testing/testdata/cli/tests/config/set/config_set_invalid_config.txt b/edq/testing/testdata/cli/tests/config/set/config_set_invalid_config.txt new file mode 100644 index 0000000..d5fc506 --- /dev/null +++ b/edq/testing/testdata/cli/tests/config/set/config_set_invalid_config.txt @@ -0,0 +1,10 @@ + +{ + "cli": "edq.cli.config.set", + "arguments": [ + "useruser@test.edulinq.org", + ], + "error": true +} +--- +builtins.ValueError: Invalid configuration option 'useruser@test.edulinq.org'. Configuration options must be provided in the format `=` when passed via the CLI. diff --git a/edq/testing/testdata/cli/tests/config/set/config_set_invalid_key.txt b/edq/testing/testdata/cli/tests/config/set/config_set_invalid_key.txt new file mode 100644 index 0000000..2f8d3a7 --- /dev/null +++ b/edq/testing/testdata/cli/tests/config/set/config_set_invalid_key.txt @@ -0,0 +1,10 @@ +{ + "cli": "edq.cli.config.set", + "arguments": [ + "=user@test.edulinq.org", + ], + "error": true +} +--- +builtins.ValueError: Found an empty configuration option key associated with the value 'user@test.edulinq.org'. + From ce57cd53440b3d708c6f095d745232badb7c10b5 Mon Sep 17 00:00:00 2001 From: BatuhanSA Date: Sat, 1 Nov 2025 18:01:27 -0700 Subject: [PATCH 19/19] Deleted some white sapce. --- edq/core/config.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/edq/core/config.py b/edq/core/config.py index 14d5379..c03af5a 100644 --- a/edq/core/config.py +++ b/edq/core/config.py @@ -51,7 +51,6 @@ def write_config_to_file(file_path: str, config_to_write: typing.Dict[str, str]) edq.util.dirent.mkdir(os.path.dirname(file_path)) edq.util.json.dump_path(config, file_path, indent = 4) - def get_global_config_path(config_filename: str) -> str: """ Get the path for the global config file. """ @@ -284,7 +283,7 @@ def load_config_into_args( The keys of `cli_arg_config_map` represent attributes in the CLI arguments (`args`), while the values represent the desired config name this argument should be set as. For example, a `cli_arg_config_map` of `{'foo': 'baz'}` will make the CLI argument `--foo bar` - be equivalent to "--config baz=bar". + be equivalent to `--config baz=bar`. """ if (cli_arg_config_map is None):