Skip to content

Commit b4f7d94

Browse files
Support external kernels (#961)
* Support external kernels * Ignore linter rule * Check if connection directory exists * Use load_connection_info()
1 parent c08b87c commit b4f7d94

File tree

3 files changed

+100
-2
lines changed

3 files changed

+100
-2
lines changed

jupyter_client/manager.py

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,8 @@ async def wrapper(self, *args, **kwargs):
8585
out = await method(self, *args, **kwargs)
8686
# Add a small sleep to ensure tests can capture the state before done
8787
await asyncio.sleep(0.01)
88-
self._ready.set_result(None)
88+
if self.owns_kernel:
89+
self._ready.set_result(None)
8990
return out
9091
except Exception as e:
9192
self._ready.set_exception(e)
@@ -105,6 +106,7 @@ class KernelManager(ConnectionFileMixin):
105106

106107
def __init__(self, *args, **kwargs):
107108
"""Initialize a kernel manager."""
109+
self._owns_kernel = kwargs.pop("owns_kernel", True)
108110
super().__init__(**kwargs)
109111
self._shutdown_status = _ShutdownStatus.Unset
110112
self._attempted_start = False
@@ -495,6 +497,9 @@ async def _async_shutdown_kernel(self, now: bool = False, restart: bool = False)
495497
Will this kernel be restarted after it is shutdown. When this
496498
is True, connection files will not be cleaned up.
497499
"""
500+
if not self.owns_kernel:
501+
return
502+
498503
self.shutting_down = True # Used by restarter to prevent race condition
499504
# Stop monitoring for restarting while we shutdown.
500505
self.stop_restarter()
@@ -558,6 +563,10 @@ async def _async_restart_kernel(
558563

559564
restart_kernel = run_sync(_async_restart_kernel)
560565

566+
@property
567+
def owns_kernel(self) -> bool:
568+
return self._owns_kernel
569+
561570
@property
562571
def has_kernel(self) -> bool:
563572
"""Has a kernel process been started that we are actively managing."""
@@ -646,6 +655,9 @@ async def _async_signal_kernel(self, signum: int) -> None:
646655

647656
async def _async_is_alive(self) -> bool:
648657
"""Is the kernel process still running?"""
658+
if not self.owns_kernel:
659+
return True
660+
649661
if self.has_kernel:
650662
assert self.provisioner is not None
651663
ret = await self.provisioner.poll()

jupyter_client/multikernelmanager.py

Lines changed: 54 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,20 +2,23 @@
22
# Copyright (c) Jupyter Development Team.
33
# Distributed under the terms of the Modified BSD License.
44
import asyncio
5+
import json
56
import os
67
import socket
78
import typing as t
89
import uuid
910
from functools import wraps
11+
from pathlib import Path
1012

1113
import zmq
1214
from traitlets import Any, Bool, Dict, DottedObjectName, Instance, Unicode, default, observe
1315
from traitlets.config.configurable import LoggingConfigurable
1416
from traitlets.utils.importstring import import_item
1517

18+
from .connect import KernelConnectionInfo
1619
from .kernelspec import NATIVE_KERNEL_NAME, KernelSpecManager
1720
from .manager import KernelManager
18-
from .utils import ensure_async, run_sync
21+
from .utils import ensure_async, run_sync, utcnow
1922

2023

2124
class DuplicateKernelError(Exception):
@@ -105,9 +108,14 @@ def _context_default(self) -> zmq.Context:
105108
return zmq.Context()
106109

107110
connection_dir = Unicode("")
111+
external_connection_dir = Unicode(None, allow_none=True)
108112

109113
_kernels = Dict()
110114

115+
def __init__(self, *args, **kwargs):
116+
super().__init__(*args, **kwargs)
117+
self.kernel_id_to_connection_file = {}
118+
111119
def __del__(self):
112120
"""Handle garbage collection. Destroy context if applicable."""
113121
if self._created_context and self.context and not self.context.closed:
@@ -123,6 +131,51 @@ def __del__(self):
123131

124132
def list_kernel_ids(self) -> t.List[str]:
125133
"""Return a list of the kernel ids of the active kernels."""
134+
if self.external_connection_dir is not None:
135+
external_connection_dir = Path(self.external_connection_dir)
136+
if external_connection_dir.is_dir():
137+
connection_files = [p for p in external_connection_dir.iterdir() if p.is_file()]
138+
139+
# remove kernels (whose connection file has disappeared) from our list
140+
k = list(self.kernel_id_to_connection_file.keys())
141+
v = list(self.kernel_id_to_connection_file.values())
142+
for connection_file in list(self.kernel_id_to_connection_file.values()):
143+
if connection_file not in connection_files:
144+
kernel_id = k[v.index(connection_file)]
145+
del self.kernel_id_to_connection_file[kernel_id]
146+
del self._kernels[kernel_id]
147+
148+
# add kernels (whose connection file appeared) to our list
149+
for connection_file in connection_files:
150+
if connection_file in self.kernel_id_to_connection_file.values():
151+
continue
152+
try:
153+
connection_info: KernelConnectionInfo = json.loads(
154+
connection_file.read_text()
155+
)
156+
except Exception: # noqa: S112
157+
continue
158+
self.log.debug("Loading connection file %s", connection_file)
159+
if not ("kernel_name" in connection_info and "key" in connection_info):
160+
continue
161+
# it looks like a connection file
162+
kernel_id = self.new_kernel_id()
163+
self.kernel_id_to_connection_file[kernel_id] = connection_file
164+
km = self.kernel_manager_factory(
165+
parent=self,
166+
log=self.log,
167+
owns_kernel=False,
168+
)
169+
km.load_connection_info(connection_info)
170+
km.last_activity = utcnow()
171+
km.execution_state = "idle"
172+
km.connections = 1
173+
km.kernel_id = kernel_id
174+
km.kernel_name = connection_info["kernel_name"]
175+
km.ready.set_result(None)
176+
177+
self._kernels[kernel_id] = km
178+
126179
# Create a copy so we can iterate over kernels in operations
127180
# that delete keys.
128181
return list(self._kernels.keys())

jupyter_client/utils.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
- vendor functions from ipython_genutils that should be retired at some point.
55
"""
66
import os
7+
from datetime import datetime, timedelta, tzinfo
78

89
from jupyter_core.utils import ensure_async, run_sync # noqa: F401 # noqa: F401
910

@@ -83,3 +84,35 @@ def _expand_path(s):
8384
if os.name == "nt":
8485
s = s.replace("IPYTHON_TEMP", "$\\")
8586
return s
87+
88+
89+
# constant for zero offset
90+
ZERO = timedelta(0)
91+
92+
93+
class tzUTC(tzinfo): # noqa
94+
"""tzinfo object for UTC (zero offset)"""
95+
96+
def utcoffset(self, d):
97+
"""Compute utcoffset."""
98+
return ZERO
99+
100+
def dst(self, d):
101+
"""Compute dst."""
102+
return ZERO
103+
104+
105+
UTC = tzUTC() # type:ignore
106+
107+
108+
def utc_aware(unaware):
109+
"""decorator for adding UTC tzinfo to datetime's utcfoo methods"""
110+
111+
def utc_method(*args, **kwargs):
112+
dt = unaware(*args, **kwargs)
113+
return dt.replace(tzinfo=UTC)
114+
115+
return utc_method
116+
117+
118+
utcnow = utc_aware(datetime.utcnow)

0 commit comments

Comments
 (0)