euporie 2.8.1__py3-none-any.whl → 2.8.5__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/_commands.py +143 -0
- euporie/console/_settings.py +58 -0
- euporie/console/app.py +25 -71
- euporie/console/tabs/console.py +267 -147
- euporie/core/__init__.py +1 -9
- euporie/core/__main__.py +31 -5
- euporie/core/_settings.py +104 -0
- euporie/core/app/__init__.py +3 -0
- euporie/core/app/_commands.py +70 -0
- euporie/core/app/_settings.py +427 -0
- euporie/core/{app.py → app/app.py} +214 -572
- euporie/core/app/base.py +51 -0
- euporie/core/{current.py → app/current.py} +13 -4
- euporie/core/app/cursor.py +35 -0
- euporie/core/app/dummy.py +12 -0
- euporie/core/app/launch.py +28 -0
- euporie/core/bars/__init__.py +11 -0
- euporie/core/bars/command.py +182 -0
- euporie/core/bars/menu.py +258 -0
- euporie/core/{widgets → bars}/search.py +154 -57
- euporie/core/{widgets → bars}/status.py +9 -26
- euporie/core/clipboard.py +19 -80
- euporie/core/comm/base.py +8 -6
- euporie/core/comm/ipywidgets.py +21 -12
- euporie/core/comm/registry.py +2 -1
- euporie/core/commands.py +11 -5
- euporie/core/completion.py +3 -2
- euporie/core/config.py +368 -341
- euporie/core/convert/__init__.py +0 -30
- euporie/core/convert/datum.py +131 -60
- euporie/core/convert/formats/__init__.py +31 -0
- euporie/core/convert/formats/ansi.py +46 -30
- euporie/core/convert/formats/common.py +11 -23
- euporie/core/convert/formats/html.py +45 -40
- euporie/core/convert/formats/pil.py +1 -1
- euporie/core/convert/formats/png.py +3 -5
- euporie/core/convert/formats/sixel.py +3 -3
- euporie/core/convert/registry.py +11 -8
- euporie/core/convert/utils.py +50 -23
- euporie/core/diagnostics.py +2 -2
- euporie/core/filters.py +72 -82
- euporie/core/format.py +13 -2
- euporie/core/ft/ansi.py +1 -1
- euporie/core/ft/html.py +36 -36
- euporie/core/ft/table.py +1 -3
- euporie/core/ft/utils.py +4 -1
- euporie/core/graphics.py +216 -124
- euporie/core/history.py +2 -2
- euporie/core/inspection.py +3 -2
- euporie/core/io.py +207 -28
- euporie/core/kernel/__init__.py +1 -0
- euporie/core/{kernel.py → kernel/client.py} +100 -139
- euporie/core/kernel/manager.py +114 -0
- euporie/core/key_binding/bindings/__init__.py +2 -8
- euporie/core/key_binding/bindings/basic.py +47 -7
- euporie/core/key_binding/bindings/completion.py +3 -8
- euporie/core/key_binding/bindings/micro.py +5 -7
- euporie/core/key_binding/bindings/mouse.py +26 -24
- euporie/core/key_binding/bindings/terminal.py +193 -0
- euporie/core/key_binding/bindings/vi.py +46 -0
- euporie/core/key_binding/key_processor.py +43 -2
- euporie/core/key_binding/registry.py +2 -0
- euporie/core/key_binding/utils.py +22 -2
- euporie/core/keys.py +7156 -93
- euporie/core/layout/cache.py +35 -25
- euporie/core/layout/containers.py +280 -74
- euporie/core/layout/decor.py +5 -5
- euporie/core/layout/mouse.py +1 -1
- euporie/core/layout/print.py +16 -3
- euporie/core/layout/scroll.py +26 -28
- euporie/core/log.py +75 -60
- euporie/core/lsp.py +118 -24
- euporie/core/margins.py +60 -31
- euporie/core/path.py +2 -1
- euporie/core/renderer.py +58 -17
- euporie/core/style.py +60 -40
- euporie/core/suggest.py +103 -85
- euporie/core/tabs/__init__.py +34 -0
- euporie/core/tabs/_settings.py +113 -0
- euporie/core/tabs/base.py +11 -435
- euporie/core/tabs/kernel.py +420 -0
- euporie/core/tabs/notebook.py +20 -54
- euporie/core/utils.py +98 -6
- euporie/core/validation.py +1 -1
- euporie/core/widgets/_settings.py +188 -0
- euporie/core/widgets/cell.py +90 -158
- euporie/core/widgets/cell_outputs.py +25 -36
- euporie/core/widgets/decor.py +11 -41
- euporie/core/widgets/dialog.py +55 -44
- euporie/core/widgets/display.py +27 -24
- euporie/core/widgets/file_browser.py +5 -26
- euporie/core/widgets/forms.py +16 -12
- euporie/core/widgets/inputs.py +37 -81
- euporie/core/widgets/layout.py +7 -6
- euporie/core/widgets/logo.py +49 -0
- euporie/core/widgets/menu.py +13 -11
- euporie/core/widgets/pager.py +8 -11
- euporie/core/widgets/palette.py +6 -6
- euporie/hub/app.py +52 -31
- euporie/notebook/_commands.py +24 -0
- euporie/notebook/_settings.py +107 -0
- euporie/notebook/app.py +109 -210
- euporie/notebook/filters.py +1 -1
- euporie/notebook/tabs/__init__.py +46 -7
- euporie/notebook/tabs/_commands.py +714 -0
- euporie/notebook/tabs/_settings.py +32 -0
- euporie/notebook/tabs/display.py +2 -2
- euporie/notebook/tabs/edit.py +12 -7
- euporie/notebook/tabs/json.py +3 -3
- euporie/notebook/tabs/log.py +1 -18
- euporie/notebook/tabs/notebook.py +21 -674
- euporie/notebook/widgets/_commands.py +11 -0
- euporie/notebook/widgets/_settings.py +19 -0
- euporie/notebook/widgets/side_bar.py +14 -34
- euporie/preview/_settings.py +104 -0
- euporie/preview/app.py +8 -30
- euporie/preview/tabs/notebook.py +15 -86
- euporie/web/tabs/web.py +4 -6
- euporie/web/widgets/webview.py +5 -12
- {euporie-2.8.1.dist-info → euporie-2.8.5.dist-info}/METADATA +11 -15
- euporie-2.8.5.dist-info/RECORD +172 -0
- {euporie-2.8.1.dist-info → euporie-2.8.5.dist-info}/WHEEL +1 -1
- {euporie-2.8.1.dist-info → euporie-2.8.5.dist-info}/entry_points.txt +2 -2
- {euporie-2.8.1.dist-info → euporie-2.8.5.dist-info}/licenses/LICENSE +1 -1
- euporie/core/launch.py +0 -59
- euporie/core/terminal.py +0 -527
- euporie-2.8.1.dist-info/RECORD +0 -146
- {euporie-2.8.1.data → euporie-2.8.5.data}/data/share/applications/euporie-console.desktop +0 -0
- {euporie-2.8.1.data → euporie-2.8.5.data}/data/share/applications/euporie-notebook.desktop +0 -0
@@ -6,131 +6,34 @@ import asyncio
|
|
6
6
|
import concurrent.futures
|
7
7
|
import logging
|
8
8
|
import os
|
9
|
-
import re
|
10
|
-
import sys
|
11
9
|
import threading
|
12
10
|
from collections import defaultdict
|
13
11
|
from subprocess import PIPE, STDOUT # S404 - Security implications considered
|
14
12
|
from typing import TYPE_CHECKING, TypedDict
|
15
13
|
from uuid import uuid4
|
16
14
|
|
17
|
-
import nbformat
|
18
|
-
from _frozen_importlib import _DeadlockError
|
19
|
-
from jupyter_client import AsyncKernelManager, KernelManager
|
20
|
-
from jupyter_client.kernelspec import NATIVE_KERNEL_NAME, NoSuchKernel
|
21
|
-
from jupyter_client.provisioning import KernelProvisionerFactory as KPF
|
22
|
-
from jupyter_client.provisioning.local_provisioner import LocalProvisioner
|
23
|
-
from jupyter_core.paths import jupyter_path, jupyter_runtime_dir
|
24
15
|
from upath import UPath
|
25
16
|
|
26
17
|
if TYPE_CHECKING:
|
18
|
+
from collections.abc import Coroutine
|
27
19
|
from pathlib import Path
|
28
|
-
from typing import Any, Callable
|
20
|
+
from typing import Any, Callable
|
29
21
|
|
30
22
|
from jupyter_client import KernelClient
|
31
|
-
from jupyter_client.connect import KernelConnectionInfo
|
32
23
|
|
33
|
-
from euporie.core.
|
24
|
+
from euporie.core.tabs.kernel import KernelTab
|
34
25
|
|
35
26
|
|
36
27
|
log = logging.getLogger(__name__)
|
37
28
|
|
38
29
|
|
39
|
-
class LoggingLocalProvisioner(LocalProvisioner): # type:ignore[misc]
|
40
|
-
"""A Jupyter kernel provisionser which logs kernel output."""
|
41
|
-
|
42
|
-
async def launch_kernel(
|
43
|
-
self, cmd: list[str], **kwargs: Any
|
44
|
-
) -> KernelConnectionInfo:
|
45
|
-
"""Launch a kernel with a command."""
|
46
|
-
await super().launch_kernel(cmd, **kwargs)
|
47
|
-
|
48
|
-
def log_kernel_output(pipe: TextIO, log_func: Callable) -> None:
|
49
|
-
try:
|
50
|
-
with pipe:
|
51
|
-
for line in iter(pipe.readline, ""):
|
52
|
-
log_func(line.rstrip())
|
53
|
-
except StopIteration:
|
54
|
-
pass
|
55
|
-
|
56
|
-
if self.process is not None:
|
57
|
-
# Start thread to listen for kernel output
|
58
|
-
threading.Thread(
|
59
|
-
target=log_kernel_output,
|
60
|
-
args=(self.process.stdout, log.warning),
|
61
|
-
daemon=True,
|
62
|
-
).start()
|
63
|
-
|
64
|
-
return self.connection_info
|
65
|
-
|
66
|
-
|
67
|
-
KPF.instance().default_provisioner_name = "logging-local-provisioner"
|
68
|
-
|
69
|
-
|
70
|
-
class EuporieKernelManager(AsyncKernelManager):
|
71
|
-
"""Kernel Manager subclass.
|
72
|
-
|
73
|
-
``jupyter_client`` replaces a plain ``python`` command with the current executable,
|
74
|
-
but this is not desirable if the client is running in its own prefix (e.g. with
|
75
|
-
``pipx``). We work around this here.
|
76
|
-
|
77
|
-
See https://github.com/jupyter/jupyter_client/issues/949
|
78
|
-
"""
|
79
|
-
|
80
|
-
def format_kernel_cmd(self, extra_arguments: list[str] | None = None) -> list[str]:
|
81
|
-
"""Replace templated args (e.g. {connection_file})."""
|
82
|
-
extra_arguments = extra_arguments or []
|
83
|
-
assert self.kernel_spec is not None
|
84
|
-
cmd = self.kernel_spec.argv + extra_arguments
|
85
|
-
|
86
|
-
v_major, v_minor = sys.version_info[:2]
|
87
|
-
if cmd and cmd[0] in {
|
88
|
-
"python",
|
89
|
-
f"python{v_major}",
|
90
|
-
f"python{v_major}.{v_minor}",
|
91
|
-
}:
|
92
|
-
# If the command is `python` without an absolute path and euporie is
|
93
|
-
# running in the same prefix as the kernel_spec file is located, use
|
94
|
-
# sys.executable: otherwise fall back to the executable in the base prefix
|
95
|
-
if (
|
96
|
-
os.path.commonpath((sys.prefix, self.kernel_spec.resource_dir))
|
97
|
-
== sys.prefix
|
98
|
-
):
|
99
|
-
cmd[0] = sys.executable
|
100
|
-
else:
|
101
|
-
cmd[0] = sys._base_executable # type: ignore [attr-defined]
|
102
|
-
|
103
|
-
# Make sure to use the realpath for the connection_file
|
104
|
-
# On windows, when running with the store python, the connection_file path
|
105
|
-
# is not usable by non python kernels because the path is being rerouted when
|
106
|
-
# inside of a store app.
|
107
|
-
# See this bug here: https://bugs.python.org/issue41196
|
108
|
-
ns = {
|
109
|
-
"connection_file": os.path.realpath(self.connection_file),
|
110
|
-
"prefix": sys.prefix,
|
111
|
-
}
|
112
|
-
|
113
|
-
if self.kernel_spec:
|
114
|
-
ns["resource_dir"] = self.kernel_spec.resource_dir
|
115
|
-
|
116
|
-
if self._launch_args:
|
117
|
-
ns.update({str(k): str(v) for k, v in self._launch_args.items()})
|
118
|
-
|
119
|
-
pat = re.compile(r"\{([A-Za-z0-9_]+)\}")
|
120
|
-
|
121
|
-
def _from_ns(match: re.Match) -> str:
|
122
|
-
"""Get the key out of ns if it's there, otherwise no change."""
|
123
|
-
return ns.get(match.group(1), match.group())
|
124
|
-
|
125
|
-
return [pat.sub(_from_ns, arg) for arg in cmd]
|
126
|
-
|
127
|
-
|
128
30
|
class MsgCallbacks(TypedDict, total=False):
|
129
31
|
"""Typed dictionary for named message callbacks."""
|
130
32
|
|
131
33
|
get_input: Callable[[str, bool], None] | None
|
132
34
|
set_execution_count: Callable[[int], None] | None
|
133
|
-
add_output: Callable[[dict[str, Any]], None] | None
|
35
|
+
add_output: Callable[[dict[str, Any], bool], None] | None
|
36
|
+
add_input: Callable[[dict[str, Any], bool], None] | None
|
134
37
|
clear_output: Callable[[bool], None] | None
|
135
38
|
done: Callable[[dict[str, Any]], None] | None
|
136
39
|
set_metadata: Callable[[tuple[str, ...], Any], None] | None
|
@@ -151,6 +54,8 @@ class Kernel:
|
|
151
54
|
Has the ability to run itself in it's own thread.
|
152
55
|
"""
|
153
56
|
|
57
|
+
_CLIENT_ID = f"euporie-{os.getpid()}"
|
58
|
+
|
154
59
|
def __init__(
|
155
60
|
self,
|
156
61
|
kernel_tab: KernelTab,
|
@@ -170,6 +75,15 @@ class Kernel:
|
|
170
75
|
kernel connection information
|
171
76
|
|
172
77
|
"""
|
78
|
+
from jupyter_core.paths import jupyter_path
|
79
|
+
|
80
|
+
from euporie.core.kernel.manager import (
|
81
|
+
EuporieKernelManager,
|
82
|
+
set_default_provisioner,
|
83
|
+
)
|
84
|
+
|
85
|
+
set_default_provisioner()
|
86
|
+
|
173
87
|
self.threaded = threaded
|
174
88
|
if threaded:
|
175
89
|
self.loop = asyncio.new_event_loop()
|
@@ -341,6 +255,8 @@ class Kernel:
|
|
341
255
|
@property
|
342
256
|
def missing(self) -> bool:
|
343
257
|
"""Return True if the requested kernel is not found."""
|
258
|
+
from jupyter_client.kernelspec import NoSuchKernel
|
259
|
+
|
344
260
|
try:
|
345
261
|
self.km.kernel_spec # noqa B018
|
346
262
|
except NoSuchKernel:
|
@@ -373,6 +289,8 @@ class Kernel:
|
|
373
289
|
|
374
290
|
async def start_(self) -> None:
|
375
291
|
"""Start the kernel asynchronously and set its status."""
|
292
|
+
from jupyter_core.paths import jupyter_runtime_dir
|
293
|
+
|
376
294
|
if self.km.kernel_name is None:
|
377
295
|
self.status = "error"
|
378
296
|
log.debug("Starting kernel")
|
@@ -401,16 +319,18 @@ class Kernel:
|
|
401
319
|
# Otherwise, start a new kernel using the kernel manager
|
402
320
|
else:
|
403
321
|
runtime_dir.mkdir(exist_ok=True, parents=True)
|
404
|
-
|
322
|
+
for attempt in range(1, 4):
|
405
323
|
try:
|
406
324
|
# TODO - send stdout to log
|
407
325
|
await self.km.start_kernel(stdout=PIPE, stderr=STDOUT, text=True)
|
408
|
-
except _DeadlockError:
|
409
|
-
# Keep trying if we get an import deadlock
|
410
|
-
await asyncio.sleep(0.1)
|
411
|
-
continue
|
412
326
|
except Exception as e:
|
413
|
-
log.error(
|
327
|
+
log.error(
|
328
|
+
"Kernel '%s' could not start on attempt %s",
|
329
|
+
self.km.kernel_name,
|
330
|
+
attempt,
|
331
|
+
)
|
332
|
+
if attempt > 2:
|
333
|
+
continue
|
414
334
|
self.status = "error"
|
415
335
|
self.error = e
|
416
336
|
else:
|
@@ -418,12 +338,14 @@ class Kernel:
|
|
418
338
|
# Create a client for the newly started kernel
|
419
339
|
if self.km.has_kernel:
|
420
340
|
self.kc = self.km.client()
|
421
|
-
|
341
|
+
break
|
422
342
|
|
423
343
|
await self.post_start_()
|
424
344
|
|
425
345
|
async def post_start_(self) -> None:
|
426
346
|
"""Wait for the kernel to become ready."""
|
347
|
+
from jupyter_client.kernelspec import NoSuchKernel
|
348
|
+
|
427
349
|
try:
|
428
350
|
ks = self.km.kernel_spec
|
429
351
|
except NoSuchKernel as e:
|
@@ -457,6 +379,9 @@ class Kernel:
|
|
457
379
|
]
|
458
380
|
self.dead = False
|
459
381
|
|
382
|
+
# Set username so we can identify our own messages
|
383
|
+
self.kc.session.username = self._CLIENT_ID
|
384
|
+
|
460
385
|
# Start monitoring the kernel status
|
461
386
|
if self.monitor_task is not None:
|
462
387
|
self.monitor_task.cancel()
|
@@ -473,6 +398,8 @@ class Kernel:
|
|
473
398
|
timeout: How long to wait until failure is assumed
|
474
399
|
|
475
400
|
"""
|
401
|
+
from jupyter_client.kernelspec import NATIVE_KERNEL_NAME
|
402
|
+
|
476
403
|
# Attempt to import ipykernel if it is installed
|
477
404
|
# ipykernel is imported by jupyter_client, but since starting the kernel runs
|
478
405
|
# in another thread, we do the import here first to prevent import deadlocks,
|
@@ -507,22 +434,24 @@ class Kernel:
|
|
507
434
|
rsp = await msg_getter_coro()
|
508
435
|
# Run msg type handler
|
509
436
|
msg_type = rsp.get("header", {}).get("msg_type")
|
437
|
+
own = rsp.get("parent_header", {}).get("username") == self._CLIENT_ID
|
510
438
|
if callable(handler := getattr(self, f"on_{channel}_{msg_type}", None)):
|
511
|
-
handler(rsp)
|
439
|
+
handler(rsp, own)
|
512
440
|
else:
|
513
|
-
self.on_unhandled(channel, rsp)
|
441
|
+
self.on_unhandled(channel, rsp, own)
|
514
442
|
|
515
|
-
def on_unhandled(self, channel: str, rsp: dict[str, Any]) -> None:
|
443
|
+
def on_unhandled(self, channel: str, rsp: dict[str, Any], own: bool) -> None:
|
516
444
|
"""Report unhandled messages to the debug log."""
|
517
445
|
log.debug(
|
518
|
-
"Unhandled %s message:\nparent_id = '%s'\ntype = '%s'\ncontent='%s'",
|
446
|
+
"Unhandled %s message:\nparent_id = '%s'\ntype = '%s'\ncontent='%s'\nown: %s",
|
519
447
|
channel,
|
520
448
|
rsp.get("parent_header", {}).get("msg_id"),
|
521
449
|
rsp["header"]["msg_type"],
|
522
450
|
rsp.get("content"),
|
451
|
+
own,
|
523
452
|
)
|
524
453
|
|
525
|
-
def on_stdin_input_request(self, rsp: dict[str, Any]) -> None:
|
454
|
+
def on_stdin_input_request(self, rsp: dict[str, Any], own: bool) -> None:
|
526
455
|
"""Call ``get_input`` callback for a stdin input request message."""
|
527
456
|
msg_id = rsp.get("parent_header", {}).get("msg_id")
|
528
457
|
content = rsp.get("content", {})
|
@@ -532,7 +461,7 @@ class Kernel:
|
|
532
461
|
content.get("password", False),
|
533
462
|
)
|
534
463
|
|
535
|
-
def on_shell_status(self, rsp: dict[str, Any]) -> None:
|
464
|
+
def on_shell_status(self, rsp: dict[str, Any], own: bool) -> None:
|
536
465
|
"""Call ``set_execution_count`` callback for a shell status response."""
|
537
466
|
msg_id = rsp.get("parent_header", {}).get("msg_id")
|
538
467
|
content = rsp.get("content", {})
|
@@ -548,7 +477,7 @@ class Kernel:
|
|
548
477
|
):
|
549
478
|
set_execution_count(execution_count)
|
550
479
|
|
551
|
-
def on_shell_execute_reply(self, rsp: dict[str, Any]) -> None:
|
480
|
+
def on_shell_execute_reply(self, rsp: dict[str, Any], own: bool) -> None:
|
552
481
|
"""Call callbacks for a shell execute reply response."""
|
553
482
|
msg_id = rsp.get("parent_header", {}).get("msg_id")
|
554
483
|
content = rsp.get("content", {})
|
@@ -575,11 +504,14 @@ class Kernel:
|
|
575
504
|
if callable(
|
576
505
|
add_output := self.msg_id_callbacks[msg_id]["add_output"]
|
577
506
|
) and (data := payload.get("data", {})):
|
507
|
+
import nbformat
|
508
|
+
|
578
509
|
add_output(
|
579
510
|
nbformat.v4.new_output(
|
580
511
|
"execute_result",
|
581
512
|
data=data,
|
582
|
-
)
|
513
|
+
),
|
514
|
+
own,
|
583
515
|
)
|
584
516
|
elif source == "set_next_input":
|
585
517
|
if callable(
|
@@ -605,7 +537,7 @@ class Kernel:
|
|
605
537
|
):
|
606
538
|
done(content)
|
607
539
|
|
608
|
-
def on_shell_kernel_info_reply(self, rsp: dict[str, Any]) -> None:
|
540
|
+
def on_shell_kernel_info_reply(self, rsp: dict[str, Any], own: bool) -> None:
|
609
541
|
"""Call callbacks for a shell kernel info response."""
|
610
542
|
msg_id = rsp.get("parent_header", {}).get("msg_id")
|
611
543
|
if callable(
|
@@ -613,25 +545,25 @@ class Kernel:
|
|
613
545
|
):
|
614
546
|
set_kernel_info(rsp.get("content", {}))
|
615
547
|
|
616
|
-
def on_shell_complete_reply(self, rsp: dict[str, Any]) -> None:
|
548
|
+
def on_shell_complete_reply(self, rsp: dict[str, Any], own: bool) -> None:
|
617
549
|
"""Call callbacks for a shell completion reply response."""
|
618
550
|
msg_id = rsp.get("parent_header", {}).get("msg_id")
|
619
551
|
if callable(done := self.msg_id_callbacks[msg_id].get("done")):
|
620
552
|
done(rsp.get("content", {}))
|
621
553
|
|
622
|
-
def on_shell_history_reply(self, rsp: dict[str, Any]) -> None:
|
554
|
+
def on_shell_history_reply(self, rsp: dict[str, Any], own: bool) -> None:
|
623
555
|
"""Call callbacks for a shell history reply response."""
|
624
556
|
msg_id = rsp.get("parent_header", {}).get("msg_id")
|
625
557
|
if callable(done := self.msg_id_callbacks[msg_id].get("done")):
|
626
558
|
done(rsp.get("content", {}))
|
627
559
|
|
628
|
-
def on_shell_inspect_reply(self, rsp: dict[str, Any]) -> None:
|
560
|
+
def on_shell_inspect_reply(self, rsp: dict[str, Any], own: bool) -> None:
|
629
561
|
"""Call callbacks for a shell inspection reply response."""
|
630
562
|
msg_id = rsp.get("parent_header", {}).get("msg_id")
|
631
563
|
if callable(done := self.msg_id_callbacks[msg_id].get("done")):
|
632
564
|
done(rsp.get("content", {}))
|
633
565
|
|
634
|
-
def on_shell_is_complete_reply(self, rsp: dict[str, Any]) -> None:
|
566
|
+
def on_shell_is_complete_reply(self, rsp: dict[str, Any], own: bool) -> None:
|
635
567
|
"""Call callbacks for a shell completeness reply response."""
|
636
568
|
msg_id = rsp.get("parent_header", {}).get("msg_id")
|
637
569
|
if callable(
|
@@ -641,7 +573,7 @@ class Kernel:
|
|
641
573
|
):
|
642
574
|
completeness_status(rsp.get("content", {}))
|
643
575
|
|
644
|
-
def on_iopub_status(self, rsp: dict[str, Any]) -> None:
|
576
|
+
def on_iopub_status(self, rsp: dict[str, Any], own: bool) -> None:
|
645
577
|
"""Call callbacks for an iopub status response."""
|
646
578
|
msg_id = rsp.get("parent_header", {}).get("msg_id")
|
647
579
|
status = rsp.get("content", {}).get("execution_state")
|
@@ -670,9 +602,11 @@ class Kernel:
|
|
670
602
|
rsp["header"]["date"].isoformat(),
|
671
603
|
)
|
672
604
|
|
673
|
-
def on_iopub_execute_input(self, rsp: dict[str, Any]) -> None:
|
605
|
+
def on_iopub_execute_input(self, rsp: dict[str, Any], own: bool) -> None:
|
674
606
|
"""Call callbacks for an iopub execute input response."""
|
675
607
|
msg_id = rsp.get("parent_header", {}).get("msg_id")
|
608
|
+
content = rsp.get("content", {})
|
609
|
+
|
676
610
|
if self.kernel_tab.app.config.record_cell_timing and callable(
|
677
611
|
set_metadata := self.msg_id_callbacks[msg_id]["set_metadata"]
|
678
612
|
):
|
@@ -681,23 +615,42 @@ class Kernel:
|
|
681
615
|
rsp["header"]["date"].isoformat(),
|
682
616
|
)
|
683
617
|
|
684
|
-
|
618
|
+
execution_count: int | None = None
|
619
|
+
if (execution_count := content.get("execution_count")) and (
|
620
|
+
callable(
|
621
|
+
set_execution_count := self.msg_id_callbacks[msg_id][
|
622
|
+
"set_execution_count"
|
623
|
+
]
|
624
|
+
)
|
625
|
+
):
|
626
|
+
set_execution_count(execution_count)
|
627
|
+
|
628
|
+
if callable(add_input := self.msg_id_callbacks[msg_id].get("add_input")):
|
629
|
+
add_input(content, own)
|
630
|
+
|
631
|
+
def on_iopub_display_data(self, rsp: dict[str, Any], own: bool) -> None:
|
685
632
|
"""Call callbacks for an iopub display data response."""
|
686
633
|
msg_id = rsp.get("parent_header", {}).get("msg_id")
|
687
634
|
if callable(add_output := self.msg_id_callbacks[msg_id]["add_output"]):
|
688
|
-
|
635
|
+
import nbformat
|
689
636
|
|
690
|
-
|
637
|
+
add_output(nbformat.v4.output_from_msg(rsp), own)
|
638
|
+
|
639
|
+
def on_iopub_update_display_data(self, rsp: dict[str, Any], own: bool) -> None:
|
691
640
|
"""Call callbacks for an iopub update display data response."""
|
692
641
|
msg_id = rsp.get("parent_header", {}).get("msg_id")
|
693
642
|
if callable(add_output := self.msg_id_callbacks[msg_id]["add_output"]):
|
694
|
-
|
643
|
+
import nbformat
|
644
|
+
|
645
|
+
add_output(nbformat.v4.output_from_msg(rsp), own)
|
695
646
|
|
696
|
-
def on_iopub_execute_result(self, rsp: dict[str, Any]) -> None:
|
647
|
+
def on_iopub_execute_result(self, rsp: dict[str, Any], own: bool) -> None:
|
697
648
|
"""Call callbacks for an iopub execute result response."""
|
698
649
|
msg_id = rsp.get("parent_header", {}).get("msg_id")
|
699
650
|
if callable(add_output := self.msg_id_callbacks[msg_id]["add_output"]):
|
700
|
-
|
651
|
+
import nbformat
|
652
|
+
|
653
|
+
add_output(nbformat.v4.output_from_msg(rsp), own)
|
701
654
|
|
702
655
|
if (execution_count := rsp.get("content", {}).get("execution_count")) and (
|
703
656
|
callable(
|
@@ -708,21 +661,25 @@ class Kernel:
|
|
708
661
|
):
|
709
662
|
set_execution_count(execution_count)
|
710
663
|
|
711
|
-
def on_iopub_error(self, rsp: dict[str,
|
664
|
+
def on_iopub_error(self, rsp: dict[str, Any], own: bool) -> None:
|
712
665
|
"""Call callbacks for an iopub error response."""
|
713
666
|
msg_id = rsp.get("parent_header", {}).get("msg_id", "")
|
714
667
|
if callable(add_output := self.msg_id_callbacks[msg_id].get("add_output")):
|
715
|
-
|
668
|
+
import nbformat
|
669
|
+
|
670
|
+
add_output(nbformat.v4.output_from_msg(rsp), own)
|
716
671
|
if callable(done := self.msg_id_callbacks[msg_id].get("done")):
|
717
672
|
done(rsp.get("content", {}))
|
718
673
|
|
719
|
-
def on_iopub_stream(self, rsp: dict[str, Any]) -> None:
|
674
|
+
def on_iopub_stream(self, rsp: dict[str, Any], own: bool) -> None:
|
720
675
|
"""Call callbacks for an iopub stream response."""
|
721
676
|
msg_id = rsp.get("parent_header", {}).get("msg_id")
|
722
677
|
if callable(add_output := self.msg_id_callbacks[msg_id]["add_output"]):
|
723
|
-
|
678
|
+
import nbformat
|
724
679
|
|
725
|
-
|
680
|
+
add_output(nbformat.v4.output_from_msg(rsp), own)
|
681
|
+
|
682
|
+
def on_iopub_clear_output(self, rsp: dict[str, Any], own: bool) -> None:
|
726
683
|
"""Call callbacks for an iopub clear output response."""
|
727
684
|
# Clear cell output, either now or when we get the next output
|
728
685
|
msg_id = rsp.get("parent_header", {}).get("msg_id")
|
@@ -737,7 +694,7 @@ class Kernel:
|
|
737
694
|
)
|
738
695
|
'''
|
739
696
|
|
740
|
-
def on_iopub_comm_open(self, rsp: dict[str, Any]) -> None:
|
697
|
+
def on_iopub_comm_open(self, rsp: dict[str, Any], own: bool) -> None:
|
741
698
|
"""Call callbacks for an comm open response."""
|
742
699
|
# TODO
|
743
700
|
# "If the target_name key is not found on the receiving side, then it should
|
@@ -747,13 +704,13 @@ class Kernel:
|
|
747
704
|
content=rsp.get("content", {}), buffers=rsp.get("buffers", [])
|
748
705
|
)
|
749
706
|
|
750
|
-
def on_iopub_comm_msg(self, rsp: dict[str, Any]) -> None:
|
707
|
+
def on_iopub_comm_msg(self, rsp: dict[str, Any], own: bool) -> None:
|
751
708
|
"""Call callbacks for an iopub comm message response."""
|
752
709
|
self.kernel_tab.comm_msg(
|
753
710
|
content=rsp.get("content", {}), buffers=rsp.get("buffers", [])
|
754
711
|
)
|
755
712
|
|
756
|
-
def on_iopub_comm_close(self, rsp: dict[str, Any]) -> None:
|
713
|
+
def on_iopub_comm_close(self, rsp: dict[str, Any], own: bool) -> None:
|
757
714
|
"""Call callbacks for an iopub comm close response."""
|
758
715
|
self.kernel_tab.comm_close(
|
759
716
|
content=rsp.get("content", {}), buffers=rsp.get("buffers", [])
|
@@ -795,7 +752,7 @@ class Kernel:
|
|
795
752
|
source: str,
|
796
753
|
get_input: Callable[[str, bool], None] | None = None,
|
797
754
|
set_execution_count: Callable[[int], None] | None = None,
|
798
|
-
add_output: Callable[[dict[str, Any]], None] | None = None,
|
755
|
+
add_output: Callable[[dict[str, Any], bool], None] | None = None,
|
799
756
|
clear_output: Callable[[bool], None] | None = None,
|
800
757
|
done: Callable[[dict[str, Any]], None] | None = None,
|
801
758
|
set_metadata: Callable[[tuple[str, ...], Any], None] | None = None,
|
@@ -1121,6 +1078,8 @@ class Kernel:
|
|
1121
1078
|
kernel's event loop to finish.
|
1122
1079
|
"""
|
1123
1080
|
if self.km.has_kernel:
|
1081
|
+
from jupyter_client import KernelManager
|
1082
|
+
|
1124
1083
|
log.debug("Interrupting kernel %s", self.id)
|
1125
1084
|
KernelManager.interrupt_kernel(self.km)
|
1126
1085
|
|
@@ -1156,6 +1115,8 @@ class Kernel:
|
|
1156
1115
|
cb: Callback to run once restarted
|
1157
1116
|
|
1158
1117
|
"""
|
1118
|
+
from euporie.core.kernel.manager import EuporieKernelManager
|
1119
|
+
|
1159
1120
|
self.connection_file = connection_file
|
1160
1121
|
self.status = "starting"
|
1161
1122
|
|
@@ -0,0 +1,114 @@
|
|
1
|
+
"""Contain classes relating to kernel management."""
|
2
|
+
|
3
|
+
from __future__ import annotations
|
4
|
+
|
5
|
+
import logging
|
6
|
+
import os
|
7
|
+
import re
|
8
|
+
import sys
|
9
|
+
import threading
|
10
|
+
from typing import TYPE_CHECKING
|
11
|
+
|
12
|
+
from jupyter_client import AsyncKernelManager
|
13
|
+
from jupyter_client.provisioning.local_provisioner import LocalProvisioner
|
14
|
+
|
15
|
+
if TYPE_CHECKING:
|
16
|
+
from typing import Any, Callable, TextIO
|
17
|
+
|
18
|
+
from jupyter_client.connect import KernelConnectionInfo
|
19
|
+
|
20
|
+
|
21
|
+
log = logging.getLogger(__name__)
|
22
|
+
|
23
|
+
|
24
|
+
class LoggingLocalProvisioner(LocalProvisioner): # type:ignore[misc]
|
25
|
+
"""A Jupyter kernel provisionser which logs kernel output."""
|
26
|
+
|
27
|
+
async def launch_kernel(
|
28
|
+
self, cmd: list[str], **kwargs: Any
|
29
|
+
) -> KernelConnectionInfo:
|
30
|
+
"""Launch a kernel with a command."""
|
31
|
+
await super().launch_kernel(cmd, **kwargs)
|
32
|
+
|
33
|
+
def log_kernel_output(pipe: TextIO, log_func: Callable) -> None:
|
34
|
+
try:
|
35
|
+
with pipe:
|
36
|
+
for line in iter(pipe.readline, ""):
|
37
|
+
log_func(line.rstrip())
|
38
|
+
except StopIteration:
|
39
|
+
pass
|
40
|
+
|
41
|
+
if self.process is not None:
|
42
|
+
# Start thread to listen for kernel output
|
43
|
+
threading.Thread(
|
44
|
+
target=log_kernel_output,
|
45
|
+
args=(self.process.stdout, log.warning),
|
46
|
+
daemon=True,
|
47
|
+
).start()
|
48
|
+
|
49
|
+
return self.connection_info
|
50
|
+
|
51
|
+
|
52
|
+
def set_default_provisioner() -> None:
|
53
|
+
"""Set the default kernel provisioner to euporie's logging provisioner."""
|
54
|
+
from jupyter_client.provisioning import KernelProvisionerFactory as KPF
|
55
|
+
|
56
|
+
KPF.instance().default_provisioner_name = "logging-local-provisioner"
|
57
|
+
|
58
|
+
|
59
|
+
class EuporieKernelManager(AsyncKernelManager):
|
60
|
+
"""Kernel Manager subclass.
|
61
|
+
|
62
|
+
``jupyter_client`` replaces a plain ``python`` command with the current executable,
|
63
|
+
but this is not desirable if the client is running in its own prefix (e.g. with
|
64
|
+
``pipx``). We work around this here.
|
65
|
+
|
66
|
+
See https://github.com/jupyter/jupyter_client/issues/949
|
67
|
+
"""
|
68
|
+
|
69
|
+
def format_kernel_cmd(self, extra_arguments: list[str] | None = None) -> list[str]:
|
70
|
+
"""Replace templated args (e.g. {connection_file})."""
|
71
|
+
extra_arguments = extra_arguments or []
|
72
|
+
assert self.kernel_spec is not None
|
73
|
+
cmd = self.kernel_spec.argv + extra_arguments
|
74
|
+
|
75
|
+
v_major, v_minor = sys.version_info[:2]
|
76
|
+
if cmd and cmd[0] in {
|
77
|
+
"python",
|
78
|
+
f"python{v_major}",
|
79
|
+
f"python{v_major}.{v_minor}",
|
80
|
+
}:
|
81
|
+
# If the command is `python` without an absolute path and euporie is
|
82
|
+
# running in the same prefix as the kernel_spec file is located, use
|
83
|
+
# sys.executable: otherwise fall back to the executable in the base prefix
|
84
|
+
if (
|
85
|
+
os.path.commonpath((sys.prefix, self.kernel_spec.resource_dir))
|
86
|
+
== sys.prefix
|
87
|
+
):
|
88
|
+
cmd[0] = sys.executable
|
89
|
+
else:
|
90
|
+
cmd[0] = sys._base_executable # type: ignore [attr-defined]
|
91
|
+
|
92
|
+
# Make sure to use the realpath for the connection_file
|
93
|
+
# On windows, when running with the store python, the connection_file path
|
94
|
+
# is not usable by non python kernels because the path is being rerouted when
|
95
|
+
# inside of a store app.
|
96
|
+
# See this bug here: https://bugs.python.org/issue41196
|
97
|
+
ns = {
|
98
|
+
"connection_file": os.path.realpath(self.connection_file),
|
99
|
+
"prefix": sys.prefix,
|
100
|
+
}
|
101
|
+
|
102
|
+
if self.kernel_spec:
|
103
|
+
ns["resource_dir"] = self.kernel_spec.resource_dir
|
104
|
+
|
105
|
+
if self._launch_args:
|
106
|
+
ns.update({str(k): str(v) for k, v in self._launch_args.items()})
|
107
|
+
|
108
|
+
pat = re.compile(r"\{([A-Za-z0-9_]+)\}")
|
109
|
+
|
110
|
+
def _from_ns(match: re.Match) -> str:
|
111
|
+
"""Get the key out of ns if it's there, otherwise no change."""
|
112
|
+
return ns.get(match.group(1), match.group())
|
113
|
+
|
114
|
+
return [pat.sub(_from_ns, arg) for arg in cmd]
|
@@ -1,11 +1,5 @@
|
|
1
1
|
"""Define collections of generic key-bindings which do not belong to widgets."""
|
2
2
|
|
3
|
-
from
|
4
|
-
basic,
|
5
|
-
completion,
|
6
|
-
micro,
|
7
|
-
mouse,
|
8
|
-
page_navigation,
|
9
|
-
)
|
3
|
+
from . import basic, completion, micro, mouse, page_navigation, vi
|
10
4
|
|
11
|
-
__all__ = ["basic", "completion", "micro", "mouse", "page_navigation"]
|
5
|
+
__all__ = ["basic", "completion", "micro", "mouse", "page_navigation", "vi"]
|