You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
142 lines
5.1 KiB
Python
142 lines
5.1 KiB
Python
"""
|
|
This module contains a Jupyter Server extension that attempts to
|
|
make classic server and notebook extensions work in the new server.
|
|
|
|
Unfortunately, you'll notice that requires some major monkey-patching.
|
|
The goal is that this extension will only be used as a temporary
|
|
patch to transition extension authors from classic notebook server to jupyter_server.
|
|
"""
|
|
import os
|
|
import types
|
|
import inspect
|
|
from functools import wraps
|
|
from jupyter_core.paths import jupyter_config_path
|
|
from traitlets.traitlets import is_trait
|
|
|
|
|
|
from jupyter_server.services.config.manager import ConfigManager
|
|
from .traits import NotebookAppTraits
|
|
|
|
|
|
class ClassProxyError(Exception):
|
|
pass
|
|
|
|
|
|
def proxy(obj1, obj2, name, overwrite=False):
|
|
"""Redirects a method, property, or trait from object 1 to object 2."""
|
|
if hasattr(obj1, name) and overwrite is False:
|
|
raise ClassProxyError(
|
|
"Cannot proxy the attribute '{name}' from {cls2} because "
|
|
"{cls1} already has this attribute.".format(
|
|
name=name,
|
|
cls1=obj1.__class__,
|
|
cls2=obj2.__class__
|
|
)
|
|
)
|
|
attr = getattr(obj2, name)
|
|
|
|
# First check if this thing is a trait (see traitlets)
|
|
cls_attr = getattr(obj2.__class__, name)
|
|
if is_trait(cls_attr) or type(attr) == property:
|
|
thing = property(lambda self: getattr(obj2, name))
|
|
|
|
elif isinstance(attr, types.MethodType):
|
|
@wraps(attr)
|
|
def thing(self, *args, **kwargs):
|
|
return attr(*args, **kwargs)
|
|
|
|
# Anything else appended on the class is just an attribute of the class.
|
|
else:
|
|
thing = attr
|
|
|
|
setattr(obj1.__class__, name, thing)
|
|
|
|
|
|
def public_members(obj):
|
|
members = inspect.getmembers(obj)
|
|
return [m for m, _ in members if not m.startswith('_')]
|
|
|
|
|
|
def diff_members(obj1, obj2):
|
|
"""Return all attribute names found in obj2 but not obj1"""
|
|
m1 = public_members(obj1)
|
|
m2 = public_members(obj2)
|
|
return set(m2).difference(m1)
|
|
|
|
|
|
def get_nbserver_extensions(config_dirs):
|
|
cm = ConfigManager(read_config_path=config_dirs)
|
|
section = cm.get("jupyter_notebook_config")
|
|
extensions = section.get('NotebookApp', {}).get('nbserver_extensions', {})
|
|
return extensions
|
|
|
|
|
|
def _link_jupyter_server_extension(serverapp):
|
|
# Get the extension manager from the server
|
|
manager = serverapp.extension_manager
|
|
logger = serverapp.log
|
|
|
|
# Hack that patches the enabled extensions list, prioritizing
|
|
# jupyter nbclassic. In the future, it would be much better
|
|
# to incorporate a dependency injection system in the
|
|
# Extension manager that allows extensions to list
|
|
# their dependency tree and sort that way.
|
|
def sorted_extensions(self):
|
|
"""Dictionary with extension package names as keys
|
|
and an ExtensionPackage objects as values.
|
|
"""
|
|
# Sort the keys and
|
|
keys = sorted(self.extensions.keys())
|
|
keys.remove("notebook_shim")
|
|
keys = ["notebook_shim"] + keys
|
|
return {key: self.extensions[key] for key in keys}
|
|
|
|
manager.__class__.sorted_extensions = property(sorted_extensions)
|
|
|
|
# Look to see if nbclassic is enabled. if so,
|
|
# link the nbclassic extension here to load
|
|
# its config. Then, port its config to the serverapp
|
|
# for backwards compatibility.
|
|
try:
|
|
pkg = manager.extensions["notebook_shim"]
|
|
pkg.link_point("notebook_shim", serverapp)
|
|
point = pkg.extension_points["notebook_shim"]
|
|
nbapp = point.app
|
|
except Exception:
|
|
nbapp = NotebookAppTraits()
|
|
|
|
# Proxy NotebookApp traits through serverapp to notebookapp.
|
|
members = diff_members(serverapp, nbapp)
|
|
for m in members:
|
|
proxy(serverapp, nbapp, m)
|
|
|
|
# Find jupyter server extensions listed as notebook server extensions.
|
|
jupyter_paths = jupyter_config_path()
|
|
config_dirs = jupyter_paths + [serverapp.config_dir]
|
|
nbserver_extensions = get_nbserver_extensions(config_dirs)
|
|
|
|
# Link all extensions found in the old locations for
|
|
# notebook server extensions.
|
|
for name, enabled in nbserver_extensions.items():
|
|
# If the extension is already enabled in the manager, i.e.
|
|
# because it was discovered already by Jupyter Server
|
|
# through its jupyter_server_config, then don't re-enable here.
|
|
if name not in manager.extensions:
|
|
successful = manager.add_extension(name, enabled=enabled)
|
|
if successful:
|
|
logger.info(
|
|
"{name} | extension was found and enabled by notebook_shim. "
|
|
"Consider moving the extension to Jupyter Server's "
|
|
"extension paths.".format(name=name)
|
|
)
|
|
manager.link_extension(name)
|
|
|
|
def _load_jupyter_server_extension(serverapp):
|
|
# Patch the config service manager to find the
|
|
# proper path for old notebook frontend extensions
|
|
config_manager = serverapp.config_manager
|
|
read_config_path = config_manager.read_config_path
|
|
read_config_path += [os.path.join(p, 'nbconfig')
|
|
for p in jupyter_config_path()]
|
|
config_manager.read_config_path = read_config_path
|