From a2e7383eca207e9fc826353090812d58be1f5959 Mon Sep 17 00:00:00 2001 From: Daniel Mach Date: Thu, 19 Oct 2023 14:23:04 +0200 Subject: [PATCH] Allow undefined fields in Options and HostOptions Plugins seem to be using oscrc and osc.conf.config to store their config options. All fields that are not known to osc are now stored in the 'extra_fields' dictionary and handled in __getitem__() and __setitem__() as they were regular fields. Such values are not checked for their types and the dictionary simply holds strings obtained from oscrc or anything the plugins set through the python API. --- osc/conf.py | 46 ++++++++++++++++++++++++++++++++++++++++++++-- tests/test_conf.py | 18 ++++++++++++++++++ 2 files changed, 62 insertions(+), 2 deletions(-) diff --git a/osc/conf.py b/osc/conf.py index d5579083..87043c5c 100644 --- a/osc/conf.py +++ b/osc/conf.py @@ -124,6 +124,10 @@ HttpHeader = NewType("HttpHeader", Tuple[str, str]) class OscOptions(BaseModel): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.extra_fields = {} + # compat function with the config dict def _get_field_name(self, name): if name in self.__fields__: @@ -139,8 +143,10 @@ class OscOptions(BaseModel): # compat function with the config dict def __getitem__(self, name): field_name = self._get_field_name(name) + if field_name is None: - field_name = name + return self.extra_fields[name] + try: return getattr(self, field_name) except AttributeError: @@ -149,8 +155,11 @@ class OscOptions(BaseModel): # compat function with the config dict def __setitem__(self, name, value): field_name = self._get_field_name(name) + if field_name is None: - field_name = name + self.extra_fields[name] = value + return + setattr(self, field_name, value) # compat function with the config dict @@ -1836,6 +1845,17 @@ def get_config(override_conffile=None, config = Options() config.conffile = conffile + # read 'debug' value before it gets properly stored into Options for early debug messages + if override_debug: + debug_str = str(override_debug) + elif "OSC_DEBUG" in os.environ: + debug_str = os.environ["OSC_DEBUG"] + elif "debug" in cp["general"]: + debug_str = cp["general"]["debug"] + else: + debug_str = "0" + debug = True if debug_str.strip().lower() in ("1", "yes", "true", "on") else False + # read host options first in order to populate apiurl aliases urls = [i for i in cp.sections() if i != "general"] for url in urls: @@ -1845,8 +1865,10 @@ def get_config(override_conffile=None, raise oscerr.ConfigMissingCredentialsError(f"No user found in section {url}", conffile, url) host_options = HostOptions(apiurl=apiurl, username=username, _parent=config) + known_ini_keys = set() for name, field in host_options.__fields__.items(): ini_key = field.extra.get("ini_key", name) + known_ini_keys.add(ini_key) if name == "password": # we need to handle the password first because it may be stored in a keyring instead of a config file @@ -1862,6 +1884,15 @@ def get_config(override_conffile=None, host_options.set_value_from_string(name, value) + for key, value in cp[url].items(): + if key.startswith("_"): + continue + if key in known_ini_keys: + continue + if debug: + print(f"DEBUG: Config option '[{url}]/{key}' doesn't map to any HostOptions field", file=sys.stderr) + host_options[key] = value + scheme = urlsplit(apiurl)[0] if scheme == "http" and not host_options.allow_http: msg = "The apiurl '{apiurl}' uses HTTP protocol without any encryption.\n" @@ -1872,8 +1903,10 @@ def get_config(override_conffile=None, config.api_host_options[apiurl] = host_options # read the main options + known_ini_keys = set() for name, field in config.__fields__.items(): ini_key = field.extra.get("ini_key", name) + known_ini_keys.add(ini_key) env_key = f"OSC_{name.upper()}" # priority: env, overrides, config @@ -1900,6 +1933,15 @@ def get_config(override_conffile=None, config.set_value_from_string(name, value) + for key, value in cp["general"].items(): + if key.startswith("_"): + continue + if key in known_ini_keys: + continue + if debug: + print(f"DEBUG: Config option '[general]/{key}' doesn't map to any Options field", file=sys.stderr) + config[key] = value + if overrides: unused_overrides_str = ", ".join((f"'{i}'" for i in overrides)) raise oscerr.ConfigError(f"Unknown config options: {unused_overrides_str}", "") diff --git a/tests/test_conf.py b/tests/test_conf.py index 9de79528..f7826eb8 100644 --- a/tests/test_conf.py +++ b/tests/test_conf.py @@ -78,6 +78,7 @@ maintained_update_project_attribute = OBS:UpdateProject show_download_progress = 0 vc-cmd = /usr/lib/build/vc status_mtime_heuristic = 0 +plugin-option = plugin-general-option [https://api.opensuse.org] credentials_mgr_class=osc.credentials.PlaintextConfigFileCredentialsManager @@ -97,6 +98,7 @@ trusted_prj = openSUSE:* SUSE:* downloadurl = http://example.com/ sshkey = ~/.ssh/id_rsa.pub disable_hdrmd5_check = 0 +plugin-option = plugin-host-option """ @@ -401,6 +403,22 @@ class TestExampleConfig(unittest.TestCase): host_options = self.config["api_host_options"][self.config["apiurl"]] self.assertEqual(host_options["disable_hdrmd5_check"], False) + def test_extra_fields(self): + self.assertEqual(self.config["plugin-option"], "plugin-general-option") + self.assertEqual(self.config.extra_fields, {"plugin-option": "plugin-general-option"}) + + self.config["new-option"] = "value" + self.assertEqual(self.config["new-option"], "value") + self.assertEqual(self.config.extra_fields, {"plugin-option": "plugin-general-option", "new-option": "value"}) + + host_options = self.config["api_host_options"][self.config["apiurl"]] + self.assertEqual(host_options["plugin-option"], "plugin-host-option") + self.assertEqual(host_options.extra_fields, {"plugin-option": "plugin-host-option"}) + + host_options["new-option"] = "value" + self.assertEqual(host_options["new-option"], "value") + self.assertEqual(host_options.extra_fields, {"plugin-option": "plugin-host-option", "new-option": "value"}) + class TestFromParent(unittest.TestCase): def setUp(self):