Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Special case ExtensionApp that starts the ServerApp #401

Merged
merged 1 commit into from
Feb 3, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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