diff options
-rw-r--r-- | fg21sim/configs/manager.py | 92 |
1 files changed, 86 insertions, 6 deletions
diff --git a/fg21sim/configs/manager.py b/fg21sim/configs/manager.py index 2252e61..5d7cf12 100644 --- a/fg21sim/configs/manager.py +++ b/fg21sim/configs/manager.py @@ -97,6 +97,22 @@ class ConfigManager: if userconfig: self.read_userconfig(userconfig) + def merge(self, config): + """Simply merge the given configurations without validation. + + Parameters + ---------- + config : `~ConfigObj`, dict, str, or list[str] + Supplied configurations to be merged. + """ + if not isinstance(config, ConfigObj): + try: + config = ConfigObj(config, interpolation=False) + except ConfigObjError as e: + logger.exception(e) + raise ConfigError(e) + self._config.merge(config) + def read_config(self, config): """Read, validate and merge the input config. @@ -113,8 +129,8 @@ class ConfigManager: except ConfigObjError as e: raise ConfigError(e) newconfig = self._validate(newconfig) - self._config.merge(newconfig) - logger.info("Loaded additional config: {0}".format(config)) + self.merge(newconfig) + logger.info("Loaded additional config") def read_userconfig(self, userconfig): """Read user configuration file, validate, and merge into the @@ -164,7 +180,7 @@ class ConfigManager: try: results = config.validate(validator, preserve_errors=True) except ConfigObjError as e: - raise ConfigError(e.message) + raise ConfigError(e) if results is not True: error_msg = "" for (section_list, key, res) in flatten_errors(config, results): @@ -190,8 +206,8 @@ class ConfigManager: return config.get(key, fallback) def getn(self, key, from_default=False): - """Get the config value from the nested dictionary configs using - a list of keys or a "sep"-separated keys string. + """Get the value of a config option specified by the input key from + from the configurations which is a nested dictionary. Parameters ---------- @@ -227,7 +243,71 @@ class ConfigManager: try: return reduce(operator.getitem, key, config) except (KeyError, TypeError): - raise KeyError("%s: invalid key") + raise KeyError("%s: invalid key" % "/".join(key)) + + def setn(self, key, value): + """Set the value of config option specified by a list of keys or a + "sep"-separated keys string. + + The supplied key-value config pair is first used to create a + temporary ``ConfigObj`` instance, which is then validated against + the configuration specifications. + If validated to be *valid*, the input key-value pair is then *merged* + into the configurations, otherwise, a ``ConfigError`` raised. + + NOTE/XXX + -------- + Given a ``ConfigObj`` instance with an option that does NOT exist in + the specifications, it will simply *pass* the validation against the + specifications. + There seems no way to prevent the ``Validator`` from accepting the + config options that does NOT exist in the specification. + Therefore, try to get the option value specified by the input key + first, if no ``KeyError`` raised, then it is a valid key. + + Parameters + ---------- + key : str, or list[str] + List of keys or a string of keys separated by a the ``/`` + character to specify the item in the ``self._config``, which + is a nested dictionary. + e.g., ``["section1", "key2"]``, ``"section1/key2"`` + value : str, bool, int, float, list + The value can be any acceptable type to ``ConfigObj``. + + Raises + ------ + KeyError : + The input key specifies a non-exist config option. + ConfigError : + The value fails to pass the validation against specifications. + """ + try: + val_old = self.getn(key) + except KeyError as e: + raise KeyError(e) + if val_old == value: + # No need to set this option value + return + # Create a nested dictionary from the input key-value pair + # Credit: + # * Stackoverflow: Convert a list into a nested dictionary + # https://stackoverflow.com/a/6689604/4856091 + if isinstance(key, str): + key = key.split("/") + d = reduce(lambda x, y: {y: x}, reversed(key), value) + # Create the temporary ``ConfigObj`` instance and validate it + config_new = ConfigObj(d, interpolation=False, + configspec=self._configspec) + config_new = self._validate(config_new) + # NOTE: + # The validated ``config_new`` is populated with all other options + # from the specifications. + val_new = reduce(operator.getitem, key, config_new) + d2 = reduce(lambda x, y: {y: x}, reversed(key), val_new) + self.merge(d2) + logger.info("Set config: {key}: {val_new} <= {val_old}".format( + key="/".join(key), val_new=val_new, val_old=val_old)) def get_path(self, key): """Return the absolute path of the file/directory specified by the |