Skip to content

Commit feb052b

Browse files
committed
track linking of extension to prevent duplicate loads
1 parent a790cf4 commit feb052b

File tree

3 files changed

+92
-78
lines changed

3 files changed

+92
-78
lines changed

jupyter_server/extension/application.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,9 @@ def get_extension_point(cls):
173173
ServerApp,
174174
]
175175

176+
# A ServerApp is not defined yet, but will be initialized below.
177+
serverapp = None
178+
176179
@property
177180
def static_url_prefix(self):
178181
return "/static/{name}/".format(
@@ -361,7 +364,7 @@ def initialize(self):
361364
3) Points Tornado Webapp to templates and
362365
static assets.
363366
"""
364-
if not hasattr(self, 'serverapp'):
367+
if not self.serverapp:
365368
msg = (
366369
"This extension has no attribute `serverapp`. "
367370
"Try calling `.link_to_serverapp()` before calling "
@@ -397,7 +400,9 @@ def _load_jupyter_server_extension(cls, serverapp):
397400
extension_manager = serverapp.extension_manager
398401
try:
399402
# Get loaded extension from serverapp.
400-
extension = extension_manager.extension_points[cls.name].app
403+
pkg = extension_manager.enabled_extensions[cls.name]
404+
point = pkg.extension_points[cls.name]
405+
extension = point.app
401406
except KeyError:
402407
extension = cls()
403408
extension._link_jupyter_server_extension(serverapp)

jupyter_server/extension/manager.py

Lines changed: 78 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -21,15 +21,6 @@
2121
class ExtensionPoint(HasTraits):
2222
"""A simple API for connecting to a Jupyter Server extension
2323
point defined by metadata and importable from a Python package.
24-
25-
Usage:
26-
27-
metadata = {
28-
"module": "extension_module",
29-
"":
30-
}
31-
32-
point = ExtensionPoint(metadata)
3324
"""
3425
metadata = Dict()
3526

@@ -58,6 +49,10 @@ def _valid_metadata(self, proposed):
5849
metadata["app"] = app()
5950
return metadata
6051

52+
@property
53+
def linked(self):
54+
return self._linked
55+
6156
@property
6257
def app(self):
6358
"""If the metadata includes an `app` field"""
@@ -103,7 +98,10 @@ def link(self, serverapp):
10398
# Otherwise return a dummy function.
10499
lambda serverapp: None
105100
)
106-
return linker(serverapp)
101+
# Capture output to return
102+
out = linker(serverapp)
103+
# Store that this extension has been linked
104+
return out
107105

108106
def load(self, serverapp):
109107
"""Load the extension in a Jupyter ServerApp object.
@@ -130,6 +128,9 @@ class ExtensionPackage(HasTraits):
130128
"""
131129
name = Unicode(help="Name of the an importable Python package.")
132130

131+
# A dictionary that stores whether the extension point has been linked.
132+
_linked_points = {}
133+
133134
@validate("name")
134135
def _validate_name(self, proposed):
135136
name = proposed['value']
@@ -157,85 +158,96 @@ def extension_points(self):
157158
"""A dictionary of extension points."""
158159
return self._extension_points
159160

161+
def link_point(self, point_name, serverapp):
162+
linked = self._linked_points.get(point_name, False)
163+
if not linked:
164+
point = self.extension_points[point_name]
165+
point.link(serverapp)
166+
167+
def load_point(self, point_name, serverapp):
168+
point = self.extension_points[point_name]
169+
point.load(serverapp)
170+
171+
def link_all_points(self, serverapp):
172+
for point_name in self.extension_points:
173+
self.link_point(point_name, serverapp)
174+
175+
def load_all_points(self, serverapp):
176+
for point_name in self.extension_points:
177+
self.load_point(point_name, serverapp)
178+
160179

161180
class ExtensionManager(LoggingConfigurable):
162181
"""High level interface for findind, validating,
163182
linking, loading, and managing Jupyter Server extensions.
164183
165184
Usage:
166-
167185
m = ExtensionManager(jpserver_extensions=extensions)
168186
"""
169-
parent = Instance(
170-
klass="jupyter_server.serverapp.ServerApp",
171-
allow_none=True
172-
)
173-
174-
jpserver_extensions = Dict(
175-
help=(
176-
"A dictionary with extension package names "
177-
"as keys and booleans to enable as values."
178-
)
179-
)
180-
181-
@default('jpserver_extensions')
182-
def _default_jpserver_extensions(self):
183-
return self.parent.jpserver_extensions
184-
185-
@validate('jpserver_extensions')
186-
def _validate_jpserver_extensions(self, proposed):
187-
jpserver_extensions = proposed['value']
188-
self._extensions = {}
189-
# Iterate over dictionary items and validate that
190-
# we can interface with each extension. If the extension
191-
# fails to interface, throw a warning through the logger
192-
# interface.
193-
for package_name, enabled in jpserver_extensions.items():
194-
if enabled:
195-
try:
196-
self._extensions[package_name] = ExtensionPackage(
197-
name=package_name
198-
)
199-
# Raise a warning if the extension cannot be loaded.
200-
except Exception as e:
201-
self.log.warning(e)
202-
return jpserver_extensions
187+
# The `enabled_extensions` attribute provides a dictionary
188+
# with extension names mapped to their ExtensionPackage interface
189+
# (see above). This manager simplifies the interaction between the
190+
# ServerApp and the extensions being appended.
191+
_enabled_extensions = {}
192+
# The `_linked_extensions` attribute tracks when each extension
193+
# has been successfully linked to a ServerApp. This helps prevent
194+
# extensions from being re-linked recursively unintentionally if another
195+
# extension attempts to link extensions again.
196+
_linked_extensions = {}
203197

204198
@property
205-
def extensions(self):
199+
def enabled_extensions(self):
206200
"""Dictionary with extension package names as keys
207201
and an ExtensionPackage objects as values.
208202
"""
209-
return self._extensions
203+
return dict(sorted(self._enabled_extensions.items()))
210204

211-
@property
212-
def extension_points(self):
213-
points = {}
214-
for ext in self.extensions.values():
215-
points.update(ext.extension_points)
216-
return points
205+
def from_jpserver_extensions(self, jpserver_extensions):
206+
"""Add extensions from 'jpserver_extensions'-like dictionary."""
207+
for name, enabled in jpserver_extensions.items():
208+
if enabled:
209+
self.add_extension(name)
210+
211+
def add_extension(self, extension_name):
212+
try:
213+
extpkg = ExtensionPackage(name=extension_name)
214+
self._enabled_extensions[extension_name] = extpkg
215+
# Raise a warning if the extension cannot be loaded.
216+
except Exception as e:
217+
self.log.warning(e)
218+
219+
def link_extension(self, name, serverapp):
220+
linked = self._linked_extensions.get(name, False)
221+
extension = self.enabled_extensions[name]
222+
if not linked:
223+
try:
224+
extension.link_all_points(serverapp)
225+
self.log.debug("The '{}' extension was successfully linked.".format(name))
226+
except Exception as e:
227+
self.log.warning(e)
228+
229+
def load_extension(self, name, serverapp):
230+
extension = self.enabled_extensions.get(name)
231+
try:
232+
extension.load_all_points(serverapp)
233+
except Exception as e:
234+
self.log.warning(e)
217235

218-
def link_extensions(self, serverapp):
236+
def link_all_extensions(self, serverapp):
219237
"""Link all enabled extensions
220-
to an instance of ServerApp
238+
to an instance of ServerApp
221239
"""
222240
# Sort the extension names to enforce deterministic linking
223241
# order.
224-
for name, ext in sorted(self.extension_points.items()):
225-
try:
226-
ext.link(serverapp)
227-
except Exception as e:
228-
self.log.warning(e)
242+
for name in self.enabled_extensions:
243+
self.link_extension(name, serverapp)
229244

230-
def load_extensions(self, serverapp):
245+
def load_all_extensions(self, serverapp):
231246
"""Load all enabled extensions and append them to
232247
the parent ServerApp.
233248
"""
234249
# Sort the extension names to enforce deterministic loading
235250
# order.
236-
for name, ext in sorted(self.extension_points.items()):
237-
try:
238-
ext.load(serverapp)
239-
except Exception as e:
240-
self.log.warning(e)
251+
for name in self.enabled_extensions:
252+
self.load_extension(name, serverapp)
241253

jupyter_server/serverapp.py

Lines changed: 7 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1474,9 +1474,9 @@ def init_components(self):
14741474

14751475
def find_server_extensions(self):
14761476
"""
1477-
Searches Jupyter paths for jpserver_extensions and captures
1478-
metadata for all enabled extensions.
1477+
Searches Jupyter paths for jpserver_extensions.
14791478
"""
1479+
14801480
# Walk through all config files looking for jpserver_extensions.
14811481
#
14821482
# Each extension will likely have a JSON config file enabling itself in
@@ -1508,13 +1508,10 @@ def init_server_extensions(self):
15081508
this instance will inherit the ServerApp's config object
15091509
and load its own config.
15101510
"""
1511-
1512-
# Initialize each extension
1513-
self.extension_manager = ExtensionManager(
1514-
logger=self.log,
1515-
jpserver_extensions=self.jpserver_extensions
1516-
)
1517-
self.extension_manager.link_extensions(self)
1511+
# Create an instance of the ExtensionManager.
1512+
self.extension_manager = ExtensionManager(logger=self.log)
1513+
self.extension_manager.from_jpserver_extensions(self.jpserver_extensions)
1514+
self.extension_manager.link_all_extensions(self)
15181515

15191516
def load_server_extensions(self):
15201517
"""Load any extensions specified by config.
@@ -1524,7 +1521,7 @@ def load_server_extensions(self):
15241521
15251522
The extension API is experimental, and may change in future releases.
15261523
"""
1527-
self.extension_manager.load_extensions(self)
1524+
self.extension_manager.load_all_extensions(self)
15281525

15291526
def init_mime_overrides(self):
15301527
# On some Windows machines, an application has registered incorrect

0 commit comments

Comments
 (0)