import logging import os from typing import Union import tomlkit from deprecation import deprecated from aw_core import dirs logger = logging.getLogger(__name__) def _merge(a: dict, b: dict, path=None): """ Recursively merges b into a, with b taking precedence. From: https://stackoverflow.com/a/7205107/965332 """ if path is None: path = [] for key in b: if key in a: if isinstance(a[key], dict) and isinstance(b[key], dict): _merge(a[key], b[key], path + [str(key)]) elif a[key] == b[key]: pass # same leaf value else: a[key] = b[key] else: a[key] = b[key] return a def _comment_out_toml(s: str): # Only comment out keys, not headers or empty lines return "\n".join( [ "#" + line if line.strip() and not line.strip().startswith("[") else line for line in s.split("\n") ] ) def load_config_toml( appname: str, default_config: str ) -> Union[dict, tomlkit.container.Container]: config_dir = dirs.get_config_dir(appname) config_file_path = os.path.join(config_dir, f"{appname}.toml") # Run early to ensure input is valid toml before writing default_config_toml = tomlkit.parse(default_config) # Override defaults from existing config file if os.path.isfile(config_file_path): with open(config_file_path) as f: config = f.read() config_toml = tomlkit.parse(config) else: # If file doesn't exist, write with commented-out default config with open(config_file_path, "w") as f: f.write(_comment_out_toml(default_config)) config_toml = dict() config = _merge(default_config_toml, config_toml) return config def save_config_toml(appname: str, config: str) -> None: # Check that passed config string is valid toml assert tomlkit.parse(config) config_dir = dirs.get_config_dir(appname) config_file_path = os.path.join(config_dir, f"{appname}.toml") with open(config_file_path, "w") as f: f.write(config) @deprecated( details="Use the load_config_toml function instead", deprecated_in="0.5.3", ) def load_config(appname, default_config): """ Take the defaults, and if a config file exists, use the settings specified there as overrides for their respective defaults. """ config = default_config config_dir = dirs.get_config_dir(appname) config_file_path = os.path.join(config_dir, f"{appname}.toml") # Override defaults from existing config file if os.path.isfile(config_file_path): with open(config_file_path) as f: config.read_file(f) # Overwrite current config file (necessary in case new default would be added) save_config(appname, config) return config @deprecated( details="Use the save_config_toml function instead", deprecated_in="0.5.3", ) def save_config(appname, config): config_dir = dirs.get_config_dir(appname) config_file_path = os.path.join(config_dir, f"{appname}.ini") with open(config_file_path, "w") as f: config.write(f) # Flush and fsync to lower risk of corrupted files f.flush() os.fsync(f.fileno())