diff --git a/notebook/auth/__main__.py b/notebook/auth/__main__.py index 0239839759..ff413b534e 100644 --- a/notebook/auth/__main__.py +++ b/notebook/auth/__main__.py @@ -1,6 +1,6 @@ from notebook.auth import passwd from getpass import getpass -from traitlets.config.manager import BaseJSONConfigManager +from notebook.config_manager import BaseJSONConfigManager from jupyter_core.paths import jupyter_config_dir import argparse import sys diff --git a/notebook/bundler/bundlerextensions.py b/notebook/bundler/bundlerextensions.py index 71896c2cad..576336c3ad 100644 --- a/notebook/bundler/bundlerextensions.py +++ b/notebook/bundler/bundlerextensions.py @@ -5,10 +5,10 @@ from ..extensions import BaseExtensionApp, _get_config_dir, GREEN_ENABLED, RED_DISABLED from .._version import __version__ +from notebook.config_manager import BaseJSONConfigManager from jupyter_core.paths import jupyter_config_path -from traitlets.config.manager import BaseJSONConfigManager from traitlets.utils.importstring import import_item from traitlets import Bool diff --git a/notebook/bundler/tests/test_bundlerextension.py b/notebook/bundler/tests/test_bundlerextension.py index b50966f0c4..98e9bf0844 100644 --- a/notebook/bundler/tests/test_bundlerextension.py +++ b/notebook/bundler/tests/test_bundlerextension.py @@ -15,10 +15,10 @@ from ipython_genutils.tempdir import TemporaryDirectory from ipython_genutils import py3compat -from traitlets.config.manager import BaseJSONConfigManager from traitlets.tests.utils import check_help_all_output import notebook.nbextensions as nbextensions +from notebook.config_manager import BaseJSONConfigManager from ..bundlerextensions import (_get_config_dir, enable_bundler_python, disable_bundler_python) diff --git a/notebook/config_manager.py b/notebook/config_manager.py new file mode 100644 index 0000000000..d90e3e6ba5 --- /dev/null +++ b/notebook/config_manager.py @@ -0,0 +1,109 @@ +# coding: utf-8 +"""Manager to read and modify config data in JSON files.""" + +# Copyright (c) Jupyter 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, Bool + + +def recursive_update(target, new): + """Recursively update one dictionary using another. + + None values will delete their keys. + """ + for k, v in new.items(): + if isinstance(v, dict): + if k not in target: + target[k] = {} + recursive_update(target[k], v) + if not target[k]: + # Prune empty subdicts + del target[k] + + elif v is None: + target.pop(k, None) + + else: + target[k] = v + + +class BaseJSONConfigManager(LoggingConfigurable): + """General JSON config manager + + 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): + """Will try to create the config_dir directory.""" + try: + os.makedirs(self.config_dir, 0o755) + except OSError as e: + if e.errno != errno.EEXIST: + raise + + def file_name(self, section_name): + """Returns the json filename for the section_name: {config_dir}/{section_name}.json""" + return os.path.join(self.config_dir, section_name+'.json') + + def directory(self, section_name): + """Returns the directory name for the section name: {config_dir}/{section_name}.d""" + 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. + """ + 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 + self.log.debug('Paths used for configuration of %s: \n\t%s', section_name, '\n\t'.join(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. + """ + filename = self.file_name(section_name) + self.ensure_config_dir_exists() + + if PY3: + f = io.open(filename, 'w', encoding='utf-8') + else: + f = open(filename, 'wb') + with f: + json.dump(data, f, indent=2) + + def update(self, section_name, new_data): + """Modify the config section by recursively updating it with new_data. + + Returns the modified config data as a dictionary. + """ + data = self.get(section_name) + recursive_update(data, new_data) + self.set(section_name, data) + return data diff --git a/notebook/nbextensions.py b/notebook/nbextensions.py index 1e35876033..a6b5400e44 100644 --- a/notebook/nbextensions.py +++ b/notebook/nbextensions.py @@ -28,8 +28,8 @@ from ipython_genutils.py3compat import string_types, cast_unicode_py2 from ipython_genutils.tempdir import TemporaryDirectory from ._version import __version__ +from .config_manager import BaseJSONConfigManager -from traitlets.config.manager import BaseJSONConfigManager from traitlets.utils.importstring import import_item DEPRECATED_ARGUMENT = object() diff --git a/notebook/serverextensions.py b/notebook/serverextensions.py index 504d451db1..583806a127 100644 --- a/notebook/serverextensions.py +++ b/notebook/serverextensions.py @@ -11,12 +11,12 @@ from jupyter_core.paths import jupyter_config_path from ._version import __version__ +from .config_manager import BaseJSONConfigManager from .extensions import ( BaseExtensionApp, _get_config_dir, GREEN_ENABLED, RED_DISABLED, GREEN_OK, RED_X ) from traitlets import Bool from traitlets.utils.importstring import import_item -from traitlets.config.manager import BaseJSONConfigManager # ------------------------------------------------------------------------------ diff --git a/notebook/services/config/manager.py b/notebook/services/config/manager.py index a0b2d1d756..59f267dd96 100644 --- a/notebook/services/config/manager.py +++ b/notebook/services/config/manager.py @@ -5,7 +5,7 @@ import os.path -from traitlets.config.manager import BaseJSONConfigManager, recursive_update +from notebook.config_manager import BaseJSONConfigManager, recursive_update from jupyter_core.paths import jupyter_config_dir, jupyter_config_path from traitlets import Unicode, Instance, List, observe, default from traitlets.config import LoggingConfigurable diff --git a/notebook/tests/test_config_manager.py b/notebook/tests/test_config_manager.py new file mode 100644 index 0000000000..04ea9c4435 --- /dev/null +++ b/notebook/tests/test_config_manager.py @@ -0,0 +1,39 @@ +import json +import os +import shutil +import tempfile + +from notebook.config_manager import BaseJSONConfigManager + + +def test_json(): + tmpdir = tempfile.mkdtemp() + try: + 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 + finally: + shutil.rmtree(tmpdir) + + diff --git a/notebook/tests/test_nbextensions.py b/notebook/tests/test_nbextensions.py index 564cbcf9a2..aab27e2a3d 100644 --- a/notebook/tests/test_nbextensions.py +++ b/notebook/tests/test_nbextensions.py @@ -30,7 +30,7 @@ validate_nbextension, validate_nbextension_python ) -from traitlets.config.manager import BaseJSONConfigManager +from notebook.config_manager import BaseJSONConfigManager def touch(file_name, mtime=None): diff --git a/notebook/tests/test_serverextensions.py b/notebook/tests/test_serverextensions.py index 2e67d4b360..df9c0c6ef4 100644 --- a/notebook/tests/test_serverextensions.py +++ b/notebook/tests/test_serverextensions.py @@ -10,7 +10,7 @@ from ipython_genutils.tempdir import TemporaryDirectory from ipython_genutils import py3compat -from traitlets.config.manager import BaseJSONConfigManager +from notebook.config_manager import BaseJSONConfigManager from traitlets.tests.utils import check_help_all_output from jupyter_core import paths