Skip to content

Commit 6f9a43a

Browse files
committed
Special case ExtensionApp that starts the ServerApp
1 parent 5013f62 commit 6f9a43a

File tree

7 files changed

+240
-124
lines changed

7 files changed

+240
-124
lines changed

docs/source/developers/extensions.rst

Lines changed: 16 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -128,7 +128,7 @@ The basic structure of an ExtensionApp is shown below:
128128
129129
# -------------- Required traits --------------
130130
name = "myextension"
131-
extension_url = "/myextension"
131+
default_url = "/myextension"
132132
load_other_extensions = True
133133
134134
# --- ExtensionApp traits you can configure ---
@@ -167,7 +167,7 @@ Methods
167167
Properties
168168

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

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

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

309309
.. code-block:: python
310310
311-
# This is typically defined in the root `__init__.py`
311+
# This is typically defined in the root `__init__.py`
312312
# file of the extension package.
313313
load_jupyter_server_extension = MyExtensionApp.load_classic_server_extension
314314
@@ -483,7 +483,7 @@ There are a few key steps to make this happen:
483483
.. code-block:: python
484484
485485
def load_jupyter_server_extension(nb_server_app):
486-
486+
487487
web_app = nb_server_app.web_app
488488
host_pattern = '.*$'
489489
base_url = web_app.settings['base_url']
@@ -495,50 +495,50 @@ There are a few key steps to make this happen:
495495
496496
# Favicon redirects.
497497
favicon_redirects = [
498-
(
499-
url_path_join(base_url, "/static/favicons/favicon.ico"),
498+
(
499+
url_path_join(base_url, "/static/favicons/favicon.ico"),
500500
RedirectHandler,
501501
{"url": url_path_join(serverapp.base_url, "static/base/images/favicon.ico")
502502
),
503503
(
504-
url_path_join(base_url, "/static/favicons/favicon-busy-1.ico"),
504+
url_path_join(base_url, "/static/favicons/favicon-busy-1.ico"),
505505
RedirectHandler,
506506
{"url": url_path_join(serverapp.base_url, "static/base/images/favicon-busy-1.ico")}
507507
),
508508
(
509-
url_path_join(base_url, "/static/favicons/favicon-busy-2.ico"),
509+
url_path_join(base_url, "/static/favicons/favicon-busy-2.ico"),
510510
RedirectHandler,
511511
{"url": url_path_join(serverapp.base_url, "static/base/images/favicon-busy-2.ico")}
512512
),
513513
(
514-
url_path_join(base_url, "/static/favicons/favicon-busy-3.ico"),
514+
url_path_join(base_url, "/static/favicons/favicon-busy-3.ico"),
515515
RedirectHandler,
516516
{"url": url_path_join(serverapp.base_url, "static/base/images/favicon-busy-3.ico")}
517517
),
518518
(
519-
url_path_join(base_url, "/static/favicons/favicon-file.ico"),
519+
url_path_join(base_url, "/static/favicons/favicon-file.ico"),
520520
RedirectHandler,
521521
{"url": url_path_join(serverapp.base_url, "static/base/images/favicon-file.ico")}
522522
),
523523
(
524-
url_path_join(base_url, "/static/favicons/favicon-notebook.ico"),
524+
url_path_join(base_url, "/static/favicons/favicon-notebook.ico"),
525525
RedirectHandler,
526526
{"url": url_path_join(serverapp.base_url, "static/base/images/favicon-notebook.ico")}
527527
),
528528
(
529-
url_path_join(base_url, "/static/favicons/favicon-terminal.ico"),
529+
url_path_join(base_url, "/static/favicons/favicon-terminal.ico"),
530530
RedirectHandler,
531531
{"url": url_path_join(serverapp.base_url, "static/base/images/favicon-terminal.ico")}
532532
),
533533
(
534-
url_path_join(base_url, "/static/logo/logo.png"),
534+
url_path_join(base_url, "/static/logo/logo.png"),
535535
RedirectHandler,
536536
{"url": url_path_join(serverapp.base_url, "static/base/images/logo.png")}
537537
),
538538
]
539539
540540
web_app.add_handlers(
541-
host_pattern,
541+
host_pattern,
542542
custom_handlers + favicon_redirects
543543
)
544544

jupyter_server/extension/application.py

Lines changed: 54 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
from jinja2 import Environment, FileSystemLoader
66

7+
from traitlets.config import Config
78
from traitlets import (
89
HasTraits,
910
Unicode,
@@ -12,7 +13,6 @@
1213
Bool,
1314
default
1415
)
15-
from traitlets.config import Config
1616
from tornado.log import LogFormatter
1717
from tornado.web import RedirectHandler
1818

@@ -186,6 +186,9 @@ def get_extension_point(cls):
186186
def _default_url(self):
187187
return self.extension_url
188188

189+
# Is this linked to a serverapp yet?
190+
_linked = Bool(False)
191+
189192
# Extension can configure the ServerApp from the command-line
190193
classes = [
191194
ServerApp,
@@ -196,9 +199,6 @@ def _default_url(self):
196199

197200
_log_formatter_cls = LogFormatter
198201

199-
# Whether this app is the starter app
200-
_is_starter_app = False
201-
202202
@default('log_level')
203203
def _default_log_level(self):
204204
return logging.INFO
@@ -333,14 +333,14 @@ def _prepare_templates(self):
333333
})
334334
self.initialize_templates()
335335

336-
@classmethod
337-
def _jupyter_server_config(cls):
336+
def _jupyter_server_config(self):
338337
base_config = {
339338
"ServerApp": {
340-
"jpserver_extensions": {cls.get_extension_package(): True},
339+
"default_url": self.default_url,
340+
"open_browser": self.open_browser
341341
}
342342
}
343-
base_config["ServerApp"].update(cls.serverapp_config)
343+
base_config["ServerApp"].update(self.serverapp_config)
344344
return base_config
345345

346346
def _link_jupyter_server_extension(self, serverapp):
@@ -351,6 +351,10 @@ def _link_jupyter_server_extension(self, serverapp):
351351
the command line contains traits for the ExtensionApp
352352
or the ExtensionApp's config files have server
353353
settings.
354+
355+
Note, the ServerApp has not initialized the Tornado
356+
Web Application yet, so do not try to affect the
357+
`web_app` attribute.
354358
"""
355359
self.serverapp = serverapp
356360
# Load config from an ExtensionApp's config files.
@@ -370,23 +374,8 @@ def _link_jupyter_server_extension(self, serverapp):
370374
# ServerApp, do it here.
371375
# i.e. ServerApp traits <--- ExtensionApp config
372376
self.serverapp.update_config(self.config)
373-
374-
@classmethod
375-
def initialize_server(cls, argv=[], load_other_extensions=True, **kwargs):
376-
"""Creates an instance of ServerApp where this extension is enabled
377-
(superceding disabling found in other config from files).
378-
379-
This is necessary when launching the ExtensionApp directly from
380-
the `launch_instance` classmethod.
381-
"""
382-
# The ExtensionApp needs to add itself as enabled extension
383-
# to the jpserver_extensions trait, so that the ServerApp
384-
# initializes it.
385-
config = Config(cls._jupyter_server_config())
386-
serverapp = ServerApp.instance(**kwargs, argv=[], config=config)
387-
cls._is_starter_app = True
388-
serverapp.initialize(argv=argv, find_extensions=load_other_extensions)
389-
return serverapp
377+
# Acknowledge that this extension has been linked.
378+
self._linked = True
390379

391380
def initialize(self):
392381
"""Initialize the extension app. The
@@ -440,12 +429,7 @@ def _load_jupyter_server_extension(cls, serverapp):
440429
except KeyError:
441430
extension = cls()
442431
extension._link_jupyter_server_extension(serverapp)
443-
if cls._is_starter_app:
444-
serverapp._starter_app = extension
445432
extension.initialize()
446-
# Set the serverapp's default url to the extension's url.
447-
if cls._is_starter_app:
448-
serverapp.default_url = extension.default_url
449433
return extension
450434

451435
@classmethod
@@ -478,6 +462,24 @@ def load_classic_server_extension(cls, serverapp):
478462
])
479463
extension.initialize()
480464

465+
@classmethod
466+
def initialize_server(cls, argv=[], load_other_extensions=True, **kwargs):
467+
"""Creates an instance of ServerApp and explicitly sets
468+
this extension to enabled=True (i.e. superceding disabling
469+
found in other config from files).
470+
471+
The `launch_instance` method uses this method to initialize
472+
and start a server.
473+
"""
474+
serverapp = ServerApp.instance(
475+
jpserver_extensions={cls.get_extension_package(): True}, **kwargs)
476+
serverapp.initialize(
477+
argv=argv,
478+
starter_extension=cls.name,
479+
find_extensions=cls.load_other_extensions,
480+
)
481+
return serverapp
482+
481483
@classmethod
482484
def launch_instance(cls, argv=None, **kwargs):
483485
"""Launch the extension like an application. Initializes+configs a stock server
@@ -489,27 +491,29 @@ def launch_instance(cls, argv=None, **kwargs):
489491
args = sys.argv[1:] # slice out extension config.
490492
else:
491493
args = argv
492-
# Check for subcommands
494+
495+
# Handle all "stops" that could happen before
496+
# continuing to launch a server+extension.
493497
subapp = _preparse_for_subcommand(cls, args)
494498
if subapp:
495499
subapp.start()
496-
else:
497-
# Check for help, version, and generate-config arguments
498-
# before initializing server to make sure these
499-
# arguments trigger actions from the extension not the server.
500-
_preparse_for_stopping_flags(cls, args)
501-
# Get a jupyter server instance.
502-
serverapp = cls.initialize_server(
503-
argv=args,
504-
load_other_extensions=cls.load_other_extensions
500+
return
501+
502+
# Check for help, version, and generate-config arguments
503+
# before initializing server to make sure these
504+
# arguments trigger actions from the extension not the server.
505+
_preparse_for_stopping_flags(cls, args)
506+
507+
serverapp = cls.initialize_server(argv=args)
508+
509+
# Log if extension is blocking other extensions from loading.
510+
if not cls.load_other_extensions:
511+
serverapp.log.info(
512+
"{ext_name} is running without loading "
513+
"other extensions.".format(ext_name=cls.name)
505514
)
506-
# Log if extension is blocking other extensions from loading.
507-
if not cls.load_other_extensions:
508-
serverapp.log.info(
509-
"{ext_name} is running without loading "
510-
"other extensions.".format(ext_name=cls.name)
511-
)
512-
try:
513-
serverapp.start()
514-
except NoStart:
515-
pass
515+
# Start the server.
516+
try:
517+
serverapp.start()
518+
except NoStart:
519+
pass

jupyter_server/extension/manager.py

Lines changed: 28 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
import importlib
22

3-
from traitlets.config import LoggingConfigurable
3+
from traitlets.config import LoggingConfigurable, Config
4+
45
from traitlets import (
56
HasTraits,
67
Dict,
78
Unicode,
89
Bool,
10+
Any,
911
validate
1012
)
1113

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

26-
def __init__(self, *args, **kwargs):
27-
# Store extension points that have been linked.
28-
self._app = None
29-
super().__init__(*args, **kwargs)
29+
metadata = Dict()
3030

3131
@validate('metadata')
3232
def _valid_metadata(self, proposed):
@@ -54,13 +54,30 @@ def _valid_metadata(self, proposed):
5454

5555
@property
5656
def linked(self):
57+
"""Has this extension point been linked to the server.
58+
59+
Will pull from ExtensionApp's trait, if this point
60+
is an instance of ExtensionApp.
61+
"""
62+
if self.app:
63+
return self.app._linked
5764
return self._linked
5865

5966
@property
6067
def app(self):
6168
"""If the metadata includes an `app` field"""
6269
return self._app
6370

71+
@property
72+
def config(self):
73+
"""Return any configuration provided by this extension point."""
74+
if self.app:
75+
return self.app._jupyter_server_config()
76+
# At some point, we might want to add logic to load config from
77+
# disk when extensions don't use ExtensionApp.
78+
else:
79+
return {}
80+
6481
@property
6582
def module_name(self):
6683
"""Name of the Python package module where the extension's
@@ -119,8 +136,11 @@ def link(self, serverapp):
119136
This looks for a `_link_jupyter_server_extension` function
120137
in the extension's module or ExtensionApp class.
121138
"""
122-
linker = self._get_linker()
123-
return linker(serverapp)
139+
if not self.linked:
140+
linker = self._get_linker()
141+
linker(serverapp)
142+
# Store this extension as already linked.
143+
self._linked = True
124144

125145
def load(self, serverapp):
126146
"""Load the extension in a Jupyter ServerApp object.

0 commit comments

Comments
 (0)