diff --git a/notebook/notebookapp.py b/notebook/notebookapp.py index 8b821ef4c..414121f5b 100644 --- a/notebook/notebookapp.py +++ b/notebook/notebookapp.py @@ -78,7 +78,7 @@ from .services.sessions.sessionmanager import SessionManager from .auth.login import LoginHandler from .auth.logout import LogoutHandler -from .base.handlers import FileFindHandler, IPythonHandler +from .base.handlers import FileFindHandler from traitlets.config import Config from traitlets.config.application import catch_config_error, boolean_flag @@ -91,7 +91,7 @@ from jupyter_client.session import Session from nbformat.sign import NotebookNotary from traitlets import ( Dict, Unicode, Integer, List, Bool, Bytes, Instance, - TraitError, Type, Float + TraitError, Type, Float, observe, default, validate ) from ipython_genutils import py3compat from jupyter_core.paths import jupyter_runtime_dir, jupyter_path @@ -144,7 +144,7 @@ def load_handlers(name): class NotebookWebApplication(web.Application): - def __init__(self, ipython_app, kernel_manager, contents_manager, + def __init__(self, jupyter_app, kernel_manager, contents_manager, session_manager, kernel_spec_manager, config_manager, log, base_url, default_url, settings_overrides, jinja_env_options): @@ -157,14 +157,14 @@ class NotebookWebApplication(web.Application): log.info(DEV_NOTE_NPM) settings = self.init_settings( - ipython_app, kernel_manager, contents_manager, + jupyter_app, kernel_manager, contents_manager, 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, + def init_settings(self, jupyter_app, kernel_manager, contents_manager, session_manager, kernel_spec_manager, config_manager, log, base_url, default_url, settings_overrides, @@ -172,7 +172,7 @@ class NotebookWebApplication(web.Application): _template_path = settings_overrides.get( "template_path", - ipython_app.template_file_path, + jupyter_app.template_file_path, ) if isinstance(_template_path, py3compat.string_types): _template_path = (_template_path,) @@ -191,7 +191,7 @@ class NotebookWebApplication(web.Application): # reset the cache on server restart version_hash = datetime.datetime.now().strftime("%Y%m%d%H%M%S") - if ipython_app.ignore_minified_js: + if jupyter_app.ignore_minified_js: log.warn("""The `ignore_minified_js` flag is deprecated and no longer works. Alternatively use `npm run build:watch` when working on the notebook's Javascript and LESS""") @@ -203,8 +203,8 @@ class NotebookWebApplication(web.Application): base_url=base_url, default_url=default_url, template_path=template_path, - static_path=ipython_app.static_file_path, - static_custom_path=ipython_app.static_custom_path, + static_path=jupyter_app.static_file_path, + static_custom_path=jupyter_app.static_custom_path, static_handler_class = FileFindHandler, static_url_prefix = url_path_join(base_url,'/static/'), static_handler_args = { @@ -212,19 +212,19 @@ class NotebookWebApplication(web.Application): 'no_cache_paths': [url_path_join(base_url, 'static', 'custom')], }, version_hash=version_hash, - ignore_minified_js=ipython_app.ignore_minified_js, + ignore_minified_js=jupyter_app.ignore_minified_js, # rate limits - iopub_msg_rate_limit=ipython_app.iopub_msg_rate_limit, - iopub_data_rate_limit=ipython_app.iopub_data_rate_limit, - rate_limit_window=ipython_app.rate_limit_window, + iopub_msg_rate_limit=jupyter_app.iopub_msg_rate_limit, + iopub_data_rate_limit=jupyter_app.iopub_data_rate_limit, + rate_limit_window=jupyter_app.rate_limit_window, # authentication - cookie_secret=ipython_app.cookie_secret, + cookie_secret=jupyter_app.cookie_secret, login_url=url_path_join(base_url,'/login'), - login_handler_class=ipython_app.login_handler_class, - logout_handler_class=ipython_app.logout_handler_class, - password=ipython_app.password, + login_handler_class=jupyter_app.login_handler_class, + logout_handler_class=jupyter_app.logout_handler_class, + password=jupyter_app.password, # managers kernel_manager=kernel_manager, @@ -234,13 +234,13 @@ class NotebookWebApplication(web.Application): config_manager=config_manager, # IPython stuff - jinja_template_vars=ipython_app.jinja_template_vars, - nbextensions_path=ipython_app.nbextensions_path, - websocket_url=ipython_app.websocket_url, - mathjax_url=ipython_app.mathjax_url, - mathjax_config=ipython_app.mathjax_config, - config=ipython_app.config, - config_dir=ipython_app.config_dir, + jinja_template_vars=jupyter_app.jinja_template_vars, + nbextensions_path=jupyter_app.nbextensions_path, + websocket_url=jupyter_app.websocket_url, + mathjax_url=jupyter_app.mathjax_url, + mathjax_config=jupyter_app.mathjax_config, + config=jupyter_app.config, + config_dir=jupyter_app.config_dir, jinja2_env=env, terminals_available=False, # Set later if terminals are available ) @@ -426,14 +426,17 @@ class NotebookApp(JupyterApp): _log_formatter_cls = LogFormatter - def _log_level_default(self): + @default('log_level') + def _default_log_level(self): return logging.INFO - def _log_datefmt_default(self): + @default('log_datefmt') + def _default_log_datefmt(self): """Exclude date from default date format""" return "%H:%M:%S" - def _log_format_default(self): + @default('log_format') + def _default_log_format(self): """override default log format to include time""" return u"%(color)s[%(levelname)1.1s %(asctime)s.%(msecs).03d %(name)s]%(end_color)s %(message)s" @@ -484,7 +487,9 @@ class NotebookApp(JupyterApp): ip = Unicode('localhost', config=True, help="The IP address the notebook server will listen on." ) - def _ip_default(self): + + @default('ip') + def _default_ip(self): """Return localhost if available, 127.0.0.1 otherwise. On some (horribly broken) systems, localhost cannot be bound. @@ -499,12 +504,17 @@ class NotebookApp(JupyterApp): s.close() return 'localhost' - def _ip_changed(self, name, old, new): - if new == u'*': self.ip = u'' + @validate('ip') + def _valdate_ip(self, proposal): + value = proposal['value'] + if value == u'*': + value = u'' + return value port = Integer(8888, config=True, help="The port the notebook server will listen on." ) + port_retries = Integer(50, config=True, help="The number of additional ports to try if the specified port is not available." ) @@ -524,7 +534,9 @@ class NotebookApp(JupyterApp): cookie_secret_file = Unicode(config=True, help="""The file where the cookie secret is stored.""" ) - def _cookie_secret_file_default(self): + + @default('cookie_secret_file') + def _default_cookie_secret_file(self): return os.path.join(self.runtime_dir, 'notebook_cookie_secret') cookie_secret = Bytes(b'', config=True, @@ -536,7 +548,9 @@ class NotebookApp(JupyterApp): cookie_secret stored in plaintext (you can read the value from a file). """ ) - def _cookie_secret_default(self): + + @default('cookie_secret') + def _default_cookie_secret(self): if os.path.exists(self.cookie_secret_file): with io.open(self.cookie_secret_file, 'rb') as f: return f.read() @@ -599,9 +613,11 @@ class NotebookApp(JupyterApp): webapp_settings = Dict(config=True, help="DEPRECATED, use tornado_settings" ) - def _webapp_settings_changed(self, name, old, new): + + @observe('webapp_settings') + def _update_webapp_settings(self, change): self.log.warning("\n webapp_settings is deprecated, use tornado_settings.\n") - self.tornado_settings = new + self.tornado_settings = change['new'] tornado_settings = Dict(config=True, help="Supply overrides for the tornado.web.Application that the " @@ -636,9 +652,11 @@ class NotebookApp(JupyterApp): When disabled, equations etc. will appear as their untransformed TeX source. """ ) - def _enable_mathjax_changed(self, name, old, new): + + @observe('enable_mathjax') + def _update_enable_mathjax(self, change): """set mathjax url to empty if mathjax is disabled""" - if not new: + if not change['new']: self.mathjax_url = u'' base_url = Unicode('/', config=True, @@ -647,16 +665,22 @@ class NotebookApp(JupyterApp): Leading and trailing slashes can be omitted, and will automatically be added. ''') - def _base_url_changed(self, name, old, new): - if not new.startswith('/'): - self.base_url = '/'+new - elif not new.endswith('/'): - self.base_url = new+'/' + + @validate('base_url') + def _update_base_url(self, proposal): + value = proposal['value'] + if not value.startswith('/'): + value = '/' + value + elif not value.endswith('/'): + value = value + '/' + return value base_project_url = Unicode('/', config=True, help="""DEPRECATED use base_url""") - def _base_project_url_changed(self, name, old, new): + + @observe('base_project_url') + def _update_base_project_url(self, change): self.log.warning("base_project_url is deprecated, use base_url") - self.base_url = new + self.base_url = change['new'] extra_static_paths = List(Unicode(), config=True, help="""Extra paths to search for serving static files. @@ -673,7 +697,9 @@ class NotebookApp(JupyterApp): static_custom_path = List(Unicode(), help="""Path to search for custom.js, css""" ) - def _static_custom_path_default(self): + + @default('static_custom_path') + def _default_static_custom_path(self): return [ os.path.join(d, 'custom') for d in ( self.config_dir, @@ -715,16 +741,21 @@ class NotebookApp(JupyterApp): Should be in the form of an HTTP origin: ws[s]://hostname[:port] """ ) + mathjax_url = Unicode("", config=True, help="""The url for MathJax.js.""" ) - def _mathjax_url_default(self): + + @default('mathjax_url') + def _default_mathjax_url(self): if not self.enable_mathjax: return u'' static_url_prefix = self.tornado_settings.get("static_url_prefix", "static") return url_path_join(static_url_prefix, 'components', 'MathJax', 'MathJax.js') - def _mathjax_url_changed(self, name, old, new): + @observe('mathjax_url') + def _update_mathjax_url(self, change): + new = change['new'] if new and not self.enable_mathjax: # enable_mathjax=False overrides mathjax_url self.mathjax_url = u'' @@ -735,8 +766,9 @@ class NotebookApp(JupyterApp): help="""The MathJax.js configuration file that is to be used.""" ) - def _mathjax_config_changed(self, name, old, new): - self.log.info("Using MathJax configuration file: %s", new) + @observe('mathjax_config') + def _update_mathjax_config(self, change): + self.log.info("Using MathJax configuration file: %s", change['new']) contents_manager_class = Type( default_value=FileContentsManager, @@ -744,11 +776,13 @@ class NotebookApp(JupyterApp): config=True, help='The notebook manager class to use.' ) + kernel_manager_class = Type( default_value=MappingKernelManager, config=True, help='The kernel manager class to use.' ) + session_manager_class = Type( default_value=SessionManager, config=True, @@ -796,7 +830,8 @@ class NotebookApp(JupyterApp): info_file = Unicode() - def _info_file_default(self): + @default('info_file') + def _default_info_file(self): info_file = "nbserver-%s.json" % os.getpid() return os.path.join(self.runtime_dir, info_file) @@ -805,10 +840,12 @@ class NotebookApp(JupyterApp): DISABLED: use %pylab or %matplotlib in the notebook to enable matplotlib. """ ) - def _pylab_changed(self, name, old, new): + + @observe('pylab') + def _update_pylab(self, change): """when --pylab is specified, display a warning and exit""" if new != 'warn': - backend = ' %s' % new + backend = ' %s' % change['new'] else: backend = '' self.log.error("Support for specifying --pylab on the command line has been removed.") @@ -821,21 +858,22 @@ class NotebookApp(JupyterApp): help="The directory to use for notebooks and kernels." ) - def _notebook_dir_default(self): + @default('notebook_dir') + def _default_notebook_dir(self): if self.file_to_run: return os.path.dirname(os.path.abspath(self.file_to_run)) else: return py3compat.getcwd() - def _notebook_dir_validate(self, value, trait): + @validate('notebook_dir') + def _notebook_dir_validate(self, proposal): + value = proposal['value'] # Strip any trailing slashes # *except* if it's root _, path = os.path.splitdrive(value) if path == os.sep: return value - value = value.rstrip(os.sep) - if not os.path.isabs(value): # If we receive a non-absolute path, make it absolute. value = os.path.abspath(value) @@ -843,9 +881,11 @@ class NotebookApp(JupyterApp): raise TraitError("No such notebook dir: %r" % value) return value - def _notebook_dir_changed(self, name, old, new): + @observe('notebook_dir') + def _update_notebook_dir(self, change): """Do a bit of validation of the notebook dir.""" # setting App.notebook_dir implies setting notebook and kernel dirs as well + new = change['new'] self.config.FileContentsManager.root_dir = new self.config.MappingKernelManager.root_dir = new @@ -853,9 +893,11 @@ class NotebookApp(JupyterApp): server_extensions = List(Unicode(), config=True, help=("DEPRECATED use the nbserver_extensions dict instead") ) - def _server_extensions_changed(self, name, old, new): + + @observe('server_extensions') + def _update_server_extensions(self, change): self.log.warning("server_extensions is deprecated, use nbserver_extensions") - self.server_extensions = new + self.server_extensions = change['new'] nbserver_extensions = Dict({}, config=True, help=("Dict of Python modules to load as notebook server extensions." diff --git a/notebook/services/config/manager.py b/notebook/services/config/manager.py index 600c0c4c1..e49da5f62 100644 --- a/notebook/services/config/manager.py +++ b/notebook/services/config/manager.py @@ -7,7 +7,7 @@ import os.path from traitlets.config.manager import BaseJSONConfigManager, recursive_update from jupyter_core.paths import jupyter_config_dir, jupyter_config_path -from traitlets import Unicode, Instance, List +from traitlets import Unicode, Instance, List, observe, default from traitlets.config import LoggingConfigurable @@ -35,16 +35,23 @@ class ConfigManager(LoggingConfigurable): # Private API read_config_path = List(Unicode()) - def _read_config_path_default(self): + + @default('read_config_path') + def _default_read_config_path(self): return [os.path.join(p, 'nbconfig') for p in jupyter_config_path()] write_config_dir = Unicode() - def _write_config_dir_default(self): + + @default('write_config_dir') + def _default_write_config_dir(self): return os.path.join(jupyter_config_dir(), 'nbconfig') write_config_manager = Instance(BaseJSONConfigManager) - def _write_config_manager_default(self): + + @default('write_config_manager') + def _default_write_config_manager(self): return BaseJSONConfigManager(config_dir=self.write_config_dir) - def _write_config_dir_changed(self, name, old, new): + @observe('write_config_dir') + def _update_write_config_dir(self, change): self.write_config_manager = BaseJSONConfigManager(config_dir=self.write_config_dir) diff --git a/notebook/services/contents/filemanager.py b/notebook/services/contents/filemanager.py index 34a342462..e59496a62 100644 --- a/notebook/services/contents/filemanager.py +++ b/notebook/services/contents/filemanager.py @@ -17,9 +17,8 @@ from .filecheckpoints import FileCheckpoints from .fileio import FileManagerMixin from .manager import ContentsManager - from ipython_genutils.importstring import import_item -from traitlets import Any, Unicode, Bool, TraitError +from traitlets import Any, Unicode, Bool, TraitError, observe, default, validate from ipython_genutils.py3compat import getcwd, string_types from . import tz from notebook.utils import ( @@ -59,14 +58,17 @@ class FileContentsManager(FileManagerMixin, ContentsManager): root_dir = Unicode(config=True) - def _root_dir_default(self): + @default('root_dir') + def _default_root_dir(self): try: return self.parent.notebook_dir except AttributeError: return getcwd() save_script = Bool(False, config=True, help='DEPRECATED, use post_save_hook. Will be removed in Notebook 5.0') - def _save_script_changed(self): + + @observe('save_script') + def _update_save_script(self): self.log.warning(""" `--script` is deprecated and will be removed in notebook 5.0. @@ -101,12 +103,15 @@ class FileContentsManager(FileManagerMixin, ContentsManager): - contents_manager: this ContentsManager instance """ ) - def _post_save_hook_changed(self, name, old, new): - if new and isinstance(new, string_types): - self.post_save_hook = import_item(self.post_save_hook) - elif new: - if not callable(new): - raise TraitError("post_save_hook must be callable") + + @validate('post_save_hook') + def _validate_post_save_hook(self, proposal): + value = proposal['value'] + if isinstance(value, string_types): + value = import_item(value) + if not callable(value): + raise TraitError("post_save_hook must be callable") + return value def run_post_save_hook(self, model, os_path): """Run the post-save hook if defined, and log errors""" @@ -117,14 +122,16 @@ class FileContentsManager(FileManagerMixin, ContentsManager): except Exception: self.log.error("Post-save hook failed on %s", os_path, exc_info=True) - def _root_dir_changed(self, name, old, new): + @validate('root_dir') + def _validate_root_dir(self, proposal): """Do a bit of validation of the root_dir.""" - if not os.path.isabs(new): + value = proposal['value'] + if not os.path.isabs(value): # If we receive a non-absolute path, make it absolute. - self.root_dir = os.path.abspath(new) - return - if not os.path.isdir(new): - raise TraitError("%r is not a directory" % new) + value = os.path.abspath(value) + if not os.path.isdir(value): + raise TraitError("%r is not a directory" % value) + return value def _checkpoints_class_default(self): return FileCheckpoints diff --git a/notebook/services/contents/manager.py b/notebook/services/contents/manager.py index b4e563724..056556f70 100644 --- a/notebook/services/contents/manager.py +++ b/notebook/services/contents/manager.py @@ -24,6 +24,8 @@ from traitlets import ( TraitError, Type, Unicode, + validate, + default, ) from ipython_genutils.py3compat import string_types @@ -91,12 +93,15 @@ class ContentsManager(LoggingConfigurable): - contents_manager: this ContentsManager instance """ ) - def _pre_save_hook_changed(self, name, old, new): - if new and isinstance(new, string_types): - self.pre_save_hook = import_item(self.pre_save_hook) - elif new: - if not callable(new): - raise TraitError("pre_save_hook must be callable") + + @validate('pre_save_hook') + def _validate_pre_save_hook(self, proposal): + value = proposal['value'] + if isinstance(value, string_types): + value = import_item(self.pre_save_hook) + if not callable(value): + raise TraitError("pre_save_hook must be callable") + return value def run_pre_save_hook(self, model, path, **kwargs): """Run the pre-save hook if defined, and log errors""" @@ -111,10 +116,12 @@ class ContentsManager(LoggingConfigurable): checkpoints = Instance(Checkpoints, config=True) checkpoints_kwargs = Dict(config=True) - def _checkpoints_default(self): + @default('checkpoints') + def _default_checkpoints(self): return self.checkpoints_class(**self.checkpoints_kwargs) - def _checkpoints_kwargs_default(self): + @default('checkpoints_kwargs') + def _default_checkpoints_kwargs(self): return dict( parent=self, log=self.log, diff --git a/notebook/services/kernels/kernelmanager.py b/notebook/services/kernels/kernelmanager.py index 6297eb5ad..2577737f4 100644 --- a/notebook/services/kernels/kernelmanager.py +++ b/notebook/services/kernels/kernelmanager.py @@ -14,7 +14,7 @@ from tornado.concurrent import Future from tornado.ioloop import IOLoop from jupyter_client.multikernelmanager import MultiKernelManager -from traitlets import List, Unicode, TraitError +from traitlets import List, Unicode, TraitError, default, validate from notebook.utils import to_os_path from ipython_genutils.py3compat import getcwd @@ -23,27 +23,31 @@ from ipython_genutils.py3compat import getcwd class MappingKernelManager(MultiKernelManager): """A KernelManager that handles notebook mapping and HTTP error handling""" - def _kernel_manager_class_default(self): + @default('kernel_manager_class') + def _default_kernel_manager_class(self): return "jupyter_client.ioloop.IOLoopKernelManager" kernel_argv = List(Unicode()) root_dir = Unicode(config=True) - def _root_dir_default(self): + @default('root_dir') + def _default_root_dir(self): try: return self.parent.notebook_dir except AttributeError: return getcwd() - def _root_dir_changed(self, name, old, new): + @validate('root_dir') + def _update_root_dir(self, proposal): """Do a bit of validation of the root dir.""" - if not os.path.isabs(new): + value = proposal['value'] + if not os.path.isabs(value): # If we receive a non-absolute path, make it absolute. - self.root_dir = os.path.abspath(new) - return - if not os.path.exists(new) or not os.path.isdir(new): - raise TraitError("kernel root dir %r is not a directory" % new) + value = os.path.abspath(value) + if not os.path.exists(value) or not os.path.isdir(value): + raise TraitError("kernel root dir %r is not a directory" % value) + return value #------------------------------------------------------------------------- # Methods for managing kernels and sessions diff --git a/setup.py b/setup.py index dc1ff9631..a0daada46 100755 --- a/setup.py +++ b/setup.py @@ -78,7 +78,7 @@ for more information. version = version, scripts = glob(pjoin('scripts', '*')), packages = find_packages(), - package_data = find_package_data(), + package_data = find_package_data(), author = 'Jupyter Development Team', author_email = 'jupyter@googlegroups.com', url = 'http://jupyter.org', @@ -146,7 +146,7 @@ install_requires = [ 'jinja2', 'tornado>=4', 'ipython_genutils', - 'traitlets', + 'traitlets>=4.2.1', 'jupyter_core', 'jupyter_client', 'nbformat',