Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

merge nbserver_extensions #2108

Merged
merged 5 commits into from
Feb 2, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 3 additions & 15 deletions notebook/nbextensions.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,8 @@
from urllib import urlretrieve

from jupyter_core.paths import (
jupyter_data_dir, jupyter_config_dir, jupyter_config_path,
SYSTEM_JUPYTER_PATH, ENV_JUPYTER_PATH, ENV_CONFIG_PATH, SYSTEM_CONFIG_PATH
jupyter_data_dir, jupyter_config_path, jupyter_path,
SYSTEM_JUPYTER_PATH, ENV_JUPYTER_PATH,
)
from ipython_genutils.path import ensure_dir_exists
from ipython_genutils.py3compat import string_types, cast_unicode_py2
Expand Down Expand Up @@ -484,7 +484,7 @@ def validate_nbextension(require, logger=None):
infos = []

js_exists = False
for exts in _nbextension_dirs():
for exts in jupyter_path('nbextensions'):
# Does the Javascript entrypoint actually exist on disk?
js = u"{}.js".format(os.path.join(exts, *require.split("/")))
js_exists = os.path.exists(js)
Expand Down Expand Up @@ -1014,18 +1014,6 @@ def _get_nbextension_dir(user=False, sys_prefix=False, prefix=None, nbextensions
return nbext


def _nbextension_dirs():
"""The possible locations of nbextensions.

Returns a list of known base extension locations
"""
return [
pjoin(jupyter_data_dir(), u'nbextensions'),
pjoin(ENV_JUPYTER_PATH[0], u'nbextensions'),
pjoin(SYSTEM_JUPYTER_PATH[0], 'nbextensions')
]


def _get_nbextension_metadata(module):
"""Get the list of nbextension paths associated with a Python module.

Expand Down
26 changes: 23 additions & 3 deletions notebook/notebookapp.py
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@
from jupyter_core.application import (
JupyterApp, base_flags, base_aliases,
)
from jupyter_core.paths import jupyter_config_path
from jupyter_client import KernelManager
from jupyter_client.kernelspec import KernelSpecManager, NoSuchKernel, NATIVE_KERNEL_NAME
from jupyter_client.session import Session
Expand Down Expand Up @@ -1232,9 +1233,28 @@ def init_server_extensions(self):
# in the new traitlet
if not modulename in self.nbserver_extensions:
self.nbserver_extensions[modulename] = True

for modulename in sorted(self.nbserver_extensions):
if self.nbserver_extensions[modulename]:

# Load server extensions with ConfigManager.
# This enables merging on keys, which we want for extension enabling.
# Regular config loading only merges at the class level,
# so each level (user > env > system) clobbers the previous.
config_path = jupyter_config_path()
if self.config_dir not in config_path:
# add self.config_dir to the front, if set manually
config_path.insert(0, self.config_dir)
manager = ConfigManager(read_config_path=config_path)
section = manager.get(self.config_file_name)
extensions = section.get('NotebookApp', {}).get('nbserver_extensions', {})

for modulename, enabled in self.nbserver_extensions.items():
if modulename not in extensions:
# not present in `extensions` means it comes from Python config,
# so we need to add it.
# Otherwise, trust ConfigManager to have loaded it.
extensions[modulename] = enabled

for modulename, enabled in sorted(extensions.items()):
if enabled:
try:
mod = importlib.import_module(modulename)
func = getattr(mod, 'load_jupyter_server_extension', None)
Expand Down
102 changes: 80 additions & 22 deletions notebook/tests/test_serverextensions.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import imp
import os
import sys
from unittest import TestCase
Expand All @@ -11,9 +12,10 @@

from traitlets.config.manager import BaseJSONConfigManager
from traitlets.tests.utils import check_help_all_output
from jupyter_core import paths

from notebook.serverextensions import toggle_serverextension_python
from notebook import nbextensions
from notebook import nbextensions, serverextensions, extensions
from notebook.notebookapp import NotebookApp
from notebook.nbextensions import _get_config_dir

Expand All @@ -33,8 +35,24 @@ def test_help_output():
check_help_all_output('notebook.serverextensions', ['install'])
check_help_all_output('notebook.serverextensions', ['uninstall'])

outer_file = __file__

class TestInstallServerExtension(TestCase):
class MockExtensionModule(object):
__file__ = outer_file

@staticmethod
def _jupyter_server_extension_paths():
return [{
'module': '_mockdestination/index'
}]

loaded = False

def load_jupyter_server_extension(self, app):
self.loaded = True


class MockEnvTestCase(TestCase):

def tempdir(self):
td = TemporaryDirectory()
Expand All @@ -43,40 +61,55 @@ def tempdir(self):

def setUp(self):
self.tempdirs = []
self._mock_extensions = []

self.test_dir = self.tempdir()
self.data_dir = os.path.join(self.test_dir, 'data')
self.config_dir = os.path.join(self.test_dir, 'config')
self.system_data_dir = os.path.join(self.test_dir, 'system_data')
self.system_config_dir = os.path.join(self.test_dir, 'system_config')
self.system_path = [self.system_data_dir]
self.system_config_path = [self.system_config_dir]

self.patch_env = patch.dict('os.environ', {
self.patches = []
p = patch.dict('os.environ', {
'JUPYTER_CONFIG_DIR': self.config_dir,
'JUPYTER_DATA_DIR': self.data_dir,
})
self.patch_env.start()
self.patch_system_path = patch.object(nbextensions,
'SYSTEM_JUPYTER_PATH', self.system_path)
self.patch_system_path.start()
self.patches.append(p)
for mod in (paths, nbextensions):
p = patch.object(mod,
'SYSTEM_JUPYTER_PATH', self.system_path)
self.patches.append(p)
p = patch.object(mod,
'ENV_JUPYTER_PATH', [])
self.patches.append(p)
for mod in (paths, extensions):
p = patch.object(mod,
'SYSTEM_CONFIG_PATH', self.system_config_path)
self.patches.append(p)
p = patch.object(mod,
'ENV_CONFIG_PATH', [])
self.patches.append(p)
for p in self.patches:
p.start()
self.addCleanup(p.stop)
# verify our patches
self.assertEqual(paths.jupyter_config_path(), [self.config_dir] + self.system_config_path)
self.assertEqual(extensions._get_config_dir(user=False), self.system_config_dir)
self.assertEqual(paths.jupyter_path(), [self.data_dir] + self.system_path)

def tearDown(self):
self.patch_env.stop()
self.patch_system_path.stop()
for modulename in self._mock_extensions:
sys.modules.pop(modulename)

def _inject_mock_extension(self):
outer_file = __file__
def _inject_mock_extension(self, modulename='mockextension'):

class mock():
__file__ = outer_file
sys.modules[modulename] = ext = MockExtensionModule()
self._mock_extensions.append(modulename)
return ext

@staticmethod
def _jupyter_server_extension_paths():
return [{
'module': '_mockdestination/index'
}]

import sys
sys.modules['mockextension'] = mock
class TestInstallServerExtension(MockEnvTestCase):

def _get_config(self, user=True):
cm = BaseJSONConfigManager(config_dir=_get_config_dir(user))
Expand All @@ -98,13 +131,37 @@ def test_disable(self):
config = self._get_config()
assert not config['mockextension']

def test_merge_config(self):
# enabled at sys level
mock_sys = self._inject_mock_extension('mockext_sys')
# enabled at sys, disabled at user
mock_both = self._inject_mock_extension('mockext_both')
# enabled at user
mock_user = self._inject_mock_extension('mockext_user')
# enabled at Python
mock_py = self._inject_mock_extension('mockext_py')

toggle_serverextension_python('mockext_sys', enabled=True, user=False)
toggle_serverextension_python('mockext_user', enabled=True, user=True)
toggle_serverextension_python('mockext_both', enabled=True, user=False)
toggle_serverextension_python('mockext_both', enabled=False, user=True)

app = NotebookApp(nbserver_extensions={'mockext_py': True})
app.init_server_extensions()

assert mock_user.loaded
assert mock_sys.loaded
assert mock_py.loaded
assert not mock_both.loaded


class TestOrderedServerExtension(TestCase):
class TestOrderedServerExtension(MockEnvTestCase):
"""
Test that Server Extensions are loaded _in order_
"""

def setUp(self):
super(TestOrderedServerExtension, self).setUp()
mockextension1 = SimpleNamespace()
mockextension2 = SimpleNamespace()

Expand All @@ -124,6 +181,7 @@ def load_jupyter_server_extension(obj):
sys.modules['mockextension1'] = mockextension1

def tearDown(self):
super(TestOrderedServerExtension, self).tearDown()
del sys.modules['mockextension2']
del sys.modules['mockextension1']

Expand Down