# distgit_bugzilla_sync.config - handle configuration # # Copyright © 2018-2019 Red Hat, Inc. # # This copyrighted material is made available to anyone wishing to use, modify, # copy, or redistribute it subject to the terms and conditions of the GNU # General Public License v.2, or (at your option) any later version. This # program is distributed in the hope that it will be useful, but WITHOUT ANY # WARRANTY expressed or implied, including the implied warranties of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General # Public License for more details. You should have received a copy of the GNU # General Public License along with this program; if not, write to the Free # Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA # 02110-1301, USA. Any Red Hat trademarks that are incorporated in the source # code or documentation are not subject to the GNU General Public License and # may only be used or replicated with the express permission of Red Hat, Inc. import copy import os from typing import Optional, Sequence import toml _here = os.path.dirname(__file__) _default_conf_root = os.path.join(_here, "default-config-files") _system_conf_root = os.path.join(os.path.sep, "etc", "distgit-bugzilla-sync") config_files = { "default": { "configuration": os.path.join(_default_conf_root, "configuration.toml"), "email_overrides": os.path.join(_default_conf_root, "email_overrides.toml"), }, "system": { "configuration": os.path.join(_system_conf_root, "configuration.toml"), "email_overrides": os.path.join(_system_conf_root, "email_overrides.toml"), }, } class ConfigDict(dict): """Dictionary class for configuration data. This contains a modified update() method which allows deep-merging other dictionaries.""" def update(self, other: dict): """Merge data from another dictionary into this one. Fails if any node isn't of the same type in both, to detect configuration format issues early.""" self_set = set(self) other_set = set(other) type_error = False # keys that are in both self and other for k in self_set & other_set: self_val = self[k] other_val = other[k] if isinstance(other_val, dict): if isinstance(self_val, ConfigDict): self_val.update(other_val) elif isinstance(self_val, dict): self[k] = ConfigDict(self_val) self[k].update(other_val) else: type_error = True elif isinstance(other_val, type(self_val)): self[k] = other_val else: type_error = True if type_error: raise TypeError( f"[{k!r}] values of incompatible types: {self_val!r}, {other_val!r}" ) # add keys -> values which are missing for k in other_set - self_set: v = other[k] if isinstance(v, dict): self[k] = ConfigDict(v) else: self[k] = v def load_configuration( addl_config_files: Optional[Sequence[str]] = None, addl_email_overrides_files: Optional[Sequence[str]] = None, ): """Load stored configuration. This function loads default, system-wide, and if specified, additional configuration files into the `config` and `email_overrides` dictionaries.""" if addl_config_files is None: addl_config_files = () if addl_email_overrides_files is None: addl_email_overrides_files = () # Load default files. try: default_config = toml.load(config_files["default"]["configuration"]) except FileNotFoundError as e: raise RuntimeError( f"Default configuration file {config_files['default']['configuration']} not found." ) from e try: default_email_overrides = toml.load(config_files["default"]["email_overrides"]) except FileNotFoundError as e: raise RuntimeError( f"Default email overrides file {config_files['default']['email_overrides']} not found." ) from e # Attempt to load system-wide files. try: system_config = toml.load(config_files["system"]["configuration"]) except FileNotFoundError: system_config = {} try: system_email_overrides = toml.load(config_files["system"]["email_overrides"]) except FileNotFoundError: system_email_overrides = {} # Load additional files (say, specified on the command line), if any. addl_config = ConfigDict() for addl_config_file in addl_config_files: try: addl_config.update(toml.load(addl_config_file)) except FileNotFoundError as e: raise RuntimeError( f"Additional configuration file {addl_config_file} not found." ) from e addl_email_overrides = ConfigDict() for addl_email_overrides_file in addl_email_overrides_files: try: addl_email_overrides.update(toml.load(addl_email_overrides_file)) except FileNotFoundError as e: raise RuntimeError( f"Additional email overrides file {addl_email_overrides_file} not found." ) from e config.clear() config.update(default_config) config.update(system_config) config.update(addl_config) email_overrides.clear() email_overrides.update(default_email_overrides) email_overrides.update(system_email_overrides) email_overrides.update(addl_email_overrides) for env in config["environments"].values(): # Fill environments with default data. env_values = copy.deepcopy(config) del env_values["environments"] # Values specified in the environments should take precedence. env_values.update(env) env.clear() env.update(env_values) config = ConfigDict() email_overrides = ConfigDict()