Skip to content

Commit 90d30d2

Browse files
authored
Merge pull request #2108 from minrk/merge-server-extensions
merge nbserver_extensions
2 parents 3b82344 + 6b4ec57 commit 90d30d2

File tree

3 files changed

+106
-40
lines changed

3 files changed

+106
-40
lines changed

notebook/nbextensions.py

Lines changed: 3 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,8 @@
2121
from urllib import urlretrieve
2222

2323
from jupyter_core.paths import (
24-
jupyter_data_dir, jupyter_config_dir, jupyter_config_path,
25-
SYSTEM_JUPYTER_PATH, ENV_JUPYTER_PATH, ENV_CONFIG_PATH, SYSTEM_CONFIG_PATH
24+
jupyter_data_dir, jupyter_config_path, jupyter_path,
25+
SYSTEM_JUPYTER_PATH, ENV_JUPYTER_PATH,
2626
)
2727
from ipython_genutils.path import ensure_dir_exists
2828
from ipython_genutils.py3compat import string_types, cast_unicode_py2
@@ -484,7 +484,7 @@ def validate_nbextension(require, logger=None):
484484
infos = []
485485

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

10161016

1017-
def _nbextension_dirs():
1018-
"""The possible locations of nbextensions.
1019-
1020-
Returns a list of known base extension locations
1021-
"""
1022-
return [
1023-
pjoin(jupyter_data_dir(), u'nbextensions'),
1024-
pjoin(ENV_JUPYTER_PATH[0], u'nbextensions'),
1025-
pjoin(SYSTEM_JUPYTER_PATH[0], 'nbextensions')
1026-
]
1027-
1028-
10291017
def _get_nbextension_metadata(module):
10301018
"""Get the list of nbextension paths associated with a Python module.
10311019

notebook/notebookapp.py

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,7 @@
8686
from jupyter_core.application import (
8787
JupyterApp, base_flags, base_aliases,
8888
)
89+
from jupyter_core.paths import jupyter_config_path
8990
from jupyter_client import KernelManager
9091
from jupyter_client.kernelspec import KernelSpecManager, NoSuchKernel, NATIVE_KERNEL_NAME
9192
from jupyter_client.session import Session
@@ -1232,9 +1233,28 @@ def init_server_extensions(self):
12321233
# in the new traitlet
12331234
if not modulename in self.nbserver_extensions:
12341235
self.nbserver_extensions[modulename] = True
1235-
1236-
for modulename in sorted(self.nbserver_extensions):
1237-
if self.nbserver_extensions[modulename]:
1236+
1237+
# Load server extensions with ConfigManager.
1238+
# This enables merging on keys, which we want for extension enabling.
1239+
# Regular config loading only merges at the class level,
1240+
# so each level (user > env > system) clobbers the previous.
1241+
config_path = jupyter_config_path()
1242+
if self.config_dir not in config_path:
1243+
# add self.config_dir to the front, if set manually
1244+
config_path.insert(0, self.config_dir)
1245+
manager = ConfigManager(read_config_path=config_path)
1246+
section = manager.get(self.config_file_name)
1247+
extensions = section.get('NotebookApp', {}).get('nbserver_extensions', {})
1248+
1249+
for modulename, enabled in self.nbserver_extensions.items():
1250+
if modulename not in extensions:
1251+
# not present in `extensions` means it comes from Python config,
1252+
# so we need to add it.
1253+
# Otherwise, trust ConfigManager to have loaded it.
1254+
extensions[modulename] = enabled
1255+
1256+
for modulename, enabled in sorted(extensions.items()):
1257+
if enabled:
12381258
try:
12391259
mod = importlib.import_module(modulename)
12401260
func = getattr(mod, 'load_jupyter_server_extension', None)

notebook/tests/test_serverextensions.py

Lines changed: 80 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import imp
12
import os
23
import sys
34
from unittest import TestCase
@@ -11,9 +12,10 @@
1112

1213
from traitlets.config.manager import BaseJSONConfigManager
1314
from traitlets.tests.utils import check_help_all_output
15+
from jupyter_core import paths
1416

1517
from notebook.serverextensions import toggle_serverextension_python
16-
from notebook import nbextensions
18+
from notebook import nbextensions, serverextensions, extensions
1719
from notebook.notebookapp import NotebookApp
1820
from notebook.nbextensions import _get_config_dir
1921

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

38+
outer_file = __file__
3639

37-
class TestInstallServerExtension(TestCase):
40+
class MockExtensionModule(object):
41+
__file__ = outer_file
42+
43+
@staticmethod
44+
def _jupyter_server_extension_paths():
45+
return [{
46+
'module': '_mockdestination/index'
47+
}]
48+
49+
loaded = False
50+
51+
def load_jupyter_server_extension(self, app):
52+
self.loaded = True
53+
54+
55+
class MockEnvTestCase(TestCase):
3856

3957
def tempdir(self):
4058
td = TemporaryDirectory()
@@ -43,40 +61,55 @@ def tempdir(self):
4361

4462
def setUp(self):
4563
self.tempdirs = []
64+
self._mock_extensions = []
4665

4766
self.test_dir = self.tempdir()
4867
self.data_dir = os.path.join(self.test_dir, 'data')
4968
self.config_dir = os.path.join(self.test_dir, 'config')
5069
self.system_data_dir = os.path.join(self.test_dir, 'system_data')
70+
self.system_config_dir = os.path.join(self.test_dir, 'system_config')
5171
self.system_path = [self.system_data_dir]
72+
self.system_config_path = [self.system_config_dir]
5273

53-
self.patch_env = patch.dict('os.environ', {
74+
self.patches = []
75+
p = patch.dict('os.environ', {
5476
'JUPYTER_CONFIG_DIR': self.config_dir,
5577
'JUPYTER_DATA_DIR': self.data_dir,
5678
})
57-
self.patch_env.start()
58-
self.patch_system_path = patch.object(nbextensions,
59-
'SYSTEM_JUPYTER_PATH', self.system_path)
60-
self.patch_system_path.start()
79+
self.patches.append(p)
80+
for mod in (paths, nbextensions):
81+
p = patch.object(mod,
82+
'SYSTEM_JUPYTER_PATH', self.system_path)
83+
self.patches.append(p)
84+
p = patch.object(mod,
85+
'ENV_JUPYTER_PATH', [])
86+
self.patches.append(p)
87+
for mod in (paths, extensions):
88+
p = patch.object(mod,
89+
'SYSTEM_CONFIG_PATH', self.system_config_path)
90+
self.patches.append(p)
91+
p = patch.object(mod,
92+
'ENV_CONFIG_PATH', [])
93+
self.patches.append(p)
94+
for p in self.patches:
95+
p.start()
96+
self.addCleanup(p.stop)
97+
# verify our patches
98+
self.assertEqual(paths.jupyter_config_path(), [self.config_dir] + self.system_config_path)
99+
self.assertEqual(extensions._get_config_dir(user=False), self.system_config_dir)
100+
self.assertEqual(paths.jupyter_path(), [self.data_dir] + self.system_path)
61101

62102
def tearDown(self):
63-
self.patch_env.stop()
64-
self.patch_system_path.stop()
103+
for modulename in self._mock_extensions:
104+
sys.modules.pop(modulename)
65105

66-
def _inject_mock_extension(self):
67-
outer_file = __file__
106+
def _inject_mock_extension(self, modulename='mockextension'):
68107

69-
class mock():
70-
__file__ = outer_file
108+
sys.modules[modulename] = ext = MockExtensionModule()
109+
self._mock_extensions.append(modulename)
110+
return ext
71111

72-
@staticmethod
73-
def _jupyter_server_extension_paths():
74-
return [{
75-
'module': '_mockdestination/index'
76-
}]
77-
78-
import sys
79-
sys.modules['mockextension'] = mock
112+
class TestInstallServerExtension(MockEnvTestCase):
80113

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

134+
def test_merge_config(self):
135+
# enabled at sys level
136+
mock_sys = self._inject_mock_extension('mockext_sys')
137+
# enabled at sys, disabled at user
138+
mock_both = self._inject_mock_extension('mockext_both')
139+
# enabled at user
140+
mock_user = self._inject_mock_extension('mockext_user')
141+
# enabled at Python
142+
mock_py = self._inject_mock_extension('mockext_py')
143+
144+
toggle_serverextension_python('mockext_sys', enabled=True, user=False)
145+
toggle_serverextension_python('mockext_user', enabled=True, user=True)
146+
toggle_serverextension_python('mockext_both', enabled=True, user=False)
147+
toggle_serverextension_python('mockext_both', enabled=False, user=True)
148+
149+
app = NotebookApp(nbserver_extensions={'mockext_py': True})
150+
app.init_server_extensions()
151+
152+
assert mock_user.loaded
153+
assert mock_sys.loaded
154+
assert mock_py.loaded
155+
assert not mock_both.loaded
156+
101157

102-
class TestOrderedServerExtension(TestCase):
158+
class TestOrderedServerExtension(MockEnvTestCase):
103159
"""
104160
Test that Server Extensions are loaded _in order_
105161
"""
106162

107163
def setUp(self):
164+
super(TestOrderedServerExtension, self).setUp()
108165
mockextension1 = SimpleNamespace()
109166
mockextension2 = SimpleNamespace()
110167

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

126183
def tearDown(self):
184+
super(TestOrderedServerExtension, self).tearDown()
127185
del sys.modules['mockextension2']
128186
del sys.modules['mockextension1']
129187

0 commit comments

Comments
 (0)