Skip to content

Commit aa7420f

Browse files
minrkgogasca
authored andcommitted
Backport PR jupyter-server#1180: Only load enabled extension packages
1 parent ef9663e commit aa7420f

File tree

2 files changed

+62
-41
lines changed

2 files changed

+62
-41
lines changed

jupyter_server/extension/manager.py

+37-39
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import importlib
22

33
from tornado.gen import multi
4-
from traitlets import Any, Bool, Dict, HasTraits, Instance, Unicode, default, observe
4+
from traitlets import Any, Bool, Dict, HasTraits, Instance, List, Unicode, default, observe
55
from traitlets import validate as validate_trait
66
from traitlets.config import LoggingConfigurable
77

@@ -156,54 +156,52 @@ class ExtensionPackage(HasTraits):
156156
ext_name = "my_extensions"
157157
extpkg = ExtensionPackage(name=ext_name)
158158
"""
159-
160159
name = Unicode(help="Name of the an importable Python package.")
161-
enabled = Bool(False).tag(config=True)
162-
163-
def __init__(self, *args, **kwargs):
164-
# Store extension points that have been linked.
165-
self._linked_points = {}
166-
super().__init__(*args, **kwargs)
160+
enabled = Bool(False, help="Whether the extension package is enabled.")
167161

168-
_linked_points: dict = {}
162+
_linked_points = Dict()
163+
extension_points = Dict()
164+
module = Any(allow_none=True, help="The module for this extension package. None if not enabled")
165+
metadata = List(Dict(), help="Extension metadata loaded from the extension package.")
166+
version = Unicode(
167+
help="""
168+
The version of this extension package, if it can be found.
169+
Otherwise, an empty string.
170+
""",
171+
)
169172

170-
@validate_trait("name")
171-
def _validate_name(self, proposed):
172-
name = proposed["value"]
173-
self._extension_points = {}
173+
@default("version")
174+
def _load_version(self):
175+
if not self.enabled:
176+
return ""
177+
return getattr(self.module, "__version__", "")
178+
179+
def __init__(self, **kwargs):
180+
"""Initialize an extension package."""
181+
super().__init__(**kwargs)
182+
if self.enabled:
183+
self._load_metadata()
184+
185+
def _load_metadata(self):
186+
"""Import package and load metadata
187+
188+
Only used if extension package is enabled
189+
"""
190+
name = self.name
174191
try:
175-
self._module, self._metadata = get_metadata(name)
192+
self.module, self.metadata = get_metadata(name, logger=self.log)
176193
except ImportError as e:
177-
raise ExtensionModuleNotFound(
178-
"The module '{name}' could not be found ({e}). Are you "
179-
"sure the extension is installed?".format(name=name, e=e)
194+
msg = (
195+
f"The module '{name}' could not be found ({e}). Are you "
196+
"sure the extension is installed?"
180197
)
198+
raise ExtensionModuleNotFound(msg) from None
181199
# Create extension point interfaces for each extension path.
182-
for m in self._metadata:
200+
for m in self.metadata:
183201
point = ExtensionPoint(metadata=m)
184-
self._extension_points[point.name] = point
202+
self.extension_points[point.name] = point
185203
return name
186204

187-
@property
188-
def module(self):
189-
"""Extension metadata loaded from the extension package."""
190-
return self._module
191-
192-
@property
193-
def version(self):
194-
"""Get the version of this package, if it's given. Otherwise, return an empty string"""
195-
return getattr(self._module, "__version__", "")
196-
197-
@property
198-
def metadata(self):
199-
"""Extension metadata loaded from the extension package."""
200-
return self._metadata
201-
202-
@property
203-
def extension_points(self):
204-
"""A dictionary of extension points."""
205-
return self._extension_points
206-
207205
def validate(self):
208206
"""Validate all extension points in this package."""
209207
for extension in self.extension_points.values():

tests/extension/test_manager.py

+25-2
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import os
2+
import sys
23
import unittest.mock as mock
34

45
import pytest
@@ -60,7 +61,7 @@ def test_extension_package_api():
6061
path1 = metadata_list[0]
6162
app = path1["app"]
6263

63-
e = ExtensionPackage(name="tests.extension.mockextensions")
64+
e = ExtensionPackage(name="tests.extension.mockextensions", enabled=True)
6465
e.extension_points
6566
assert hasattr(e, "extension_points")
6667
assert len(e.extension_points) == len(metadata_list)
@@ -70,7 +71,9 @@ def test_extension_package_api():
7071

7172
def test_extension_package_notfound_error():
7273
with pytest.raises(ExtensionModuleNotFound):
73-
ExtensionPackage(name="nonexistent")
74+
ExtensionPackage(name="nonexistent", enabled=True)
75+
# no raise if not enabled
76+
ExtensionPackage(name="nonexistent", enabled=False)
7477

7578

7679
def _normalize_path(path_list):
@@ -132,3 +135,23 @@ def test_extension_manager_fail_load(jp_serverapp):
132135
jp_serverapp.reraise_server_extension_failures = True
133136
with pytest.raises(RuntimeError):
134137
manager.load_extension(name)
138+
139+
140+
@pytest.mark.parametrize("has_app", [True, False])
141+
def test_disable_no_import(jp_serverapp, has_app):
142+
# de-import modules so we can detect if they are re-imported
143+
disabled_ext = "tests.extension.mockextensions.mock1"
144+
enabled_ext = "tests.extension.mockextensions.mock2"
145+
sys.modules.pop(disabled_ext, None)
146+
sys.modules.pop(enabled_ext, None)
147+
148+
manager = ExtensionManager(serverapp=jp_serverapp if has_app else None)
149+
manager.add_extension(disabled_ext, enabled=False)
150+
manager.add_extension(enabled_ext, enabled=True)
151+
assert disabled_ext not in sys.modules
152+
assert enabled_ext in sys.modules
153+
154+
ext_pkg = manager.extensions[disabled_ext]
155+
assert ext_pkg.extension_points == {}
156+
assert ext_pkg.version == ""
157+
assert ext_pkg.metadata == []

0 commit comments

Comments
 (0)