euporie 2.8.6__py3-none-any.whl → 2.8.7__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- euporie/console/app.py +2 -0
- euporie/console/tabs/console.py +27 -17
- euporie/core/__init__.py +2 -2
- euporie/core/app/_commands.py +4 -21
- euporie/core/app/app.py +13 -7
- euporie/core/bars/command.py +9 -6
- euporie/core/bars/search.py +43 -2
- euporie/core/border.py +7 -2
- euporie/core/comm/base.py +2 -2
- euporie/core/comm/ipywidgets.py +3 -3
- euporie/core/commands.py +44 -8
- euporie/core/completion.py +14 -6
- euporie/core/convert/datum.py +7 -7
- euporie/core/data_structures.py +20 -1
- euporie/core/filters.py +8 -0
- euporie/core/ft/html.py +47 -40
- euporie/core/graphics.py +11 -3
- euporie/core/history.py +15 -5
- euporie/core/inspection.py +16 -9
- euporie/core/kernel/__init__.py +53 -1
- euporie/core/kernel/base.py +571 -0
- euporie/core/kernel/{client.py → jupyter.py} +173 -430
- euporie/core/kernel/{manager.py → jupyter_manager.py} +4 -3
- euporie/core/kernel/local.py +694 -0
- euporie/core/key_binding/bindings/basic.py +6 -3
- euporie/core/keys.py +26 -25
- euporie/core/layout/cache.py +31 -7
- euporie/core/layout/containers.py +88 -13
- euporie/core/layout/scroll.py +45 -148
- euporie/core/log.py +1 -1
- euporie/core/style.py +2 -1
- euporie/core/suggest.py +155 -74
- euporie/core/tabs/__init__.py +10 -0
- euporie/core/tabs/_commands.py +76 -0
- euporie/core/tabs/_settings.py +16 -0
- euporie/core/tabs/base.py +22 -8
- euporie/core/tabs/kernel.py +81 -35
- euporie/core/tabs/notebook.py +14 -22
- euporie/core/utils.py +1 -1
- euporie/core/validation.py +8 -8
- euporie/core/widgets/_settings.py +19 -2
- euporie/core/widgets/cell.py +31 -31
- euporie/core/widgets/cell_outputs.py +10 -1
- euporie/core/widgets/dialog.py +30 -75
- euporie/core/widgets/forms.py +71 -59
- euporie/core/widgets/inputs.py +7 -4
- euporie/core/widgets/layout.py +281 -93
- euporie/core/widgets/menu.py +55 -15
- euporie/core/widgets/palette.py +3 -1
- euporie/core/widgets/tree.py +86 -76
- euporie/notebook/app.py +35 -16
- euporie/notebook/tabs/edit.py +4 -4
- euporie/notebook/tabs/json.py +6 -2
- euporie/notebook/tabs/notebook.py +26 -8
- euporie/preview/tabs/notebook.py +17 -13
- euporie/web/tabs/web.py +22 -3
- euporie/web/widgets/webview.py +3 -0
- {euporie-2.8.6.dist-info → euporie-2.8.7.dist-info}/METADATA +1 -1
- {euporie-2.8.6.dist-info → euporie-2.8.7.dist-info}/RECORD +64 -61
- {euporie-2.8.6.dist-info → euporie-2.8.7.dist-info}/entry_points.txt +1 -1
- {euporie-2.8.6.dist-info → euporie-2.8.7.dist-info}/licenses/LICENSE +1 -1
- {euporie-2.8.6.data → euporie-2.8.7.data}/data/share/applications/euporie-console.desktop +0 -0
- {euporie-2.8.6.data → euporie-2.8.7.data}/data/share/applications/euporie-notebook.desktop +0 -0
- {euporie-2.8.6.dist-info → euporie-2.8.7.dist-info}/WHEEL +0 -0
@@ -3,23 +3,24 @@
|
|
3
3
|
from __future__ import annotations
|
4
4
|
|
5
5
|
import asyncio
|
6
|
-
import concurrent.futures
|
7
6
|
import logging
|
8
7
|
import os
|
9
|
-
import threading
|
10
8
|
from collections import defaultdict
|
9
|
+
from functools import partial
|
11
10
|
from subprocess import PIPE, STDOUT # S404 - Security implications considered
|
12
|
-
from typing import TYPE_CHECKING,
|
11
|
+
from typing import TYPE_CHECKING, cast
|
13
12
|
from uuid import uuid4
|
14
13
|
|
15
14
|
from upath import UPath
|
16
15
|
|
16
|
+
from euporie.core.kernel.base import BaseKernel, KernelInfo, MsgCallbacks
|
17
|
+
|
17
18
|
if TYPE_CHECKING:
|
18
|
-
from collections.abc import Coroutine
|
19
19
|
from pathlib import Path
|
20
|
-
from typing import Any, Callable
|
20
|
+
from typing import Any, Callable, Unpack
|
21
21
|
|
22
22
|
from jupyter_client import KernelClient
|
23
|
+
from jupyter_client.kernelspec import KernelSpecManager
|
23
24
|
|
24
25
|
from euporie.core.tabs.kernel import KernelTab
|
25
26
|
|
@@ -27,175 +28,112 @@ if TYPE_CHECKING:
|
|
27
28
|
log = logging.getLogger(__name__)
|
28
29
|
|
29
30
|
|
30
|
-
class
|
31
|
-
"""Typed dictionary for named message callbacks."""
|
32
|
-
|
33
|
-
get_input: Callable[[str, bool], None] | None
|
34
|
-
set_execution_count: Callable[[int], None] | None
|
35
|
-
add_output: Callable[[dict[str, Any], bool], None] | None
|
36
|
-
add_input: Callable[[dict[str, Any], bool], None] | None
|
37
|
-
clear_output: Callable[[bool], None] | None
|
38
|
-
done: Callable[[dict[str, Any]], None] | None
|
39
|
-
set_metadata: Callable[[tuple[str, ...], Any], None] | None
|
40
|
-
set_status: Callable[[str], None] | None
|
41
|
-
set_kernel_info: Callable[[dict[str, Any]], None] | None
|
42
|
-
completeness_status: Callable[[dict[str, Any]], None] | None
|
43
|
-
dead: Callable[[], None] | None
|
44
|
-
# Payloads
|
45
|
-
page: Callable[[list[dict], int], None] | None
|
46
|
-
set_next_input: Callable[[str, bool], None] | None
|
47
|
-
edit_magic: Callable[[str, int], None] | None
|
48
|
-
ask_exit: Callable[[bool], None] | None
|
49
|
-
|
50
|
-
|
51
|
-
class Kernel:
|
31
|
+
class JupyterKernel(BaseKernel):
|
52
32
|
"""Run a notebook kernel and communicates with it asynchronously.
|
53
33
|
|
54
34
|
Has the ability to run itself in it's own thread.
|
55
35
|
"""
|
56
36
|
|
57
|
-
|
37
|
+
_client_id = f"euporie-{os.getpid()}"
|
38
|
+
_spec_manager: KernelSpecManager
|
39
|
+
|
40
|
+
@classmethod
|
41
|
+
def variants(cls) -> list[KernelInfo]:
|
42
|
+
"""Return available kernel specifications."""
|
43
|
+
from jupyter_core.paths import jupyter_runtime_dir
|
44
|
+
|
45
|
+
try:
|
46
|
+
manager = cls._spec_manager
|
47
|
+
except AttributeError:
|
48
|
+
from jupyter_client.kernelspec import KernelSpecManager
|
49
|
+
from jupyter_core.paths import jupyter_path
|
50
|
+
|
51
|
+
manager = cls._spec_manager = KernelSpecManager()
|
52
|
+
# Set the kernel folder list to prevent the default method from running.
|
53
|
+
# This prevents the kernel spec manager from loading IPython, just for the
|
54
|
+
# purpose of adding the depreciated :file:`.ipython/kernels` folder to the list
|
55
|
+
# of kernel search paths. Without this, having IPython installed causes a
|
56
|
+
# import race condition error where IPython was imported in the main thread for
|
57
|
+
# displaying LaTeX and in the kernel thread to discover kernel paths.
|
58
|
+
# Also this speeds up launch since importing IPython is pretty slow.
|
59
|
+
manager.kernel_dirs = jupyter_path("kernels")
|
60
|
+
|
61
|
+
return [
|
62
|
+
KernelInfo(
|
63
|
+
name=name,
|
64
|
+
display_name=info.get("spec", {}).get("display_name", name),
|
65
|
+
factory=partial(cls, kernel_name=name),
|
66
|
+
kind="new",
|
67
|
+
type=cls,
|
68
|
+
)
|
69
|
+
for name, info in manager.get_all_specs().items()
|
70
|
+
] + [
|
71
|
+
KernelInfo(
|
72
|
+
name=path.name,
|
73
|
+
display_name=path.name,
|
74
|
+
factory=partial(cls, connection_file=path),
|
75
|
+
kind="existing",
|
76
|
+
type=cls,
|
77
|
+
)
|
78
|
+
for path in UPath(jupyter_runtime_dir()).glob("kernel-*.json")
|
79
|
+
]
|
58
80
|
|
59
81
|
def __init__(
|
60
82
|
self,
|
61
83
|
kernel_tab: KernelTab,
|
62
|
-
threaded: bool = True,
|
63
|
-
allow_stdin: bool = False,
|
64
84
|
default_callbacks: MsgCallbacks | None = None,
|
85
|
+
allow_stdin: bool = False,
|
86
|
+
*,
|
87
|
+
kernel_name: str | None = None,
|
65
88
|
connection_file: Path | None = None,
|
89
|
+
**kwargs: Any,
|
66
90
|
) -> None:
|
67
|
-
"""
|
91
|
+
"""Initialize the JupyterKernel.
|
68
92
|
|
69
93
|
Args:
|
70
94
|
kernel_tab: The notebook this kernel belongs to
|
71
|
-
threaded: If :py:const:`True`, run kernel communication in a separate thread
|
72
95
|
allow_stdin: Whether the kernel is allowed to request input
|
73
96
|
default_callbacks: The default callbacks to use on receipt of a message
|
74
|
-
|
97
|
+
kernel_name: Name of the Jupyter kernel to launch
|
98
|
+
connection_file: Path to a file from which to load or to which to save
|
75
99
|
kernel connection information
|
76
|
-
|
100
|
+
kwargs: Additional key-word arguments
|
77
101
|
"""
|
78
|
-
|
102
|
+
super().__init__(
|
103
|
+
kernel_tab=kernel_tab,
|
104
|
+
allow_stdin=allow_stdin,
|
105
|
+
default_callbacks=default_callbacks,
|
106
|
+
)
|
79
107
|
|
80
|
-
from euporie.core.kernel.
|
108
|
+
from euporie.core.kernel.jupyter_manager import (
|
81
109
|
EuporieKernelManager,
|
82
110
|
set_default_provisioner,
|
83
111
|
)
|
84
112
|
|
85
113
|
set_default_provisioner()
|
86
114
|
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
115
|
+
if kernel_name is None and connection_file is not None:
|
116
|
+
import json
|
117
|
+
|
118
|
+
try:
|
119
|
+
connection_info = json.loads(connection_file.read_text())
|
120
|
+
except json.decoder.JSONDecodeError:
|
121
|
+
connection_info = {}
|
122
|
+
kernel_name = connection_info.get("kernel_name", "python3")
|
95
123
|
|
96
|
-
|
124
|
+
if kernel_name is None and connection_file is None:
|
125
|
+
raise ValueError("Must provide `kernel_name` or `connection_file`")
|
97
126
|
|
98
|
-
self.kernel_tab = kernel_tab
|
99
127
|
self.connection_file = connection_file
|
128
|
+
self.km = EuporieKernelManager(kernel_name=kernel_name)
|
100
129
|
self.kc: KernelClient | None = None
|
101
|
-
self.km = EuporieKernelManager(
|
102
|
-
kernel_name=str(kernel_tab.kernel_name),
|
103
|
-
)
|
104
|
-
self._status = "stopped"
|
105
|
-
self.error: Exception | None = None
|
106
|
-
self.dead = False
|
107
130
|
self.monitor_task: asyncio.Task | None = None
|
108
|
-
|
109
|
-
self.coros: dict[str, concurrent.futures.Future] = {}
|
110
131
|
self.poll_tasks: list[asyncio.Task] = []
|
111
|
-
|
112
|
-
self.default_callbacks = MsgCallbacks(
|
113
|
-
{
|
114
|
-
"get_input": None,
|
115
|
-
"set_execution_count": None,
|
116
|
-
"add_output": None,
|
117
|
-
"clear_output": None,
|
118
|
-
"done": None,
|
119
|
-
"set_metadata": None,
|
120
|
-
"set_status": None,
|
121
|
-
}
|
122
|
-
)
|
123
|
-
if default_callbacks is not None:
|
124
|
-
self.default_callbacks.update(default_callbacks)
|
125
|
-
|
126
132
|
self.msg_id_callbacks: dict[str, MsgCallbacks] = defaultdict(
|
127
133
|
# Return a copy of the default callbacks
|
128
134
|
lambda: MsgCallbacks(dict(self.default_callbacks)) # type: ignore # mypy #8890
|
129
135
|
)
|
130
136
|
|
131
|
-
# Set the kernel folder list to prevent the default method from running.
|
132
|
-
# This prevents the kernel spec manager from loading IPython, just for the
|
133
|
-
# purpose of adding the depreciated :file:`.ipython/kernels` folder to the list
|
134
|
-
# of kernel search paths. Without this, having IPython installed causes a
|
135
|
-
# import race condition error where IPython was imported in the main thread for
|
136
|
-
# displaying LaTeX and in the kernel thread to discover kernel paths.
|
137
|
-
# Also this speeds up launch since importing IPython is pretty slow.
|
138
|
-
self.km.kernel_spec_manager.kernel_dirs = jupyter_path("kernels")
|
139
|
-
|
140
|
-
def _setup_loop(self) -> None:
|
141
|
-
"""Set the current loop the the kernel's event loop.
|
142
|
-
|
143
|
-
This method is intended to be run in the kernel thread.
|
144
|
-
"""
|
145
|
-
asyncio.set_event_loop(self.loop)
|
146
|
-
self.status_change_event = asyncio.Event()
|
147
|
-
self.loop.run_forever()
|
148
|
-
|
149
|
-
def _aodo(
|
150
|
-
self,
|
151
|
-
coro: Coroutine,
|
152
|
-
wait: bool = False,
|
153
|
-
callback: Callable | None = None,
|
154
|
-
timeout: int | float | None = None,
|
155
|
-
single: bool = False,
|
156
|
-
) -> Any:
|
157
|
-
"""Schedule a coroutine in the kernel's event loop.
|
158
|
-
|
159
|
-
Optionally waits for the results (blocking the main thread). Optionally
|
160
|
-
schedules a callback to run when the coroutine has completed or timed out.
|
161
|
-
|
162
|
-
Args:
|
163
|
-
coro: The coroutine to run
|
164
|
-
wait: If :py:const:`True`, block until the kernel has started
|
165
|
-
callback: A function to run when the coroutine completes. The result from
|
166
|
-
the coroutine will be passed as an argument
|
167
|
-
timeout: The number of seconds to allow the coroutine to run if waiting
|
168
|
-
single: If :py:const:`True`, any futures for previous instances of the
|
169
|
-
coroutine will be cancelled
|
170
|
-
|
171
|
-
Returns:
|
172
|
-
The result of the coroutine
|
173
|
-
|
174
|
-
"""
|
175
|
-
future = asyncio.run_coroutine_threadsafe(coro, self.loop)
|
176
|
-
|
177
|
-
# Cancel previous future instances if required
|
178
|
-
if single and self.coros.get(coro.__name__):
|
179
|
-
self.coros[coro.__name__].cancel()
|
180
|
-
self.coros[coro.__name__] = future
|
181
|
-
|
182
|
-
if wait:
|
183
|
-
result = None
|
184
|
-
try:
|
185
|
-
result = future.result(timeout)
|
186
|
-
except concurrent.futures.TimeoutError:
|
187
|
-
log.error("Operation '%s' timed out", coro)
|
188
|
-
future.cancel()
|
189
|
-
finally:
|
190
|
-
if callable(callback):
|
191
|
-
callback(result)
|
192
|
-
return result
|
193
|
-
else:
|
194
|
-
if callable(callback):
|
195
|
-
future.add_done_callback(lambda f: callback(f.result()))
|
196
|
-
return None
|
197
|
-
return None
|
198
|
-
|
199
137
|
def _set_living_status(self, alive: bool) -> None:
|
200
138
|
"""Set the life status of the kernel."""
|
201
139
|
if not alive:
|
@@ -214,7 +152,7 @@ class Kernel:
|
|
214
152
|
await asyncio.sleep(1)
|
215
153
|
# Check kernel is alive - use client rather than manager if we have one
|
216
154
|
# as we could be connected to a kernel not started by the manager
|
217
|
-
if self.kc:
|
155
|
+
if self.kc and self.status != "starting":
|
218
156
|
alive = await self.kc._async_is_alive()
|
219
157
|
self._set_living_status(alive)
|
220
158
|
# Stop the timer if the kernel is dead
|
@@ -223,35 +161,6 @@ class Kernel:
|
|
223
161
|
else:
|
224
162
|
break
|
225
163
|
|
226
|
-
@property
|
227
|
-
def status(self) -> str:
|
228
|
-
"""Retrieve the current kernel status.
|
229
|
-
|
230
|
-
Returns:
|
231
|
-
The kernel status
|
232
|
-
|
233
|
-
"""
|
234
|
-
return self._status
|
235
|
-
|
236
|
-
@status.setter
|
237
|
-
def status(self, value: str) -> None:
|
238
|
-
"""Set the kernel status."""
|
239
|
-
self.status_change_event.set()
|
240
|
-
self._status = value
|
241
|
-
self.status_change_event.clear()
|
242
|
-
|
243
|
-
def wait_for_status(self, status: str = "idle") -> None:
|
244
|
-
"""Block until the kernel reaches a given status value."""
|
245
|
-
if self.status != status:
|
246
|
-
|
247
|
-
async def _wait() -> None:
|
248
|
-
while self.status != status:
|
249
|
-
await asyncio.wait_for(
|
250
|
-
self.status_change_event.wait(), timeout=None
|
251
|
-
)
|
252
|
-
|
253
|
-
self._aodo(_wait(), wait=True)
|
254
|
-
|
255
164
|
@property
|
256
165
|
def missing(self) -> bool:
|
257
166
|
"""Return True if the requested kernel is not found."""
|
@@ -272,12 +181,7 @@ class Kernel:
|
|
272
181
|
else:
|
273
182
|
return None
|
274
183
|
|
275
|
-
|
276
|
-
def specs(self) -> dict[str, dict]:
|
277
|
-
"""Return a list of available kernelspecs."""
|
278
|
-
return self.km.kernel_spec_manager.get_all_specs()
|
279
|
-
|
280
|
-
async def stop_(self, cb: Callable[[], Any] | None = None) -> None:
|
184
|
+
async def stop_async(self, cb: Callable[[], Any] | None = None) -> None:
|
281
185
|
"""Stop the kernel asynchronously."""
|
282
186
|
for task in self.poll_tasks:
|
283
187
|
task.cancel()
|
@@ -287,7 +191,7 @@ class Kernel:
|
|
287
191
|
await self.km.shutdown_kernel()
|
288
192
|
log.debug("Kernel %s shutdown", self.id)
|
289
193
|
|
290
|
-
async def
|
194
|
+
async def start_async(self) -> None:
|
291
195
|
"""Start the kernel asynchronously and set its status."""
|
292
196
|
from jupyter_core.paths import jupyter_runtime_dir
|
293
197
|
|
@@ -340,9 +244,19 @@ class Kernel:
|
|
340
244
|
self.kc = self.km.client()
|
341
245
|
break
|
342
246
|
|
343
|
-
await self.
|
247
|
+
await self.post_start()
|
344
248
|
|
345
|
-
|
249
|
+
@property
|
250
|
+
def spec(self) -> dict[str, str]:
|
251
|
+
"""The kernelspec metadata for the current kernel instance."""
|
252
|
+
assert self.km.kernel_spec is not None
|
253
|
+
return {
|
254
|
+
"name": self.km.kernel_name,
|
255
|
+
"display_name": self.km.kernel_spec.display_name,
|
256
|
+
"language": self.km.kernel_spec.language,
|
257
|
+
}
|
258
|
+
|
259
|
+
async def post_start(self) -> None:
|
346
260
|
"""Wait for the kernel to become ready."""
|
347
261
|
from jupyter_client.kernelspec import NoSuchKernel
|
348
262
|
|
@@ -353,34 +267,30 @@ class Kernel:
|
|
353
267
|
self.status = "error"
|
354
268
|
log.error("Selected kernel '%s' not registered", self.km.kernel_name)
|
355
269
|
else:
|
356
|
-
if ks is not None:
|
357
|
-
|
358
|
-
|
359
|
-
|
360
|
-
|
361
|
-
|
362
|
-
|
363
|
-
|
364
|
-
|
365
|
-
|
366
|
-
|
367
|
-
|
368
|
-
|
369
|
-
self.
|
370
|
-
self.
|
371
|
-
|
372
|
-
|
373
|
-
|
374
|
-
|
375
|
-
|
376
|
-
|
377
|
-
|
378
|
-
|
379
|
-
|
380
|
-
self.dead = False
|
381
|
-
|
382
|
-
# Set username so we can identify our own messages
|
383
|
-
self.kc.session.username = self._CLIENT_ID
|
270
|
+
if ks is not None and self.kc is not None:
|
271
|
+
log.debug("Waiting for kernel to become ready")
|
272
|
+
try:
|
273
|
+
await self.kc._async_wait_for_ready(timeout=30)
|
274
|
+
except RuntimeError as e:
|
275
|
+
log.error("Error connecting to kernel")
|
276
|
+
self.error = e
|
277
|
+
self.status = "error"
|
278
|
+
else:
|
279
|
+
log.debug("Kernel %s ready", self.id)
|
280
|
+
self.status = "idle"
|
281
|
+
self.error = None
|
282
|
+
self.poll_tasks = [
|
283
|
+
asyncio.create_task(self.poll("shell")),
|
284
|
+
asyncio.create_task(self.poll("iopub")),
|
285
|
+
asyncio.create_task(self.poll("stdin")),
|
286
|
+
]
|
287
|
+
self.dead = False
|
288
|
+
|
289
|
+
# Set username so we can identify our own messages
|
290
|
+
self.kc.session.username = self._client_id
|
291
|
+
|
292
|
+
# Send empty execution request to get current execution count
|
293
|
+
self.kc.execute("", store_history=False, silent=True, allow_stdin=False)
|
384
294
|
|
385
295
|
# Start monitoring the kernel status
|
386
296
|
if self.monitor_task is not None:
|
@@ -411,14 +321,7 @@ class Kernel:
|
|
411
321
|
log.debug("Imported `ipykernel` to prevent import deadlock")
|
412
322
|
except ImportError:
|
413
323
|
pass
|
414
|
-
|
415
|
-
# Start the kernel
|
416
|
-
self._aodo(
|
417
|
-
self.start_(),
|
418
|
-
timeout=timeout,
|
419
|
-
wait=wait,
|
420
|
-
callback=cb,
|
421
|
-
)
|
324
|
+
super().start(cb, wait, timeout)
|
422
325
|
|
423
326
|
async def poll(self, channel: str) -> None:
|
424
327
|
"""Poll for messages on a channel, and signal when they arrive.
|
@@ -434,7 +337,7 @@ class Kernel:
|
|
434
337
|
rsp = await msg_getter_coro()
|
435
338
|
# Run msg type handler
|
436
339
|
msg_type = rsp.get("header", {}).get("msg_type")
|
437
|
-
own = rsp.get("parent_header", {}).get("username") == self.
|
340
|
+
own = rsp.get("parent_header", {}).get("username") == self._client_id
|
438
341
|
if callable(handler := getattr(self, f"on_{channel}_{msg_type}", None)):
|
439
342
|
handler(rsp, own)
|
440
343
|
else:
|
@@ -490,7 +393,9 @@ class Kernel:
|
|
490
393
|
rsp["header"]["date"].isoformat(),
|
491
394
|
)
|
492
395
|
|
493
|
-
if (
|
396
|
+
if (
|
397
|
+
(execution_count := content.get("execution_count")) is not None
|
398
|
+
) and callable(
|
494
399
|
set_execution_count := self.msg_id_callbacks[msg_id]["set_execution_count"]
|
495
400
|
):
|
496
401
|
set_execution_count(execution_count)
|
@@ -573,6 +478,14 @@ class Kernel:
|
|
573
478
|
):
|
574
479
|
completeness_status(rsp.get("content", {}))
|
575
480
|
|
481
|
+
def on_iopub_shutdown_reply(self, rsp: dict[str, Any], own: bool) -> None:
|
482
|
+
"""Handle iopub shutdown reply messages."""
|
483
|
+
if not rsp.get("content", {}).get("restart"):
|
484
|
+
# Stop monitoring the kernel
|
485
|
+
if self.monitor_task is not None:
|
486
|
+
self.monitor_task.cancel()
|
487
|
+
self._set_living_status(False)
|
488
|
+
|
576
489
|
def on_iopub_status(self, rsp: dict[str, Any], own: bool) -> None:
|
577
490
|
"""Call callbacks for an iopub status response."""
|
578
491
|
msg_id = rsp.get("parent_header", {}).get("msg_id")
|
@@ -741,22 +654,10 @@ class Kernel:
|
|
741
654
|
log.debug("Cannot run cell because kernel has not started")
|
742
655
|
# TODO - queue cells for execution
|
743
656
|
else:
|
744
|
-
|
745
|
-
self.run_(source, **callbacks),
|
746
|
-
wait=wait,
|
747
|
-
callback=callback,
|
748
|
-
)
|
657
|
+
super().run(source, wait, callback, **callbacks)
|
749
658
|
|
750
|
-
async def
|
751
|
-
self,
|
752
|
-
source: str,
|
753
|
-
get_input: Callable[[str, bool], None] | None = None,
|
754
|
-
set_execution_count: Callable[[int], None] | None = None,
|
755
|
-
add_output: Callable[[dict[str, Any], bool], None] | None = None,
|
756
|
-
clear_output: Callable[[bool], None] | None = None,
|
757
|
-
done: Callable[[dict[str, Any]], None] | None = None,
|
758
|
-
set_metadata: Callable[[tuple[str, ...], Any], None] | None = None,
|
759
|
-
set_status: Callable[[str], None] | None = None,
|
659
|
+
async def run_async(
|
660
|
+
self, source: str, **local_callbacks: Unpack[MsgCallbacks]
|
760
661
|
) -> None:
|
761
662
|
"""Run the code cell and and set the response callbacks, optionally waiting."""
|
762
663
|
if self.kc is None:
|
@@ -768,25 +669,23 @@ class Kernel:
|
|
768
669
|
allow_stdin=self.allow_stdin,
|
769
670
|
)
|
770
671
|
|
771
|
-
|
772
|
-
|
773
|
-
|
774
|
-
|
775
|
-
|
776
|
-
|
777
|
-
|
672
|
+
if done := local_callbacks.get("done"):
|
673
|
+
|
674
|
+
def wrapped_done(content: dict[str, Any]) -> None:
|
675
|
+
"""Set the event after the ``done`` callback has completed."""
|
676
|
+
# Run the original callback
|
677
|
+
if callable(done):
|
678
|
+
done(content)
|
679
|
+
# Set the event
|
680
|
+
event.set()
|
681
|
+
|
682
|
+
local_callbacks["done"] = wrapped_done
|
778
683
|
|
779
|
-
callbacks = {
|
780
|
-
"get_input": get_input,
|
781
|
-
"set_execution_count": set_execution_count,
|
782
|
-
"add_output": add_output,
|
783
|
-
"clear_output": clear_output,
|
784
|
-
"set_metadata": set_metadata,
|
785
|
-
"set_status": set_status,
|
786
|
-
"done": wrapped_done,
|
787
|
-
}
|
788
684
|
self.msg_id_callbacks[msg_id].update(
|
789
|
-
|
685
|
+
cast(
|
686
|
+
"MsgCallbacks",
|
687
|
+
{k: v for k, v in local_callbacks.items() if v is not None},
|
688
|
+
)
|
790
689
|
)
|
791
690
|
# Wait for "done" callback to be called
|
792
691
|
try:
|
@@ -827,8 +726,8 @@ class Kernel:
|
|
827
726
|
if self.kc is not None:
|
828
727
|
self.kc.comm_info(target_name=target_name)
|
829
728
|
|
830
|
-
async def
|
831
|
-
self,
|
729
|
+
async def complete_async(
|
730
|
+
self, source: str, cursor_pos: int, timeout: int = 60
|
832
731
|
) -> list[dict]:
|
833
732
|
"""Request code completions from the kernel, asynchronously."""
|
834
733
|
results: list[dict] = []
|
@@ -866,7 +765,7 @@ class Kernel:
|
|
866
765
|
)
|
867
766
|
event.set()
|
868
767
|
|
869
|
-
msg_id = self.kc.complete(
|
768
|
+
msg_id = self.kc.complete(source, cursor_pos)
|
870
769
|
self.msg_id_callbacks[msg_id].update({"done": process_complete_reply})
|
871
770
|
|
872
771
|
try:
|
@@ -876,32 +775,12 @@ class Kernel:
|
|
876
775
|
|
877
776
|
return results
|
878
777
|
|
879
|
-
def
|
880
|
-
"""Request code completions from the kernel.
|
881
|
-
|
882
|
-
Args:
|
883
|
-
code: The code string to retrieve completions for
|
884
|
-
cursor_pos: The position of the cursor in the code string
|
885
|
-
|
886
|
-
Returns:
|
887
|
-
A list of dictionaries defining completion entries. The dictionaries
|
888
|
-
contain ``text`` (the completion text), ``start_position`` (the stating
|
889
|
-
position of the completion text), and optionally ``display_meta``
|
890
|
-
(a string containing additional data about the completion type)
|
891
|
-
|
892
|
-
"""
|
893
|
-
return self._aodo(
|
894
|
-
self.complete_(code, cursor_pos),
|
895
|
-
wait=True,
|
896
|
-
single=True,
|
897
|
-
)
|
898
|
-
|
899
|
-
async def history_(
|
778
|
+
async def history_async(
|
900
779
|
self,
|
901
780
|
pattern: str = "",
|
902
781
|
n: int = 1,
|
903
782
|
hist_access_type: str = "search",
|
904
|
-
timeout: int =
|
783
|
+
timeout: int = 60,
|
905
784
|
) -> list[tuple[int, int, str]] | None:
|
906
785
|
"""Retrieve history from the kernel asynchronously."""
|
907
786
|
await asyncio.sleep(0.1) # Add a tiny timeout so we don't spam the kernel
|
@@ -933,29 +812,9 @@ class Kernel:
|
|
933
812
|
|
934
813
|
return results
|
935
814
|
|
936
|
-
def
|
937
|
-
self, pattern: str = "", n: int = 1, hist_access_type: str = "search"
|
938
|
-
) -> list[tuple[int, int, str]] | None:
|
939
|
-
"""Retrieve history from the kernel.
|
940
|
-
|
941
|
-
Args:
|
942
|
-
pattern: The pattern to search for
|
943
|
-
n: the number of history items to return
|
944
|
-
hist_access_type: How to access the history ('range', 'tail' or 'search')
|
945
|
-
|
946
|
-
Returns:
|
947
|
-
A list of history items, consisting of tuples (session, line_number, input)
|
948
|
-
|
949
|
-
"""
|
950
|
-
return self._aodo(
|
951
|
-
self.history_(pattern, n, hist_access_type),
|
952
|
-
wait=True,
|
953
|
-
single=True,
|
954
|
-
)
|
955
|
-
|
956
|
-
async def inspect_(
|
815
|
+
async def inspect_async(
|
957
816
|
self,
|
958
|
-
|
817
|
+
source: str,
|
959
818
|
cursor_pos: int,
|
960
819
|
detail_level: int = 0,
|
961
820
|
timeout: int = 2,
|
@@ -978,7 +837,9 @@ class Kernel:
|
|
978
837
|
event.set()
|
979
838
|
|
980
839
|
log.debug("Requesting contextual help from the kernel")
|
981
|
-
msg_id = self.kc.inspect(
|
840
|
+
msg_id = self.kc.inspect(
|
841
|
+
source, cursor_pos=cursor_pos, detail_level=detail_level
|
842
|
+
)
|
982
843
|
self.msg_id_callbacks[msg_id].update({"done": process_inspect_reply})
|
983
844
|
|
984
845
|
try:
|
@@ -988,35 +849,9 @@ class Kernel:
|
|
988
849
|
|
989
850
|
return result
|
990
851
|
|
991
|
-
def
|
852
|
+
async def is_complete_async(
|
992
853
|
self,
|
993
|
-
|
994
|
-
cursor_pos: int,
|
995
|
-
callback: Callable[[dict[str, Any]], None] | None = None,
|
996
|
-
) -> str:
|
997
|
-
"""Request code inspection from the kernel.
|
998
|
-
|
999
|
-
Args:
|
1000
|
-
code: The code string to retrieve completions for
|
1001
|
-
cursor_pos: The position of the cursor in the code string
|
1002
|
-
callback: A function to run when the inspection result arrives. The result
|
1003
|
-
is passed as an argument.
|
1004
|
-
|
1005
|
-
Returns:
|
1006
|
-
A string containing useful information about the code at the current cursor
|
1007
|
-
position
|
1008
|
-
|
1009
|
-
"""
|
1010
|
-
return self._aodo(
|
1011
|
-
self.inspect_(code, cursor_pos),
|
1012
|
-
wait=False,
|
1013
|
-
callback=callback,
|
1014
|
-
single=True,
|
1015
|
-
)
|
1016
|
-
|
1017
|
-
async def is_complete_(
|
1018
|
-
self,
|
1019
|
-
code: str,
|
854
|
+
source: str,
|
1020
855
|
timeout: int | float = 0.1,
|
1021
856
|
) -> dict[str, Any]:
|
1022
857
|
"""Ask the kernel to determine if code is complete asynchronously."""
|
@@ -1032,7 +867,7 @@ class Kernel:
|
|
1032
867
|
result.update(content)
|
1033
868
|
event.set()
|
1034
869
|
|
1035
|
-
msg_id = self.kc.is_complete(
|
870
|
+
msg_id = self.kc.is_complete(source)
|
1036
871
|
self.msg_id_callbacks[msg_id].update(
|
1037
872
|
{"completeness_status": process_is_complete_reply}
|
1038
873
|
)
|
@@ -1044,31 +879,10 @@ class Kernel:
|
|
1044
879
|
|
1045
880
|
return result
|
1046
881
|
|
1047
|
-
def
|
1048
|
-
|
1049
|
-
|
1050
|
-
|
1051
|
-
wait: bool = False,
|
1052
|
-
callback: Callable[[dict[str, Any]], None] | None = None,
|
1053
|
-
) -> dict[str, Any]:
|
1054
|
-
"""Request code completeness status from the kernel.
|
1055
|
-
|
1056
|
-
Args:
|
1057
|
-
code: The code string to check the completeness status of
|
1058
|
-
timeout: How long to wait for a kernel response
|
1059
|
-
wait: Whether to wait for the response
|
1060
|
-
callback: A function to run when the inspection result arrives. The result
|
1061
|
-
is passed as an argument.
|
1062
|
-
|
1063
|
-
Returns:
|
1064
|
-
A string describing the completeness status
|
1065
|
-
|
1066
|
-
"""
|
1067
|
-
return self._aodo(
|
1068
|
-
self.is_complete_(code, timeout),
|
1069
|
-
wait=wait,
|
1070
|
-
callback=callback,
|
1071
|
-
)
|
882
|
+
def input(self, text: str) -> None:
|
883
|
+
"""Send input to the kernel."""
|
884
|
+
if self.kc:
|
885
|
+
self.kc.input(text)
|
1072
886
|
|
1073
887
|
def interrupt(self) -> None:
|
1074
888
|
"""Interrupt the kernel.
|
@@ -1083,63 +897,20 @@ class Kernel:
|
|
1083
897
|
log.debug("Interrupting kernel %s", self.id)
|
1084
898
|
KernelManager.interrupt_kernel(self.km)
|
1085
899
|
|
1086
|
-
async def
|
900
|
+
async def restart_async(self) -> None:
|
1087
901
|
"""Restart the kernel asyncchronously."""
|
1088
|
-
self.dead = True
|
1089
902
|
log.debug("Restarting kernel `%s`", self.id)
|
903
|
+
# Cancel polling tasks
|
904
|
+
for task in self.poll_tasks:
|
905
|
+
task.cancel()
|
1090
906
|
self.error = None
|
1091
907
|
self.status = "starting"
|
1092
|
-
|
908
|
+
try:
|
909
|
+
await self.km.restart_kernel(now=True)
|
910
|
+
await self.post_start()
|
911
|
+
except asyncio.exceptions.InvalidStateError:
|
912
|
+
await self.start_async()
|
1093
913
|
log.debug("Kernel %s restarted", self.id)
|
1094
|
-
await self.post_start_()
|
1095
|
-
|
1096
|
-
def restart(self, wait: bool = False, cb: Callable | None = None) -> None:
|
1097
|
-
"""Restart the current kernel."""
|
1098
|
-
self._aodo(
|
1099
|
-
self.restart_(),
|
1100
|
-
wait=wait,
|
1101
|
-
callback=cb,
|
1102
|
-
)
|
1103
|
-
|
1104
|
-
def change(
|
1105
|
-
self,
|
1106
|
-
name: str | None,
|
1107
|
-
connection_file: Path | None = None,
|
1108
|
-
cb: Callable | None = None,
|
1109
|
-
) -> None:
|
1110
|
-
"""Change the kernel.
|
1111
|
-
|
1112
|
-
Args:
|
1113
|
-
name: The name of the kernel to change to
|
1114
|
-
connection_file: The path to the connection file to use
|
1115
|
-
cb: Callback to run once restarted
|
1116
|
-
|
1117
|
-
"""
|
1118
|
-
from euporie.core.kernel.manager import EuporieKernelManager
|
1119
|
-
|
1120
|
-
self.connection_file = connection_file
|
1121
|
-
self.status = "starting"
|
1122
|
-
|
1123
|
-
# Update the tab's kernel spec
|
1124
|
-
spec = self.specs.get(name or "", {}).get("spec", {})
|
1125
|
-
self.kernel_tab.metadata["kernelspec"] = {
|
1126
|
-
"name": name,
|
1127
|
-
"display_name": spec.get("display_name", ""),
|
1128
|
-
"language": spec.get("language", ""),
|
1129
|
-
}
|
1130
|
-
|
1131
|
-
# Stop the old kernel
|
1132
|
-
if self.km.has_kernel:
|
1133
|
-
self.stop()
|
1134
|
-
|
1135
|
-
# Create a new kernel manager instance
|
1136
|
-
del self.km
|
1137
|
-
kwargs = {} if name is None else {"kernel_name": name}
|
1138
|
-
self.km = EuporieKernelManager(**kwargs)
|
1139
|
-
self.error = None
|
1140
|
-
|
1141
|
-
# Start the kernel
|
1142
|
-
self.start(cb=cb)
|
1143
914
|
|
1144
915
|
def stop(self, cb: Callable | None = None, wait: bool = False) -> None:
|
1145
916
|
"""Stop the current kernel.
|
@@ -1155,17 +926,9 @@ class Kernel:
|
|
1155
926
|
if callable(cb):
|
1156
927
|
cb()
|
1157
928
|
else:
|
1158
|
-
|
1159
|
-
# This helps us leave a little earlier
|
1160
|
-
if not wait:
|
1161
|
-
self.interrupt()
|
1162
|
-
self._aodo(
|
1163
|
-
self.stop_(),
|
1164
|
-
callback=cb,
|
1165
|
-
wait=wait,
|
1166
|
-
)
|
929
|
+
super().stop(cb, wait)
|
1167
930
|
|
1168
|
-
async def
|
931
|
+
async def shutdown_async(self) -> None:
|
1169
932
|
"""Shut down the kernel and close the event loop if running in a thread."""
|
1170
933
|
# Stop monitoring the kernel
|
1171
934
|
if self.monitor_task is not None:
|
@@ -1175,23 +938,3 @@ class Kernel:
|
|
1175
938
|
# Stop kernel
|
1176
939
|
if self.km.has_kernel:
|
1177
940
|
await self.km.shutdown_kernel(now=True)
|
1178
|
-
# Stop event loop
|
1179
|
-
if self.threaded:
|
1180
|
-
self.loop.stop()
|
1181
|
-
|
1182
|
-
def shutdown(self, wait: bool = False) -> None:
|
1183
|
-
"""Shutdown the kernel and close the kernel's thread.
|
1184
|
-
|
1185
|
-
This is intended to be run when the notebook is closed: the
|
1186
|
-
:py:class:`~euporie.core.tabs.notebook.Kernel` cannot be restarted after this.
|
1187
|
-
|
1188
|
-
Args:
|
1189
|
-
wait: Whether to block until shutdown completes
|
1190
|
-
|
1191
|
-
"""
|
1192
|
-
self._aodo(
|
1193
|
-
self.shutdown_(),
|
1194
|
-
wait=wait,
|
1195
|
-
)
|
1196
|
-
if self.threaded:
|
1197
|
-
self.thread.join(timeout=5)
|