diff --git a/edq/cli/config/set.py b/edq/cli/config/set.py new file mode 100644 index 0000000..98b2ec8 --- /dev/null +++ b/edq/cli/config/set.py @@ -0,0 +1,81 @@ +""" +Set a configuration option. +""" + +import argparse +import sys +import typing + +import edq.core.argparser +import edq.core.config + +def run_cli(args: argparse.Namespace) -> int: + """ Run the CLI. """ + + 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))): + 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[edq.core.config.FILENAME_KEY] + 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, config) + elif (args.write_file_path is not None): + edq.core.config.write_config_to_file(args.write_file_path, config) + + 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 additional 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_argument_group("set config options").add_mutually_exclusive_group() + + group.add_argument('--local', + action = 'store_true', dest = 'write_local', + 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 = ('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 = ('Write config option(s) to the specified config file.' + + " If the given file path doesn't exist, it will be created.") + ) + +if (__name__ == '__main__'): + sys.exit(main()) diff --git a/edq/core/config.py b/edq/core/config.py index fae78ff..c03af5a 100644 --- a/edq/core/config.py +++ b/edq/core/config.py @@ -14,7 +14,9 @@ CONFIG_PATHS_KEY: str = 'config_paths' CONFIGS_KEY: str = 'configs' -GLOBAL_CONFIG_KEY: str = 'global_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" @@ -37,6 +39,18 @@ def __eq__(self, other: object) -> bool: def __str__(self) -> str: return f"({self.label}, {self.path})" +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(config_to_write) + + 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. """ @@ -47,7 +61,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 +72,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[FILENAME_KEY] = 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)) + 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)): @@ -76,6 +94,8 @@ def get_tiered_config( local_config_root_cutoff = local_config_root_cutoff, ) + 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) @@ -86,18 +106,8 @@ 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}'.") + 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) @@ -108,7 +118,31 @@ def get_tiered_config( config.pop(ignore_config, None) sources.pop(ignore_config, None) - return config, sources + return config, sources, config_params + +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. """ + + 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 = _validate_config_key(key, value) + + return key, value + +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 '{config_value}'.") + + return key def _load_config_file( config_path: str, @@ -120,9 +154,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) @@ -202,12 +234,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_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.' @@ -215,7 +249,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, + 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.' @@ -224,7 +258,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.' @@ -237,6 +271,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 +294,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..5314f3d 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, ), @@ -128,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), }, }, { @@ -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_PATH_KEY: os.path.join(temp_dir, "global", edq.core.config.DEFAULT_CONFIG_FILENAME), + }, None, ), @@ -148,11 +157,14 @@ 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_PATH_KEY: os.path.join(temp_dir, "empty", edq.core.config.DEFAULT_CONFIG_FILENAME), + }, None, ), @@ -161,11 +173,12 @@ 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), }, }, {}, {}, + {}, "Found an empty configuration option key associated with the value 'user@test.edulinq.org'.", ), @@ -174,11 +187,14 @@ 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_PATH_KEY: os.path.join(temp_dir, "dir-config", edq.core.config.DEFAULT_CONFIG_FILENAME), + }, None, ), @@ -187,11 +203,14 @@ 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_PATH_KEY: os.path.join(temp_dir, "empty-dir", "non-existent-config.json"), + }, None, ), @@ -200,11 +219,12 @@ 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), }, }, {}, {}, + {}, "Failed to read JSON file", ), @@ -213,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", ], @@ -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_PATH_KEY: os.path.join(temp_dir, "multiple-options", edq.core.config.DEFAULT_CONFIG_FILENAME), + }, None, ), @@ -236,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", ], @@ -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_PATH_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_PATH_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_PATH_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_PATH_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_PATH_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_PATH_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_PATH_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_PATH_KEY: os.path.join(temp_dir, "simple", edq.core.config.DEFAULT_CONFIG_FILENAME) + }, None, ), @@ -431,6 +484,12 @@ 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_PATH_KEY: os.path.join( + temp_dir, "nested", "nest1", "nest2b", + edq.core.config.DEFAULT_CONFIG_FILENAME, + ), + }, None, ), @@ -461,6 +520,7 @@ def test_get_tiered_config_base(self): path = os.path.join(temp_dir, "nested", edq.core.config.DEFAULT_CONFIG_FILENAME), ), }, + {}, None, ), @@ -484,6 +544,7 @@ def test_get_tiered_config_base(self): path = os.path.join(temp_dir, "simple", edq.core.config.DEFAULT_CONFIG_FILENAME), ), }, + {}, None, ), @@ -499,6 +560,7 @@ def test_get_tiered_config_base(self): }, {}, {}, + {}, None, ), @@ -514,6 +576,7 @@ def test_get_tiered_config_base(self): }, {}, {}, + {}, "Found an empty configuration option key associated with the value 'user@test.edulinq.org'.", ), @@ -529,6 +592,7 @@ def test_get_tiered_config_base(self): }, {}, {}, + {}, "IsADirectoryError", ), @@ -544,6 +608,7 @@ def test_get_tiered_config_base(self): }, {}, {}, + {}, "FileNotFoundError", ), @@ -559,6 +624,7 @@ def test_get_tiered_config_base(self): }, {}, {}, + {}, "Failed to read JSON file", ), @@ -584,6 +650,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 +676,7 @@ def test_get_tiered_config_base(self): path = os.path.join(temp_dir, "simple", edq.core.config.DEFAULT_CONFIG_FILENAME), ), }, + {}, None, ), @@ -631,6 +699,7 @@ def test_get_tiered_config_base(self): { "user": edq.core.config.ConfigSource(label = edq.core.config.CONFIG_SOURCE_CLI), }, + {}, None, ), @@ -646,6 +715,7 @@ def test_get_tiered_config_base(self): }, {}, {}, + {}, "Found an empty configuration option key associated with the value 'user@test.edulinq.org'.", ), @@ -665,6 +735,7 @@ def test_get_tiered_config_base(self): { "user": edq.core.config.ConfigSource(label = edq.core.config.CONFIG_SOURCE_CLI), }, + {}, None, ), @@ -684,6 +755,7 @@ def test_get_tiered_config_base(self): { "pass": edq.core.config.ConfigSource(label = edq.core.config.CONFIG_SOURCE_CLI), }, + {}, None, ), @@ -699,6 +771,7 @@ def test_get_tiered_config_base(self): }, {}, {}, + {}, "Invalid configuration option 'useruser@test.edulinq.org'.", ), @@ -722,6 +795,7 @@ def test_get_tiered_config_base(self): { "user": edq.core.config.ConfigSource(label = edq.core.config.CONFIG_SOURCE_CLI), }, + {}, None, ), @@ -744,6 +818,7 @@ def test_get_tiered_config_base(self): { "user": edq.core.config.ConfigSource(label = edq.core.config.CONFIG_SOURCE_CLI), }, + {}, None, ), @@ -754,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), }, }, { @@ -766,6 +841,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_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, ), @@ -777,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), }, }, { @@ -789,6 +868,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_PATH_KEY: os.path.join(temp_dir, "global", edq.core.config.DEFAULT_CONFIG_FILENAME), + }, None, ), @@ -800,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), }, }, { @@ -809,6 +891,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_PATH_KEY: os.path.join(temp_dir, "global", edq.core.config.DEFAULT_CONFIG_FILENAME), + }, None, ), @@ -831,6 +916,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_PATH_KEY: os.path.join(temp_dir, "simple", edq.core.config.DEFAULT_CONFIG_FILENAME), + }, None, ), @@ -850,6 +938,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_PATH_KEY: os.path.join(temp_dir, "simple", edq.core.config.DEFAULT_CONFIG_FILENAME), + }, None, ), @@ -872,6 +963,7 @@ def test_get_tiered_config_base(self): { "user": edq.core.config.ConfigSource(label = edq.core.config.CONFIG_SOURCE_CLI), }, + {}, None, ), @@ -886,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), }, }, { @@ -895,6 +987,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_PATH_KEY: os.path.join(temp_dir, "global", edq.core.config.DEFAULT_CONFIG_FILENAME), + }, None, ), @@ -906,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), }, }, { @@ -915,6 +1010,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_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, ), @@ -926,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), }, }, { @@ -938,6 +1037,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_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, ), @@ -960,6 +1063,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_PATH_KEY: os.path.join(temp_dir, "simple", edq.core.config.DEFAULT_CONFIG_FILENAME), + }, None, ), @@ -974,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), }, }, { @@ -988,23 +1094,27 @@ def test_get_tiered_config_base(self): ), "pass": edq.core.config.ConfigSource(label = edq.core.config.CONFIG_SOURCE_CLI), }, + { + 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, ), ] 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) 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 ) @@ -1012,12 +1122,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_PATH_KEY, None) + if (global_file_used is None): + 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_PATH_KEY, None) + if (local_file_used is 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): + 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) @@ -1027,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) @@ -1036,3 +1159,113 @@ 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/testing/cli.py b/edq/testing/cli.py index 6e0e151..172c40b 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.") @@ -288,6 +296,9 @@ def __method(self: edq.testing.unittest.BaseTest, 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: @@ -311,6 +322,7 @@ def __method(self: edq.testing.unittest.BaseTest, 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): 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..54e209f --- /dev/null +++ b/edq/testing/testdata/cli/tests/config/set/config_set_base_file.txt @@ -0,0 +1,9 @@ +{ + "cli": "edq.cli.config.set", + "arguments": [ + "user=user@test.edulinq.org", + "--file", "__TEMP_DIR__(edq-config.json)", + ], + "work_dir": "__TEMP_DIR__()" +} +--- 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..7ee6b98 --- /dev/null +++ b/edq/testing/testdata/cli/tests/config/set/config_set_base_global.txt @@ -0,0 +1,10 @@ +{ + "cli": "edq.cli.config.set", + "arguments": [ + "user=user@test.edulinq.org", + "--global", + "--config-global", "__TEMP_DIR__(edq-config.json)", + ], + "work_dir": "__TEMP_DIR__()" +} +--- 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..fee5a77 --- /dev/null +++ b/edq/testing/testdata/cli/tests/config/set/config_set_base_local.txt @@ -0,0 +1,9 @@ +{ + "cli": "edq.cli.config.set", + "arguments": [ + "user=user@test.edulinq.org", + "--local" + ], + "work_dir": "__TEMP_DIR__()" +} +--- 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'. + 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