"""Dynamic parameters and easy access but no IDE completion. """ import json import logging import os import os.path logging.basicConfig(level=logging.DEBUG) JSON_CONFIG = """{ "url": "https://download.data.grandlyon.com/ws/ldata/velov.stations/all.json?maxfeatures=5&start=1", "delay": 15, "format": "json", "maxitems": 20 }""" class ConfigEntry: """Stores information about a config entry""" def __init__(self, value=None, env=None, doc=None, default=None): """Initialize parameter's informations. :param value: Current value of the parameter :param env: Name of environment variable for the parameter, defaults to None :param doc: Description of the parameter, defaults to None :param default: Default value for the parameter, defaults to None """ self.value = value self.env = env self.doc = doc self.default = default class Config: def __init__(self): self.parameters = {} # self.env_override() def add_parameter(self, name, env=None, doc=None, default=None): if name is not self.parameters.keys(): # If no environment variable is specified, use uppercased # parameter name if env is None: env = name.upper() # Use default for parameter value at creation self.parameters[name] = ConfigEntry(default, env, doc, default) else: logging.error("%s is already in Config.parameters dictionnary", name) def env_override(self): for key in self.parameters.keys(): self.parameters[key].value = os.environ.get( self.parameters[key].env, self.parameters[key].value ) def __getattr__(self, name): if name in self.parameters.keys(): return self.parameters[name].value else: logging.error("%s does not exist in Config.parameters dictionnary", name) raise AttributeError def set_value(self, name, value): if name in self.parameters.keys(): self.parameters[name].value = value else: logging.error("%s does not exist in Config.parameters dictionnary", name) raise AttributeError def load_config_from_file(self, filename): """Loads configuration from a file, overriding existing configuration""" if not os.path.exists(filename): logging.warning("Could not load config from %s: file not found", filename) return format = "json" # default format if filename.endswith(".json"): format = "json" else: logging.warn( "Unknown config file extension for %s, trying default format: %s", filename, format, ) with open(filename, "r") as input: if format == "json": file_config = json.load(input) self.load_config(file_config) else: logging.error("Unknow format: %s", format) raise "Unknown format: " + format def load_config(self, config, prefix=""): """Loads a config from a json-like structure. TODO read : https://stackoverflow.com/questions/6027558/flatten-nested-dictionaries-compressing-keys?noredirect=1&lq=1 :param config: the structure to use :param prefix: the key used as the prefix value for keys found in config or the key itself if config is a litteral """ if type(config) == dict: for k in config: key = prefix + "." + k if prefix != "" else k if key in self.parameters.keys(): self.parameters[key].value = config[k] else: self.load_config(config[k], key) elif type(config) == list: for i, value in enumerate(config): key = prefix + "." + str(i) if prefix != "" else str(i) if key in self.parameters.keys(): self.parameters[key].value = value else: self.load_config(value, key) else: # config is assumed to be a litteral with an unknown key self.add_parameter(prefix, None, "", config) def print_config(self): for k, v in self.parameters.items(): print(f"{k} -> {v.value}") if __name__ == "__main__": config = Config() # filename = os.path.expandvars("$HOME/Progs/python/json/http-fetcher-config.json") # config.load_config_from_file(filename) jsonconf = json.loads(JSON_CONFIG) config.load_config(jsonconf) config.print_config() config.env_override() # Check __getattr__ works # We have dynamic parameters, we have an easy write # but no completion in IDE print(f"config.delay = {config.delay}") config.set_value("delay", 30) config.print_config()