Skip to content

Add support for renaming kernelspecs on the fly. #1267

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

Open
wants to merge 9 commits into
base: main
Choose a base branch
from
6 changes: 6 additions & 0 deletions docs/source/api/jupyter_server.services.kernelspecs.rst
Original file line number Diff line number Diff line change
@@ -10,6 +10,12 @@ Submodules
:undoc-members:
:show-inheritance:


.. automodule:: jupyter_server.services.kernelspecs.renaming
:members:
:undoc-members:
:show-inheritance:

Module contents
---------------

57 changes: 49 additions & 8 deletions jupyter_server/gateway/managers.py
Original file line number Diff line number Diff line change
@@ -16,17 +16,18 @@
from jupyter_client.clientabc import KernelClientABC
from jupyter_client.kernelspec import KernelSpecManager
from jupyter_client.managerabc import KernelManagerABC
from jupyter_core.utils import ensure_async
from jupyter_core.utils import ensure_async, run_sync
from tornado import web
from tornado.escape import json_decode, json_encode, url_escape, utf8
from traitlets import DottedObjectName, Instance, Type, default
from traitlets import DottedObjectName, Instance, Type, Unicode, default, observe

from .._tz import UTC, utcnow
from ..services.kernels.kernelmanager import (
AsyncMappingKernelManager,
ServerKernelManager,
emit_kernel_action_event,
)
from ..services.kernelspecs.renaming import RenamingKernelSpecManagerMixin, normalize_kernel_name
from ..services.sessions.sessionmanager import SessionManager
from ..utils import url_path_join
from .gateway_client import GatewayClient, gateway_request
@@ -60,7 +61,8 @@ def remove_kernel(self, kernel_id):
except KeyError:
pass

async def start_kernel(self, *, kernel_id=None, path=None, **kwargs):
@normalize_kernel_name
async def start_kernel(self, *, kernel_id=None, path=None, renamed_kernel=None, **kwargs):
"""Start a kernel for a session and return its kernel_id.

Parameters
@@ -80,6 +82,10 @@ async def start_kernel(self, *, kernel_id=None, path=None, **kwargs):

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

default_kernel_name = Unicode(allow_none=True)

# Use a hidden trait for the default kernel name we get from the remote.
#
# This is automatically copied to the corresponding public trait.
#
# We use two layers of trait so that sub classes can modify the public
# trait without confusing the logic that tracks changes to the remote
# default kernel name.
_remote_default_kernel_name = Unicode(allow_none=True)

@default("default_kernel_name")
def _default_default_kernel_name(self):
# The default kernel name is taken from the remote gateway
run_sync(self.get_all_specs)()
return self._remote_default_kernel_name

@observe("_remote_default_kernel_name")
def _observe_remote_default_kernel_name(self, change):
self.default_kernel_name = change.new

def __init__(self, **kwargs):
"""Initialize a gateway kernel spec manager."""
super().__init__(**kwargs)
@@ -273,14 +300,13 @@ async def get_all_specs(self):
# If different log a warning and reset the default. However, the
# caller of this method will still return this server's value until
# the next fetch of kernelspecs - at which time they'll match.
km = self.parent.kernel_manager
remote_default_kernel_name = fetched_kspecs.get("default")
if remote_default_kernel_name != km.default_kernel_name:
if remote_default_kernel_name != self._remote_default_kernel_name:
self.log.info(
f"Default kernel name on Gateway server ({remote_default_kernel_name}) differs from "
f"Notebook server ({km.default_kernel_name}). Updating to Gateway server's value."
f"Notebook server ({self._remote_default_kernel_name}). Updating to Gateway server's value."
)
km.default_kernel_name = remote_default_kernel_name
self._remote_default_kernel_name = remote_default_kernel_name

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


class GatewayRenamingKernelSpecManager(RenamingKernelSpecManagerMixin, GatewayKernelSpecManager):
spec_name_prefix = Unicode(
"remote-", help="Prefix to be added onto the front of kernel spec names."
)

display_name_suffix = Unicode(
" (Remote)",
config=True,
help="Suffix to be added onto the end of kernel spec display names.",
)


class GatewaySessionManager(SessionManager):
"""A gateway session manager."""

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

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

if kernel_id is None:
kernel_name = kwargs.get("kernel_name", "python3")
self.log.debug("Request new kernel at: %s" % self.kernels_url)
self.kernel_name = kernel_name
self.log.debug(f"Request new kernel at: {self.kernels_url} using {kernel_name}")

# Let KERNEL_USERNAME take precedent over http_user config option.
if os.environ.get("KERNEL_USERNAME") is None and GatewayClient.instance().http_user:
63 changes: 59 additions & 4 deletions jupyter_server/services/kernels/kernelmanager.py
Original file line number Diff line number Diff line change
@@ -17,6 +17,7 @@
from typing import Optional

from jupyter_client.ioloop.manager import AsyncIOLoopKernelManager
from jupyter_client.kernelspec import NATIVE_KERNEL_NAME
from jupyter_client.multikernelmanager import AsyncMultiKernelManager, MultiKernelManager
from jupyter_client.session import Session
from jupyter_core.paths import exists
@@ -38,6 +39,7 @@
TraitError,
Unicode,
default,
observe,
validate,
)

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

from ..kernelspecs.renaming import normalize_kernel_name


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

# TODO DEC 2022: Revise the type-ignore once the signatures have been changed upstream
# https://github.com/jupyter/jupyter_client/pull/905
async def _async_start_kernel( # type:ignore[override]
self, *, kernel_id: Optional[str] = None, path: Optional[ApiPath] = None, **kwargs: str
@normalize_kernel_name
async def _async_start_kernel(
self,
*,
kernel_id: Optional[str] = None,
path: Optional[ApiPath] = None,
renamed_kernel: Optional[str] = None,
**kwargs: str,
) -> str:
"""Start a kernel for a session and return its kernel_id.

@@ -231,6 +241,8 @@ async def _async_start_kernel( # type:ignore[override]
assert kernel_id is not None, "Never Fail, but necessary for mypy "
kwargs["kernel_id"] = kernel_id
kernel_id = await self.pinned_superclass._async_start_kernel(self, **kwargs)
if renamed_kernel:
self._kernels[kernel_id].kernel_name = renamed_kernel
self._kernel_connections[kernel_id] = 0
task = asyncio.create_task(self._finish_kernel_start(kernel_id))
if not getattr(self, "use_pending_kernels", None):
@@ -261,7 +273,7 @@ async def _async_start_kernel( # type:ignore[override]
# see https://github.com/jupyter-server/jupyter_server/issues/1165
# this assignment is technically incorrect, but might need a change of API
# in jupyter_client.
start_kernel = _async_start_kernel # type:ignore[assignment]
start_kernel = _async_start_kernel

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

# AsyncMappingKernelManager inherits as much as possible from MappingKernelManager,
# overriding only what is different.
class AsyncMappingKernelManager(MappingKernelManager, AsyncMultiKernelManager): # type:ignore[misc]
class AsyncMappingKernelManager(MappingKernelManager, AsyncMultiKernelManager):
"""An asynchronous mapping kernel manager."""

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

@default("default_kernel_name")
def _default_default_kernel_name(self):
if (
hasattr(self.kernel_spec_manager, "default_kernel_name")
and self.kernel_spec_manager.default_kernel_name
):
return self.kernel_spec_manager.default_kernel_name
return NATIVE_KERNEL_NAME

@observe("default_kernel_name")
def _observe_default_kernel_name(self, change):
if (
hasattr(self.kernel_spec_manager, "default_kernel_name")
and self.kernel_spec_manager.default_kernel_name
):
# If the kernel spec manager defines a default kernel name, treat that
# one as authoritative.
kernel_name = change.new
if kernel_name == self.kernel_spec_manager.default_kernel_name:
return
self.log.debug(
f"The MultiKernelManager default kernel name '{kernel_name}'"
" differs from the KernelSpecManager default kernel name"
f" '{self.kernel_spec_manager.default_kernel_name}'..."
" Using the kernel spec manager's default name."
)
self.default_kernel_name = self.kernel_spec_manager.default_kernel_name

def _on_kernel_spec_manager_default_kernel_name_changed(self, change):
# Sync the kernel-spec-manager's trait to the multi-kernel-manager's trait.
kernel_name = change.new
if kernel_name is None:
return
self.log.debug(f"KernelSpecManager default kernel name changed: {kernel_name}")
self.default_kernel_name = kernel_name

def __init__(self, **kwargs):
"""Initialize an async mapping kernel manager."""
self.pinned_superclass = MultiKernelManager
self._pending_kernel_tasks = {}
self.pinned_superclass.__init__(self, **kwargs)
self.last_kernel_activity = utcnow()

if hasattr(self.kernel_spec_manager, "default_kernel_name"):
self.kernel_spec_manager.observe(
self._on_kernel_spec_manager_default_kernel_name_changed, "default_kernel_name"
)
if not self.kernel_spec_manager.default_kernel_name:
self.kernel_spec_manager.default_kernel_name = self.default_kernel_name


def emit_kernel_action_event(success_msg: str = ""): # type: ignore
"""Decorate kernel action methods to
2 changes: 1 addition & 1 deletion jupyter_server/services/kernelspecs/handlers.py
Original file line number Diff line number Diff line change
@@ -64,10 +64,10 @@ async def get(self):
"""Get the list of kernel specs."""
ksm = self.kernel_spec_manager
km = self.kernel_manager
kspecs = await ensure_async(ksm.get_all_specs())
model = {}
model["default"] = km.default_kernel_name
model["kernelspecs"] = specs = {}
kspecs = await ensure_async(ksm.get_all_specs())
for kernel_name, kernel_info in kspecs.items():
try:
if is_kernelspec_model(kernel_info):
164 changes: 164 additions & 0 deletions jupyter_server/services/kernelspecs/renaming.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
"""Support for renaming kernel specs at runtime."""
# Copyright (c) Jupyter Development Team.
# Distributed under the terms of the Modified BSD License.
from functools import wraps
from typing import Any, Dict, Tuple

from jupyter_client.kernelspec import KernelSpecManager
from jupyter_core.utils import ensure_async, run_sync
from traitlets import Unicode, default, observe
from traitlets.config import LoggingConfigurable


def normalize_kernel_name(method):
@wraps(method)
async def wrapped_method(self, *args, **kwargs):
kernel_name = kwargs.get("kernel_name", None)
if (
kernel_name
and hasattr(self, "kernel_spec_manager")
and hasattr(self.kernel_spec_manager, "original_kernel_name")
):
original_kernel_name = self.kernel_spec_manager.original_kernel_name(kernel_name)
if kernel_name != original_kernel_name:
self.log.debug(
f"Renamed kernel '{kernel_name}' replaced with original kernel name '{original_kernel_name}'"
)
kwargs["renamed_kernel"] = kernel_name
kwargs["kernel_name"] = original_kernel_name

return await method(self, *args, **kwargs)

return wrapped_method


class RenamingKernelSpecManagerMixin(LoggingConfigurable):
"""KernelSpecManager mixin that renames kernel specs.
The base KernelSpecManager class only has synchronous methods, but some child
classes (in particular, GatewayKernelManager) change those methods to be async.
In order to support both versions, we provide both synchronous and async versions
of all the relevant kernel spec manager methods. We first do the renaming in the
async version, but override the KernelSpecManager base methods using the
synchronous versions.
"""

spec_name_prefix = Unicode(help="Prefix to be added onto the front of kernel spec names.")

display_name_suffix = Unicode(
config=True, help="Suffix to be added onto the end of kernel spec display names."
)

display_name_format = Unicode(
config=True, help="Format for rewritten kernel spec display names."
)

@default("display_name_format")
def _default_display_name_format(self):
if self.display_name_suffix:
return "{}" + self.display_name_suffix
return "{}"

default_kernel_name = Unicode(allow_none=True)

@observe("default_kernel_name")
def _observe_default_kernel_name(self, change):
kernel_name = change.new
if self.original_kernel_name(kernel_name) is not kernel_name:
# The default kernel name has already been renamed
return
updated_kernel_name = self.rename_kernel(kernel_name)
self.log.debug(f"Renaming default kernel name {kernel_name} to {updated_kernel_name}")
self.default_kernel_name = updated_kernel_name

def rename_kernel(self, kernel_name: str) -> str:
"""Rename the supplied kernel spec based on the configured format string."""
if kernel_name.startswith(self.spec_name_prefix):
return kernel_name
return self.spec_name_prefix + kernel_name

def original_kernel_name(self, kernel_name: str) -> str:
if not kernel_name.startswith(self.spec_name_prefix):
return kernel_name
return kernel_name[len(self.spec_name_prefix) :]

def _update_display_name(self, display_name: str) -> str:
if not display_name:
return display_name
return self.display_name_format.format(display_name)

def _update_spec(self, original_name: str, kernel_spec: Dict) -> Tuple[str, Dict]:
original_prefix = f"/kernelspecs/{original_name}"
spec_name = self.rename_kernel(original_name)
new_prefix = f"/kernelspecs/{spec_name}"

kernel_spec["name"] = spec_name
kernel_spec["spec"] = kernel_spec.get("spec", {})
kernel_spec["resources"] = kernel_spec.get("resources", {})

spec = kernel_spec["spec"]
spec["display_name"] = self._update_display_name(spec.get("display_name", ""))

resources = kernel_spec["resources"]
for name, value in resources.items():
resources[name] = value.replace(original_prefix, new_prefix)
return spec_name, kernel_spec

async def async_get_all_specs(self):
ks: Dict = {}
original_ks = await ensure_async(super().get_all_specs()) # type:ignore[misc]
for s, k in original_ks.items():
spec_name, kernel_spec = self._update_spec(s, k)
ks[spec_name] = kernel_spec
return ks

def get_all_specs(self):
return run_sync(self.async_get_all_specs)()

async def async_get_kernel_spec(self, kernel_name: str, *args: Any, **kwargs: Any) -> Any:
original_kernel_name = self.original_kernel_name(kernel_name)
self.log.debug(f"Found original kernel name '{original_kernel_name}' for '{kernel_name}'")
kspec = await ensure_async(
super().get_kernel_spec(original_kernel_name, *args, **kwargs) # type:ignore[misc]
)
if original_kernel_name == kernel_name:
# The kernel wasn't renamed, so don't modify its contents
return kspec

# KernelSpecManager and GatewayKernelSpec manager return different types for the
# wrapped `get_kernel_spec` call (KernelSpec vs. Dict). To accommodate both,
# we check the type of the returned value and operate on the two different
# types as appropriate.
if isinstance(kspec, dict):
kspec["name"] = kernel_name
kspec["display_name"] = self._update_display_name(kspec.get("display_name", ""))
else:
kspec.name = kernel_name
kspec.display_name = self._update_display_name(kspec.display_name)
return kspec

def get_kernel_spec(self, kernel_name: str, *args: Any, **kwargs: Any) -> Any:
return run_sync(self.async_get_kernel_spec)(kernel_name, *args, **kwargs)

async def get_kernel_spec_resource(self, kernel_name: str, *args: Any, **kwargs: Any) -> Any:
if not hasattr(super(), "get_kernel_spec_resource"):
return None
kernel_name = self.original_kernel_name(kernel_name)
return await ensure_async(
super().get_kernel_spec_resource(kernel_name, *args, **kwargs) # type:ignore[misc]
)


class RenamingKernelSpecManager(RenamingKernelSpecManagerMixin, KernelSpecManager):
"""KernelSpecManager that renames kernels"""

spec_name_prefix = Unicode(
"local-", help="Prefix to be added onto the front of kernel spec names."
)

display_name_suffix = Unicode(
" (Local)",
config=True,
help="Suffix to be added onto the end of kernel spec display names.",
)
115 changes: 96 additions & 19 deletions tests/services/kernels/test_api.py
Original file line number Diff line number Diff line change
@@ -58,6 +58,42 @@ async def _(kernel_id, ready=None):
"kernel_manager_class": "jupyter_server.services.kernels.kernelmanager.AsyncMappingKernelManager"
}
},
{
"ServerApp": {
"kernel_manager_class": "jupyter_server.services.kernels.kernelmanager.AsyncMappingKernelManager",
"kernel_spec_manager_class": "jupyter_server.services.kernelspecs.renaming.RenamingKernelSpecManager",
},
},
{
"ServerApp": {
"kernel_manager_class": "jupyter_server.services.kernels.kernelmanager.AsyncMappingKernelManager",
"kernel_spec_manager_class": "jupyter_server.services.kernelspecs.renaming.RenamingKernelSpecManager",
},
"AsyncMappingKernelManager": {"default_kernel_name": NATIVE_KERNEL_NAME},
},
{
"ServerApp": {
"kernel_manager_class": "jupyter_server.services.kernels.kernelmanager.AsyncMappingKernelManager",
"kernel_spec_manager_class": "jupyter_server.services.kernelspecs.renaming.RenamingKernelSpecManager",
},
"RenamingKernelSpecManager": {"default_kernel_name": NATIVE_KERNEL_NAME},
},
{
"ServerApp": {
"kernel_manager_class": "jupyter_server.services.kernels.kernelmanager.AsyncMappingKernelManager",
"kernel_spec_manager_class": "jupyter_server.services.kernelspecs.renaming.RenamingKernelSpecManager",
},
"AsyncMappingKernelManager": {"default_kernel_name": "local-" + NATIVE_KERNEL_NAME},
"RenamingKernelSpecManager": {"default_kernel_name": "not-found"},
},
{
"ServerApp": {
"kernel_manager_class": "jupyter_server.services.kernels.kernelmanager.AsyncMappingKernelManager",
"kernel_spec_manager_class": "jupyter_server.services.kernelspecs.renaming.RenamingKernelSpecManager",
},
"AsyncMappingKernelManager": {"default_kernel_name": NATIVE_KERNEL_NAME},
"RenamingKernelSpecManager": {"default_kernel_name": "not-found"},
},
]


@@ -66,13 +102,22 @@ async def _(kernel_id, ready=None):
# See https://github.com/jupyter-server/jupyter_server/issues/672
if os.name != "nt" and jupyter_client._version.version_info >= (7, 1):
# Add a pending kernels condition
c = {
"ServerApp": {
"kernel_manager_class": "jupyter_server.services.kernels.kernelmanager.AsyncMappingKernelManager"
cs = [
{
"ServerApp": {
"kernel_manager_class": "jupyter_server.services.kernels.kernelmanager.AsyncMappingKernelManager"
},
"AsyncMappingKernelManager": {"use_pending_kernels": True},
},
"AsyncMappingKernelManager": {"use_pending_kernels": True},
}
configs.append(c)
{
"ServerApp": {
"kernel_manager_class": "jupyter_server.services.kernels.kernelmanager.AsyncMappingKernelManager",
"kernel_spec_manager_class": "jupyter_server.services.kernelspecs.renaming.RenamingKernelSpecManager",
},
"AsyncMappingKernelManager": {"use_pending_kernels": True},
},
]
configs.extend(cs)


@pytest.fixture(params=configs)
@@ -102,15 +147,44 @@ async def test_default_kernels(jp_fetch, jp_base_url):


@pytest.mark.timeout(TEST_TIMEOUT)
async def test_main_kernel_handler(jp_fetch, jp_base_url, jp_serverapp, pending_kernel_is_ready):
async def test_kernels_with_default_kernelspec(jp_fetch, jp_base_url, jp_kernelspecs):
r = await jp_fetch("api", "kernels", method="POST", allow_nonstandard_methods=True)
kernel = json.loads(r.body.decode())
assert r.headers["location"] == url_path_join(jp_base_url, "/api/kernels/", kernel["id"])
assert r.code == 201
assert isinstance(kernel, dict)

report_uri = url_path_join(jp_base_url, "/api/security/csp-report")
expected_csp = "; ".join(
["frame-ancestors 'self'", "report-uri " + report_uri, "default-src 'none'"]
)
assert r.headers["Content-Security-Policy"] == expected_csp

# Verify that the default kernel was created using the default kernelspec
r2 = await jp_fetch("api", "kernelspecs", method="GET")
model = json.loads(r2.body.decode())
assert isinstance(model, dict)
assert model["default"] == kernel["name"]
specs = model["kernelspecs"]
assert isinstance(specs, dict)
assert kernel["name"] in specs


@pytest.mark.timeout(TEST_TIMEOUT)
async def test_main_kernel_handler(
jp_server_config, jp_fetch, jp_base_url, jp_serverapp, pending_kernel_is_ready
):
# Start the first kernel
r = await jp_fetch(
"api", "kernels", method="POST", body=json.dumps({"name": NATIVE_KERNEL_NAME})
spec_name_prefix = jp_server_config.get("RenamingKernelSpecManager", {}).get(
"spec_name_prefix", ""
)
kernel_name = spec_name_prefix + NATIVE_KERNEL_NAME
r = await jp_fetch("api", "kernels", method="POST", body=json.dumps({"name": kernel_name}))
kernel1 = json.loads(r.body.decode())
assert r.headers["location"] == url_path_join(jp_base_url, "/api/kernels/", kernel1["id"])
assert r.code == 201
assert isinstance(kernel1, dict)
assert kernel1["name"] == kernel_name

report_uri = url_path_join(jp_base_url, "/api/security/csp-report")
expected_csp = "; ".join(
@@ -128,11 +202,10 @@ async def test_main_kernel_handler(jp_fetch, jp_base_url, jp_serverapp, pending_
await pending_kernel_is_ready(kernel1["id"])

# Start a second kernel
r = await jp_fetch(
"api", "kernels", method="POST", body=json.dumps({"name": NATIVE_KERNEL_NAME})
)
r = await jp_fetch("api", "kernels", method="POST", body=json.dumps({"name": kernel_name}))
kernel2 = json.loads(r.body.decode())
assert isinstance(kernel2, dict)
assert kernel2["name"] == kernel_name
await pending_kernel_is_ready(kernel1["id"])

# Get kernel list again
@@ -176,19 +249,21 @@ async def test_main_kernel_handler(jp_fetch, jp_base_url, jp_serverapp, pending_
"api",
"kernels",
method="POST",
body=json.dumps({"name": NATIVE_KERNEL_NAME, "path": "/foo"}),
body=json.dumps({"name": kernel_name, "path": "/foo"}),
)
kernel3 = json.loads(r.body.decode())
assert isinstance(kernel3, dict)
await pending_kernel_is_ready(kernel3["id"])


@pytest.mark.timeout(TEST_TIMEOUT)
async def test_kernel_handler(jp_fetch, jp_serverapp, pending_kernel_is_ready):
async def test_kernel_handler(jp_server_config, jp_fetch, jp_serverapp, pending_kernel_is_ready):
# Create a kernel
r = await jp_fetch(
"api", "kernels", method="POST", body=json.dumps({"name": NATIVE_KERNEL_NAME})
spec_name_prefix = jp_server_config.get("RenamingKernelSpecManager", {}).get(
"spec_name_prefix", ""
)
kernel_name = spec_name_prefix + NATIVE_KERNEL_NAME
r = await jp_fetch("api", "kernels", method="POST", body=json.dumps({"name": kernel_name}))
kernel_id = json.loads(r.body.decode())["id"]
r = await jp_fetch("api", "kernels", kernel_id, method="GET")
kernel = json.loads(r.body.decode())
@@ -257,11 +332,13 @@ async def test_kernel_handler_startup_error_pending(


@pytest.mark.timeout(TEST_TIMEOUT)
async def test_connection(jp_fetch, jp_ws_fetch, jp_http_port, jp_auth_header):
async def test_connection(jp_server_config, jp_fetch, jp_ws_fetch, jp_http_port, jp_auth_header):
# Create kernel
r = await jp_fetch(
"api", "kernels", method="POST", body=json.dumps({"name": NATIVE_KERNEL_NAME})
spec_name_prefix = jp_server_config.get("RenamingKernelSpecManager", {}).get(
"spec_name_prefix", ""
)
kernel_name = spec_name_prefix + NATIVE_KERNEL_NAME
r = await jp_fetch("api", "kernels", method="POST", body=json.dumps({"name": kernel_name}))
kid = json.loads(r.body.decode())["id"]

# Get kernel info
52 changes: 44 additions & 8 deletions tests/services/kernelspecs/test_api.py
Original file line number Diff line number Diff line change
@@ -8,7 +8,26 @@
from ...utils import expected_http_error, some_resource


async def test_list_kernelspecs_bad(jp_fetch, jp_kernelspecs, jp_data_dir, jp_serverapp):
@pytest.fixture(params=[False, True])
def jp_rename_kernels(request):
return request.param


@pytest.fixture
def jp_argv(jp_rename_kernels):
argv = []
if jp_rename_kernels:
argv.extend(
[
"--ServerApp.kernel_spec_manager_class=jupyter_server.services.kernelspecs.renaming.RenamingKernelSpecManager",
]
)
return argv


async def test_list_kernelspecs_bad(
jp_rename_kernels, jp_fetch, jp_kernelspecs, jp_data_dir, jp_serverapp
):
app: ServerApp = jp_serverapp
default = app.kernel_manager.default_kernel_name
bad_kernel_dir = jp_data_dir.joinpath(jp_data_dir, "kernels", "bad2")
@@ -25,7 +44,7 @@ async def test_list_kernelspecs_bad(jp_fetch, jp_kernelspecs, jp_data_dir, jp_se
assert len(specs) > 2


async def test_list_kernelspecs(jp_fetch, jp_kernelspecs, jp_serverapp):
async def test_list_kernelspecs(jp_rename_kernels, jp_fetch, jp_kernelspecs, jp_serverapp):
app: ServerApp = jp_serverapp
default = app.kernel_manager.default_kernel_name
r = await jp_fetch("api", "kernelspecs", method="GET")
@@ -37,21 +56,38 @@ async def test_list_kernelspecs(jp_fetch, jp_kernelspecs, jp_serverapp):
assert len(specs) > 2

def is_sample_kernelspec(s):
return s["name"] == "sample" and s["spec"]["display_name"] == "Test kernel"
if jp_rename_kernels:
return (
s["name"] == "local-sample" and s["spec"]["display_name"] == "Test kernel (Local)"
)
else:
return s["name"] == "sample" and s["spec"]["display_name"] == "Test kernel"

def is_default_kernelspec(s):
return s["name"] == default

assert any(is_sample_kernelspec(s) for s in specs.values()), specs
assert any(is_default_kernelspec(s) for s in specs.values()), specs
assert any(
is_default_kernelspec(s) for s in specs.values()
), f"Default kernel name {default} not found in {specs}"


async def test_get_kernelspecs(jp_fetch, jp_kernelspecs):
r = await jp_fetch("api", "kernelspecs", "Sample", method="GET")
async def test_get_kernelspecs(jp_rename_kernels, jp_fetch, jp_kernelspecs):
kernel_name = "Sample"
if jp_rename_kernels:
kernel_name = "local-sample"
r = await jp_fetch("api", "kernelspecs", kernel_name, method="GET")
model = json.loads(r.body.decode())
assert model["name"].lower() == "sample"
if jp_rename_kernels:
assert model["name"].lower() == "local-sample"
else:
assert model["name"].lower() == "sample"

assert isinstance(model["spec"], dict)
assert model["spec"]["display_name"] == "Test kernel"
if jp_rename_kernels:
assert model["spec"]["display_name"] == "Test kernel (Local)"
else:
assert model["spec"]["display_name"] == "Test kernel"
assert isinstance(model["resources"], dict)


350 changes: 203 additions & 147 deletions tests/test_gateway.py

Large diffs are not rendered by default.