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.
Files changed (131) hide show
  1. euporie/console/_commands.py +143 -0
  2. euporie/console/_settings.py +58 -0
  3. euporie/console/app.py +25 -71
  4. euporie/console/tabs/console.py +58 -62
  5. euporie/core/__init__.py +1 -1
  6. euporie/core/__main__.py +28 -11
  7. euporie/core/_settings.py +109 -0
  8. euporie/core/app/__init__.py +3 -0
  9. euporie/core/app/_commands.py +95 -0
  10. euporie/core/app/_settings.py +457 -0
  11. euporie/core/{app.py → app/app.py} +212 -576
  12. euporie/core/app/base.py +51 -0
  13. euporie/core/{current.py → app/current.py} +13 -4
  14. euporie/core/app/cursor.py +35 -0
  15. euporie/core/app/dummy.py +12 -0
  16. euporie/core/app/launch.py +28 -0
  17. euporie/core/bars/__init__.py +11 -0
  18. euporie/core/bars/command.py +205 -0
  19. euporie/core/bars/menu.py +258 -0
  20. euporie/core/{widgets → bars}/search.py +20 -16
  21. euporie/core/{widgets → bars}/status.py +6 -23
  22. euporie/core/clipboard.py +19 -80
  23. euporie/core/comm/base.py +8 -6
  24. euporie/core/comm/ipywidgets.py +16 -7
  25. euporie/core/comm/registry.py +2 -1
  26. euporie/core/commands.py +10 -20
  27. euporie/core/completion.py +3 -2
  28. euporie/core/config.py +368 -341
  29. euporie/core/convert/__init__.py +0 -30
  30. euporie/core/convert/datum.py +116 -53
  31. euporie/core/convert/formats/__init__.py +31 -0
  32. euporie/core/convert/formats/ansi.py +9 -23
  33. euporie/core/convert/formats/common.py +11 -23
  34. euporie/core/convert/formats/html.py +45 -40
  35. euporie/core/convert/formats/pil.py +1 -1
  36. euporie/core/convert/formats/png.py +3 -5
  37. euporie/core/convert/formats/sixel.py +3 -3
  38. euporie/core/convert/registry.py +4 -6
  39. euporie/core/convert/utils.py +41 -4
  40. euporie/core/diagnostics.py +2 -2
  41. euporie/core/filters.py +98 -40
  42. euporie/core/format.py +2 -3
  43. euporie/core/ft/ansi.py +1 -1
  44. euporie/core/ft/html.py +12 -21
  45. euporie/core/ft/table.py +1 -3
  46. euporie/core/ft/utils.py +4 -1
  47. euporie/core/graphics.py +386 -133
  48. euporie/core/history.py +2 -2
  49. euporie/core/inspection.py +3 -2
  50. euporie/core/io.py +207 -28
  51. euporie/core/kernel/__init__.py +1 -0
  52. euporie/core/{kernel.py → kernel/client.py} +45 -108
  53. euporie/core/kernel/manager.py +114 -0
  54. euporie/core/key_binding/bindings/__init__.py +1 -8
  55. euporie/core/key_binding/bindings/basic.py +47 -7
  56. euporie/core/key_binding/bindings/completion.py +3 -8
  57. euporie/core/key_binding/bindings/micro.py +1 -6
  58. euporie/core/key_binding/bindings/mouse.py +2 -2
  59. euporie/core/key_binding/bindings/terminal.py +193 -0
  60. euporie/core/key_binding/key_processor.py +43 -2
  61. euporie/core/key_binding/registry.py +2 -0
  62. euporie/core/key_binding/utils.py +22 -2
  63. euporie/core/keys.py +7156 -93
  64. euporie/core/layout/cache.py +3 -3
  65. euporie/core/layout/containers.py +48 -4
  66. euporie/core/layout/decor.py +2 -2
  67. euporie/core/layout/mouse.py +1 -1
  68. euporie/core/layout/print.py +2 -1
  69. euporie/core/layout/scroll.py +39 -34
  70. euporie/core/log.py +76 -64
  71. euporie/core/lsp.py +118 -24
  72. euporie/core/margins.py +1 -1
  73. euporie/core/path.py +62 -13
  74. euporie/core/renderer.py +58 -17
  75. euporie/core/style.py +57 -39
  76. euporie/core/suggest.py +103 -85
  77. euporie/core/tabs/__init__.py +32 -0
  78. euporie/core/tabs/_settings.py +113 -0
  79. euporie/core/tabs/base.py +80 -470
  80. euporie/core/tabs/kernel.py +419 -0
  81. euporie/core/tabs/notebook.py +24 -101
  82. euporie/core/utils.py +92 -15
  83. euporie/core/validation.py +1 -1
  84. euporie/core/widgets/_settings.py +188 -0
  85. euporie/core/widgets/cell.py +19 -50
  86. euporie/core/widgets/cell_outputs.py +25 -36
  87. euporie/core/widgets/decor.py +11 -41
  88. euporie/core/widgets/dialog.py +62 -27
  89. euporie/core/widgets/display.py +12 -15
  90. euporie/core/widgets/file_browser.py +2 -23
  91. euporie/core/widgets/forms.py +8 -5
  92. euporie/core/widgets/inputs.py +13 -70
  93. euporie/core/widgets/layout.py +2 -1
  94. euporie/core/widgets/logo.py +49 -0
  95. euporie/core/widgets/menu.py +10 -8
  96. euporie/core/widgets/pager.py +6 -10
  97. euporie/core/widgets/palette.py +6 -6
  98. euporie/hub/app.py +52 -35
  99. euporie/notebook/_commands.py +24 -0
  100. euporie/notebook/_settings.py +107 -0
  101. euporie/notebook/app.py +49 -171
  102. euporie/notebook/filters.py +1 -1
  103. euporie/notebook/tabs/__init__.py +46 -7
  104. euporie/notebook/tabs/_commands.py +714 -0
  105. euporie/notebook/tabs/_settings.py +32 -0
  106. euporie/notebook/tabs/display.py +4 -4
  107. euporie/notebook/tabs/edit.py +11 -44
  108. euporie/notebook/tabs/json.py +5 -5
  109. euporie/notebook/tabs/log.py +1 -18
  110. euporie/notebook/tabs/notebook.py +11 -660
  111. euporie/notebook/widgets/_commands.py +11 -0
  112. euporie/notebook/widgets/_settings.py +19 -0
  113. euporie/notebook/widgets/side_bar.py +14 -34
  114. euporie/preview/_settings.py +104 -0
  115. euporie/preview/app.py +6 -31
  116. euporie/preview/tabs/notebook.py +6 -72
  117. euporie/web/__init__.py +1 -0
  118. euporie/web/tabs/__init__.py +14 -0
  119. euporie/web/tabs/web.py +11 -6
  120. euporie/web/widgets/__init__.py +1 -0
  121. euporie/web/widgets/webview.py +5 -15
  122. {euporie-2.8.4.dist-info → euporie-2.8.6.dist-info}/METADATA +10 -8
  123. euporie-2.8.6.dist-info/RECORD +175 -0
  124. {euporie-2.8.4.dist-info → euporie-2.8.6.dist-info}/WHEEL +1 -1
  125. {euporie-2.8.4.dist-info → euporie-2.8.6.dist-info}/entry_points.txt +2 -2
  126. {euporie-2.8.4.dist-info → euporie-2.8.6.dist-info}/licenses/LICENSE +1 -1
  127. euporie/core/launch.py +0 -64
  128. euporie/core/terminal.py +0 -522
  129. euporie-2.8.4.dist-info/RECORD +0 -147
  130. {euporie-2.8.4.data → euporie-2.8.6.data}/data/share/applications/euporie-console.desktop +0 -0
  131. {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 typing import AsyncGenerator, Iterable
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
 
@@ -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, Sequence
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 typing import TYPE_CHECKING
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 typing import IO, Any, Callable, ContextManager, TextIO
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
- _response_prefix_re = re.compile(
22
- r"""^\x1b(
23
- \][^\\\x07]* # Operating System Commands
24
- |
25
- _[^\\]* # Application Program Command
26
- |
27
- \[\?[\d;]* # Primary device attribute responses
28
- |
29
- P[ -~]*(\x1b|x1b\\)?
30
- )\Z""",
31
- re.VERBOSE,
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 = super().__missing__(prefix)
39
- if not result and _response_prefix_re.match(prefix):
40
- result = True
41
- self[prefix] = result
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.queries: dict[Keys, re.Pattern] = {}
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.queries.items():
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(self, input_ready_callback: Callable[[], None]) -> ContextManager[None]:
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 enable_mouse_support(self) -> None:
78
- """Additionally enable SGR-pixel mouse positioning."""
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 disable_mouse_support(self) -> None:
83
- """Additionally disable SGR-pixel mouse positioning."""
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, Coroutine, TextIO
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.comm.base import KernelTab
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
- while True:
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("Kernel '%s' could not start", self.km.kernel_name)
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
- break
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