mirror of
https://github.com/jupyter/notebook.git
synced 2024-12-03 03:41:14 +08:00
Merge pull request #879 from ellisonbg/nbext-pain
New nbextensions installation API
This commit is contained in:
commit
41d6da235c
@ -0,0 +1,280 @@
|
||||
{
|
||||
"cells": [
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"# Distributing Jupyter Extensions as Python Packages"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## Overview\n",
|
||||
"### How can the notebook be extended?\n",
|
||||
"The Jupyter Notebook client and server application are both deeply customizable. Their behavior can be extended by creating, respectively:\n",
|
||||
"\n",
|
||||
"- nbextension: a notebook extension\n",
|
||||
" - a single JS file, or directory of JavaScript, Cascading StyleSheets, etc. that contain at\n",
|
||||
" minimum a JavaScript module packaged as an\n",
|
||||
" [AMD modules](https://en.wikipedia.org/wiki/Asynchronous_module_definition)\n",
|
||||
" that exports a function `load_ipython_extension`\n",
|
||||
"- server extension: an importable Python module\n",
|
||||
" - that implements `load_jupyter_server_extension`"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"### Why create a Python package for Jupyter extensions?\n",
|
||||
"Since it is rare to have a server extension that does not have any frontend components (an nbextension), for convenience and consistency, all these client and server extensions with their assets can be packaged and versioned together as a Python package with a few simple commands. This makes installing the package of extensions easier and less error-prone for the user. "
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## Installation of Jupyter Extensions\n",
|
||||
"### Install a Python package containing Jupyter Extensions\n",
|
||||
"There are several ways that you may get a Python package containing Jupyter Extensions. Commonly, you will use a package manager for your system:\n",
|
||||
"```shell\n",
|
||||
"pip install helpful_package\n",
|
||||
"# or\n",
|
||||
"conda install helpful_package\n",
|
||||
"# or\n",
|
||||
"apt-get install helpful_package\n",
|
||||
"\n",
|
||||
"# where 'helpful_package' is a Python package containing one or more Jupyter Extensions\n",
|
||||
"```"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"### Enable a Server Extension\n",
|
||||
"\n",
|
||||
"The simplest case would be to enable a server extension which has no frontend components. \n",
|
||||
"\n",
|
||||
"A `pip` user that wants their configuration stored in their home directory would type the following command:\n",
|
||||
"```shell\n",
|
||||
"jupyter serverextension enable --py helpful_package\n",
|
||||
"```\n",
|
||||
"\n",
|
||||
"Alternatively, a `virtualenv` or `conda` user can pass `--sys-prefix` which keeps their environment isolated and reproducible. For example:\n",
|
||||
"```shell\n",
|
||||
"# Make sure that your virtualenv or conda environment is activated\n",
|
||||
"[source] activate my-environment\n",
|
||||
"\n",
|
||||
"jupyter serverextension enable --py helpful_package --sys-prefix\n",
|
||||
"```"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"### Install the nbextension assets"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"If a package also has an nbextension with frontend assets that must be available (but not neccessarily enabled by default), install these assets with the following command:\n",
|
||||
"```shell\n",
|
||||
"jupyter nbextension install --py helpful_package # or --sys-prefix if using virtualenv or conda\n",
|
||||
"```"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"### Enable nbextension assets\n",
|
||||
"If a package has assets that should be loaded every time a Jupyter app (e.g. lab, notebook, dashboard, terminal) is loaded in the browser, the following command can be used to enable the nbextension:\n",
|
||||
"```shell\n",
|
||||
"jupyter nbextension enable --py helpful_package # or --sys-prefix if using virtualenv or conda\n",
|
||||
"```"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## Did it work? Check by listing Jupyter Extensions.\n",
|
||||
"After running one or more extension installation steps, you can list what is presently known about nbextensions or server extension. The following commands will list which extensions are available, whether they are enabled, and other extension details:\n",
|
||||
"\n",
|
||||
"```shell\n",
|
||||
"jupyter nbextension list\n",
|
||||
"jupyter serverextension list\n",
|
||||
"```"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## Additional resources on creating and distributing packages \n",
|
||||
"\n",
|
||||
"> Of course, in addition to the files listed, there are number of other files one needs to build a proper package. Here are some good resources:\n",
|
||||
"- [The Hitchhiker's Guide to Packaging](http://the-hitchhikers-guide-to-packaging.readthedocs.org/en/latest/quickstart.html)\n",
|
||||
"- [Repository Structure and Python](http://www.kennethreitz.org/essays/repository-structure-and-python) by Kenneth Reitz\n",
|
||||
"\n",
|
||||
"> How you distribute them, too, is important:\n",
|
||||
"- [Packaging and Distributing Projects](http://python-packaging-user-guide.readthedocs.org/en/latest/distributing/)\n",
|
||||
"- [conda: Building packages](http://conda.pydata.org/docs/building/build.html)\n",
|
||||
"\n",
|
||||
"> Here are some tools to get you started:\n",
|
||||
"- [generator-nbextension](https://github.com/Anaconda-Server/generator-nbextension)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## Example - Server extension"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"### Creating a Python package with a server extension\n",
|
||||
"\n",
|
||||
"Here is an example of a python module which contains a server extension directly on itself. It has this directory structure:\n",
|
||||
"```\n",
|
||||
"- setup.py\n",
|
||||
"- MANIFEST.in\n",
|
||||
"- my_module/\n",
|
||||
" - __init__.py\n",
|
||||
"```"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"### Defining the server extension\n",
|
||||
"This example shows that the server extension and its `load_jupyter_server_extension` function are defined in the `__init__.py` file.\n",
|
||||
"#### `my_module/__init__.py`\n",
|
||||
"\n",
|
||||
"```python\n",
|
||||
"def _jupyter_server_extension_paths():\n",
|
||||
" return [{\n",
|
||||
" \"module\": \"my_module\"\n",
|
||||
" }]\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"def load_jupyter_server_extension(nbapp):\n",
|
||||
" nbapp.log.info(\"my module enabled!\")\n",
|
||||
"```"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"### Install and enable the server extension\n",
|
||||
"Which a user can install with:\n",
|
||||
"```\n",
|
||||
"jupyter serverextension enable --py my_module [--sys-prefix]\n",
|
||||
"```"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## Example - Server extension and nbextension"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"### Creating a Python package with a server extension and nbextension\n",
|
||||
"Here is another server extension, with a front-end module. It assumes this directory structure:\n",
|
||||
"\n",
|
||||
"```\n",
|
||||
"- setup.py\n",
|
||||
"- MANIFEST.in\n",
|
||||
"- my_fancy_module/\n",
|
||||
" - __init__.py\n",
|
||||
" - static/\n",
|
||||
" index.js\n",
|
||||
"```"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {
|
||||
"collapsed": true
|
||||
},
|
||||
"source": [
|
||||
"### Defining the server extension and nbextension\n",
|
||||
"This example again shows that the server extension and its `load_jupyter_server_extension` function are defined in the `__init__.py` file. This time, there is also a function `_jupyter_nbextension_path` for the nbextension.\n",
|
||||
"\n",
|
||||
"#### `my_fancy_module/__init__.py`\n",
|
||||
"\n",
|
||||
"```python\n",
|
||||
"def _jupyter_server_extension_paths():\n",
|
||||
" return [{\n",
|
||||
" \"module\": \"my_fancy_module\"\n",
|
||||
" }]\n",
|
||||
"\n",
|
||||
"# Jupyter Extension points\n",
|
||||
"def _jupyter_nbextension_paths():\n",
|
||||
" return [dict(\n",
|
||||
" section=\"notebook\",\n",
|
||||
" # the path is relative to the `my_fancy_module` directory\n",
|
||||
" src=\"static\",\n",
|
||||
" # directory in the `nbextension/` namespace\n",
|
||||
" dest=\"my_fancy_module\",\n",
|
||||
" # _also_ in the `nbextension/` namespace\n",
|
||||
" require=\"my_fancy_module/index\")]\n",
|
||||
"\n",
|
||||
"def load_jupyter_server_extension(nbapp):\n",
|
||||
" nbapp.log.info(\"my module enabled!\")\n",
|
||||
"```"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"### Install and enable the server extension and nbextension\n",
|
||||
"\n",
|
||||
"The user can install and enable the extensions with the following set of commands:\n",
|
||||
"```\n",
|
||||
"jupyter nbextension install --py my_fancy_module [--sys-prefix|--user]\n",
|
||||
"jupyter nbextension enable --py my_fancy_module [--sys-prefix|--system]\n",
|
||||
"jupyter serverextension enable --py my_fancy_module [--sys-prefix|--system]\n",
|
||||
"```"
|
||||
]
|
||||
}
|
||||
],
|
||||
"metadata": {
|
||||
"kernelspec": {
|
||||
"display_name": "Python 3",
|
||||
"language": "python",
|
||||
"name": "python3"
|
||||
},
|
||||
"language_info": {
|
||||
"codemirror_mode": {
|
||||
"name": "ipython",
|
||||
"version": 3
|
||||
},
|
||||
"file_extension": ".py",
|
||||
"mimetype": "text/x-python",
|
||||
"name": "python",
|
||||
"nbconvert_exporter": "python",
|
||||
"pygments_lexer": "ipython3",
|
||||
"version": "3.5.1"
|
||||
}
|
||||
},
|
||||
"nbformat": 4,
|
||||
"nbformat_minor": 0
|
||||
}
|
@ -203,10 +203,12 @@ the prefix. For the action name, the following guidelines should be considered:
|
||||
Installing and enabling extensions
|
||||
----------------------------------
|
||||
|
||||
You can install your nbextension with the command:
|
||||
You can install your nbextension with the command::
|
||||
|
||||
jupyter nbextension install path/to/my_extension/
|
||||
jupyter nbextension install path/to/my_extension/ [--user|--sys-prefix]
|
||||
|
||||
The default installation is system-wide. You can use ``--user`` to do a per-user installation,
|
||||
or ``--sys-prefix`` to install to Python's prefix (e.g. in a virtual or conda environment).
|
||||
Where my_extension is the directory containing the Javascript files.
|
||||
This will copy it to a Jupyter data directory (the exact location is platform
|
||||
dependent - see :ref:`jupyter_path`).
|
||||
@ -215,11 +217,15 @@ For development, you can use the ``--symlink`` flag to symlink your extension
|
||||
rather than copying it, so there's no need to reinstall after changes.
|
||||
|
||||
To use your extension, you'll also need to **enable** it, which tells the
|
||||
notebook interface to load it. You can do that with another command:
|
||||
notebook interface to load it. You can do that with another command::
|
||||
|
||||
jupyter nbextension enable my_extension/main
|
||||
jupyter nbextension enable my_extension/main [--sys-prefix]
|
||||
|
||||
The argument refers to the Javascript module containing your
|
||||
``load_ipython_extension`` function, which is ``my_extension/main.js`` in this
|
||||
example. There is a corresponding ``disable`` command to stop using an
|
||||
extension without uninstalling it.
|
||||
|
||||
.. versionchanged:: 4.2
|
||||
|
||||
Added ``--sys-prefix`` argument
|
||||
|
@ -36,8 +36,9 @@ The Jupyter notebook
|
||||
public_server
|
||||
security
|
||||
frontend_config
|
||||
extending/index
|
||||
|
||||
examples/Notebook/rstversions/Distributing Jupyter Extensions as Python Packages.rst
|
||||
extending/index.rst
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 1
|
||||
:caption: Contributor Documentation
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -227,7 +227,7 @@ class NotebookWebApplication(web.Application):
|
||||
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,
|
||||
|
||||
|
||||
# authentication
|
||||
cookie_secret=ipython_app.cookie_secret,
|
||||
login_url=url_path_join(base_url,'/login'),
|
||||
@ -849,9 +849,18 @@ class NotebookApp(JupyterApp):
|
||||
self.config.FileContentsManager.root_dir = new
|
||||
self.config.MappingKernelManager.root_dir = new
|
||||
|
||||
# TODO: Remove me in notebook 5.0
|
||||
server_extensions = List(Unicode(), config=True,
|
||||
help=("Python modules to load as notebook server extensions. "
|
||||
"This is an experimental API, and may change in future releases.")
|
||||
help=("DEPRECATED use the nbserver_extensions dict instead")
|
||||
)
|
||||
def _server_extensions_changed(self, name, old, new):
|
||||
self.log.warning("server_extensions is deprecated, use nbserver_extensions")
|
||||
self.server_extensions = new
|
||||
|
||||
nbserver_extensions = Dict({}, config=True,
|
||||
help=("Dict of Python modules to load as notebook server extensions."
|
||||
"Entry values can be used to enable and disable the loading of"
|
||||
"the extensions.")
|
||||
)
|
||||
|
||||
reraise_server_extension_failures = Bool(
|
||||
@ -1097,17 +1106,26 @@ class NotebookApp(JupyterApp):
|
||||
|
||||
The extension API is experimental, and may change in future releases.
|
||||
"""
|
||||
|
||||
# TODO: Remove me in notebook 5.0
|
||||
for modulename in self.server_extensions:
|
||||
try:
|
||||
mod = importlib.import_module(modulename)
|
||||
func = getattr(mod, 'load_jupyter_server_extension', None)
|
||||
if func is not None:
|
||||
func(self)
|
||||
except Exception:
|
||||
if self.reraise_server_extension_failures:
|
||||
raise
|
||||
self.log.warning("Error loading server extension %s", modulename,
|
||||
exc_info=True)
|
||||
# Don't override disable state of the extension if it already exist
|
||||
# in the new traitlet
|
||||
if not modulename in self.nbserver_extensions:
|
||||
self.nbserver_extensions[modulename] = True
|
||||
|
||||
for modulename in self.nbserver_extensions:
|
||||
if self.nbserver_extensions[modulename]:
|
||||
try:
|
||||
mod = importlib.import_module(modulename)
|
||||
func = getattr(mod, 'load_jupyter_server_extension', None)
|
||||
if func is not None:
|
||||
func(self)
|
||||
except Exception:
|
||||
if self.reraise_server_extension_failures:
|
||||
raise
|
||||
self.log.warning("Error loading server extension %s", modulename,
|
||||
exc_info=True)
|
||||
|
||||
@catch_config_error
|
||||
def initialize(self, argv=None):
|
||||
|
341
notebook/serverextensions.py
Normal file
341
notebook/serverextensions.py
Normal file
@ -0,0 +1,341 @@
|
||||
# coding: utf-8
|
||||
"""Utilities for installing server extensions for the notebook"""
|
||||
|
||||
# Copyright (c) Jupyter Development Team.
|
||||
# Distributed under the terms of the Modified BSD License.
|
||||
|
||||
from __future__ import print_function
|
||||
|
||||
import importlib
|
||||
import sys
|
||||
|
||||
|
||||
from jupyter_core.paths import jupyter_config_path
|
||||
from ._version import __version__
|
||||
from .nbextensions import (
|
||||
JupyterApp, BaseNBExtensionApp, _get_config_dir,
|
||||
GREEN_ENABLED, RED_DISABLED,
|
||||
GREEN_OK, RED_X,
|
||||
)
|
||||
|
||||
from traitlets import Bool
|
||||
from traitlets.utils.importstring import import_item
|
||||
from traitlets.config.manager import BaseJSONConfigManager
|
||||
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# Public API
|
||||
# ------------------------------------------------------------------------------
|
||||
class ArgumentConflict(ValueError):
|
||||
pass
|
||||
|
||||
|
||||
def toggle_serverextension_python(import_name, enabled=None, parent=None,
|
||||
user=True, sys_prefix=False, logger=None):
|
||||
"""Toggle a server extension.
|
||||
|
||||
By default, toggles the extension in the system-wide Jupyter configuration
|
||||
location (e.g. /usr/local/etc/jupyter).
|
||||
|
||||
Parameters
|
||||
----------
|
||||
|
||||
import_name : str
|
||||
Importable Python module (dotted-notation) exposing the magic-named
|
||||
`load_jupyter_server_extension` function
|
||||
enabled : bool [default: None]
|
||||
Toggle state for the extension. Set to None to toggle, True to enable,
|
||||
and False to disable the extension.
|
||||
parent : Configurable [default: None]
|
||||
user : bool [default: True]
|
||||
Toggle in the user's configuration location (e.g. ~/.jupyter).
|
||||
sys_prefix : bool [default: False]
|
||||
Toggle in the current Python environment's configuration location
|
||||
(e.g. ~/.envs/my-env/etc/jupyter). Will override `user`.
|
||||
logger : Jupyter logger [optional]
|
||||
Logger instance to use
|
||||
"""
|
||||
user = False if sys_prefix else user
|
||||
config_dir = _get_config_dir(user=user, sys_prefix=sys_prefix)
|
||||
cm = BaseJSONConfigManager(parent=parent, config_dir=config_dir)
|
||||
cfg = cm.get("jupyter_notebook_config")
|
||||
server_extensions = (
|
||||
cfg.setdefault("NotebookApp", {})
|
||||
.setdefault("nbserver_extensions", {})
|
||||
)
|
||||
|
||||
old_enabled = server_extensions.get(import_name, None)
|
||||
new_enabled = enabled if enabled is not None else not old_enabled
|
||||
|
||||
if logger:
|
||||
if new_enabled:
|
||||
logger.info(u"Enabling: %s" % (import_name))
|
||||
else:
|
||||
logger.info(u"Disabling: %s" % (import_name))
|
||||
|
||||
server_extensions[import_name] = new_enabled
|
||||
|
||||
if logger:
|
||||
logger.info(u"- Writing config: {}".format(config_dir))
|
||||
|
||||
cm.update("jupyter_notebook_config", cfg)
|
||||
|
||||
if new_enabled:
|
||||
validate_serverextension(import_name, logger)
|
||||
|
||||
|
||||
def validate_serverextension(import_name, logger=None):
|
||||
"""Assess the health of an installed server extension
|
||||
|
||||
Returns a list of validation warnings.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
|
||||
import_name : str
|
||||
Importable Python module (dotted-notation) exposing the magic-named
|
||||
`load_jupyter_server_extension` function
|
||||
logger : Jupyter logger [optional]
|
||||
Logger instance to use
|
||||
"""
|
||||
|
||||
warnings = []
|
||||
infos = []
|
||||
|
||||
func = None
|
||||
|
||||
if logger:
|
||||
logger.info(" - Validating...")
|
||||
|
||||
try:
|
||||
mod = importlib.import_module(import_name)
|
||||
func = getattr(mod, 'load_jupyter_server_extension', None)
|
||||
except Exception:
|
||||
logger.warning("Error loading server extension %s", import_name)
|
||||
|
||||
import_msg = u" {} is {} importable?"
|
||||
if func is not None:
|
||||
infos.append(import_msg.format(GREEN_OK, import_name))
|
||||
else:
|
||||
warnings.append(import_msg.format(RED_X, import_name))
|
||||
|
||||
post_mortem = u" {} {} {}"
|
||||
if logger:
|
||||
if warnings:
|
||||
[logger.info(info) for info in infos]
|
||||
[logger.warn(warning) for warning in warnings]
|
||||
else:
|
||||
logger.info(post_mortem.format(import_name, "", GREEN_OK))
|
||||
|
||||
return warnings
|
||||
|
||||
|
||||
# ----------------------------------------------------------------------
|
||||
# Applications
|
||||
# ----------------------------------------------------------------------
|
||||
|
||||
flags = {}
|
||||
flags.update(JupyterApp.flags)
|
||||
flags.pop("y", None)
|
||||
flags.pop("generate-config", None)
|
||||
flags.update({
|
||||
"user" : ({
|
||||
"ToggleServerExtensionApp" : {
|
||||
"user" : True,
|
||||
}}, "Perform the operation for the current user"
|
||||
),
|
||||
"system" : ({
|
||||
"ToggleServerExtensionApp" : {
|
||||
"user" : False,
|
||||
"sys_prefix": False,
|
||||
}}, "Perform the operation system-wide"
|
||||
),
|
||||
"sys-prefix" : ({
|
||||
"ToggleServerExtensionApp" : {
|
||||
"sys_prefix" : True,
|
||||
}}, "Use sys.prefix as the prefix for installing server extensions"
|
||||
),
|
||||
"py" : ({
|
||||
"ToggleServerExtensionApp" : {
|
||||
"python" : True,
|
||||
}}, "Install from a Python package"
|
||||
),
|
||||
})
|
||||
flags['python'] = flags['py']
|
||||
|
||||
|
||||
class ToggleServerExtensionApp(BaseNBExtensionApp):
|
||||
"""A base class for enabling/disabling extensions"""
|
||||
name = "jupyter serverextension enable/disable"
|
||||
description = "Enable/disable a server extension using frontend configuration files."
|
||||
|
||||
aliases = {}
|
||||
flags = flags
|
||||
|
||||
user = Bool(True, config=True, help="Whether to do a user install")
|
||||
sys_prefix = Bool(False, config=True, help="Use the sys.prefix as the prefix")
|
||||
python = Bool(False, config=True, help="Install from a Python package")
|
||||
|
||||
def toggle_server_extension(self, import_name):
|
||||
"""Change the status of a named server extension.
|
||||
|
||||
Uses the value of `self._toggle_value`.
|
||||
|
||||
Parameters
|
||||
---------
|
||||
|
||||
import_name : str
|
||||
Importable Python module (dotted-notation) exposing the magic-named
|
||||
`load_jupyter_server_extension` function
|
||||
"""
|
||||
toggle_serverextension_python(
|
||||
import_name, self._toggle_value, parent=self, user=self.user,
|
||||
sys_prefix=self.sys_prefix, logger=self.log)
|
||||
|
||||
def toggle_server_extension_python(self, package):
|
||||
"""Change the status of some server extensions in a Python package.
|
||||
|
||||
Uses the value of `self._toggle_value`.
|
||||
|
||||
Parameters
|
||||
---------
|
||||
|
||||
package : str
|
||||
Importable Python module exposing the
|
||||
magic-named `_jupyter_server_extension_paths` function
|
||||
"""
|
||||
m, server_exts = _get_server_extension_metadata(package)
|
||||
for server_ext in server_exts:
|
||||
module = server_ext['module']
|
||||
self.toggle_server_extension(module)
|
||||
|
||||
def start(self):
|
||||
"""Perform the App's actions as configured"""
|
||||
if not self.extra_args:
|
||||
sys.exit('Please specify a server extension/package to enable or disable')
|
||||
for arg in self.extra_args:
|
||||
if self.python:
|
||||
self.toggle_server_extension_python(arg)
|
||||
else:
|
||||
self.toggle_server_extension(arg)
|
||||
|
||||
|
||||
class EnableServerExtensionApp(ToggleServerExtensionApp):
|
||||
"""An App that enables (and validates) Server Extensions"""
|
||||
name = "jupyter serverextension enable"
|
||||
description = """
|
||||
Enable a serverextension in configuration.
|
||||
|
||||
Usage
|
||||
jupyter serverextension enable [--system|--sys-prefix]
|
||||
"""
|
||||
_toggle_value = True
|
||||
|
||||
|
||||
class DisableServerExtensionApp(ToggleServerExtensionApp):
|
||||
"""An App that disables Server Extensions"""
|
||||
name = "jupyter serverextension disable"
|
||||
description = """
|
||||
Disable a serverextension in configuration.
|
||||
|
||||
Usage
|
||||
jupyter serverextension disable [--system|--sys-prefix]
|
||||
"""
|
||||
_toggle_value = False
|
||||
|
||||
|
||||
class ListServerExtensionsApp(BaseNBExtensionApp):
|
||||
"""An App that lists (and validates) Server Extensions"""
|
||||
name = "jupyter serverextension list"
|
||||
version = __version__
|
||||
description = "List all server extensions known by the configuration system"
|
||||
|
||||
def list_server_extensions(self):
|
||||
"""List all enabled and disabled server extensions, by config path
|
||||
|
||||
Enabled extensions are validated, potentially generating warnings.
|
||||
"""
|
||||
config_dirs = jupyter_config_path()
|
||||
for config_dir in config_dirs:
|
||||
cm = BaseJSONConfigManager(parent=self, config_dir=config_dir)
|
||||
data = cm.get("jupyter_notebook_config")
|
||||
server_extensions = (
|
||||
data.setdefault("NotebookApp", {})
|
||||
.setdefault("nbserver_extensions", {})
|
||||
)
|
||||
if server_extensions:
|
||||
print(u'config dir: {}'.format(config_dir))
|
||||
for import_name, enabled in server_extensions.items():
|
||||
print(u' {} {}'.format(
|
||||
import_name,
|
||||
GREEN_ENABLED if enabled else RED_DISABLED))
|
||||
validate_serverextension(import_name, self.log)
|
||||
|
||||
def start(self):
|
||||
"""Perform the App's actions as configured"""
|
||||
self.list_server_extensions()
|
||||
|
||||
|
||||
_examples = """
|
||||
jupyter serverextension list # list all configured server extensions
|
||||
jupyter serverextension enable --py <packagename> # enable all server extensions in a Python package
|
||||
jupyter serverextension disable --py <packagename> # disable all server extensions in a Python package
|
||||
"""
|
||||
|
||||
|
||||
class ServerExtensionApp(BaseNBExtensionApp):
|
||||
"""Root level server extension app"""
|
||||
name = "jupyter serverextension"
|
||||
version = __version__
|
||||
description = "Work with Jupyter server extensions"
|
||||
examples = _examples
|
||||
|
||||
subcommands = dict(
|
||||
enable=(EnableServerExtensionApp, "Enable an server extension"),
|
||||
disable=(DisableServerExtensionApp, "Disable an server extension"),
|
||||
list=(ListServerExtensionsApp, "List server extensions")
|
||||
)
|
||||
|
||||
def start(self):
|
||||
"""Perform the App's actions as configured"""
|
||||
super(ServerExtensionApp, self).start()
|
||||
|
||||
# The above should have called a subcommand and raised NoStart; if we
|
||||
# get here, it didn't, so we should self.log.info a message.
|
||||
subcmds = ", ".join(sorted(self.subcommands))
|
||||
sys.exit("Please supply at least one subcommand: %s" % subcmds)
|
||||
|
||||
|
||||
main = ServerExtensionApp.launch_instance
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# Private API
|
||||
# ------------------------------------------------------------------------------
|
||||
|
||||
|
||||
def _get_server_extension_metadata(module):
|
||||
"""Load server extension metadata from a module.
|
||||
|
||||
Returns a tuple of (
|
||||
the package as loaded
|
||||
a list of server extension specs: [
|
||||
{
|
||||
"module": "mockextension"
|
||||
}
|
||||
]
|
||||
)
|
||||
|
||||
Parameters
|
||||
----------
|
||||
|
||||
module : str
|
||||
Importable Python module exposing the
|
||||
magic-named `_jupyter_server_extension_paths` function
|
||||
"""
|
||||
m = import_item(module)
|
||||
if not hasattr(m, '_jupyter_server_extension_paths'):
|
||||
raise KeyError(u'The Python module {} does not include any valid server extensions'.format(module))
|
||||
return m, m._jupyter_server_extension_paths()
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
@ -5,13 +5,46 @@
|
||||
|
||||
import os.path
|
||||
|
||||
from traitlets.config.manager import BaseJSONConfigManager
|
||||
from jupyter_core.paths import jupyter_config_dir
|
||||
from traitlets import Unicode
|
||||
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.config import LoggingConfigurable
|
||||
|
||||
class ConfigManager(BaseJSONConfigManager):
|
||||
|
||||
class ConfigManager(LoggingConfigurable):
|
||||
"""Config Manager used for storing notebook frontend config"""
|
||||
|
||||
config_dir = Unicode(config=True)
|
||||
def _config_dir_default(self):
|
||||
|
||||
# Public API
|
||||
|
||||
def get(self, section_name):
|
||||
"""Get the config from all config sections."""
|
||||
config = {}
|
||||
for p in self.read_config_path:
|
||||
cm = BaseJSONConfigManager(config_dir=p)
|
||||
recursive_update(config, cm.get(section_name))
|
||||
return config
|
||||
|
||||
def set(self, section_name, data):
|
||||
"""Set the config only to the user's config."""
|
||||
return self.write_config_manager.set(section_name, data)
|
||||
|
||||
def update(self, section_name, new_data):
|
||||
"""Update the config only to the user's config."""
|
||||
return self.write_config_manager.update(section_name, new_data)
|
||||
|
||||
# Private API
|
||||
|
||||
read_config_path = List(Unicode())
|
||||
def _read_config_path_default(self):
|
||||
return [os.path.join(p, 'nbconfig') for p in jupyter_config_path()]
|
||||
|
||||
write_config_dir = Unicode()
|
||||
def _write_config_dir_default(self):
|
||||
return os.path.join(jupyter_config_dir(), 'nbconfig')
|
||||
|
||||
write_config_manager = Instance(BaseJSONConfigManager)
|
||||
def _write_config_manager_default(self):
|
||||
return BaseJSONConfigManager(config_dir=self.write_config_dir)
|
||||
|
||||
def _write_config_dir_changed(self, name, old, new):
|
||||
self.write_config_manager = BaseJSONConfigManager(config_dir=self.write_config_dir)
|
||||
|
@ -46,11 +46,27 @@ define([
|
||||
* @return {Promise} that resolves to a list of loaded module handles.
|
||||
*/
|
||||
var load_extensions = function () {
|
||||
console.log('load_extensions', arguments);
|
||||
return Promise.all(Array.prototype.map.call(arguments, load_extension)).catch(function(err) {
|
||||
console.error("Failed to load extension" + (err.requireModules.length>1?'s':'') + ":", err.requireModules, err);
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Return a list of extensions that should be active
|
||||
* The config for nbextensions comes in as a dict where keys are
|
||||
* nbextensions paths and the values are a bool indicating if it
|
||||
* should be active. This returns a list of nbextension paths
|
||||
* where the value is true
|
||||
*/
|
||||
function filter_extensions(nbext_config) {
|
||||
var active = [];
|
||||
Object.keys(nbext_config).forEach(function (nbext) {
|
||||
if (nbext_config[nbext]) {active.push(nbext);}
|
||||
});
|
||||
return active;
|
||||
}
|
||||
|
||||
/**
|
||||
* Wait for a config section to load, and then load the extensions specified
|
||||
* in a 'load_extensions' key inside it.
|
||||
@ -58,9 +74,8 @@ define([
|
||||
function load_extensions_from_config(section) {
|
||||
section.loaded.then(function() {
|
||||
if (section.data.load_extensions) {
|
||||
var nbextension_paths = Object.getOwnPropertyNames(
|
||||
section.data.load_extensions);
|
||||
load_extensions.apply(this, nbextension_paths);
|
||||
var active = filter_extensions(section.data.load_extensions);
|
||||
load_extensions.apply(this, active);
|
||||
}
|
||||
});
|
||||
}
|
||||
@ -909,7 +924,7 @@ define([
|
||||
var format_datetime = function(date) {
|
||||
var text = moment(date).fromNow();
|
||||
return text === 'a few seconds ago' ? 'seconds ago' : text;
|
||||
}
|
||||
};
|
||||
|
||||
var datetime_sort_helper = function(a, b, order) {
|
||||
if (moment(a).isBefore(moment(b))) {
|
||||
@ -919,11 +934,12 @@ define([
|
||||
} else {
|
||||
return (order == 1) ? 1 : -1;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
var utils = {
|
||||
load_extension: load_extension,
|
||||
load_extensions: load_extensions,
|
||||
filter_extensions: filter_extensions,
|
||||
load_extensions_from_config: load_extensions_from_config,
|
||||
regex_split : regex_split,
|
||||
uuid : uuid,
|
||||
|
@ -66,6 +66,8 @@ require([
|
||||
// compat with old IPython, remove for IPython > 3.0
|
||||
window.CodeMirror = CodeMirror;
|
||||
|
||||
// Setup all of the config related things
|
||||
|
||||
var common_options = {
|
||||
ws_url : utils.get_body_data("wsUrl"),
|
||||
base_url : utils.get_body_data("baseUrl"),
|
||||
@ -77,6 +79,9 @@ require([
|
||||
config_section.load();
|
||||
var common_config = new configmod.ConfigSection('common', common_options);
|
||||
common_config.load();
|
||||
|
||||
// Instantiate the main objects
|
||||
|
||||
var page = new page.Page();
|
||||
var pager = new pager.Pager('div#pager', {
|
||||
events: events});
|
||||
@ -90,7 +95,7 @@ require([
|
||||
var save_widget = new savewidget.SaveWidget('span#save_widget', {
|
||||
events: events,
|
||||
keyboard_manager: keyboard_manager});
|
||||
acts.extend_env({save_widget:save_widget})
|
||||
acts.extend_env({save_widget:save_widget});
|
||||
var contents = new contentsModule.Contents({
|
||||
base_url: common_options.base_url,
|
||||
common_config: common_config
|
||||
@ -181,7 +186,8 @@ require([
|
||||
enumerable: true,
|
||||
configurable: false
|
||||
});
|
||||
|
||||
|
||||
// Now actually load nbextensions
|
||||
utils.load_extensions_from_config(config_section);
|
||||
utils.load_extensions_from_config(common_config);
|
||||
notebook.load_notebook(common_options.notebook_path);
|
||||
|
@ -18,6 +18,9 @@ require([
|
||||
requirejs(['custom/custom'], function() {});
|
||||
page = new page.Page();
|
||||
|
||||
var config = new configmod.ConfigSection('terminal',
|
||||
{base_url: utils.get_body_data('baseUrl')});
|
||||
config.load();
|
||||
var common_config = new configmod.ConfigSection('common',
|
||||
{base_url: utils.get_body_data('baseUrl')});
|
||||
common_config.load();
|
||||
@ -32,7 +35,8 @@ require([
|
||||
var ws_url = location.protocol.replace('http', 'ws') + "//" + location.host
|
||||
+ base_url + ws_path;
|
||||
|
||||
var header = $("#header")[0]
|
||||
var header = $("#header")[0];
|
||||
|
||||
function calculate_size() {
|
||||
var height = $(window).height() - header.offsetHeight;
|
||||
var width = $('#terminado-container').width();
|
||||
@ -49,6 +53,7 @@ require([
|
||||
|
||||
page.show_site();
|
||||
|
||||
utils.load_extensions_from_config(config);
|
||||
utils.load_extensions_from_config(common_config);
|
||||
|
||||
window.onresize = function() {
|
||||
|
@ -32,10 +32,8 @@ require([
|
||||
"use strict";
|
||||
requirejs(['custom/custom'], function() {});
|
||||
|
||||
IPython.NotebookList = notebooklist.NotebookList;
|
||||
// Setup all of the config related things
|
||||
|
||||
page = new page.Page();
|
||||
|
||||
var common_options = {
|
||||
base_url: utils.get_body_data("baseUrl"),
|
||||
notebook_path: utils.get_body_data("notebookPath"),
|
||||
@ -46,6 +44,10 @@ require([
|
||||
var common_config = new config.ConfigSection('common', common_options);
|
||||
common_config.load();
|
||||
|
||||
// Instantiate the main objects
|
||||
|
||||
page = new page.Page();
|
||||
|
||||
var session_list = new sesssionlist.SesssionList($.extend({
|
||||
events: events},
|
||||
common_options));
|
||||
@ -53,6 +55,7 @@ require([
|
||||
base_url: common_options.base_url,
|
||||
common_config: common_config
|
||||
});
|
||||
IPython.NotebookList = notebooklist.NotebookList;
|
||||
var notebook_list = new notebooklist.NotebookList('#notebook_list', $.extend({
|
||||
contents: contents,
|
||||
session_list: session_list},
|
||||
@ -141,6 +144,8 @@ require([
|
||||
IPython.new_notebook_widget = new_buttons;
|
||||
|
||||
events.trigger('app_initialized.DashboardApp');
|
||||
|
||||
// Now actually load nbextensions
|
||||
utils.load_extensions_from_config(cfg);
|
||||
utils.load_extensions_from_config(common_config);
|
||||
|
||||
|
@ -30,7 +30,6 @@ data-base-url="{{base_url | urlencode}}"
|
||||
data-ws-url="{{ws_url | urlencode}}"
|
||||
data-notebook-name="{{notebook_name | urlencode}}"
|
||||
data-notebook-path="{{notebook_path | urlencode}}"
|
||||
|
||||
{% endblock %}
|
||||
|
||||
|
||||
|
@ -8,7 +8,6 @@
|
||||
data-base-url="{{base_url | urlencode}}"
|
||||
data-notebook-path="{{notebook_path | urlencode}}"
|
||||
data-terminals-available="{{terminals_available}}"
|
||||
|
||||
{% endblock %}
|
||||
|
||||
|
||||
|
1
notebook/tests/mockextension/index.js
Normal file
1
notebook/tests/mockextension/index.js
Normal file
@ -0,0 +1 @@
|
||||
console.log('z');
|
@ -6,7 +6,6 @@
|
||||
|
||||
import glob
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
import tarfile
|
||||
import zipfile
|
||||
@ -23,7 +22,14 @@ import ipython_genutils.testing.decorators as dec
|
||||
from ipython_genutils import py3compat
|
||||
from ipython_genutils.tempdir import TemporaryDirectory
|
||||
from notebook import nbextensions
|
||||
from notebook.nbextensions import install_nbextension, check_nbextension
|
||||
from notebook.nbextensions import (install_nbextension, check_nbextension,
|
||||
enable_nbextension, disable_nbextension,
|
||||
install_nbextension_python, uninstall_nbextension_python,
|
||||
enable_nbextension_python, disable_nbextension_python, _get_config_dir,
|
||||
validate_nbextension, validate_nbextension_python
|
||||
)
|
||||
|
||||
from traitlets.config.manager import BaseJSONConfigManager
|
||||
|
||||
|
||||
def touch(file, mtime=None):
|
||||
@ -47,7 +53,7 @@ class TestInstallNBExtension(TestCase):
|
||||
|
||||
def setUp(self):
|
||||
self.tempdirs = []
|
||||
src = self.src = self.tempdir()
|
||||
self.src = self.tempdir()
|
||||
self.files = files = [
|
||||
pjoin(u'ƒile'),
|
||||
pjoin(u'∂ir', u'ƒile1'),
|
||||
@ -197,7 +203,7 @@ class TestInstallNBExtension(TestCase):
|
||||
install_nbextension(src)
|
||||
self.assert_installed(fname)
|
||||
dest = pjoin(self.system_nbext, fname)
|
||||
old_mtime = os.stat(dest).st_mtime
|
||||
os.stat(dest).st_mtime
|
||||
with open(src, 'w') as f:
|
||||
f.write('overwrite')
|
||||
touch(src, mtime + 10)
|
||||
@ -225,7 +231,7 @@ class TestInstallNBExtension(TestCase):
|
||||
stderr = StringIO()
|
||||
with patch.object(sys, 'stdout', stdout), \
|
||||
patch.object(sys, 'stderr', stderr):
|
||||
install_nbextension(self.src, verbose=0)
|
||||
install_nbextension(self.src)
|
||||
self.assertEqual(stdout.getvalue(), '')
|
||||
self.assertEqual(stderr.getvalue(), '')
|
||||
|
||||
@ -343,3 +349,130 @@ class TestInstallNBExtension(TestCase):
|
||||
|
||||
with self.assertRaises(ValueError):
|
||||
install_nbextension(zsrc, destination='foo')
|
||||
|
||||
def test_nbextension_enable(self):
|
||||
with TemporaryDirectory() as d:
|
||||
f = u'ƒ.js'
|
||||
src = pjoin(d, f)
|
||||
touch(src)
|
||||
install_nbextension(src, user=True)
|
||||
enable_nbextension(section='notebook', require=u'ƒ')
|
||||
|
||||
config_dir = os.path.join(_get_config_dir(user=True), 'nbconfig')
|
||||
cm = BaseJSONConfigManager(config_dir=config_dir)
|
||||
enabled = cm.get('notebook').get('load_extensions', {}).get(u'ƒ', False)
|
||||
assert enabled
|
||||
|
||||
def test_nbextension_disable(self):
|
||||
self.test_nbextension_enable()
|
||||
disable_nbextension(section='notebook', require=u'ƒ')
|
||||
|
||||
config_dir = os.path.join(_get_config_dir(user=True), 'nbconfig')
|
||||
cm = BaseJSONConfigManager(config_dir=config_dir)
|
||||
enabled = cm.get('notebook').get('load_extensions', {}).get(u'ƒ', False)
|
||||
assert not enabled
|
||||
|
||||
|
||||
def _mock_extension_spec_meta(self, section='notebook'):
|
||||
return {
|
||||
'section': section,
|
||||
'src': 'mockextension',
|
||||
'dest': '_mockdestination',
|
||||
'require': '_mockdestination/index'
|
||||
}
|
||||
|
||||
def _inject_mock_extension(self, section='notebook'):
|
||||
outer_file = __file__
|
||||
|
||||
meta = self._mock_extension_spec_meta(section)
|
||||
|
||||
class mock():
|
||||
__file__ = outer_file
|
||||
|
||||
@staticmethod
|
||||
def _jupyter_nbextension_paths():
|
||||
return [meta]
|
||||
|
||||
import sys
|
||||
sys.modules['mockextension'] = mock
|
||||
|
||||
def test_nbextensionpy_files(self):
|
||||
self._inject_mock_extension()
|
||||
install_nbextension_python('mockextension')
|
||||
|
||||
assert check_nbextension('_mockdestination/index.js')
|
||||
assert check_nbextension(['_mockdestination/index.js'])
|
||||
|
||||
def test_nbextensionpy_user_files(self):
|
||||
self._inject_mock_extension()
|
||||
install_nbextension_python('mockextension', user=True)
|
||||
|
||||
assert check_nbextension('_mockdestination/index.js', user=True)
|
||||
assert check_nbextension(['_mockdestination/index.js'], user=True)
|
||||
|
||||
def test_nbextensionpy_uninstall_files(self):
|
||||
self._inject_mock_extension()
|
||||
install_nbextension_python('mockextension', user=True)
|
||||
uninstall_nbextension_python('mockextension', user=True)
|
||||
|
||||
assert not check_nbextension('_mockdestination/index.js')
|
||||
assert not check_nbextension(['_mockdestination/index.js'])
|
||||
|
||||
def test_nbextensionpy_enable(self):
|
||||
self._inject_mock_extension('notebook')
|
||||
install_nbextension_python('mockextension', user=True)
|
||||
enable_nbextension_python('mockextension')
|
||||
|
||||
config_dir = os.path.join(_get_config_dir(user=True), 'nbconfig')
|
||||
cm = BaseJSONConfigManager(config_dir=config_dir)
|
||||
enabled = cm.get('notebook').get('load_extensions', {}).get('_mockdestination/index', False)
|
||||
assert enabled
|
||||
|
||||
def test_nbextensionpy_disable(self):
|
||||
self._inject_mock_extension('notebook')
|
||||
install_nbextension_python('mockextension', user=True)
|
||||
enable_nbextension_python('mockextension')
|
||||
disable_nbextension_python('mockextension', user=True)
|
||||
|
||||
config_dir = os.path.join(_get_config_dir(user=True), 'nbconfig')
|
||||
cm = BaseJSONConfigManager(config_dir=config_dir)
|
||||
enabled = cm.get('notebook').get('load_extensions', {}).get('_mockdestination/index', False)
|
||||
assert not enabled
|
||||
|
||||
def test_nbextensionpy_validate(self):
|
||||
self._inject_mock_extension('notebook')
|
||||
|
||||
paths = install_nbextension_python('mockextension', user=True)
|
||||
enable_nbextension_python('mockextension')
|
||||
|
||||
meta = self._mock_extension_spec_meta()
|
||||
warnings = validate_nbextension_python(meta, paths[0])
|
||||
self.assertEqual([], warnings, warnings)
|
||||
|
||||
def test_nbextensionpy_validate_bad(self):
|
||||
# Break the metadata (correct file will still be copied)
|
||||
self._inject_mock_extension('notebook')
|
||||
|
||||
paths = install_nbextension_python('mockextension', user=True)
|
||||
|
||||
enable_nbextension_python('mockextension')
|
||||
|
||||
meta = self._mock_extension_spec_meta()
|
||||
meta.update(require="bad-require")
|
||||
|
||||
warnings = validate_nbextension_python(meta, paths[0])
|
||||
self.assertNotEqual([], warnings, warnings)
|
||||
|
||||
def test_nbextension_validate(self):
|
||||
# Break the metadata (correct file will still be copied)
|
||||
self._inject_mock_extension('notebook')
|
||||
|
||||
install_nbextension_python('mockextension', user=True)
|
||||
enable_nbextension_python('mockextension')
|
||||
|
||||
warnings = validate_nbextension("_mockdestination/index")
|
||||
self.assertEqual([], warnings, warnings)
|
||||
|
||||
def test_nbextension_validate_bad(self):
|
||||
warnings = validate_nbextension("this-doesn't-exist")
|
||||
self.assertNotEqual([], warnings, warnings)
|
||||
|
43
notebook/tests/test_serverextensions.py
Normal file
43
notebook/tests/test_serverextensions.py
Normal file
@ -0,0 +1,43 @@
|
||||
from unittest import TestCase
|
||||
|
||||
from traitlets.config.manager import BaseJSONConfigManager
|
||||
|
||||
from notebook.serverextensions import toggle_serverextension_python
|
||||
from notebook.nbextensions import _get_config_dir
|
||||
|
||||
|
||||
class TestInstallServerExtension(TestCase):
|
||||
def _inject_mock_extension(self):
|
||||
outer_file = __file__
|
||||
|
||||
class mock():
|
||||
__file__ = outer_file
|
||||
|
||||
@staticmethod
|
||||
def _jupyter_server_extension_paths():
|
||||
return [{
|
||||
'module': '_mockdestination/index'
|
||||
}]
|
||||
|
||||
import sys
|
||||
sys.modules['mockextension'] = mock
|
||||
|
||||
def _get_config(self, user=True):
|
||||
cm = BaseJSONConfigManager(config_dir=_get_config_dir(user))
|
||||
data = cm.get("jupyter_notebook_config")
|
||||
return data.get("NotebookApp", {}).get("nbserver_extensions", {})
|
||||
|
||||
def test_enable(self):
|
||||
self._inject_mock_extension()
|
||||
toggle_serverextension_python('mockextension', True)
|
||||
|
||||
config = self._get_config()
|
||||
assert config['mockextension']
|
||||
|
||||
def test_disable(self):
|
||||
self._inject_mock_extension()
|
||||
toggle_serverextension_python('mockextension', True)
|
||||
toggle_serverextension_python('mockextension', False)
|
||||
|
||||
config = self._get_config()
|
||||
assert not config['mockextension']
|
@ -37,6 +37,7 @@ class TreeHandler(IPythonHandler):
|
||||
def get(self, path=''):
|
||||
path = path.strip('/')
|
||||
cm = self.contents_manager
|
||||
|
||||
if cm.dir_exists(path=path):
|
||||
if cm.is_hidden(path):
|
||||
self.log.info("Refusing to serve hidden directory, via 404 Error")
|
||||
|
6
scripts/jupyter-serverextension
Normal file
6
scripts/jupyter-serverextension
Normal file
@ -0,0 +1,6 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
from notebook.serverextensions import main
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
Loading…
Reference in New Issue
Block a user