Skip to content

Commit 703a711

Browse files
authored
Merge pull request #248 from Zsailer/extension-loading
Add new extension manager API
2 parents 13dd92e + 4d1f8e5 commit 703a711

23 files changed

+882
-452
lines changed

examples/simple/pyproject.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
[build-system]
2+
requires = ["jupyter_packaging~=0.5.0", "setuptools>=40.8.0", "wheel"]
3+
build-backend = "setuptools.build_meta"

examples/simple/setup.py

Lines changed: 19 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,40 +1,46 @@
1-
import os, setuptools
2-
from setuptools import find_packages
1+
import os
2+
from setuptools import setup
3+
from jupyter_packaging import create_cmdclass
4+
35

46
VERSION = '0.0.1'
57

8+
69
def get_data_files():
710
"""Get the data files for the package.
811
"""
912
data_files = [
10-
('etc/jupyter/jupyter_server_config.d', ['etc/jupyter/jupyter_server_config.d/simple_ext1.json']),
11-
('etc/jupyter/jupyter_server_config.d', ['etc/jupyter/jupyter_server_config.d/simple_ext2.json']),
12-
('etc/jupyter/jupyter_server_config.d', ['etc/jupyter/jupyter_server_config.d/simple_ext11.json']),
13+
('etc/jupyter/jupyter_server_config.d', 'etc/jupyter/jupyter_server_config.d/', '*.json'),
1314
]
1415
def add_data_files(path):
1516
for (dirpath, dirnames, filenames) in os.walk(path):
1617
if filenames:
17-
data_files.append((dirpath, [os.path.join(dirpath, filename) for filename in filenames]))
18+
paths = [(dirpath, dirpath, filename) for filename in filenames]
19+
data_files.extend(paths)
1820
# Add all static and templates folders.
1921
add_data_files('simple_ext1/static')
2022
add_data_files('simple_ext1/templates')
2123
add_data_files('simple_ext2/static')
2224
add_data_files('simple_ext2/templates')
2325
return data_files
2426

25-
setuptools.setup(
27+
28+
cmdclass = create_cmdclass(
29+
data_files_spec=get_data_files()
30+
)
31+
32+
setup_args = dict(
2633
name = 'jupyter_server_example',
2734
version = VERSION,
2835
description = 'Jupyter Server Example',
2936
long_description = open('README.md').read(),
30-
packages = find_packages(),
3137
python_requires = '>=3.5',
3238
install_requires = [
3339
'jupyter_server',
3440
'jinja2',
3541
],
3642
include_package_data=True,
37-
data_files = get_data_files(),
43+
cmdclass = cmdclass,
3844
entry_points = {
3945
'console_scripts': [
4046
'jupyter-simple-ext1 = simple_ext1.application:main',
@@ -43,3 +49,7 @@ def add_data_files(path):
4349
]
4450
},
4551
)
52+
53+
54+
if __name__ == '__main__':
55+
setup(**setup_args)

jupyter_server/config_manager.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -118,7 +118,6 @@ def set(self, section_name, data):
118118
# Generate the JSON up front, since it could raise an exception,
119119
# in order to avoid writing half-finished corrupted data to disk.
120120
json_content = json.dumps(data, indent=2)
121-
122121
if PY3:
123122
f = io.open(filename, 'w', encoding='utf-8')
124123
else:

jupyter_server/extension/application.py

Lines changed: 47 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -144,12 +144,27 @@ class method. This method can be set as a entry_point in
144144
# side-by-side when launched directly.
145145
load_other_extensions = True
146146

147+
# A useful class property that subclasses can override to
148+
# configure the underlying Jupyter Server when this extension
149+
# is launched directly (using its `launch_instance` method).
150+
serverapp_config = {
151+
"open_browser": True
152+
}
153+
147154
# The extension name used to name the jupyter config
148155
# file, jupyter_{name}_config.
149156
# This should also match the jupyter subcommand used to launch
150157
# this extension from the CLI, e.g. `jupyter {name}`.
151158
name = None
152159

160+
@classmethod
161+
def get_extension_package(cls):
162+
return cls.__module__.split('.')[0]
163+
164+
@classmethod
165+
def get_extension_point(cls):
166+
return cls.__module__
167+
153168
# Extension URL sets the default landing page for this extension.
154169
extension_url = "/"
155170

@@ -158,6 +173,9 @@ class method. This method can be set as a entry_point in
158173
ServerApp,
159174
]
160175

176+
# A ServerApp is not defined yet, but will be initialized below.
177+
serverapp = None
178+
161179
@property
162180
def static_url_prefix(self):
163181
return "/static/{name}/".format(
@@ -281,28 +299,17 @@ def _prepare_templates(self):
281299
self.initialize_templates()
282300

283301
@classmethod
284-
def initialize_server(cls, argv=[], load_other_extensions=True, **kwargs):
285-
"""Creates an instance of ServerApp where this extension is enabled
286-
(superceding disabling found in other config from files).
287-
288-
This is necessary when launching the ExtensionApp directly from
289-
the `launch_instance` classmethod.
290-
"""
291-
# The ExtensionApp needs to add itself as enabled extension
292-
# to the jpserver_extensions trait, so that the ServerApp
293-
# initializes it.
294-
config = Config({
302+
def _jupyter_server_config(cls):
303+
base_config = {
295304
"ServerApp": {
296-
"jpserver_extensions": {cls.name: True},
297-
"open_browser": cls.open_browser,
305+
"jpserver_extensions": {cls.get_extension_package(): True},
298306
"default_url": cls.extension_url
299307
}
300-
})
301-
serverapp = ServerApp.instance(**kwargs, argv=[], config=config)
302-
serverapp.initialize(argv=argv, find_extensions=load_other_extensions)
303-
return serverapp
308+
}
309+
base_config["ServerApp"].update(cls.serverapp_config)
310+
return base_config
304311

305-
def link_to_serverapp(self, serverapp):
312+
def _link_jupyter_server_extension(self, serverapp):
306313
"""Link the ExtensionApp to an initialized ServerApp.
307314
308315
The ServerApp is stored as an attribute and config
@@ -315,7 +322,7 @@ def link_to_serverapp(self, serverapp):
315322
# Load config from an ExtensionApp's config files.
316323
self.load_config_file()
317324
# ServerApp's config might have picked up
318-
# CLI config for the ExtensionApp. We call
325+
# config for the ExtensionApp. We call
319326
# update_config to update ExtensionApp's
320327
# traits with these values found in ServerApp's
321328
# config.
@@ -330,6 +337,22 @@ def link_to_serverapp(self, serverapp):
330337
# i.e. ServerApp traits <--- ExtensionApp config
331338
self.serverapp.update_config(self.config)
332339

340+
@classmethod
341+
def initialize_server(cls, argv=[], load_other_extensions=True, **kwargs):
342+
"""Creates an instance of ServerApp where this extension is enabled
343+
(superceding disabling found in other config from files).
344+
345+
This is necessary when launching the ExtensionApp directly from
346+
the `launch_instance` classmethod.
347+
"""
348+
# The ExtensionApp needs to add itself as enabled extension
349+
# to the jpserver_extensions trait, so that the ServerApp
350+
# initializes it.
351+
config = Config(cls._jupyter_server_config())
352+
serverapp = ServerApp.instance(**kwargs, argv=[], config=config)
353+
serverapp.initialize(argv=argv, find_extensions=load_other_extensions)
354+
return serverapp
355+
333356
def initialize(self):
334357
"""Initialize the extension app. The
335358
corresponding server app and webapp should already
@@ -341,7 +364,7 @@ def initialize(self):
341364
3) Points Tornado Webapp to templates and
342365
static assets.
343366
"""
344-
if not hasattr(self, 'serverapp'):
367+
if not self.serverapp:
345368
msg = (
346369
"This extension has no attribute `serverapp`. "
347370
"Try calling `.link_to_serverapp()` before calling "
@@ -374,12 +397,14 @@ def _load_jupyter_server_extension(cls, serverapp):
374397
"""Initialize and configure this extension, then add the extension's
375398
settings and handlers to the server's web application.
376399
"""
400+
extension_manager = serverapp.extension_manager
377401
try:
378402
# Get loaded extension from serverapp.
379-
extension = serverapp._enabled_extensions[cls.name]
403+
point = extension_manager.extension_points[cls.name]
404+
extension = point.app
380405
except KeyError:
381406
extension = cls()
382-
extension.link_to_serverapp(serverapp)
407+
extension._link_jupyter_server_extension(serverapp)
383408
extension.initialize()
384409
return extension
385410

jupyter_server/extension/config.py

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
2+
from jupyter_server.services.config.manager import ConfigManager
3+
4+
5+
DEFAULT_SECTION_NAME = "jupyter_server_config"
6+
7+
8+
class ExtensionConfigManager(ConfigManager):
9+
"""A manager class to interface with Jupyter Server Extension config
10+
found in a `config.d` folder. It is assumed that all configuration
11+
files in this directory are JSON files.
12+
"""
13+
def get_jpserver_extensions(
14+
self,
15+
section_name=DEFAULT_SECTION_NAME
16+
):
17+
"""Return the jpserver_extensions field from all
18+
config files found."""
19+
data = self.get(section_name)
20+
return (
21+
data
22+
.get("ServerApp", {})
23+
.get("jpserver_extensions", {})
24+
)
25+
26+
def enabled(
27+
self,
28+
name,
29+
section_name=DEFAULT_SECTION_NAME,
30+
include_root=True
31+
):
32+
"""Is the extension enabled?"""
33+
extensions = self.get_jpserver_extensions(section_name)
34+
try:
35+
return extensions[name]
36+
except KeyError:
37+
return False
38+
39+
def enable(self, name):
40+
data = {
41+
"ServerApp": {
42+
"jpserver_extensions": {
43+
name: True
44+
}
45+
}
46+
}
47+
self.update(name, data)
48+
49+
def disable(self, name):
50+
data = {
51+
"ServerApp": {
52+
"jpserver_extensions": {
53+
name: False
54+
}
55+
}
56+
}
57+
self.update(name, data)

0 commit comments

Comments
 (0)