diff --git a/traitlets/config/manager.py b/traitlets/config/manager.py index 5e5ebde9..05f80716 100644 --- a/traitlets/config/manager.py +++ b/traitlets/config/manager.py @@ -3,13 +3,14 @@ # Copyright (c) IPython Development Team. # Distributed under the terms of the Modified BSD License. import errno +import glob import io import json import os from six import PY3 from traitlets.config import LoggingConfigurable -from traitlets.traitlets import Unicode +from traitlets.traitlets import Unicode, Bool def recursive_update(target, new): @@ -36,10 +37,12 @@ def recursive_update(target, new): class BaseJSONConfigManager(LoggingConfigurable): """General JSON config manager - Deals with persisting/storing config in a json file + Deals with persisting/storing config in a json file with optionally + default values in a {section_name}.d directory. """ config_dir = Unicode('.') + read_directory = Bool(True) def ensure_config_dir_exists(self): try: @@ -51,18 +54,30 @@ def ensure_config_dir_exists(self): def file_name(self, section_name): return os.path.join(self.config_dir, section_name+'.json') + def directory(self, section_name): + return os.path.join(self.config_dir, section_name+'.d') + def get(self, section_name): """Retrieve the config data for the specified section. Returns the data as a dictionary, or an empty dictionary if the file doesn't exist. """ - filename = self.file_name(section_name) - if os.path.isfile(filename): - with io.open(filename, encoding='utf-8') as f: - return json.load(f) - else: - return {} + paths = [self.file_name(section_name)] + if self.read_directory: + pattern = os.path.join(self.directory(section_name), '*.json') + # These json files should be processed first so that the + # {section_name}.json take precedence. + # The idea behind this is that installing a Python package may + # put a json file somewhere in the a .d directory, while the + # .json file is probably a user configuration. + paths = sorted(glob.glob(pattern)) + paths + data = {} + for path in paths: + if os.path.isfile(path): + with io.open(path, encoding='utf-8') as f: + recursive_update(data, json.load(f)) + return data def set(self, section_name, data): """Store the given config data. diff --git a/traitlets/config/tests/test_manager.py b/traitlets/config/tests/test_manager.py new file mode 100644 index 00000000..0ce4f740 --- /dev/null +++ b/traitlets/config/tests/test_manager.py @@ -0,0 +1,34 @@ +import json +import os + +from traitlets.config.manager import BaseJSONConfigManager + + +def test_json(tmpdir): + tmpdir = str(tmpdir) # we're ok with a regular string path + with open(os.path.join(tmpdir, 'foo.json'), 'w') as f: + json.dump(dict(a=1), f) + # also make a foo.d/ directory with multiple json files + os.makedirs(os.path.join(tmpdir, 'foo.d')) + with open(os.path.join(tmpdir, 'foo.d', 'a.json'), 'w') as f: + json.dump(dict(a=2, b=1), f) + with open(os.path.join(tmpdir, 'foo.d', 'b.json'), 'w') as f: + json.dump(dict(a=3, b=2, c=3), f) + manager = BaseJSONConfigManager(config_dir=tmpdir, read_directory=False) + data = manager.get('foo') + assert 'a' in data + assert 'b' not in data + assert 'c' not in data + assert data['a'] == 1 + + manager = BaseJSONConfigManager(config_dir=tmpdir, read_directory=True) + data = manager.get('foo') + assert 'a' in data + assert 'b' in data + assert 'c' in data + # files should be read in order foo.d/a.json foo.d/b.json foo.json + assert data['a'] == 1 + assert data['b'] == 2 + assert data['c'] == 3 + +