Skip to content

Commit

Permalink
Merge pull request #401 from afshin/extension-app
Browse files Browse the repository at this point in the history
Special case ExtensionApp that starts the ServerApp
  • Loading branch information
Zsailer authored Feb 3, 2021
2 parents e78e4bf + 6f9a43a commit 1ca52a8
Show file tree
Hide file tree
Showing 7 changed files with 240 additions and 124 deletions.
32 changes: 16 additions & 16 deletions docs/source/developers/extensions.rst
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,7 @@ The basic structure of an ExtensionApp is shown below:
# -------------- Required traits --------------
name = "myextension"
extension_url = "/myextension"
default_url = "/myextension"
load_other_extensions = True
# --- ExtensionApp traits you can configure ---
Expand Down Expand Up @@ -167,7 +167,7 @@ Methods
Properties

* ``name``: the name of the extension
* ``extension_url``: the default url for this extension—i.e. the landing page for this extension when launched from the CLI.
* ``default_url``: the default url for this extension—i.e. the landing page for this extension when launched from the CLI.
* ``load_other_extensions``: a boolean enabling/disabling other extensions when launching this extension directly.

``ExtensionApp`` request handlers
Expand Down Expand Up @@ -302,13 +302,13 @@ To make your extension executable from anywhere on your system, point an entry-p
``ExtensionApp`` as a classic Notebook server extension
-------------------------------------------------------

An extension that extends ``ExtensionApp`` should still work with the old Tornado server from the classic Jupyter Notebook. The ``ExtensionApp`` class
An extension that extends ``ExtensionApp`` should still work with the old Tornado server from the classic Jupyter Notebook. The ``ExtensionApp`` class
provides a method, ``load_classic_server_extension``, that handles the extension initialization. Simply define a ``load_jupyter_server_extension`` reference
pointing at the ``load_classic_server_extension`` method:
pointing at the ``load_classic_server_extension`` method:

.. code-block:: python
# This is typically defined in the root `__init__.py`
# This is typically defined in the root `__init__.py`
# file of the extension package.
load_jupyter_server_extension = MyExtensionApp.load_classic_server_extension
Expand Down Expand Up @@ -483,7 +483,7 @@ There are a few key steps to make this happen:
.. code-block:: python
def load_jupyter_server_extension(nb_server_app):
web_app = nb_server_app.web_app
host_pattern = '.*$'
base_url = web_app.settings['base_url']
Expand All @@ -495,50 +495,50 @@ There are a few key steps to make this happen:
# Favicon redirects.
favicon_redirects = [
(
url_path_join(base_url, "/static/favicons/favicon.ico"),
(
url_path_join(base_url, "/static/favicons/favicon.ico"),
RedirectHandler,
{"url": url_path_join(serverapp.base_url, "static/base/images/favicon.ico")
),
(
url_path_join(base_url, "/static/favicons/favicon-busy-1.ico"),
url_path_join(base_url, "/static/favicons/favicon-busy-1.ico"),
RedirectHandler,
{"url": url_path_join(serverapp.base_url, "static/base/images/favicon-busy-1.ico")}
),
(
url_path_join(base_url, "/static/favicons/favicon-busy-2.ico"),
url_path_join(base_url, "/static/favicons/favicon-busy-2.ico"),
RedirectHandler,
{"url": url_path_join(serverapp.base_url, "static/base/images/favicon-busy-2.ico")}
),
(
url_path_join(base_url, "/static/favicons/favicon-busy-3.ico"),
url_path_join(base_url, "/static/favicons/favicon-busy-3.ico"),
RedirectHandler,
{"url": url_path_join(serverapp.base_url, "static/base/images/favicon-busy-3.ico")}
),
(
url_path_join(base_url, "/static/favicons/favicon-file.ico"),
url_path_join(base_url, "/static/favicons/favicon-file.ico"),
RedirectHandler,
{"url": url_path_join(serverapp.base_url, "static/base/images/favicon-file.ico")}
),
(
url_path_join(base_url, "/static/favicons/favicon-notebook.ico"),
url_path_join(base_url, "/static/favicons/favicon-notebook.ico"),
RedirectHandler,
{"url": url_path_join(serverapp.base_url, "static/base/images/favicon-notebook.ico")}
),
(
url_path_join(base_url, "/static/favicons/favicon-terminal.ico"),
url_path_join(base_url, "/static/favicons/favicon-terminal.ico"),
RedirectHandler,
{"url": url_path_join(serverapp.base_url, "static/base/images/favicon-terminal.ico")}
),
(
url_path_join(base_url, "/static/logo/logo.png"),
url_path_join(base_url, "/static/logo/logo.png"),
RedirectHandler,
{"url": url_path_join(serverapp.base_url, "static/base/images/logo.png")}
),
]
web_app.add_handlers(
host_pattern,
host_pattern,
custom_handlers + favicon_redirects
)
Expand Down
104 changes: 54 additions & 50 deletions jupyter_server/extension/application.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

from jinja2 import Environment, FileSystemLoader

from traitlets.config import Config
from traitlets import (
HasTraits,
Unicode,
Expand All @@ -12,7 +13,6 @@
Bool,
default
)
from traitlets.config import Config
from tornado.log import LogFormatter
from tornado.web import RedirectHandler

Expand Down Expand Up @@ -186,6 +186,9 @@ def get_extension_point(cls):
def _default_url(self):
return self.extension_url

# Is this linked to a serverapp yet?
_linked = Bool(False)

# Extension can configure the ServerApp from the command-line
classes = [
ServerApp,
Expand All @@ -196,9 +199,6 @@ def _default_url(self):

_log_formatter_cls = LogFormatter

# Whether this app is the starter app
_is_starter_app = False

@default('log_level')
def _default_log_level(self):
return logging.INFO
Expand Down Expand Up @@ -333,14 +333,14 @@ def _prepare_templates(self):
})
self.initialize_templates()

@classmethod
def _jupyter_server_config(cls):
def _jupyter_server_config(self):
base_config = {
"ServerApp": {
"jpserver_extensions": {cls.get_extension_package(): True},
"default_url": self.default_url,
"open_browser": self.open_browser
}
}
base_config["ServerApp"].update(cls.serverapp_config)
base_config["ServerApp"].update(self.serverapp_config)
return base_config

def _link_jupyter_server_extension(self, serverapp):
Expand All @@ -351,6 +351,10 @@ def _link_jupyter_server_extension(self, serverapp):
the command line contains traits for the ExtensionApp
or the ExtensionApp's config files have server
settings.
Note, the ServerApp has not initialized the Tornado
Web Application yet, so do not try to affect the
`web_app` attribute.
"""
self.serverapp = serverapp
# Load config from an ExtensionApp's config files.
Expand All @@ -370,23 +374,8 @@ def _link_jupyter_server_extension(self, serverapp):
# ServerApp, do it here.
# 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)
cls._is_starter_app = True
serverapp.initialize(argv=argv, find_extensions=load_other_extensions)
return serverapp
# Acknowledge that this extension has been linked.
self._linked = True

def initialize(self):
"""Initialize the extension app. The
Expand Down Expand Up @@ -440,12 +429,7 @@ def _load_jupyter_server_extension(cls, serverapp):
except KeyError:
extension = cls()
extension._link_jupyter_server_extension(serverapp)
if cls._is_starter_app:
serverapp._starter_app = extension
extension.initialize()
# Set the serverapp's default url to the extension's url.
if cls._is_starter_app:
serverapp.default_url = extension.default_url
return extension

@classmethod
Expand Down Expand Up @@ -478,6 +462,24 @@ def load_classic_server_extension(cls, serverapp):
])
extension.initialize()

@classmethod
def initialize_server(cls, argv=[], load_other_extensions=True, **kwargs):
"""Creates an instance of ServerApp and explicitly sets
this extension to enabled=True (i.e. superceding disabling
found in other config from files).
The `launch_instance` method uses this method to initialize
and start a server.
"""
serverapp = ServerApp.instance(
jpserver_extensions={cls.get_extension_package(): True}, **kwargs)
serverapp.initialize(
argv=argv,
starter_extension=cls.name,
find_extensions=cls.load_other_extensions,
)
return serverapp

@classmethod
def launch_instance(cls, argv=None, **kwargs):
"""Launch the extension like an application. Initializes+configs a stock server
Expand All @@ -489,27 +491,29 @@ def launch_instance(cls, argv=None, **kwargs):
args = sys.argv[1:] # slice out extension config.
else:
args = argv
# Check for subcommands

# Handle all "stops" that could happen before
# continuing to launch a server+extension.
subapp = _preparse_for_subcommand(cls, args)
if subapp:
subapp.start()
else:
# Check for help, version, and generate-config arguments
# before initializing server to make sure these
# arguments trigger actions from the extension not the server.
_preparse_for_stopping_flags(cls, args)
# Get a jupyter server instance.
serverapp = cls.initialize_server(
argv=args,
load_other_extensions=cls.load_other_extensions
return

# Check for help, version, and generate-config arguments
# before initializing server to make sure these
# arguments trigger actions from the extension not the server.
_preparse_for_stopping_flags(cls, args)

serverapp = cls.initialize_server(argv=args)

# Log if extension is blocking other extensions from loading.
if not cls.load_other_extensions:
serverapp.log.info(
"{ext_name} is running without loading "
"other extensions.".format(ext_name=cls.name)
)
# Log if extension is blocking other extensions from loading.
if not cls.load_other_extensions:
serverapp.log.info(
"{ext_name} is running without loading "
"other extensions.".format(ext_name=cls.name)
)
try:
serverapp.start()
except NoStart:
pass
# Start the server.
try:
serverapp.start()
except NoStart:
pass
36 changes: 28 additions & 8 deletions jupyter_server/extension/manager.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import importlib

from traitlets.config import LoggingConfigurable
from traitlets.config import LoggingConfigurable, Config

from traitlets import (
HasTraits,
Dict,
Unicode,
Bool,
Any,
validate
)

Expand All @@ -21,12 +23,10 @@ class ExtensionPoint(HasTraits):
"""A simple API for connecting to a Jupyter Server extension
point defined by metadata and importable from a Python package.
"""
metadata = Dict()
_linked = Bool(False)
_app = Any(None, allow_none=True)

def __init__(self, *args, **kwargs):
# Store extension points that have been linked.
self._app = None
super().__init__(*args, **kwargs)
metadata = Dict()

@validate('metadata')
def _valid_metadata(self, proposed):
Expand Down Expand Up @@ -54,13 +54,30 @@ def _valid_metadata(self, proposed):

@property
def linked(self):
"""Has this extension point been linked to the server.
Will pull from ExtensionApp's trait, if this point
is an instance of ExtensionApp.
"""
if self.app:
return self.app._linked
return self._linked

@property
def app(self):
"""If the metadata includes an `app` field"""
return self._app

@property
def config(self):
"""Return any configuration provided by this extension point."""
if self.app:
return self.app._jupyter_server_config()
# At some point, we might want to add logic to load config from
# disk when extensions don't use ExtensionApp.
else:
return {}

@property
def module_name(self):
"""Name of the Python package module where the extension's
Expand Down Expand Up @@ -119,8 +136,11 @@ def link(self, serverapp):
This looks for a `_link_jupyter_server_extension` function
in the extension's module or ExtensionApp class.
"""
linker = self._get_linker()
return linker(serverapp)
if not self.linked:
linker = self._get_linker()
linker(serverapp)
# Store this extension as already linked.
self._linked = True

def load(self, serverapp):
"""Load the extension in a Jupyter ServerApp object.
Expand Down
Loading

0 comments on commit 1ca52a8

Please sign in to comment.