Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
4071e08
Added set functionality, needs testing.
BatuhanSA Oct 4, 2025
1172258
Merge branch 'main' into cli-set
BatuhanSA Oct 4, 2025
aea1be8
Updated a type hint.
BatuhanSA Oct 4, 2025
df080f7
Added set config cli and basic tests.
BatuhanSA Oct 8, 2025
cb95745
Revised the set testing structure and edited some help messages.
BatuhanSA Oct 9, 2025
0730592
Merge remote-tracking branch 'upstream/main' into cli-set
BatuhanSA Oct 9, 2025
9b9d72c
Added cwd to cli testing and changed the output separator.
BatuhanSA Oct 11, 2025
71b6054
Added more tests.
BatuhanSA Oct 11, 2025
bf56258
Merge remote-tracking branch 'upstream/main' into cli-set
BatuhanSA Oct 11, 2025
cf9db93
Deleting test artifact.
BatuhanSA Oct 11, 2025
1808972
Changed the name of a testing dir.
BatuhanSA Oct 11, 2025
fc12963
Made '=' to a constant variable.
BatuhanSA Oct 11, 2025
e01ad61
Got rid of CLI_KEY_VALE_SEPERATOR
BatuhanSA Oct 11, 2025
d796b7d
Went over the code one last time before PR.
BatuhanSA Oct 11, 2025
87d8eed
Merge remote-tracking branch 'origin/main' into cli-set
BatuhanSA Oct 17, 2025
d5067d9
Added support for specifying a target direcotry when running a CLI test.
BatuhanSA Oct 17, 2025
10a2555
Added support for specifying a working directory when running a CLI t…
BatuhanSA Oct 17, 2025
b508d19
Merge remote-tracking branch 'origin/main' into cli-set
BatuhanSA Oct 24, 2025
c40e162
Relocated write config to core from cli set.
BatuhanSA Oct 24, 2025
817f502
Revised half of the PR.
BatuhanSA Oct 24, 2025
4e7ca2d
Mostly done with the PR needs testing rework.
BatuhanSA Oct 24, 2025
e43c26e
Abstracted just parsing/validation instead of the whole loop.
BatuhanSA Oct 30, 2025
9c638b5
Added the tests, need to go over the PR.
BatuhanSA Oct 30, 2025
828ccef
Merge remote-tracking branch 'origin/main' into cli-set
BatuhanSA Oct 30, 2025
657de35
Revised the PR one last time.
BatuhanSA Nov 2, 2025
bf35ea3
Merge branch 'main' into cli-set
BatuhanSA Nov 2, 2025
ce57cd5
Deleted some white sapce.
BatuhanSA Nov 2, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
81 changes: 81 additions & 0 deletions edq/cli/config/set.py
Original file line number Diff line number Diff line change
@@ -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 = "<KEY>=<VALUE>",
action = 'store', nargs = '+', type = str,
help = ('Configuration option to be set.'
+ " Expected config format is <key>=<value>."),
)

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 = "<FILE>",
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())
85 changes: 61 additions & 24 deletions edq/core/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand All @@ -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. """

Expand All @@ -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.
Expand All @@ -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)):
Expand All @@ -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)

Expand All @@ -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 `<key>=<value>` 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)
Expand All @@ -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 `<key>=<value>` 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,
Expand All @@ -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)
Expand Down Expand Up @@ -202,20 +234,22 @@ 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.'
+ ' Files are applied in the order provided and later files override earlier ones.'
+ ' 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 = "<KEY>=<VALUE>",
action = 'append', type = str, default = [],
help = ('Set a configuration option from the command-line.'
+ ' Specify options as <key>=<value> pairs.'
Expand All @@ -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.'
Expand All @@ -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:
Expand All @@ -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)
Loading