|
| 1 | +# coding: utf-8 |
| 2 | +"""Manager to read and modify config data in JSON files.""" |
| 3 | + |
| 4 | +# Copyright (c) Jupyter Development Team. |
| 5 | +# Distributed under the terms of the Modified BSD License. |
| 6 | + |
| 7 | +import errno |
| 8 | +import glob |
| 9 | +import io |
| 10 | +import json |
| 11 | +import os |
| 12 | + |
| 13 | +from six import PY3 |
| 14 | +from traitlets.config import LoggingConfigurable |
| 15 | +from traitlets.traitlets import Unicode, Bool |
| 16 | + |
| 17 | + |
| 18 | +def recursive_update(target, new): |
| 19 | + """Recursively update one dictionary using another. |
| 20 | +
|
| 21 | + None values will delete their keys. |
| 22 | + """ |
| 23 | + for k, v in new.items(): |
| 24 | + if isinstance(v, dict): |
| 25 | + if k not in target: |
| 26 | + target[k] = {} |
| 27 | + recursive_update(target[k], v) |
| 28 | + if not target[k]: |
| 29 | + # Prune empty subdicts |
| 30 | + del target[k] |
| 31 | + |
| 32 | + elif v is None: |
| 33 | + target.pop(k, None) |
| 34 | + |
| 35 | + else: |
| 36 | + target[k] = v |
| 37 | + |
| 38 | + |
| 39 | +class BaseJSONConfigManager(LoggingConfigurable): |
| 40 | + """General JSON config manager |
| 41 | + |
| 42 | + Deals with persisting/storing config in a json file with optionally |
| 43 | + default values in a {section_name}.d directory. |
| 44 | + """ |
| 45 | + |
| 46 | + config_dir = Unicode('.') |
| 47 | + read_directory = Bool(True) |
| 48 | + |
| 49 | + def ensure_config_dir_exists(self): |
| 50 | + """Will try to create the config_dir directory.""" |
| 51 | + try: |
| 52 | + os.makedirs(self.config_dir, 0o755) |
| 53 | + except OSError as e: |
| 54 | + if e.errno != errno.EEXIST: |
| 55 | + raise |
| 56 | + |
| 57 | + def file_name(self, section_name): |
| 58 | + """Returns the json filename for the section_name: {config_dir}/{section_name}.json""" |
| 59 | + return os.path.join(self.config_dir, section_name+'.json') |
| 60 | + |
| 61 | + def directory(self, section_name): |
| 62 | + """Returns the directory name for the section name: {config_dir}/{section_name}.d""" |
| 63 | + return os.path.join(self.config_dir, section_name+'.d') |
| 64 | + |
| 65 | + def get(self, section_name): |
| 66 | + """Retrieve the config data for the specified section. |
| 67 | +
|
| 68 | + Returns the data as a dictionary, or an empty dictionary if the file |
| 69 | + doesn't exist. |
| 70 | + """ |
| 71 | + paths = [self.file_name(section_name)] |
| 72 | + if self.read_directory: |
| 73 | + pattern = os.path.join(self.directory(section_name), '*.json') |
| 74 | + # These json files should be processed first so that the |
| 75 | + # {section_name}.json take precedence. |
| 76 | + # The idea behind this is that installing a Python package may |
| 77 | + # put a json file somewhere in the a .d directory, while the |
| 78 | + # .json file is probably a user configuration. |
| 79 | + paths = sorted(glob.glob(pattern)) + paths |
| 80 | + self.log.debug('Paths used for configuration of %s: \n\t%s', section_name, '\n\t'.join(paths)) |
| 81 | + data = {} |
| 82 | + for path in paths: |
| 83 | + if os.path.isfile(path): |
| 84 | + with io.open(path, encoding='utf-8') as f: |
| 85 | + recursive_update(data, json.load(f)) |
| 86 | + return data |
| 87 | + |
| 88 | + def set(self, section_name, data): |
| 89 | + """Store the given config data. |
| 90 | + """ |
| 91 | + filename = self.file_name(section_name) |
| 92 | + self.ensure_config_dir_exists() |
| 93 | + |
| 94 | + if PY3: |
| 95 | + f = io.open(filename, 'w', encoding='utf-8') |
| 96 | + else: |
| 97 | + f = open(filename, 'wb') |
| 98 | + with f: |
| 99 | + json.dump(data, f, indent=2) |
| 100 | + |
| 101 | + def update(self, section_name, new_data): |
| 102 | + """Modify the config section by recursively updating it with new_data. |
| 103 | +
|
| 104 | + Returns the modified config data as a dictionary. |
| 105 | + """ |
| 106 | + data = self.get(section_name) |
| 107 | + recursive_update(data, new_data) |
| 108 | + self.set(section_name, data) |
| 109 | + return data |
0 commit comments