Skip to content

Commit

Permalink
Merge pull request #248 from Zsailer/extension-loading
Browse files Browse the repository at this point in the history
Add new extension manager API
  • Loading branch information
blink1073 authored Jul 31, 2020
2 parents 13dd92e + 4d1f8e5 commit 703a711
Show file tree
Hide file tree
Showing 23 changed files with 882 additions and 452 deletions.
3 changes: 3 additions & 0 deletions examples/simple/pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
[build-system]
requires = ["jupyter_packaging~=0.5.0", "setuptools>=40.8.0", "wheel"]
build-backend = "setuptools.build_meta"
28 changes: 19 additions & 9 deletions examples/simple/setup.py
Original file line number Diff line number Diff line change
@@ -1,40 +1,46 @@
import os, setuptools
from setuptools import find_packages
import os
from setuptools import setup
from jupyter_packaging import create_cmdclass


VERSION = '0.0.1'


def get_data_files():
"""Get the data files for the package.
"""
data_files = [
('etc/jupyter/jupyter_server_config.d', ['etc/jupyter/jupyter_server_config.d/simple_ext1.json']),
('etc/jupyter/jupyter_server_config.d', ['etc/jupyter/jupyter_server_config.d/simple_ext2.json']),
('etc/jupyter/jupyter_server_config.d', ['etc/jupyter/jupyter_server_config.d/simple_ext11.json']),
('etc/jupyter/jupyter_server_config.d', 'etc/jupyter/jupyter_server_config.d/', '*.json'),
]
def add_data_files(path):
for (dirpath, dirnames, filenames) in os.walk(path):
if filenames:
data_files.append((dirpath, [os.path.join(dirpath, filename) for filename in filenames]))
paths = [(dirpath, dirpath, filename) for filename in filenames]
data_files.extend(paths)
# Add all static and templates folders.
add_data_files('simple_ext1/static')
add_data_files('simple_ext1/templates')
add_data_files('simple_ext2/static')
add_data_files('simple_ext2/templates')
return data_files

setuptools.setup(

cmdclass = create_cmdclass(
data_files_spec=get_data_files()
)

setup_args = dict(
name = 'jupyter_server_example',
version = VERSION,
description = 'Jupyter Server Example',
long_description = open('README.md').read(),
packages = find_packages(),
python_requires = '>=3.5',
install_requires = [
'jupyter_server',
'jinja2',
],
include_package_data=True,
data_files = get_data_files(),
cmdclass = cmdclass,
entry_points = {
'console_scripts': [
'jupyter-simple-ext1 = simple_ext1.application:main',
Expand All @@ -43,3 +49,7 @@ def add_data_files(path):
]
},
)


if __name__ == '__main__':
setup(**setup_args)
1 change: 0 additions & 1 deletion jupyter_server/config_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,6 @@ def set(self, section_name, data):
# Generate the JSON up front, since it could raise an exception,
# in order to avoid writing half-finished corrupted data to disk.
json_content = json.dumps(data, indent=2)

if PY3:
f = io.open(filename, 'w', encoding='utf-8')
else:
Expand Down
69 changes: 47 additions & 22 deletions jupyter_server/extension/application.py
Original file line number Diff line number Diff line change
Expand Up @@ -144,12 +144,27 @@ class method. This method can be set as a entry_point in
# side-by-side when launched directly.
load_other_extensions = True

# A useful class property that subclasses can override to
# configure the underlying Jupyter Server when this extension
# is launched directly (using its `launch_instance` method).
serverapp_config = {
"open_browser": True
}

# The extension name used to name the jupyter config
# file, jupyter_{name}_config.
# This should also match the jupyter subcommand used to launch
# this extension from the CLI, e.g. `jupyter {name}`.
name = None

@classmethod
def get_extension_package(cls):
return cls.__module__.split('.')[0]

@classmethod
def get_extension_point(cls):
return cls.__module__

# Extension URL sets the default landing page for this extension.
extension_url = "/"

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

# A ServerApp is not defined yet, but will be initialized below.
serverapp = None

@property
def static_url_prefix(self):
return "/static/{name}/".format(
Expand Down Expand Up @@ -281,28 +299,17 @@ def _prepare_templates(self):
self.initialize_templates()

@classmethod
def initialize_server(cls, argv=[], load_other_extensions=True, **kwargs):
"""Creates an instance of ServerApp where this extension is enabled
(superceding disabling found in other config from files).
This is necessary when launching the ExtensionApp directly from
the `launch_instance` classmethod.
"""
# The ExtensionApp needs to add itself as enabled extension
# to the jpserver_extensions trait, so that the ServerApp
# initializes it.
config = Config({
def _jupyter_server_config(cls):
base_config = {
"ServerApp": {
"jpserver_extensions": {cls.name: True},
"open_browser": cls.open_browser,
"jpserver_extensions": {cls.get_extension_package(): True},
"default_url": cls.extension_url
}
})
serverapp = ServerApp.instance(**kwargs, argv=[], config=config)
serverapp.initialize(argv=argv, find_extensions=load_other_extensions)
return serverapp
}
base_config["ServerApp"].update(cls.serverapp_config)
return base_config

def link_to_serverapp(self, serverapp):
def _link_jupyter_server_extension(self, serverapp):
"""Link the ExtensionApp to an initialized ServerApp.
The ServerApp is stored as an attribute and config
Expand All @@ -315,7 +322,7 @@ def link_to_serverapp(self, serverapp):
# Load config from an ExtensionApp's config files.
self.load_config_file()
# ServerApp's config might have picked up
# CLI config for the ExtensionApp. We call
# config for the ExtensionApp. We call
# update_config to update ExtensionApp's
# traits with these values found in ServerApp's
# config.
Expand All @@ -330,6 +337,22 @@ def link_to_serverapp(self, serverapp):
# i.e. ServerApp traits <--- ExtensionApp config
self.serverapp.update_config(self.config)

@classmethod
def initialize_server(cls, argv=[], load_other_extensions=True, **kwargs):
"""Creates an instance of ServerApp where this extension is enabled
(superceding disabling found in other config from files).
This is necessary when launching the ExtensionApp directly from
the `launch_instance` classmethod.
"""
# The ExtensionApp needs to add itself as enabled extension
# to the jpserver_extensions trait, so that the ServerApp
# initializes it.
config = Config(cls._jupyter_server_config())
serverapp = ServerApp.instance(**kwargs, argv=[], config=config)
serverapp.initialize(argv=argv, find_extensions=load_other_extensions)
return serverapp

def initialize(self):
"""Initialize the extension app. The
corresponding server app and webapp should already
Expand All @@ -341,7 +364,7 @@ def initialize(self):
3) Points Tornado Webapp to templates and
static assets.
"""
if not hasattr(self, 'serverapp'):
if not self.serverapp:
msg = (
"This extension has no attribute `serverapp`. "
"Try calling `.link_to_serverapp()` before calling "
Expand Down Expand Up @@ -374,12 +397,14 @@ def _load_jupyter_server_extension(cls, serverapp):
"""Initialize and configure this extension, then add the extension's
settings and handlers to the server's web application.
"""
extension_manager = serverapp.extension_manager
try:
# Get loaded extension from serverapp.
extension = serverapp._enabled_extensions[cls.name]
point = extension_manager.extension_points[cls.name]
extension = point.app
except KeyError:
extension = cls()
extension.link_to_serverapp(serverapp)
extension._link_jupyter_server_extension(serverapp)
extension.initialize()
return extension

Expand Down
57 changes: 57 additions & 0 deletions jupyter_server/extension/config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@

from jupyter_server.services.config.manager import ConfigManager


DEFAULT_SECTION_NAME = "jupyter_server_config"


class ExtensionConfigManager(ConfigManager):
"""A manager class to interface with Jupyter Server Extension config
found in a `config.d` folder. It is assumed that all configuration
files in this directory are JSON files.
"""
def get_jpserver_extensions(
self,
section_name=DEFAULT_SECTION_NAME
):
"""Return the jpserver_extensions field from all
config files found."""
data = self.get(section_name)
return (
data
.get("ServerApp", {})
.get("jpserver_extensions", {})
)

def enabled(
self,
name,
section_name=DEFAULT_SECTION_NAME,
include_root=True
):
"""Is the extension enabled?"""
extensions = self.get_jpserver_extensions(section_name)
try:
return extensions[name]
except KeyError:
return False

def enable(self, name):
data = {
"ServerApp": {
"jpserver_extensions": {
name: True
}
}
}
self.update(name, data)

def disable(self, name):
data = {
"ServerApp": {
"jpserver_extensions": {
name: False
}
}
}
self.update(name, data)
Loading

0 comments on commit 703a711

Please sign in to comment.