diff --git a/notebook/auth/__main__.py b/notebook/auth/__main__.py index 023983975..67a2cfd21 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.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 71896c2ca..53962a175 100644 --- a/notebook/bundler/bundlerextensions.py +++ b/notebook/bundler/bundlerextensions.py @@ -5,10 +5,10 @@ import os from ..extensions import BaseExtensionApp, _get_config_dir, GREEN_ENABLED, RED_DISABLED from .._version import __version__ +from notebook.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 b50966f0c..ac24072e4 100644 --- a/notebook/bundler/tests/test_bundlerextension.py +++ b/notebook/bundler/tests/test_bundlerextension.py @@ -15,10 +15,10 @@ except ImportError: 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.manager import BaseJSONConfigManager from ..bundlerextensions import (_get_config_dir, enable_bundler_python, disable_bundler_python) diff --git a/notebook/manager.py b/notebook/manager.py new file mode 100644 index 000000000..05f807164 --- /dev/null +++ b/notebook/manager.py @@ -0,0 +1,103 @@ +"""Manager to read and modify config data in JSON files. +""" +# 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, 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): + try: + os.makedirs(self.config_dir, 0o755) + except OSError as e: + if e.errno != errno.EEXIST: + raise + + 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. + """ + 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. + """ + 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 1e3587603..61894c01c 100644 --- a/notebook/nbextensions.py +++ b/notebook/nbextensions.py @@ -29,7 +29,7 @@ from ipython_genutils.py3compat import string_types, cast_unicode_py2 from ipython_genutils.tempdir import TemporaryDirectory from ._version import __version__ -from traitlets.config.manager import BaseJSONConfigManager +from notebook.manager import BaseJSONConfigManager from traitlets.utils.importstring import import_item DEPRECATED_ARGUMENT = object() diff --git a/notebook/serverextensions.py b/notebook/serverextensions.py index 504d451db..d5c0e9f4e 100644 --- a/notebook/serverextensions.py +++ b/notebook/serverextensions.py @@ -16,7 +16,7 @@ from .extensions import ( ) from traitlets import Bool from traitlets.utils.importstring import import_item -from traitlets.config.manager import BaseJSONConfigManager +from notebook.manager import BaseJSONConfigManager # ------------------------------------------------------------------------------ diff --git a/notebook/services/config/manager.py b/notebook/services/config/manager.py index a0b2d1d75..c83de4af7 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.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_manager.py b/notebook/tests/test_manager.py new file mode 100644 index 000000000..e0744ad23 --- /dev/null +++ b/notebook/tests/test_manager.py @@ -0,0 +1,34 @@ +import json +import os + +from notebook.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 + + diff --git a/notebook/tests/test_nbextensions.py b/notebook/tests/test_nbextensions.py index 564cbcf9a..a0140d7d1 100644 --- a/notebook/tests/test_nbextensions.py +++ b/notebook/tests/test_nbextensions.py @@ -30,7 +30,7 @@ from notebook.nbextensions import (install_nbextension, check_nbextension, validate_nbextension, validate_nbextension_python ) -from traitlets.config.manager import BaseJSONConfigManager +from notebook.manager import BaseJSONConfigManager def touch(file_name, mtime=None): diff --git a/notebook/tests/test_serverextensions.py b/notebook/tests/test_serverextensions.py index 2e67d4b36..c967c4a04 100644 --- a/notebook/tests/test_serverextensions.py +++ b/notebook/tests/test_serverextensions.py @@ -10,7 +10,7 @@ except ImportError: from ipython_genutils.tempdir import TemporaryDirectory from ipython_genutils import py3compat -from traitlets.config.manager import BaseJSONConfigManager +from notebook.manager import BaseJSONConfigManager from traitlets.tests.utils import check_help_all_output from jupyter_core import paths