diff --git a/IPython/html/base/handlers.py b/IPython/html/base/handlers.py
index 906a44c..8680f1d 100644
--- a/IPython/html/base/handlers.py
+++ b/IPython/html/base/handlers.py
@@ -175,6 +175,10 @@ class IPythonHandler(AuthenticatedHandler):
def kernel_spec_manager(self):
return self.settings['kernel_spec_manager']
+ @property
+ def config_manager(self):
+ return self.settings['config_manager']
+
#---------------------------------------------------------------
# CORS
#---------------------------------------------------------------
diff --git a/IPython/html/notebookapp.py b/IPython/html/notebookapp.py
index 270a423..c157187 100644
--- a/IPython/html/notebookapp.py
+++ b/IPython/html/notebookapp.py
@@ -125,19 +125,21 @@ def load_handlers(name):
class NotebookWebApplication(web.Application):
def __init__(self, ipython_app, kernel_manager, contents_manager,
- cluster_manager, session_manager, kernel_spec_manager, log,
+ cluster_manager, session_manager, kernel_spec_manager,
+ config_manager, log,
base_url, default_url, settings_overrides, jinja_env_options):
settings = self.init_settings(
ipython_app, kernel_manager, contents_manager, cluster_manager,
- session_manager, kernel_spec_manager, log, base_url, default_url,
- settings_overrides, jinja_env_options)
+ session_manager, kernel_spec_manager, config_manager, log, base_url,
+ default_url, settings_overrides, jinja_env_options)
handlers = self.init_handlers(settings)
super(NotebookWebApplication, self).__init__(handlers, **settings)
def init_settings(self, ipython_app, kernel_manager, contents_manager,
cluster_manager, session_manager, kernel_spec_manager,
+ config_manager,
log, base_url, default_url, settings_overrides,
jinja_env_options=None):
@@ -172,6 +174,7 @@ class NotebookWebApplication(web.Application):
cluster_manager=cluster_manager,
session_manager=session_manager,
kernel_spec_manager=kernel_spec_manager,
+ config_manager=config_manager,
# IPython stuff
nbextensions_path = ipython_app.nbextensions_path,
@@ -607,6 +610,11 @@ class NotebookApp(BaseIPythonApplication):
help='The cluster manager class to use.'
)
+ config_manager_class = DottedObjectName('IPython.html.services.config.manager.ConfigManager',
+ config = True,
+ help='The config manager class to use'
+ )
+
kernel_spec_manager = Instance(KernelSpecManager)
def _kernel_spec_manager_default(self):
@@ -722,6 +730,10 @@ class NotebookApp(BaseIPythonApplication):
self.cluster_manager = kls(parent=self, log=self.log)
self.cluster_manager.update_profiles()
+ kls = import_item(self.config_manager_class)
+ self.config_manager = kls(parent=self, log=self.log,
+ profile_dir=self.profile_dir.location)
+
def init_logging(self):
# This prevents double log messages because tornado use a root logger that
# self.log is a child of. The logging module dipatches log messages to a log
@@ -747,6 +759,7 @@ class NotebookApp(BaseIPythonApplication):
self.web_app = NotebookWebApplication(
self, self.kernel_manager, self.contents_manager,
self.cluster_manager, self.session_manager, self.kernel_spec_manager,
+ self.config_manager,
self.log, self.base_url, self.default_url, self.tornado_settings,
self.jinja_environment_options
)
diff --git a/IPython/html/services/config/handlers.py b/IPython/html/services/config/handlers.py
index 411a0ab..a7ff896 100644
--- a/IPython/html/services/config/handlers.py
+++ b/IPython/html/services/config/handlers.py
@@ -11,85 +11,27 @@ from tornado import web
from IPython.utils.py3compat import PY3
from ...base.handlers import IPythonHandler, json_errors
-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 ConfigHandler(IPythonHandler):
SUPPORTED_METHODS = ('GET', 'PUT', 'PATCH')
- @property
- def config_dir(self):
- return os.path.join(self.profile_dir, 'nbconfig')
-
- def ensure_config_dir_exists(self):
- try:
- os.mkdir(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')
-
@web.authenticated
@json_errors
def get(self, section_name):
self.set_header("Content-Type", 'application/json')
- filename = self.file_name(section_name)
- if os.path.isfile(filename):
- with io.open(filename, encoding='utf-8') as f:
- self.finish(f.read())
- else:
- self.finish("{}")
+ self.finish(json.dumps(self.config_manager.get(section_name)))
@web.authenticated
@json_errors
def put(self, section_name):
- self.get_json_body() # Will raise 400 if content is not valid JSON
- filename = self.file_name(section_name)
- self.ensure_config_dir_exists()
- with open(filename, 'wb') as f:
- f.write(self.request.body)
+ data = self.get_json_body() # Will raise 400 if content is not valid JSON
+ self.config_manager.set(section_name, data)
self.set_status(204)
@web.authenticated
@json_errors
def patch(self, section_name):
- filename = self.file_name(section_name)
- if os.path.isfile(filename):
- with io.open(filename, encoding='utf-8') as f:
- section = json.load(f)
- else:
- section = {}
-
- update = self.get_json_body()
- recursive_update(section, update)
-
- self.ensure_config_dir_exists()
- if PY3:
- f = io.open(filename, 'w', encoding='utf-8')
- else:
- f = open(filename, 'wb')
- with f:
- json.dump(section, f)
-
+ new_data = self.get_json_body()
+ section = self.config_manager.update(section_name, new_data)
self.finish(json.dumps(section))
diff --git a/IPython/html/services/config/manager.py b/IPython/html/services/config/manager.py
new file mode 100644
index 0000000..3195244
--- /dev/null
+++ b/IPython/html/services/config/manager.py
@@ -0,0 +1,87 @@
+"""Manager to read and modify frontend config data in JSON files.
+"""
+# Copyright (c) IPython Development Team.
+# Distributed under the terms of the Modified BSD License.
+import errno
+import io
+import json
+import os
+
+from IPython.config import LoggingConfigurable
+from IPython.utils.py3compat import PY3
+from IPython.utils.traitlets import Unicode
+
+
+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 ConfigManager(LoggingConfigurable):
+ profile_dir = Unicode()
+
+ @property
+ def config_dir(self):
+ return os.path.join(self.profile_dir, 'nbconfig')
+
+ def ensure_config_dir_exists(self):
+ try:
+ os.mkdir(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 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 {}
+
+ 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)
+
+ 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