From 4a8ca85d50ad0fdd6941e7edac8cae5902e8bd5d Mon Sep 17 00:00:00 2001 From: Christian Dreher <c.dreher@kit.edu> Date: Fri, 14 Feb 2025 13:10:29 +0100 Subject: [PATCH] feat: Allow creating subconfigs from command line --- armarx_setup/cli/config/commands.py | 2 + armarx_setup/cli/config/integration.py | 12 ++++-- armarx_setup/core/config.py | 59 +++++++++++++++++++++----- armarx_setup/core/git.py | 4 +- armarx_setup/core/module/update/git.py | 4 +- tests/e2e/test_axii_config.py | 31 +++++++++----- tests/unit/core/test_config.py | 6 +-- 7 files changed, 87 insertions(+), 31 deletions(-) diff --git a/armarx_setup/cli/config/commands.py b/armarx_setup/cli/config/commands.py index a9323af34..31fef8fa4 100644 --- a/armarx_setup/cli/config/commands.py +++ b/armarx_setup/cli/config/commands.py @@ -15,6 +15,8 @@ COMMAND_GROUPS = [] # Default. shell_complete=integration.list_config_option_values) @click.option("--global", "-g", "is_global", default=False, is_flag=True, help="Set/get/unset the global variables.") @click.option("--unset", "-u", default=False, is_flag=True, help="Unsets the variable.") +@click.option("--subconfig", "-s", default=False, is_flag=True, help="Apply to whole subconfig with the name of the " + "variable.") def config(**kwargs): """Get or set global configurations.""" diff --git a/armarx_setup/cli/config/integration.py b/armarx_setup/cli/config/integration.py index ef96290ae..9bec5a4d5 100644 --- a/armarx_setup/cli/config/integration.py +++ b/armarx_setup/cli/config/integration.py @@ -6,7 +6,7 @@ import typing as ty import rich_click as click from armarx_setup import console -from armarx_setup.core.config import config as axii_config, BuiltInConfigVariables +from armarx_setup.core.config import config as axii_config, BuiltInConfigVariables, Config from armarx_setup.cli import common, complete @@ -24,7 +24,7 @@ def list_config_option_values(ctx, param, incomplete: str) -> ty.List[str]: return complete.filter_completion(possible_values, incomplete) -def config(variable_name, variable_value=None, is_global=True, unset=False): +def config(variable_name, variable_value=None, is_global=True, unset=False, subconfig=False): # A workspace must be active if global is true. if not is_global: from armarx_setup.core.workspace import Workspace @@ -38,6 +38,11 @@ def config(variable_name, variable_value=None, is_global=True, unset=False): axii_config.unset(variable_name, is_global=is_global) console.print(f"Unset '{variable_name}' ({'global' if is_global else 'workspace-specific'}).") axii_config.store_config() + # If --subconfig was set, then the intention is to create a new subconfig. + elif subconfig: + axii_config.set(variable_name, Config(), is_global=is_global) + console.print(f"Create new subconfig '{variable_name}' ({'global' if is_global else 'workspace-specific'}).") + axii_config.store_config() # Otherwise print it if found. else: value = axii_config.get(variable_name, is_global=is_global) @@ -50,7 +55,8 @@ def config(variable_name, variable_value=None, is_global=True, unset=False): variable_value_raw = axii_config.get(variable_name, interpret=False, is_global=is_global) variable_value = axii_config.get(variable_name, interpret=True, is_global=is_global) resolved = f" ({variable_value})" if variable_value_raw != variable_value else "" - console.print(f"Set '{variable_name}' ({'global' if is_global else 'workspace-specific'}) to {variable_value_raw}{resolved}.") + console.print(f"Set '{variable_name}' ({'global' if is_global else 'workspace-specific'}) to " + f"{variable_value_raw}{resolved}.") axii_config.store_config() diff --git a/armarx_setup/core/config.py b/armarx_setup/core/config.py index e6b483aab..538f502f9 100644 --- a/armarx_setup/core/config.py +++ b/armarx_setup/core/config.py @@ -7,6 +7,7 @@ import math import multiprocessing import os import typing as ty +import shutil from xdg import xdg_config_home @@ -30,11 +31,18 @@ class ConfigVariable: def __init__( self, name: str, - value_type: type + value_type: ty.Optional[type] = None, + default_value: ty.Optional[ty.Any] = None, ): + assert value_type is not None or default_value is not None, \ + "Must supply a value_type if no default_value specified." + self.name: str = name self.value_type: type = value_type - self.value: ty.Optional[value_type] = None + self.value: ty.Optional[value_type] = default_value + + if self.value_type is None: + self.value_type = type(self.value) def verify_value(self, value): """Verify the value's format. @@ -147,6 +155,9 @@ class Config: return out_dict + def __str__(self): + return self.to_dict().__str__() + def has_variable(self, name: str) -> bool: return name in self.config_vars @@ -172,6 +183,22 @@ class Config: del self.config_vars[name] +class Subconfig(Config): + pass + + +class SubconfigVariable(ConfigVariable): + def __init__(self, name: str): + default_value = Subconfig() + super().__init__(name=name, default_value=default_value) + + def parse_value(self, value: ty.Dict[str, str]) -> Config: + return Subconfig.from_dict(**value) + + def interpret_value(self, value): + return value + + class BuiltInConfigVariable(ConfigVariable): """Models a built-in config variable.""" @@ -221,7 +248,7 @@ class DefaultWorkspaceConfigVariable(BuiltInConfigVariable): return value -class GitRemotePreferencesConfig(Config): +class GitRemotePreferencesSubconfig(Config): """Override for a Config specific for git_remote_preferences with method 'preference_for_host'.""" @@ -245,17 +272,17 @@ class GitRemotePreferencesConfig(Config): return "https" # Default is HTTPS as it requires no additional setup. -class GitRemotePreferencesConfigVariable(BuiltInConfigVariable): +class GitRemotePreferencesSubconfigVariable(BuiltInConfigVariable): """Special implementation for the git_remote_preferences configuration variable.""" def __init__(self): name = "git_remote_preferences" - default_value = GitRemotePreferencesConfig() + default_value = GitRemotePreferencesSubconfig() description = "" super().__init__(name=name, default_value=default_value, description=description) def parse_value(self, value: ty.Dict[str, str]) -> Config: - return GitRemotePreferencesConfig.from_dict(**value) + return GitRemotePreferencesSubconfig.from_dict(**value) def interpret_value(self, value): return value @@ -390,7 +417,7 @@ class BuiltInConfigVariables: default_workspace = DefaultWorkspaceConfigVariable() - git_remote_preferences = GitRemotePreferencesConfigVariable() + git_remote_preferences = GitRemotePreferencesSubconfigVariable() update_job_number = UpdateJobNumberConfigVariable() @@ -436,7 +463,10 @@ class ConfigWithBuiltIns(Config): if key in BuiltInConfigVariables.get_all_names(): cfg_var = BuiltInConfigVariables.get_by_name(key) else: - cfg_var = ConfigVariable(name=key, value_type=type(value)) + if isinstance(value, dict): + cfg_var = SubconfigVariable(name=key) + else: + cfg_var = ConfigVariable(name=key, value_type=type(value)) cfg_var.set_value(value, parse=True) config_vars[key] = cfg_var return cls(config_vars=config_vars) @@ -479,6 +509,7 @@ class AxiiConfig: self.global_config_file_name = "global_config.json" self.global_config_file_path = os.path.join(self.config_dir, self.global_config_file_name) + self.global_config_backup_file_path = os.path.join(self.config_dir, "global_config_BAK.json") self.global_config: ConfigWithBuiltIns = ConfigWithBuiltIns() self._known_workspaces_file_name = "known_workspaces.json" @@ -496,9 +527,11 @@ class AxiiConfig: self._workspace_name = name self.has_local_config = self._is_workspace_active - self.local_config_dir: str | None = os.path.join(self._workspace_dir, ".axii") if self.has_local_config else None + self.local_config_dir: str | None = os.path.join(self._workspace_dir, ".axii") \ + if self.has_local_config else None self.local_config_file_name = "config.json" - self.local_config_file_path: str | None = os.path.join(self.local_config_dir, self.local_config_file_name) if self.has_local_config else None + self.local_config_file_path: str | None = os.path.join(self.local_config_dir, self.local_config_file_name) \ + if self.has_local_config else None self.local_config: Config | None = Config() if self.has_local_config else None self._build_job_number = None @@ -604,6 +637,12 @@ class AxiiConfig: with open(self.global_config_file_path, "w", encoding="utf-8") as file: json.dump(self.global_config.to_dict(), file, indent=2) + def create_global_config_backup(self): + shutil.copyfile(self.global_config_file_path, self.global_config_backup_file_path) + + def restore_global_config_backup(self): + shutil.copyfile(self.global_config_backup_file_path, self.global_config_file_path) + def get(self, name, interpret: bool = True, is_global: bool = True): """Get the value of a variable.""" diff --git a/armarx_setup/core/git.py b/armarx_setup/core/git.py index 6655083ac..eda138abe 100644 --- a/armarx_setup/core/git.py +++ b/armarx_setup/core/git.py @@ -8,7 +8,7 @@ import git from armarx_setup import console, PACKAGE_ROOT from armarx_setup.core import error -from armarx_setup.core.config import axii_config, GitRemotePreferencesConfig +from armarx_setup.core.config import axii_config, GitRemotePreferencesSubconfig class Symbols: @@ -274,7 +274,7 @@ def correct_origin_url_configuration_issue( if module.update.git.code_hoster is not None: domain = module.update.git.code_hoster.domain() - grp: GitRemotePreferencesConfig = axii_config.global_config.get("git_remote_preferences") + grp: GitRemotePreferencesSubconfig = axii_config.global_config.get("git_remote_preferences") is_https = grp.preference_for_host(domain) == "https" else: # If no code hoster is defined for the module, try to guess preferred mechanism from current diff --git a/armarx_setup/core/module/update/git.py b/armarx_setup/core/module/update/git.py index 61fa3fdd0..195e57f76 100644 --- a/armarx_setup/core/module/update/git.py +++ b/armarx_setup/core/module/update/git.py @@ -9,7 +9,7 @@ from armarx_setup.core import error from armarx_setup.core.module import common from armarx_setup.core.module.context import Hook from armarx_setup.core.util import commands -from armarx_setup.core.config import axii_config, GitRemotePreferencesConfig +from armarx_setup.core.config import axii_config, GitRemotePreferencesSubconfig git_non_interaction_env_vars = { @@ -169,7 +169,7 @@ class Git(Hook): url = self.https_url if not prefer_https and self.code_hoster is not None: - grp: GitRemotePreferencesConfig = axii_config.get("git_remote_preferences") + grp: GitRemotePreferencesSubconfig = axii_config.get("git_remote_preferences") if grp.preference_for_host(self.code_hoster.domain()) == "ssh": url = self.ssh_url diff --git a/tests/e2e/test_axii_config.py b/tests/e2e/test_axii_config.py index 9e9796ddc..bfb0acd14 100644 --- a/tests/e2e/test_axii_config.py +++ b/tests/e2e/test_axii_config.py @@ -6,7 +6,7 @@ import os import pytest from armarx_setup.core import error -from armarx_setup.core.config import axii_config +from armarx_setup.core.config import axii_config as axii_config_raw from armarx_setup.core.util.commands import run from armarx_setup.core.workspace import Workspace @@ -14,10 +14,21 @@ from tests.e2e import cmd_args, test_workspace variable_name = "e2e_test_var" +nested_variable_prefix = "e2e" +nested_variable_name = "test_var" variable_value = "true" -def test_get_nonexistent_global_config_var(): +@pytest.fixture() +def axii_config(): + """Injecting this fixture will ensure that the actual global config is backed up and restored for e2e tests.""" + + axii_config_raw.create_global_config_backup() + yield axii_config_raw + axii_config_raw.restore_global_config_backup() + + +def test_get_nonexistent_global_config_var(axii_config): """ Test getting non-existent global config variable. """ @@ -35,31 +46,29 @@ def test_get_nonexistent_global_config_var(): assert f"No such config variable '{variable_name}'." in stdout -def test_set_global_config_var(): +def test_set_and_get_global_config_var(axii_config): """ - Test setting global config variable. + Test setting and getting global config variable. """ stdout = run(f"axii config --global {variable_name} {variable_value}", **cmd_args) assert stdout == f"Set '{variable_name}' (global) to {variable_value}." - -def test_get_global_config_var(): - """ - Test getting global config variable. - """ - stdout = run(f"axii config --global {variable_name}", **cmd_args) assert stdout == "true" -def test_unset_global_config_var(): +def test_unset_global_config_var(axii_config): """ Test unsetting global config variable. """ + stdout = run(f"axii config --global {variable_name} {variable_value}", **cmd_args) + + assert stdout == f"Set '{variable_name}' (global) to {variable_value}." + stdout = run(f"axii config --global --unset {variable_name}", **cmd_args) assert stdout == f"Unset '{variable_name}' (global)." diff --git a/tests/unit/core/test_config.py b/tests/unit/core/test_config.py index be52ab334..8d5a7a63e 100644 --- a/tests/unit/core/test_config.py +++ b/tests/unit/core/test_config.py @@ -9,7 +9,7 @@ from armarx_setup.core.config import AxiiConfig, Config, ConfigVariable from armarx_setup.core.config import ConfigWithBuiltIns, BuiltInConfigVariables, \ BuiltInConfigVariable # Overrides of Config for git_remote_preferences. -from armarx_setup.core.config import GitRemotePreferencesConfig +from armarx_setup.core.config import GitRemotePreferencesSubconfig # OVerrides of ConfigVariable for build_job_number. from armarx_setup.core.config import BuildJobNumberConfigVariable @@ -101,9 +101,9 @@ def test_interpret_git_remote_preferences(): cfg = ConfigWithBuiltIns.from_dict(**cfg_dict) - git_remote_cfg_var: GitRemotePreferencesConfig = cfg.get("git_remote_preferences") + git_remote_cfg_var: GitRemotePreferencesSubconfig = cfg.get("git_remote_preferences") - assert type(git_remote_cfg_var) is GitRemotePreferencesConfig + assert type(git_remote_cfg_var) is GitRemotePreferencesSubconfig assert git_remote_cfg_var.preference_for_host("git.h2t.iar.kit.edu") == "ssh" assert git_remote_cfg_var.preference_for_host("gitlab.com") == "ssh" -- GitLab