Skip to content

Commit 3f2424c

Browse files
committed
Add support for renaming kernelspecs on the fly.
This change adds support for kernelspec managers to be configured with renaming patterns that they will apply to the kernelspecs they serve. That, in turn, will be used a future change to allow multiple kernel spec managers to be used simultaneously without their kernel spec names colliding. This functionality is provided using a mixin that can be inherited by a subclass of KernelSpecManager in order to add this renaming support. Additionally, we provide canned subclasses of both KernelSpecManager and GatewayKernelSpecManager with this new renaming feature built into them. To use the new renaming feature with a remote Kernel Gateway, the user would add the following snippet to their Jupyter config: ``` c.ServerApp.kernel_spec_manager_class = 'jupyter_server.gateway.managers.GatewayRenamingKernelSpecManager' ``` ... meanwhile, an example of using the renaming functionality with local kernelspecs, can be achieved by the user adding the following snippet to their config: ``` c.ServerApp.kernel_spec_manager_class = 'jupyter_server.services.kernelspecs.renaming.RenamingKernelSpecManager' ``` This change also fixes a pre-existing issue with the GatewayMappingKernelManager class where the `default_kernel_name` value was not set until *after* the first request to `/api/kernelspecs`, resulting in the first such call getting the wrong default kernel name (even though subsequent calls would have gotten the correct one). I confirmed that this issue with the default kernel name was already present in the codebase on the main branch before this commit.
1 parent 3ba9ac9 commit 3f2424c

File tree

8 files changed

+589
-186
lines changed

8 files changed

+589
-186
lines changed

docs/source/api/jupyter_server.services.kernelspecs.rst

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,12 @@ Submodules
1010
:undoc-members:
1111
:show-inheritance:
1212

13+
14+
.. automodule:: jupyter_server.services.kernelspecs.renaming
15+
:members:
16+
:undoc-members:
17+
:show-inheritance:
18+
1319
Module contents
1420
---------------
1521

jupyter_server/gateway/managers.py

Lines changed: 49 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -16,17 +16,18 @@
1616
from jupyter_client.clientabc import KernelClientABC
1717
from jupyter_client.kernelspec import KernelSpecManager
1818
from jupyter_client.managerabc import KernelManagerABC
19-
from jupyter_core.utils import ensure_async
19+
from jupyter_core.utils import ensure_async, run_sync
2020
from tornado import web
2121
from tornado.escape import json_decode, json_encode, url_escape, utf8
22-
from traitlets import DottedObjectName, Instance, Type, default
22+
from traitlets import DottedObjectName, Instance, Type, Unicode, default, observe
2323

2424
from .._tz import UTC, utcnow
2525
from ..services.kernels.kernelmanager import (
2626
AsyncMappingKernelManager,
2727
ServerKernelManager,
2828
emit_kernel_action_event,
2929
)
30+
from ..services.kernelspecs.renaming import RenamingKernelSpecManagerMixin, normalize_kernel_name
3031
from ..services.sessions.sessionmanager import SessionManager
3132
from ..utils import url_path_join
3233
from .gateway_client import GatewayClient, gateway_request
@@ -60,7 +61,8 @@ def remove_kernel(self, kernel_id):
6061
except KeyError:
6162
pass
6263

63-
async def start_kernel(self, *, kernel_id=None, path=None, **kwargs):
64+
@normalize_kernel_name
65+
async def start_kernel(self, *, kernel_id=None, path=None, renamed_kernel=None, **kwargs):
6466
"""Start a kernel for a session and return its kernel_id.
6567
6668
Parameters
@@ -80,6 +82,10 @@ async def start_kernel(self, *, kernel_id=None, path=None, **kwargs):
8082

8183
km = self.kernel_manager_factory(parent=self, log=self.log)
8284
await km.start_kernel(kernel_id=kernel_id, **kwargs)
85+
if renamed_kernel is not None:
86+
km.kernel_name = renamed_kernel
87+
if km.kernel:
88+
km.kernel["name"] = km.kernel_name
8389
kernel_id = km.kernel_id
8490
self._kernels[kernel_id] = km
8591
# Initialize culling if not already
@@ -210,6 +216,27 @@ async def cull_kernels(self):
210216
class GatewayKernelSpecManager(KernelSpecManager):
211217
"""A gateway kernel spec manager."""
212218

219+
default_kernel_name = Unicode(allow_none=True)
220+
221+
# Use a hidden trait for the default kernel name we get from the remote.
222+
#
223+
# This is automatically copied to the corresponding public trait.
224+
#
225+
# We use two layers of trait so that sub classes can modify the public
226+
# trait without confusing the logic that tracks changes to the remote
227+
# default kernel name.
228+
_remote_default_kernel_name = Unicode(allow_none=True)
229+
230+
@default("default_kernel_name")
231+
def _default_default_kernel_name(self):
232+
# The default kernel name is taken from the remote gateway
233+
run_sync(self.get_all_specs)()
234+
return self._remote_default_kernel_name
235+
236+
@observe("_remote_default_kernel_name")
237+
def _observe_remote_default_kernel_name(self, change):
238+
self.default_kernel_name = change.new
239+
213240
def __init__(self, **kwargs):
214241
"""Initialize a gateway kernel spec manager."""
215242
super().__init__(**kwargs)
@@ -273,14 +300,13 @@ async def get_all_specs(self):
273300
# If different log a warning and reset the default. However, the
274301
# caller of this method will still return this server's value until
275302
# the next fetch of kernelspecs - at which time they'll match.
276-
km = self.parent.kernel_manager
277303
remote_default_kernel_name = fetched_kspecs.get("default")
278-
if remote_default_kernel_name != km.default_kernel_name:
304+
if remote_default_kernel_name != self._remote_default_kernel_name:
279305
self.log.info(
280306
f"Default kernel name on Gateway server ({remote_default_kernel_name}) differs from "
281-
f"Notebook server ({km.default_kernel_name}). Updating to Gateway server's value."
307+
f"Notebook server ({self._remote_default_kernel_name}). Updating to Gateway server's value."
282308
)
283-
km.default_kernel_name = remote_default_kernel_name
309+
self._remote_default_kernel_name = remote_default_kernel_name
284310

285311
remote_kspecs = fetched_kspecs.get("kernelspecs")
286312
return remote_kspecs
@@ -345,6 +371,18 @@ async def get_kernel_spec_resource(self, kernel_name, path):
345371
return kernel_spec_resource
346372

347373

374+
class GatewayRenamingKernelSpecManager(RenamingKernelSpecManagerMixin, GatewayKernelSpecManager):
375+
spec_name_prefix = Unicode(
376+
"remote-", help="Prefix to be added onto the front of kernel spec names."
377+
)
378+
379+
display_name_suffix = Unicode(
380+
" (Remote)",
381+
config=True,
382+
help="Suffix to be added onto the end of kernel spec display names.",
383+
)
384+
385+
348386
class GatewaySessionManager(SessionManager):
349387
"""A gateway session manager."""
350388

@@ -453,6 +491,8 @@ async def refresh_model(self, model=None):
453491
# a parent instance if, say, a server extension is using another application
454492
# (e.g., papermill) that uses a KernelManager instance directly.
455493
self.parent._kernel_connections[self.kernel_id] = int(model["connections"])
494+
if self.kernel_name:
495+
model["name"] = self.kernel_name
456496

457497
self.kernel = model
458498
return model
@@ -477,7 +517,8 @@ async def start_kernel(self, **kwargs):
477517

478518
if kernel_id is None:
479519
kernel_name = kwargs.get("kernel_name", "python3")
480-
self.log.debug("Request new kernel at: %s" % self.kernels_url)
520+
self.kernel_name = kernel_name
521+
self.log.debug(f"Request new kernel at: {self.kernels_url} using {kernel_name}")
481522

482523
# Let KERNEL_USERNAME take precedent over http_user config option.
483524
if os.environ.get("KERNEL_USERNAME") is None and GatewayClient.instance().http_user:

jupyter_server/services/kernels/kernelmanager.py

Lines changed: 59 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
from typing import Optional
1818

1919
from jupyter_client.ioloop.manager import AsyncIOLoopKernelManager
20+
from jupyter_client.kernelspec import NATIVE_KERNEL_NAME
2021
from jupyter_client.multikernelmanager import AsyncMultiKernelManager, MultiKernelManager
2122
from jupyter_client.session import Session
2223
from jupyter_core.paths import exists
@@ -38,6 +39,7 @@
3839
TraitError,
3940
Unicode,
4041
default,
42+
observe,
4143
validate,
4244
)
4345

@@ -46,6 +48,8 @@
4648
from jupyter_server.prometheus.metrics import KERNEL_CURRENTLY_RUNNING_TOTAL
4749
from jupyter_server.utils import ApiPath, import_item, to_os_path
4850

51+
from ..kernelspecs.renaming import normalize_kernel_name
52+
4953

5054
class MappingKernelManager(MultiKernelManager):
5155
"""A KernelManager that handles
@@ -206,8 +210,14 @@ async def _remove_kernel_when_ready(self, kernel_id, kernel_awaitable):
206210

207211
# TODO DEC 2022: Revise the type-ignore once the signatures have been changed upstream
208212
# https://github.com/jupyter/jupyter_client/pull/905
209-
async def _async_start_kernel( # type:ignore[override]
210-
self, *, kernel_id: Optional[str] = None, path: Optional[ApiPath] = None, **kwargs: str
213+
@normalize_kernel_name
214+
async def _async_start_kernel(
215+
self,
216+
*,
217+
kernel_id: Optional[str] = None,
218+
path: Optional[ApiPath] = None,
219+
renamed_kernel: Optional[str] = None,
220+
**kwargs: str,
211221
) -> str:
212222
"""Start a kernel for a session and return its kernel_id.
213223
@@ -231,6 +241,8 @@ async def _async_start_kernel( # type:ignore[override]
231241
assert kernel_id is not None, "Never Fail, but necessary for mypy "
232242
kwargs["kernel_id"] = kernel_id
233243
kernel_id = await self.pinned_superclass._async_start_kernel(self, **kwargs)
244+
if renamed_kernel:
245+
self._kernels[kernel_id].kernel_name = renamed_kernel
234246
self._kernel_connections[kernel_id] = 0
235247
task = asyncio.create_task(self._finish_kernel_start(kernel_id))
236248
if not getattr(self, "use_pending_kernels", None):
@@ -261,7 +273,7 @@ async def _async_start_kernel( # type:ignore[override]
261273
# see https://github.com/jupyter-server/jupyter_server/issues/1165
262274
# this assignment is technically incorrect, but might need a change of API
263275
# in jupyter_client.
264-
start_kernel = _async_start_kernel # type:ignore[assignment]
276+
start_kernel = _async_start_kernel
265277

266278
async def _finish_kernel_start(self, kernel_id):
267279
"""Handle a kernel that finishes starting."""
@@ -678,7 +690,7 @@ async def cull_kernel_if_idle(self, kernel_id):
678690

679691
# AsyncMappingKernelManager inherits as much as possible from MappingKernelManager,
680692
# overriding only what is different.
681-
class AsyncMappingKernelManager(MappingKernelManager, AsyncMultiKernelManager): # type:ignore[misc]
693+
class AsyncMappingKernelManager(MappingKernelManager, AsyncMultiKernelManager):
682694
"""An asynchronous mapping kernel manager."""
683695

684696
@default("kernel_manager_class")
@@ -700,13 +712,56 @@ def _validate_kernel_manager_class(self, proposal):
700712
)
701713
return km_class_value
702714

715+
@default("default_kernel_name")
716+
def _default_default_kernel_name(self):
717+
if (
718+
hasattr(self.kernel_spec_manager, "default_kernel_name")
719+
and self.kernel_spec_manager.default_kernel_name
720+
):
721+
return self.kernel_spec_manager.default_kernel_name
722+
return NATIVE_KERNEL_NAME
723+
724+
@observe("default_kernel_name")
725+
def _observe_default_kernel_name(self, change):
726+
if (
727+
hasattr(self.kernel_spec_manager, "default_kernel_name")
728+
and self.kernel_spec_manager.default_kernel_name
729+
):
730+
# If the kernel spec manager defines a default kernel name, treat that
731+
# one as authoritative.
732+
kernel_name = change.new
733+
if kernel_name == self.kernel_spec_manager.default_kernel_name:
734+
return
735+
self.log.debug(
736+
f"The MultiKernelManager default kernel name '{kernel_name}'"
737+
" differs from the KernelSpecManager default kernel name"
738+
f" '{self.kernel_spec_manager.default_kernel_name}'..."
739+
" Using the kernel spec manager's default name."
740+
)
741+
self.default_kernel_name = self.kernel_spec_manager.default_kernel_name
742+
743+
def _on_kernel_spec_manager_default_kernel_name_changed(self, change):
744+
# Sync the kernel-spec-manager's trait to the multi-kernel-manager's trait.
745+
kernel_name = change.new
746+
if kernel_name is None:
747+
return
748+
self.log.debug(f"KernelSpecManager default kernel name changed: {kernel_name}")
749+
self.default_kernel_name = kernel_name
750+
703751
def __init__(self, **kwargs):
704752
"""Initialize an async mapping kernel manager."""
705753
self.pinned_superclass = MultiKernelManager
706754
self._pending_kernel_tasks = {}
707755
self.pinned_superclass.__init__(self, **kwargs)
708756
self.last_kernel_activity = utcnow()
709757

758+
if hasattr(self.kernel_spec_manager, "default_kernel_name"):
759+
self.kernel_spec_manager.observe(
760+
self._on_kernel_spec_manager_default_kernel_name_changed, "default_kernel_name"
761+
)
762+
if not self.kernel_spec_manager.default_kernel_name:
763+
self.kernel_spec_manager.default_kernel_name = self.default_kernel_name
764+
710765

711766
def emit_kernel_action_event(success_msg: str = ""): # type: ignore
712767
"""Decorate kernel action methods to

jupyter_server/services/kernelspecs/handlers.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,10 +64,10 @@ async def get(self):
6464
"""Get the list of kernel specs."""
6565
ksm = self.kernel_spec_manager
6666
km = self.kernel_manager
67+
kspecs = await ensure_async(ksm.get_all_specs())
6768
model = {}
6869
model["default"] = km.default_kernel_name
6970
model["kernelspecs"] = specs = {}
70-
kspecs = await ensure_async(ksm.get_all_specs())
7171
for kernel_name, kernel_info in kspecs.items():
7272
try:
7373
if is_kernelspec_model(kernel_info):

0 commit comments

Comments
 (0)