euporie 2.8.4__py3-none-any.whl → 2.8.6__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 +58 -62
- euporie/core/__init__.py +1 -1
- euporie/core/__main__.py +28 -11
- euporie/core/_settings.py +109 -0
- euporie/core/app/__init__.py +3 -0
- euporie/core/app/_commands.py +95 -0
- euporie/core/app/_settings.py +457 -0
- euporie/core/{app.py → app/app.py} +212 -576
- 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 +205 -0
- euporie/core/bars/menu.py +258 -0
- euporie/core/{widgets → bars}/search.py +20 -16
- euporie/core/{widgets → bars}/status.py +6 -23
- euporie/core/clipboard.py +19 -80
- euporie/core/comm/base.py +8 -6
- euporie/core/comm/ipywidgets.py +16 -7
- euporie/core/comm/registry.py +2 -1
- euporie/core/commands.py +10 -20
- euporie/core/completion.py +3 -2
- euporie/core/config.py +368 -341
- euporie/core/convert/__init__.py +0 -30
- euporie/core/convert/datum.py +116 -53
- euporie/core/convert/formats/__init__.py +31 -0
- euporie/core/convert/formats/ansi.py +9 -23
- 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 +4 -6
- euporie/core/convert/utils.py +41 -4
- euporie/core/diagnostics.py +2 -2
- euporie/core/filters.py +98 -40
- euporie/core/format.py +2 -3
- euporie/core/ft/ansi.py +1 -1
- euporie/core/ft/html.py +12 -21
- euporie/core/ft/table.py +1 -3
- euporie/core/ft/utils.py +4 -1
- euporie/core/graphics.py +386 -133
- 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} +45 -108
- euporie/core/kernel/manager.py +114 -0
- euporie/core/key_binding/bindings/__init__.py +1 -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 +1 -6
- euporie/core/key_binding/bindings/mouse.py +2 -2
- euporie/core/key_binding/bindings/terminal.py +193 -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 +3 -3
- euporie/core/layout/containers.py +48 -4
- euporie/core/layout/decor.py +2 -2
- euporie/core/layout/mouse.py +1 -1
- euporie/core/layout/print.py +2 -1
- euporie/core/layout/scroll.py +39 -34
- euporie/core/log.py +76 -64
- euporie/core/lsp.py +118 -24
- euporie/core/margins.py +1 -1
- euporie/core/path.py +62 -13
- euporie/core/renderer.py +58 -17
- euporie/core/style.py +57 -39
- euporie/core/suggest.py +103 -85
- euporie/core/tabs/__init__.py +32 -0
- euporie/core/tabs/_settings.py +113 -0
- euporie/core/tabs/base.py +80 -470
- euporie/core/tabs/kernel.py +419 -0
- euporie/core/tabs/notebook.py +24 -101
- euporie/core/utils.py +92 -15
- euporie/core/validation.py +1 -1
- euporie/core/widgets/_settings.py +188 -0
- euporie/core/widgets/cell.py +19 -50
- euporie/core/widgets/cell_outputs.py +25 -36
- euporie/core/widgets/decor.py +11 -41
- euporie/core/widgets/dialog.py +62 -27
- euporie/core/widgets/display.py +12 -15
- euporie/core/widgets/file_browser.py +2 -23
- euporie/core/widgets/forms.py +8 -5
- euporie/core/widgets/inputs.py +13 -70
- euporie/core/widgets/layout.py +2 -1
- euporie/core/widgets/logo.py +49 -0
- euporie/core/widgets/menu.py +10 -8
- euporie/core/widgets/pager.py +6 -10
- euporie/core/widgets/palette.py +6 -6
- euporie/hub/app.py +52 -35
- euporie/notebook/_commands.py +24 -0
- euporie/notebook/_settings.py +107 -0
- euporie/notebook/app.py +49 -171
- 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 +4 -4
- euporie/notebook/tabs/edit.py +11 -44
- euporie/notebook/tabs/json.py +5 -5
- euporie/notebook/tabs/log.py +1 -18
- euporie/notebook/tabs/notebook.py +11 -660
- 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 +6 -31
- euporie/preview/tabs/notebook.py +6 -72
- euporie/web/__init__.py +1 -0
- euporie/web/tabs/__init__.py +14 -0
- euporie/web/tabs/web.py +11 -6
- euporie/web/widgets/__init__.py +1 -0
- euporie/web/widgets/webview.py +5 -15
- {euporie-2.8.4.dist-info → euporie-2.8.6.dist-info}/METADATA +10 -8
- euporie-2.8.6.dist-info/RECORD +175 -0
- {euporie-2.8.4.dist-info → euporie-2.8.6.dist-info}/WHEEL +1 -1
- {euporie-2.8.4.dist-info → euporie-2.8.6.dist-info}/entry_points.txt +2 -2
- {euporie-2.8.4.dist-info → euporie-2.8.6.dist-info}/licenses/LICENSE +1 -1
- euporie/core/launch.py +0 -64
- euporie/core/terminal.py +0 -522
- euporie-2.8.4.dist-info/RECORD +0 -147
- {euporie-2.8.4.data → euporie-2.8.6.data}/data/share/applications/euporie-console.desktop +0 -0
- {euporie-2.8.4.data → euporie-2.8.6.data}/data/share/applications/euporie-notebook.desktop +0 -0
euporie/core/history.py
CHANGED
@@ -8,9 +8,9 @@ from typing import TYPE_CHECKING
|
|
8
8
|
from prompt_toolkit.history import History
|
9
9
|
|
10
10
|
if TYPE_CHECKING:
|
11
|
-
from
|
11
|
+
from collections.abc import AsyncGenerator, Iterable
|
12
12
|
|
13
|
-
from euporie.core.kernel import Kernel
|
13
|
+
from euporie.core.kernel.client import Kernel
|
14
14
|
|
15
15
|
log = logging.getLogger(__name__)
|
16
16
|
|
euporie/core/inspection.py
CHANGED
@@ -7,12 +7,13 @@ from abc import ABCMeta, abstractmethod
|
|
7
7
|
from typing import TYPE_CHECKING
|
8
8
|
|
9
9
|
if TYPE_CHECKING:
|
10
|
+
from collections.abc import Sequence
|
10
11
|
from pathlib import Path
|
11
|
-
from typing import Any, Callable
|
12
|
+
from typing import Any, Callable
|
12
13
|
|
13
14
|
from prompt_toolkit.document import Document
|
14
15
|
|
15
|
-
from euporie.core.kernel import Kernel
|
16
|
+
from euporie.core.kernel.client import Kernel
|
16
17
|
from euporie.core.lsp import LspClient
|
17
18
|
|
18
19
|
|
euporie/core/io.py
CHANGED
@@ -5,40 +5,115 @@ from __future__ import annotations
|
|
5
5
|
import logging
|
6
6
|
import re
|
7
7
|
from base64 import b64encode
|
8
|
-
from
|
8
|
+
from functools import lru_cache
|
9
|
+
from typing import TYPE_CHECKING, cast
|
9
10
|
|
10
11
|
from prompt_toolkit.input import vt100_parser
|
12
|
+
from prompt_toolkit.input.ansi_escape_sequences import ANSI_SEQUENCES
|
11
13
|
from prompt_toolkit.input.base import DummyInput, _dummy_context_manager
|
14
|
+
from prompt_toolkit.output.color_depth import ColorDepth
|
12
15
|
from prompt_toolkit.output.vt100 import Vt100_Output as PtkVt100_Output
|
13
16
|
|
17
|
+
from euporie.core.app.current import get_app
|
18
|
+
from euporie.core.filters import in_screen, in_tmux
|
19
|
+
|
14
20
|
if TYPE_CHECKING:
|
15
|
-
from
|
21
|
+
from contextlib import AbstractContextManager
|
22
|
+
from typing import IO, Any, Callable, TextIO
|
16
23
|
|
17
24
|
from prompt_toolkit.keys import Keys
|
18
25
|
|
26
|
+
from euporie.core.config import Config
|
27
|
+
|
19
28
|
log = logging.getLogger(__name__)
|
20
29
|
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
30
|
+
COLOR_DEPTHS = {
|
31
|
+
1: ColorDepth.DEPTH_1_BIT,
|
32
|
+
4: ColorDepth.DEPTH_4_BIT,
|
33
|
+
8: ColorDepth.DEPTH_8_BIT,
|
34
|
+
24: ColorDepth.DEPTH_24_BIT,
|
35
|
+
}
|
36
|
+
|
37
|
+
|
38
|
+
@lru_cache
|
39
|
+
def _have_termios_tty_fcntl() -> bool:
|
40
|
+
try:
|
41
|
+
import fcntl # noqa F401
|
42
|
+
import termios # noqa F401
|
43
|
+
import tty # noqa F401
|
44
|
+
except ModuleNotFoundError:
|
45
|
+
return False
|
46
|
+
else:
|
47
|
+
return True
|
48
|
+
|
49
|
+
|
50
|
+
def _tiocgwinsz() -> tuple[int, int, int, int]:
|
51
|
+
"""Get the size and pixel dimensions of the terminal with `termios`."""
|
52
|
+
import array
|
53
|
+
|
54
|
+
output = array.array("H", [0, 0, 0, 0])
|
55
|
+
if _have_termios_tty_fcntl():
|
56
|
+
import fcntl
|
57
|
+
import termios
|
58
|
+
|
59
|
+
try:
|
60
|
+
fcntl.ioctl(1, termios.TIOCGWINSZ, output)
|
61
|
+
except OSError:
|
62
|
+
pass
|
63
|
+
rows, cols, xpixels, ypixels = output
|
64
|
+
return rows, cols, xpixels, ypixels
|
65
|
+
|
66
|
+
|
67
|
+
def passthrough(cmd: str, config: Config | None = None) -> str:
|
68
|
+
"""Wrap an escape sequence for terminal passthrough."""
|
69
|
+
config = config or get_app().config
|
70
|
+
if config.multiplexer_passthrough:
|
71
|
+
if in_tmux():
|
72
|
+
cmd = cmd.replace("\x1b", "\x1b\x1b")
|
73
|
+
cmd = f"\x1bPtmux;{cmd}\x1b\\"
|
74
|
+
elif in_screen():
|
75
|
+
# Screen limits escape sequences to 768 bytes, so we have to chunk it
|
76
|
+
cmd = "".join(
|
77
|
+
f"\x1bP{cmd[i : i + 764]}\x1b\\" for i in range(0, len(cmd), 764)
|
78
|
+
)
|
79
|
+
return cmd
|
33
80
|
|
34
81
|
|
35
82
|
class _IsPrefixOfLongerMatchCache(vt100_parser._IsPrefixOfLongerMatchCache):
|
83
|
+
def __init__(self, *args: Any, **kwargs: Any) -> None:
|
84
|
+
super().__init__(*args, **kwargs)
|
85
|
+
# Pattern for any ANSI escape sequence
|
86
|
+
self._response_prefix_re = re.compile(
|
87
|
+
r"""^\x1b(
|
88
|
+
\][^\\\x07]* # Operating System Commands
|
89
|
+
|
|
90
|
+
_[^\\]* # Application Program Command
|
91
|
+
|
|
92
|
+
\[\?[\d;]* # Primary device attribute responses
|
93
|
+
|
|
94
|
+
P[ -~]*(\x1b|x1b\\)?
|
95
|
+
)\Z""",
|
96
|
+
re.VERBOSE,
|
97
|
+
)
|
98
|
+
# Generate prefix matches for all known ansi escape sequences
|
99
|
+
# This is faster than PTK's method
|
100
|
+
self._ansi_sequence_prefixes = {
|
101
|
+
seq[:i] for seq in ANSI_SEQUENCES for i in range(len(seq))
|
102
|
+
}
|
103
|
+
|
36
104
|
def __missing__(self, prefix: str) -> bool:
|
37
105
|
"""Check if the response might match an OSC or APC code, or DA response."""
|
38
|
-
result =
|
39
|
-
|
40
|
-
|
41
|
-
|
106
|
+
result = bool(
|
107
|
+
# (hard coded) If this could be a prefix of a CPR response, return True.
|
108
|
+
vt100_parser._cpr_response_prefix_re.match(prefix)
|
109
|
+
# True if this could be a mouse event sequence
|
110
|
+
or vt100_parser._mouse_event_prefix_re.match(prefix)
|
111
|
+
# True if this could be the prefix of an expected escape sequence
|
112
|
+
or prefix in self._ansi_sequence_prefixes
|
113
|
+
# If this could be a prefix of any other escape sequence, return True
|
114
|
+
or self._response_prefix_re.match(prefix)
|
115
|
+
)
|
116
|
+
self[prefix] = result
|
42
117
|
return result
|
43
118
|
|
44
119
|
|
@@ -51,14 +126,39 @@ class Vt100Parser(vt100_parser.Vt100Parser):
|
|
51
126
|
|
52
127
|
def __init__(self, *args: Any, **kwargs: Any) -> None:
|
53
128
|
"""Create a new VT100 parser."""
|
129
|
+
from euporie.core.keys import MoreKeys
|
130
|
+
|
54
131
|
super().__init__(*args, **kwargs)
|
55
|
-
self.
|
132
|
+
self.patterns: dict[Keys | MoreKeys, re.Pattern] = {
|
133
|
+
MoreKeys.ColorsResponse: re.compile(
|
134
|
+
r"^\x1b\](?P<c>(\d+;)?\d+)+;rgb:"
|
135
|
+
r"(?P<r>[0-9A-Fa-f]{2,4})\/"
|
136
|
+
r"(?P<g>[0-9A-Fa-f]{2,4})\/"
|
137
|
+
r"(?P<b>[0-9A-Fa-f]{2,4})"
|
138
|
+
# Allow BEL or ST as terminator
|
139
|
+
r"(?:\x1b\\|\x9c|\x07)"
|
140
|
+
),
|
141
|
+
MoreKeys.PixelSizeResponse: re.compile(r"^\x1b\[4;(?P<y>\d+);(?P<x>\d+)t"),
|
142
|
+
MoreKeys.KittyGraphicsStatusResponse: re.compile(
|
143
|
+
r"^\x1b_Gi=(4294967295|0);(?P<status>OK)\x1b\\"
|
144
|
+
),
|
145
|
+
MoreKeys.SixelGraphicsStatusResponse: re.compile(
|
146
|
+
r"^\x1b\[\?(?:\d+;)*(?P<sixel>4)(?:;\d+)*c"
|
147
|
+
),
|
148
|
+
MoreKeys.ItermGraphicsStatusResponse: re.compile(
|
149
|
+
r"^\x1bP>\|(?P<term>[^\x1b]+)\x1b\\"
|
150
|
+
),
|
151
|
+
MoreKeys.SgrPixelStatusResponse: re.compile(r"^\x1b\[\?1016;(?P<Pm>\d)\$"),
|
152
|
+
MoreKeys.ClipboardDataResponse: re.compile(
|
153
|
+
r"^\x1b\]52;(?:c|p)?;(?P<data>[A-Za-z0-9+/=]+)\x1b\\"
|
154
|
+
),
|
155
|
+
}
|
56
156
|
|
57
157
|
def _get_match(self, prefix: str) -> None | Keys | tuple[Keys, ...]:
|
58
158
|
"""Check for additional key matches first."""
|
59
|
-
for key, pattern in self.
|
159
|
+
for key, pattern in self.patterns.items():
|
60
160
|
if pattern.match(prefix):
|
61
|
-
return key
|
161
|
+
return cast("Keys", key)
|
62
162
|
|
63
163
|
return super()._get_match(prefix)
|
64
164
|
|
@@ -66,7 +166,9 @@ class Vt100Parser(vt100_parser.Vt100Parser):
|
|
66
166
|
class IgnoredInput(DummyInput):
|
67
167
|
"""An input which ignores input but does not immediately close the app."""
|
68
168
|
|
69
|
-
def attach(
|
169
|
+
def attach(
|
170
|
+
self, input_ready_callback: Callable[[], None]
|
171
|
+
) -> AbstractContextManager[None]:
|
70
172
|
"""Do not call the callback, so the input is never closed."""
|
71
173
|
return _dummy_context_manager()
|
72
174
|
|
@@ -74,14 +176,12 @@ class IgnoredInput(DummyInput):
|
|
74
176
|
class Vt100_Output(PtkVt100_Output):
|
75
177
|
"""A Vt100 output which enables SGR pixel mouse positioning."""
|
76
178
|
|
77
|
-
def
|
78
|
-
"""
|
79
|
-
super().enable_mouse_support()
|
179
|
+
def enable_sgr_pixel(self) -> None:
|
180
|
+
"""Enable SGR-pixel mouse positioning."""
|
80
181
|
self.write_raw("\x1b[?1016h")
|
81
182
|
|
82
|
-
def
|
83
|
-
"""
|
84
|
-
super().disable_mouse_support()
|
183
|
+
def disable_sgr_pixel(self) -> None:
|
184
|
+
"""Disable SGR-pixel mouse positioning."""
|
85
185
|
self.write_raw("\x1b[?1016l")
|
86
186
|
|
87
187
|
def enable_private_sixel_colors(self) -> None:
|
@@ -115,6 +215,44 @@ class Vt100_Output(PtkVt100_Output):
|
|
115
215
|
"""Get clipboard contents using OSC-52."""
|
116
216
|
self.write_raw("\x1b]52;c;?\x1b\\")
|
117
217
|
|
218
|
+
def get_colors(self) -> None:
|
219
|
+
"""Query terminal colors."""
|
220
|
+
self.write_raw(
|
221
|
+
passthrough(
|
222
|
+
("\x1b]10;?\x1b\\\x1b]11;?\x1b\\")
|
223
|
+
+ "".join(f"\x1b]4;{i};?\x1b\\" for i in range(16))
|
224
|
+
)
|
225
|
+
)
|
226
|
+
|
227
|
+
def get_pixel_size(self) -> None:
|
228
|
+
"""Check the terminal's dimensions in pixels."""
|
229
|
+
self.write_raw("\x1b[14t")
|
230
|
+
|
231
|
+
def get_kitty_graphics_status(self) -> None:
|
232
|
+
"""Query terminal to check for kitty graphics support."""
|
233
|
+
self.write_raw(
|
234
|
+
"\x1b[s"
|
235
|
+
+ passthrough("\x1b_gi=4294967295,s=1,v=1,a=q,t=d,f=24;aaaa\x1b\\")
|
236
|
+
+ "\x1b[u\x1b[2k"
|
237
|
+
)
|
238
|
+
|
239
|
+
def get_sixel_graphics_status(self) -> None:
|
240
|
+
"""Query terminal for sixel graphics support."""
|
241
|
+
self.write_raw(passthrough("\x1b[c"))
|
242
|
+
|
243
|
+
def get_iterm_graphics_status(self) -> None:
|
244
|
+
"""Query terminal for iTerm graphics support."""
|
245
|
+
self.write_raw(passthrough("\x1b[>q"))
|
246
|
+
|
247
|
+
def get_sgr_pixel_status(self) -> None:
|
248
|
+
"""Query terminal to check for Pixel SGR support."""
|
249
|
+
# Enable, check, disable
|
250
|
+
self.write_raw("\x1b[?1016h\x1b[?1016$p\x1b[?1016l")
|
251
|
+
|
252
|
+
def get_csiu_status(self) -> None:
|
253
|
+
"""Query terminal to check for CSI-u support."""
|
254
|
+
self.write_raw("\x1b[?u")
|
255
|
+
|
118
256
|
|
119
257
|
class PseudoTTY:
|
120
258
|
"""Make an output stream look like a TTY."""
|
@@ -139,3 +277,44 @@ class PseudoTTY:
|
|
139
277
|
def __getattr__(self, name: str) -> Any:
|
140
278
|
"""Return an attribute of the wrappeed stream."""
|
141
279
|
return getattr(self._underlying, name)
|
280
|
+
|
281
|
+
|
282
|
+
def edit_in_editor(filename: str, line_number: int = 0) -> None:
|
283
|
+
"""Suspend the current app and edit a file in an external editor."""
|
284
|
+
import os
|
285
|
+
import shlex
|
286
|
+
import subprocess
|
287
|
+
|
288
|
+
from prompt_toolkit.application.run_in_terminal import run_in_terminal
|
289
|
+
|
290
|
+
def _open_file_in_editor(filename: str) -> None:
|
291
|
+
"""Call editor executable."""
|
292
|
+
# If the 'VISUAL' or 'EDITOR' environment variable has been set, use that.
|
293
|
+
# Otherwise, fall back to the first available editor that we can find.
|
294
|
+
for editor in [
|
295
|
+
os.environ.get("VISUAL"),
|
296
|
+
os.environ.get("EDITOR"),
|
297
|
+
"editor",
|
298
|
+
"micro",
|
299
|
+
"nano",
|
300
|
+
"pico",
|
301
|
+
"vi",
|
302
|
+
"emacs",
|
303
|
+
]:
|
304
|
+
if editor:
|
305
|
+
try:
|
306
|
+
# Use 'shlex.split()' because $VISUAL can contain spaces and quotes
|
307
|
+
subprocess.call([*shlex.split(editor), filename])
|
308
|
+
return
|
309
|
+
except OSError:
|
310
|
+
# Executable does not exist, try the next one.
|
311
|
+
pass
|
312
|
+
|
313
|
+
async def run() -> None:
|
314
|
+
# Open in editor
|
315
|
+
# (We need to use `run_in_terminal`, because not all editors go to
|
316
|
+
# the alternate screen buffer, and some could influence the cursor
|
317
|
+
# position)
|
318
|
+
await run_in_terminal(lambda: _open_file_in_editor(filename), in_executor=True)
|
319
|
+
|
320
|
+
get_app().create_background_task(run())
|
@@ -0,0 +1 @@
|
|
1
|
+
"""Concerns the interaction with Jupyter kernels."""
|
@@ -6,125 +6,27 @@ 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
|
-
from _frozen_importlib import _DeadlockError
|
13
10
|
from collections import defaultdict
|
14
11
|
from subprocess import PIPE, STDOUT # S404 - Security implications considered
|
15
12
|
from typing import TYPE_CHECKING, TypedDict
|
16
13
|
from uuid import uuid4
|
17
14
|
|
18
|
-
import nbformat
|
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
|
|
@@ -173,6 +75,15 @@ class Kernel:
|
|
173
75
|
kernel connection information
|
174
76
|
|
175
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
|
+
|
176
87
|
self.threaded = threaded
|
177
88
|
if threaded:
|
178
89
|
self.loop = asyncio.new_event_loop()
|
@@ -344,6 +255,8 @@ class Kernel:
|
|
344
255
|
@property
|
345
256
|
def missing(self) -> bool:
|
346
257
|
"""Return True if the requested kernel is not found."""
|
258
|
+
from jupyter_client.kernelspec import NoSuchKernel
|
259
|
+
|
347
260
|
try:
|
348
261
|
self.km.kernel_spec # noqa B018
|
349
262
|
except NoSuchKernel:
|
@@ -376,6 +289,8 @@ class Kernel:
|
|
376
289
|
|
377
290
|
async def start_(self) -> None:
|
378
291
|
"""Start the kernel asynchronously and set its status."""
|
292
|
+
from jupyter_core.paths import jupyter_runtime_dir
|
293
|
+
|
379
294
|
if self.km.kernel_name is None:
|
380
295
|
self.status = "error"
|
381
296
|
log.debug("Starting kernel")
|
@@ -404,16 +319,18 @@ class Kernel:
|
|
404
319
|
# Otherwise, start a new kernel using the kernel manager
|
405
320
|
else:
|
406
321
|
runtime_dir.mkdir(exist_ok=True, parents=True)
|
407
|
-
|
322
|
+
for attempt in range(1, 4):
|
408
323
|
try:
|
409
324
|
# TODO - send stdout to log
|
410
325
|
await self.km.start_kernel(stdout=PIPE, stderr=STDOUT, text=True)
|
411
|
-
except _DeadlockError:
|
412
|
-
# Keep trying if we get an import deadlock
|
413
|
-
await asyncio.sleep(0.1)
|
414
|
-
continue
|
415
326
|
except Exception as e:
|
416
|
-
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
|
417
334
|
self.status = "error"
|
418
335
|
self.error = e
|
419
336
|
else:
|
@@ -421,12 +338,14 @@ class Kernel:
|
|
421
338
|
# Create a client for the newly started kernel
|
422
339
|
if self.km.has_kernel:
|
423
340
|
self.kc = self.km.client()
|
424
|
-
|
341
|
+
break
|
425
342
|
|
426
343
|
await self.post_start_()
|
427
344
|
|
428
345
|
async def post_start_(self) -> None:
|
429
346
|
"""Wait for the kernel to become ready."""
|
347
|
+
from jupyter_client.kernelspec import NoSuchKernel
|
348
|
+
|
430
349
|
try:
|
431
350
|
ks = self.km.kernel_spec
|
432
351
|
except NoSuchKernel as e:
|
@@ -479,6 +398,8 @@ class Kernel:
|
|
479
398
|
timeout: How long to wait until failure is assumed
|
480
399
|
|
481
400
|
"""
|
401
|
+
from jupyter_client.kernelspec import NATIVE_KERNEL_NAME
|
402
|
+
|
482
403
|
# Attempt to import ipykernel if it is installed
|
483
404
|
# ipykernel is imported by jupyter_client, but since starting the kernel runs
|
484
405
|
# in another thread, we do the import here first to prevent import deadlocks,
|
@@ -583,6 +504,8 @@ class Kernel:
|
|
583
504
|
if callable(
|
584
505
|
add_output := self.msg_id_callbacks[msg_id]["add_output"]
|
585
506
|
) and (data := payload.get("data", {})):
|
507
|
+
import nbformat
|
508
|
+
|
586
509
|
add_output(
|
587
510
|
nbformat.v4.new_output(
|
588
511
|
"execute_result",
|
@@ -709,18 +632,24 @@ class Kernel:
|
|
709
632
|
"""Call callbacks for an iopub display data response."""
|
710
633
|
msg_id = rsp.get("parent_header", {}).get("msg_id")
|
711
634
|
if callable(add_output := self.msg_id_callbacks[msg_id]["add_output"]):
|
635
|
+
import nbformat
|
636
|
+
|
712
637
|
add_output(nbformat.v4.output_from_msg(rsp), own)
|
713
638
|
|
714
639
|
def on_iopub_update_display_data(self, rsp: dict[str, Any], own: bool) -> None:
|
715
640
|
"""Call callbacks for an iopub update display data response."""
|
716
641
|
msg_id = rsp.get("parent_header", {}).get("msg_id")
|
717
642
|
if callable(add_output := self.msg_id_callbacks[msg_id]["add_output"]):
|
643
|
+
import nbformat
|
644
|
+
|
718
645
|
add_output(nbformat.v4.output_from_msg(rsp), own)
|
719
646
|
|
720
647
|
def on_iopub_execute_result(self, rsp: dict[str, Any], own: bool) -> None:
|
721
648
|
"""Call callbacks for an iopub execute result response."""
|
722
649
|
msg_id = rsp.get("parent_header", {}).get("msg_id")
|
723
650
|
if callable(add_output := self.msg_id_callbacks[msg_id]["add_output"]):
|
651
|
+
import nbformat
|
652
|
+
|
724
653
|
add_output(nbformat.v4.output_from_msg(rsp), own)
|
725
654
|
|
726
655
|
if (execution_count := rsp.get("content", {}).get("execution_count")) and (
|
@@ -736,6 +665,8 @@ class Kernel:
|
|
736
665
|
"""Call callbacks for an iopub error response."""
|
737
666
|
msg_id = rsp.get("parent_header", {}).get("msg_id", "")
|
738
667
|
if callable(add_output := self.msg_id_callbacks[msg_id].get("add_output")):
|
668
|
+
import nbformat
|
669
|
+
|
739
670
|
add_output(nbformat.v4.output_from_msg(rsp), own)
|
740
671
|
if callable(done := self.msg_id_callbacks[msg_id].get("done")):
|
741
672
|
done(rsp.get("content", {}))
|
@@ -744,6 +675,8 @@ class Kernel:
|
|
744
675
|
"""Call callbacks for an iopub stream response."""
|
745
676
|
msg_id = rsp.get("parent_header", {}).get("msg_id")
|
746
677
|
if callable(add_output := self.msg_id_callbacks[msg_id]["add_output"]):
|
678
|
+
import nbformat
|
679
|
+
|
747
680
|
add_output(nbformat.v4.output_from_msg(rsp), own)
|
748
681
|
|
749
682
|
def on_iopub_clear_output(self, rsp: dict[str, Any], own: bool) -> None:
|
@@ -1145,6 +1078,8 @@ class Kernel:
|
|
1145
1078
|
kernel's event loop to finish.
|
1146
1079
|
"""
|
1147
1080
|
if self.km.has_kernel:
|
1081
|
+
from jupyter_client import KernelManager
|
1082
|
+
|
1148
1083
|
log.debug("Interrupting kernel %s", self.id)
|
1149
1084
|
KernelManager.interrupt_kernel(self.km)
|
1150
1085
|
|
@@ -1180,6 +1115,8 @@ class Kernel:
|
|
1180
1115
|
cb: Callback to run once restarted
|
1181
1116
|
|
1182
1117
|
"""
|
1118
|
+
from euporie.core.kernel.manager import EuporieKernelManager
|
1119
|
+
|
1183
1120
|
self.connection_file = connection_file
|
1184
1121
|
self.status = "starting"
|
1185
1122
|
|