You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
281 lines
10 KiB
281 lines
10 KiB
# -*- coding: utf-8 -*-
|
|
# - Copyright (c) 2001-2016, IPython-Contrib Development Team
|
|
# - Copyright (c) 2016-, jupyter-contrib development team
|
|
|
|
"""Jupyter server extension to enable, disable and configure nbextensions."""
|
|
|
|
from __future__ import unicode_literals
|
|
|
|
import io
|
|
import json
|
|
import logging
|
|
import os.path
|
|
import posixpath
|
|
import re
|
|
|
|
import yaml
|
|
from notebook import version_info as nb_version_info
|
|
from notebook.base.handlers import APIHandler, IPythonHandler
|
|
from notebook.utils import url_path_join as ujoin
|
|
from notebook.utils import path2url
|
|
from tornado import web
|
|
|
|
# attempt to use LibYaml if available
|
|
try:
|
|
from yaml import CSafeLoader as SafeLoader
|
|
except ImportError:
|
|
from yaml import SafeLoader
|
|
|
|
if nb_version_info < (5, 2, 0):
|
|
from notebook.base.handlers import json_errors
|
|
else:
|
|
# for notebook >= 5.2.0, instead of using json_errors, we must subclass
|
|
# APIHandler. Since we already do this, just do nothing extra
|
|
def json_errors(method):
|
|
return method
|
|
|
|
__version__ = '0.4.1'
|
|
|
|
absolute_url_re = re.compile(r'^(f|ht)tps?://')
|
|
|
|
|
|
def _process_nbextension_spec(spec, relative_url_base=''):
|
|
"""
|
|
Sanity-check and preprocess a spec loaded from a yaml descriptor file.
|
|
|
|
Returns *either* a processed dict *or* a string error message describing
|
|
why the spec was not suitable.
|
|
"""
|
|
if not isinstance(spec, dict):
|
|
return 'spec is not a dict, but an instance of {}'.format(type(spec))
|
|
if 'Type' not in spec:
|
|
return 'spec has no Type key'
|
|
valid_types = {'IPython Notebook Extension', 'Jupyter Notebook Extension'}
|
|
if str(spec['Type']).strip() not in valid_types:
|
|
return 'spec has invalid value for Type key: {!r}'.format(spec['Type'])
|
|
if 'Main' not in spec and 'require' not in spec:
|
|
return 'spec has neither "Main" nor "require" key'
|
|
# strip .js file extension from Main to give require path
|
|
if 'require' not in spec:
|
|
spec['require'] = os.path.splitext(spec['Main'])[0]
|
|
|
|
spec.setdefault('Name', spec['require'])
|
|
spec.setdefault('Compatibility', '?.x')
|
|
spec.setdefault('Section', 'notebook')
|
|
|
|
# generate relative URLs within the nbextensions namespace,
|
|
# from urls relative to the yaml file
|
|
for from_key, to_key in {
|
|
'Link': 'readme', 'Icon': 'icon', 'Main': 'require'}.items():
|
|
# check for the to_key first, use from_key as backup
|
|
# str needed in python 3, otherwise it ends up bytes
|
|
from_val = str(spec.get(to_key, ''))
|
|
if not from_val:
|
|
from_val = str(spec.get(from_key, ''))
|
|
if not from_val:
|
|
continue
|
|
if absolute_url_re.match(from_val):
|
|
spec[to_key] = from_val
|
|
else:
|
|
spec[to_key] = posixpath.normpath(
|
|
ujoin(relative_url_base, from_val))
|
|
return spec
|
|
|
|
|
|
def get_configurable_nbextensions(
|
|
nbextension_dirs, exclude_dirs=('mathjax',), as_dict=False, log=None):
|
|
"""Build a list of configurable nbextensions based on YAML descriptor files.
|
|
|
|
descriptor files must:
|
|
- be located under one of nbextension_dirs
|
|
- have the file extension '.yaml' or '.yml'
|
|
- contain (at minimum) the following keys:
|
|
- Type: must be 'IPython Notebook Extension' or
|
|
'Jupyter Notebook Extension'
|
|
- Main: relative url of the nbextension's main javascript file
|
|
"""
|
|
extension_dict = {}
|
|
|
|
# Traverse through nbextension subdirectories to find all yaml files
|
|
# However, don't check directories twice. See
|
|
# github.com/Jupyter-contrib/jupyter_nbextensions_configurator/issues/25
|
|
already_checked = set()
|
|
for root_nbext_dir in nbextension_dirs:
|
|
if root_nbext_dir in already_checked:
|
|
continue
|
|
else:
|
|
already_checked.add(root_nbext_dir)
|
|
if log:
|
|
log.debug(
|
|
'Looking for nbextension yaml descriptor files in {}'.format(
|
|
root_nbext_dir))
|
|
for direct, dirs, files in os.walk(root_nbext_dir, followlinks=True):
|
|
# filter to exclude directories
|
|
dirs[:] = [d for d in dirs if d not in exclude_dirs]
|
|
for filename in files:
|
|
if os.path.splitext(filename)[1] not in ['.yml', '.yaml']:
|
|
continue
|
|
yaml_path = os.path.join(direct, filename)
|
|
yaml_relpath = os.path.relpath(yaml_path, root_nbext_dir)
|
|
with io.open(yaml_path, 'r', encoding='utf-8') as stream:
|
|
try:
|
|
extension = yaml.load(stream, Loader=SafeLoader)
|
|
except yaml.YAMLError:
|
|
if log:
|
|
log.warning(
|
|
'Failed to load yaml file {}'.format(
|
|
yaml_relpath))
|
|
continue
|
|
extension = _process_nbextension_spec(
|
|
extension,
|
|
relative_url_base=path2url(os.path.dirname(yaml_relpath)))
|
|
if not isinstance(extension, dict):
|
|
continue
|
|
require = extension['require']
|
|
|
|
if log:
|
|
if require in extension_dict:
|
|
msg = 'nbextension {!r} has duplicate listings'.format(
|
|
extension['require'])
|
|
msg += ' in both {!r} and {!r}'.format(
|
|
yaml_path, extension_dict[require]['yaml_path'])
|
|
log.warning(msg)
|
|
extension['duplicate'] = True
|
|
else:
|
|
log.debug('Found nbextension {!r} in {}'.format(
|
|
extension['Name'], yaml_relpath))
|
|
|
|
extension_dict[require] = {
|
|
'yaml_path': yaml_path, 'extension': extension}
|
|
if as_dict:
|
|
return extension_dict
|
|
return [val['extension'] for val in extension_dict.values()]
|
|
|
|
|
|
class ConfiguratorLogger(logging.LoggerAdapter):
|
|
"""Logging adapter to prepend the serverextension name to log messages."""
|
|
|
|
def __init__(self, logger):
|
|
super(ConfiguratorLogger, self).__init__(logger, {})
|
|
|
|
def process(self, msg, kwargs):
|
|
return '[{}] {}'.format(__name__, msg), kwargs
|
|
|
|
|
|
class NBExtensionHandlerJSON(APIHandler):
|
|
"""
|
|
Returns a json list describing the configurable nbextensions.
|
|
|
|
Based on part of notebook.services.config.handlers.ConfigHandler
|
|
"""
|
|
|
|
@APIHandler.log.getter
|
|
def log(self):
|
|
return ConfiguratorLogger(super(NBExtensionHandlerJSON, self).log)
|
|
|
|
@web.authenticated
|
|
@json_errors
|
|
def get(self):
|
|
self.set_header("Content-Type", 'application/json')
|
|
nbapp_webapp = self.application
|
|
nbextension_dirs = nbapp_webapp.settings['nbextensions_path']
|
|
extension_list = get_configurable_nbextensions(
|
|
nbextension_dirs=nbextension_dirs, log=self.log)
|
|
self.finish(json.dumps(extension_list))
|
|
|
|
|
|
class NBExtensionHandlerPage(IPythonHandler):
|
|
"""Renders the nbextension configuration interface."""
|
|
|
|
@IPythonHandler.log.getter
|
|
def log(self):
|
|
return ConfiguratorLogger(super(NBExtensionHandlerPage, self).log)
|
|
|
|
@web.authenticated
|
|
def get(self):
|
|
"""Render the nbextension configuration interface."""
|
|
self.finish(self.render_template(
|
|
'nbextensions_configurator.html',
|
|
page_title='Nbextensions Configuration',
|
|
**self.application.settings
|
|
))
|
|
|
|
|
|
class RenderExtensionHandler(IPythonHandler):
|
|
"""Renders markdown files as pages."""
|
|
|
|
@IPythonHandler.log.getter
|
|
def log(self):
|
|
return ConfiguratorLogger(super(RenderExtensionHandler, self).log)
|
|
|
|
@web.authenticated
|
|
def get(self, path):
|
|
"""Render given markdown file."""
|
|
if not path.endswith('.md'):
|
|
# for all non-markdown items, we redirect to the actual file
|
|
return self.redirect(self.base_url + path)
|
|
self.finish(self.render_template(
|
|
'rendermd.html',
|
|
md_url=path,
|
|
page_title=path,
|
|
**self.application.settings
|
|
))
|
|
|
|
|
|
def load_jupyter_server_extension(nbapp):
|
|
"""Load and initialise the server extension."""
|
|
logger = ConfiguratorLogger(nbapp.log)
|
|
logger.debug('Loading {}'.format(__version__))
|
|
webapp = nbapp.web_app
|
|
|
|
# ensure our template gets into search path
|
|
templates_dir = os.path.join(os.path.dirname(__file__), 'templates')
|
|
logger.debug(' Editing templates path to add {}'.format(templates_dir))
|
|
rootloader = webapp.settings['jinja2_env'].loader
|
|
for loader in getattr(rootloader, 'loaders', [rootloader]):
|
|
if hasattr(loader, 'searchpath') and \
|
|
templates_dir not in loader.searchpath:
|
|
loader.searchpath.append(templates_dir)
|
|
|
|
base_url = webapp.settings['base_url']
|
|
|
|
# make sure our static files are available
|
|
static_files_path = os.path.normpath(os.path.join(
|
|
os.path.dirname(__file__), 'static'))
|
|
logger.debug(
|
|
' Editing nbextensions path to add {}'.format(static_files_path))
|
|
if static_files_path not in webapp.settings['nbextensions_path']:
|
|
webapp.settings['nbextensions_path'].append(static_files_path)
|
|
|
|
# add our new custom handlers
|
|
logger.debug(' Adding new handlers')
|
|
new_handlers = [(ujoin(base_url, '/nbextensions/' + u), h) for u, h in [
|
|
(r"?", NBExtensionHandlerPage),
|
|
(r"nbextensions_configurator/list$", NBExtensionHandlerJSON),
|
|
(r"nbextensions_configurator/rendermd/(.*)", RenderExtensionHandler),
|
|
]]
|
|
webapp.add_handlers(".*$", new_handlers)
|
|
|
|
logger.info('enabled {}'.format(__version__))
|
|
|
|
def _jupyter_nbextension_paths():
|
|
return [
|
|
dict(
|
|
section="notebook",
|
|
src="static/nbextensions_configurator",
|
|
dest="nbextensions_configurator",
|
|
require='nbextensions_configurator/config_menu/main',
|
|
),
|
|
dict(
|
|
section="tree",
|
|
src="static/nbextensions_configurator",
|
|
dest="nbextensions_configurator",
|
|
require='nbextensions_configurator/tree_tab/main',
|
|
),
|
|
]
|
|
|
|
def _jupyter_server_extension_paths():
|
|
return [{
|
|
'module': __name__
|
|
}]
|