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