diff --git a/voussoirkit/configlayers.py b/voussoirkit/configlayers.py new file mode 100644 index 0000000..678f49a --- /dev/null +++ b/voussoirkit/configlayers.py @@ -0,0 +1,95 @@ +''' +The purpose of this file is to work with JSON-based config files, where you +load a default configuration, then overlay a user-supplied configuration on +top, overwriting the matching keys and keeping default values for the rest. +The functions will then suggest that the user config needs to be re-saved if +the default key set contains keys that the user key set does not. +''' +import copy +import json + +from voussoirkit import pathclass + +def recursive_dict_keys(d): + ''' + Given a dictionary, return a set containing all of its keys and the keys of + all other dictionaries that appear as values within. The subkeys will use \\ + to indicate their lineage. + + {'hi': {'ho': 'neighbor'}} + + returns + + {'hi', 'hi\\ho'} + ''' + keys = set(d.keys()) + for (key, value) in d.items(): + if isinstance(value, dict): + subkeys = {f'{key}\\{subkey}' for subkey in recursive_dict_keys(value)} + keys.update(subkeys) + return keys + +def recursive_dict_update(target, supply): + ''' + Update target using supply, but when the value is a dictionary update the + insides instead of replacing the dictionary itself. This prevents keys that + exist in the target but don't exist in the supply from being erased. + Note that we are modifying target in place. + + eg: + target = {'hi': 'ho', 'neighbor': {'name': 'Wilson'}} + supply = {'neighbor': {'behind': 'fence'}} + + result: {'hi': 'ho', 'neighbor': {'name': 'Wilson', 'behind': 'fence'}} + whereas a regular dict.update would have produced: + {'hi': 'ho', 'neighbor': {'behind': 'fence'}} + ''' + for (key, value) in supply.items(): + if isinstance(value, dict): + existing = target.get(key, None) + if existing is None: + target[key] = value + else: + recursive_dict_update(target=existing, supply=value) + else: + target[key] = value + +def layer_json(target, supply): + ''' + target is the dictionary into which the final values will be placed + (presumably loaded from a default set), and supply is a layer being applied + on top of the target (presumably a user-supplied set). needs_rewrite is + True if the target contains keys that the supply did not, indicating that + the supply is incomplete. + ''' + target_keys = recursive_dict_keys(target) + supply_keys = recursive_dict_keys(supply) + needs_rewrite = target_keys > supply_keys + recursive_dict_update(target=target, supply=supply) + return (target, needs_rewrite) + +def load_file(filepath, defaults): + ''' + Given a filepath to a user-supplied config file, and a dict of default + values, return a new dict containing the user-supplied values overlaid onto + the defaults, and needs_rewrite indicating that the user config is missing + some keys from the default config. + ''' + path = pathclass.Path(filepath) + user_config_exists = path.exists + + # This config will hold the final values. We start by loading it with the + # defaults, so that as we go through the user's config we can overwrite the + # user-specified keys, and the keys which the user does not specify will + # remain default. + config = copy.deepcopy(defaults) + needs_rewrite = False + + if user_config_exists: + with open(path.absolute_path, 'r', encoding='utf-8') as handle: + user_config = json.load(handle) + (config, needs_rewrite) = layer_json(config, user_config) + else: + needs_rewrite = True + + return (config, needs_rewrite)