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
@@ -0,0 +1,420 @@
|
|
1
|
+
"""Contain kernel tab base class."""
|
2
|
+
|
3
|
+
from __future__ import annotations
|
4
|
+
|
5
|
+
import asyncio
|
6
|
+
import logging
|
7
|
+
from abc import ABCMeta
|
8
|
+
from collections import deque
|
9
|
+
from functools import partial
|
10
|
+
from typing import TYPE_CHECKING
|
11
|
+
from weakref import WeakKeyDictionary
|
12
|
+
|
13
|
+
from prompt_toolkit.auto_suggest import DummyAutoSuggest
|
14
|
+
from prompt_toolkit.completion.base import (
|
15
|
+
DynamicCompleter,
|
16
|
+
_MergedCompleter,
|
17
|
+
)
|
18
|
+
from prompt_toolkit.history import DummyHistory, InMemoryHistory
|
19
|
+
|
20
|
+
from euporie.core.app.current import get_app
|
21
|
+
from euporie.core.comm.registry import open_comm
|
22
|
+
from euporie.core.commands import add_cmd
|
23
|
+
from euporie.core.completion import DeduplicateCompleter, KernelCompleter, LspCompleter
|
24
|
+
from euporie.core.diagnostics import Report
|
25
|
+
from euporie.core.filters import kernel_tab_has_focus
|
26
|
+
from euporie.core.format import LspFormatter
|
27
|
+
from euporie.core.history import KernelHistory
|
28
|
+
from euporie.core.inspection import (
|
29
|
+
FirstInspector,
|
30
|
+
KernelInspector,
|
31
|
+
LspInspector,
|
32
|
+
)
|
33
|
+
from euporie.core.kernel.client import Kernel, MsgCallbacks
|
34
|
+
from euporie.core.suggest import HistoryAutoSuggest
|
35
|
+
from euporie.core.tabs.base import Tab
|
36
|
+
from euporie.core.utils import run_in_thread_with_context
|
37
|
+
|
38
|
+
if TYPE_CHECKING:
|
39
|
+
from collections.abc import Sequence
|
40
|
+
from pathlib import Path
|
41
|
+
from typing import Any, Callable
|
42
|
+
|
43
|
+
from prompt_toolkit.auto_suggest import AutoSuggest
|
44
|
+
from prompt_toolkit.completion.base import Completer
|
45
|
+
from prompt_toolkit.history import History
|
46
|
+
|
47
|
+
from euporie.core.app.app import BaseApp
|
48
|
+
from euporie.core.comm.base import Comm
|
49
|
+
from euporie.core.format import Formatter
|
50
|
+
from euporie.core.inspection import Inspector
|
51
|
+
from euporie.core.lsp import LspClient
|
52
|
+
from euporie.core.widgets.inputs import KernelInput
|
53
|
+
|
54
|
+
log = logging.getLogger(__name__)
|
55
|
+
|
56
|
+
|
57
|
+
class KernelTab(Tab, metaclass=ABCMeta):
|
58
|
+
"""A Tab which connects to a kernel."""
|
59
|
+
|
60
|
+
kernel: Kernel
|
61
|
+
kernel_language: str
|
62
|
+
_metadata: dict[str, Any]
|
63
|
+
bg_init = False
|
64
|
+
|
65
|
+
default_callbacks: MsgCallbacks
|
66
|
+
allow_stdin: bool
|
67
|
+
|
68
|
+
def __init__(
|
69
|
+
self,
|
70
|
+
app: BaseApp,
|
71
|
+
path: Path | None = None,
|
72
|
+
kernel: Kernel | None = None,
|
73
|
+
comms: dict[str, Comm] | None = None,
|
74
|
+
use_kernel_history: bool = False,
|
75
|
+
connection_file: Path | None = None,
|
76
|
+
) -> None:
|
77
|
+
"""Create a new instance of a tab with a kernel."""
|
78
|
+
# Init tab
|
79
|
+
super().__init__(app, path)
|
80
|
+
|
81
|
+
self.lsps: list[LspClient] = []
|
82
|
+
self.history: History = DummyHistory()
|
83
|
+
self.inspectors: list[Inspector] = []
|
84
|
+
self.inspector = FirstInspector(lambda: self.inspectors)
|
85
|
+
self.suggester: AutoSuggest = DummyAutoSuggest()
|
86
|
+
self.completers: list[Completer] = []
|
87
|
+
self.completer = DeduplicateCompleter(
|
88
|
+
DynamicCompleter(lambda: _MergedCompleter(self.completers))
|
89
|
+
)
|
90
|
+
self.formatters: list[Formatter] = self.app.formatters
|
91
|
+
self.reports: WeakKeyDictionary[LspClient, Report] = WeakKeyDictionary()
|
92
|
+
|
93
|
+
# The client-side comm states
|
94
|
+
self.comms: dict[str, Comm] = {}
|
95
|
+
# The current kernel input
|
96
|
+
self._current_input: KernelInput | None = None
|
97
|
+
|
98
|
+
if self.bg_init:
|
99
|
+
# Load kernel in a background thread
|
100
|
+
run_in_thread_with_context(
|
101
|
+
partial(
|
102
|
+
self.init_kernel, kernel, comms, use_kernel_history, connection_file
|
103
|
+
)
|
104
|
+
)
|
105
|
+
else:
|
106
|
+
self.init_kernel(kernel, comms, use_kernel_history, connection_file)
|
107
|
+
|
108
|
+
async def load_lsps(self) -> None:
|
109
|
+
"""Load the LSP clients."""
|
110
|
+
path = self.path
|
111
|
+
|
112
|
+
# Load list of LSP clients for the tab's language
|
113
|
+
self.lsps.extend(self.app.get_language_lsps(self.language))
|
114
|
+
|
115
|
+
# Wait for all lsps to be initialized, and setup hooks as they become ready
|
116
|
+
async def _await_load(lsp: LspClient) -> LspClient:
|
117
|
+
await lsp.initialized.wait()
|
118
|
+
return lsp
|
119
|
+
|
120
|
+
for ready in asyncio.as_completed([_await_load(lsp) for lsp in self.lsps]):
|
121
|
+
lsp = await ready
|
122
|
+
# Apply open, save, and close hooks to the tab
|
123
|
+
change_handler = partial(lambda lsp, tab: self.lsp_change_handler(lsp), lsp)
|
124
|
+
close_handler = partial(lambda lsp, tab: self.lsp_close_handler(lsp), lsp)
|
125
|
+
before_save_handler = partial(
|
126
|
+
lambda lsp, tab: self.lsp_before_save_handler(lsp), lsp
|
127
|
+
)
|
128
|
+
after_save_handler = partial(
|
129
|
+
lambda lsp, tab: self.lsp_after_save_handler(lsp), lsp
|
130
|
+
)
|
131
|
+
|
132
|
+
self.on_close += close_handler
|
133
|
+
self.on_change += change_handler
|
134
|
+
self.before_save += before_save_handler
|
135
|
+
self.after_save += after_save_handler
|
136
|
+
|
137
|
+
# Listen for LSP diagnostics
|
138
|
+
lsp.on_diagnostics += self.lsp_update_diagnostics
|
139
|
+
|
140
|
+
# Add completer
|
141
|
+
completer = LspCompleter(lsp=lsp, path=path)
|
142
|
+
self.completers.append(completer)
|
143
|
+
|
144
|
+
# Add inspector
|
145
|
+
inspector = LspInspector(lsp, path)
|
146
|
+
self.inspectors.append(inspector)
|
147
|
+
|
148
|
+
# Add formatter
|
149
|
+
formatter = LspFormatter(lsp, path)
|
150
|
+
self.formatters.append(formatter)
|
151
|
+
|
152
|
+
# Remove hooks if the LSP exits
|
153
|
+
def lsp_unload(lsp: LspClient) -> None:
|
154
|
+
self.on_change -= change_handler # noqa: B023
|
155
|
+
self.before_save -= before_save_handler # noqa: B023
|
156
|
+
self.after_save -= after_save_handler # noqa: B023
|
157
|
+
self.on_close -= close_handler # noqa: B023
|
158
|
+
if completer in self.completers: # noqa: B023
|
159
|
+
self.completers.remove(completer) # noqa: B023
|
160
|
+
if inspector in self.completers: # noqa: B023
|
161
|
+
self.inspectors.remove(inspector) # noqa: B023
|
162
|
+
if formatter in self.completers: # noqa: B023
|
163
|
+
self.formatters.remove(formatter) # noqa: B023
|
164
|
+
if completer in self.completers: # noqa: B023
|
165
|
+
self.completers.remove(completer) # noqa: B023
|
166
|
+
if inspector in self.inspectors: # noqa: B023
|
167
|
+
self.inspectors.remove(inspector) # noqa: B023
|
168
|
+
if formatter in self.formatters: # noqa: B023
|
169
|
+
self.formatters.remove(formatter) # noqa: B023
|
170
|
+
|
171
|
+
lsp.on_exit += lsp_unload
|
172
|
+
|
173
|
+
# Remove the lsp exit handler if this tab closes
|
174
|
+
self.on_close += lambda tab: (
|
175
|
+
(lsp.on_exit.__isub__(lsp_unload) and None) or None # noqa: B023
|
176
|
+
) # Magical typing
|
177
|
+
|
178
|
+
# Tell the LSP we have an open file
|
179
|
+
self.lsp_open_handler(lsp)
|
180
|
+
|
181
|
+
def pre_init_kernel(self) -> None:
|
182
|
+
"""Run stuff before the kernel is loaded."""
|
183
|
+
|
184
|
+
def post_init_kernel(self) -> None:
|
185
|
+
"""Run stuff after the kernel is loaded."""
|
186
|
+
|
187
|
+
def init_kernel(
|
188
|
+
self,
|
189
|
+
kernel: Kernel | None = None,
|
190
|
+
comms: dict[str, Comm] | None = None,
|
191
|
+
use_kernel_history: bool = False,
|
192
|
+
connection_file: Path | None = None,
|
193
|
+
) -> None:
|
194
|
+
"""Set up the tab's kernel and related components."""
|
195
|
+
self.pre_init_kernel()
|
196
|
+
|
197
|
+
self.kernel_queue: deque[Callable] = deque()
|
198
|
+
|
199
|
+
if kernel:
|
200
|
+
self.kernel = kernel
|
201
|
+
self.kernel.default_callbacks = self.default_callbacks
|
202
|
+
else:
|
203
|
+
self.kernel = Kernel(
|
204
|
+
kernel_tab=self,
|
205
|
+
allow_stdin=self.allow_stdin,
|
206
|
+
default_callbacks=self.default_callbacks,
|
207
|
+
connection_file=connection_file,
|
208
|
+
)
|
209
|
+
self.comms = comms or {} # The client-side comm states
|
210
|
+
self.completers.append(KernelCompleter(self.kernel))
|
211
|
+
self.inspectors.append(KernelInspector(self.kernel))
|
212
|
+
self.use_kernel_history = use_kernel_history
|
213
|
+
self.history = (
|
214
|
+
KernelHistory(self.kernel) if use_kernel_history else InMemoryHistory()
|
215
|
+
)
|
216
|
+
self.suggester = HistoryAutoSuggest(self.history)
|
217
|
+
|
218
|
+
self.app.create_background_task(self.load_lsps())
|
219
|
+
|
220
|
+
self.post_init_kernel()
|
221
|
+
|
222
|
+
def close(self, cb: Callable | None = None) -> None:
|
223
|
+
"""Shut down kernel when tab is closed."""
|
224
|
+
if hasattr(self, "kernel"):
|
225
|
+
self.kernel.shutdown()
|
226
|
+
super().close(cb)
|
227
|
+
|
228
|
+
def interrupt_kernel(self) -> None:
|
229
|
+
"""Interrupt the current `Notebook`'s kernel."""
|
230
|
+
self.kernel.interrupt()
|
231
|
+
|
232
|
+
def restart_kernel(self, cb: Callable | None = None) -> None:
|
233
|
+
"""Restart the current `Notebook`'s kernel."""
|
234
|
+
|
235
|
+
def _cb(result: dict[str, Any]) -> None:
|
236
|
+
if callable(cb):
|
237
|
+
cb()
|
238
|
+
|
239
|
+
if confirm := self.app.dialogs.get("confirm"):
|
240
|
+
confirm.show(
|
241
|
+
message="Are you sure you want to restart the kernel?",
|
242
|
+
cb=partial(self.kernel.restart, cb=_cb),
|
243
|
+
)
|
244
|
+
else:
|
245
|
+
self.kernel.restart(cb=_cb)
|
246
|
+
|
247
|
+
def kernel_started(self, result: dict[str, Any] | None = None) -> None:
|
248
|
+
"""Task to run when the kernel has started."""
|
249
|
+
# Check kernel has not failed
|
250
|
+
if not self.kernel_name or self.kernel.missing:
|
251
|
+
if not self.kernel_name:
|
252
|
+
msg = "No kernel selected"
|
253
|
+
else:
|
254
|
+
msg = f"Kernel '{self.kernel_display_name}' not installed"
|
255
|
+
self.change_kernel(
|
256
|
+
msg=msg,
|
257
|
+
startup=True,
|
258
|
+
)
|
259
|
+
|
260
|
+
elif self.kernel.status == "error":
|
261
|
+
self.report_kernel_error(self.kernel.error)
|
262
|
+
|
263
|
+
else:
|
264
|
+
# Wait for an idle kernel
|
265
|
+
if self.kernel.status != "idle":
|
266
|
+
self.kernel.wait_for_status("idle")
|
267
|
+
|
268
|
+
# Load widget comm info
|
269
|
+
# self.kernel.comm_info(target_name="jupyter.widget")
|
270
|
+
|
271
|
+
# Load kernel info
|
272
|
+
self.kernel.info(set_kernel_info=self.set_kernel_info)
|
273
|
+
|
274
|
+
# Load kernel history
|
275
|
+
if self.use_kernel_history:
|
276
|
+
self.app.create_background_task(self.load_history())
|
277
|
+
|
278
|
+
# Run queued kernel tasks when the kernel is idle
|
279
|
+
log.debug("Running %d kernel tasks", len(self.kernel_queue))
|
280
|
+
while self.kernel_queue:
|
281
|
+
self.kernel_queue.popleft()()
|
282
|
+
|
283
|
+
self.app.invalidate()
|
284
|
+
|
285
|
+
def report_kernel_error(self, error: Exception | None) -> None:
|
286
|
+
"""Report a kernel error to the user."""
|
287
|
+
log.debug("Kernel error", exc_info=error)
|
288
|
+
|
289
|
+
async def load_history(self) -> None:
|
290
|
+
"""Load kernel history."""
|
291
|
+
try:
|
292
|
+
await self.history.load().__anext__()
|
293
|
+
except StopAsyncIteration:
|
294
|
+
pass
|
295
|
+
|
296
|
+
@property
|
297
|
+
def metadata(self) -> dict[str, Any]:
|
298
|
+
"""Return a dictionary to hold notebook / kernel metadata."""
|
299
|
+
return self._metadata
|
300
|
+
|
301
|
+
@property
|
302
|
+
def kernel_name(self) -> str:
|
303
|
+
"""Return the name of the kernel defined in the notebook JSON."""
|
304
|
+
return self.metadata.get("kernelspec", {}).get(
|
305
|
+
"name", self.app.config.kernel_name
|
306
|
+
)
|
307
|
+
|
308
|
+
@kernel_name.setter
|
309
|
+
def kernel_name(self, value: str) -> None:
|
310
|
+
"""Return the name of the kernel defined in the notebook JSON."""
|
311
|
+
self.metadata.setdefault("kernelspec", {})["name"] = value
|
312
|
+
|
313
|
+
@property
|
314
|
+
def language(self) -> str:
|
315
|
+
"""Return the name of the kernel defined in the notebook JSON."""
|
316
|
+
return self.metadata.get("kernelspec", {}).get("language")
|
317
|
+
|
318
|
+
@property
|
319
|
+
def kernel_display_name(self) -> str:
|
320
|
+
"""Return the display name of the kernel defined in the notebook JSON."""
|
321
|
+
return self.metadata.get("kernelspec", {}).get("display_name", self.kernel_name)
|
322
|
+
|
323
|
+
@property
|
324
|
+
def kernel_lang_file_ext(self) -> str:
|
325
|
+
"""Return the display name of the kernel defined in the notebook JSON."""
|
326
|
+
return self.metadata.get("language_info", {}).get("file_extension", ".py")
|
327
|
+
|
328
|
+
@property
|
329
|
+
def current_input(self) -> KernelInput:
|
330
|
+
"""Return the currently active kernel input, if any."""
|
331
|
+
from euporie.core.widgets.inputs import KernelInput
|
332
|
+
|
333
|
+
return self._current_input or KernelInput(self)
|
334
|
+
|
335
|
+
def set_kernel_info(self, info: dict) -> None:
|
336
|
+
"""Handle kernel info requests."""
|
337
|
+
self.metadata["language_info"] = info.get("language_info", {})
|
338
|
+
|
339
|
+
def change_kernel(self, msg: str | None = None, startup: bool = False) -> None:
|
340
|
+
"""Prompt the user to select a new kernel."""
|
341
|
+
kernel_specs = self.kernel.specs
|
342
|
+
|
343
|
+
# Warn the user if no kernels are installed
|
344
|
+
if not kernel_specs:
|
345
|
+
if startup and "no-kernels" in self.app.dialogs:
|
346
|
+
self.app.dialogs["no-kernels"].show()
|
347
|
+
return
|
348
|
+
|
349
|
+
# Automatically select the only kernel if there is only one
|
350
|
+
if startup and len(kernel_specs) == 1:
|
351
|
+
self.kernel.change(next(iter(kernel_specs)))
|
352
|
+
return
|
353
|
+
|
354
|
+
self.app.dialogs["change-kernel"].show(
|
355
|
+
tab=self, message=msg, kernel_specs=kernel_specs
|
356
|
+
)
|
357
|
+
|
358
|
+
def comm_open(self, content: dict, buffers: Sequence[bytes]) -> None:
|
359
|
+
"""Register a new kernel Comm object in the notebook."""
|
360
|
+
comm_id = str(content.get("comm_id"))
|
361
|
+
self.comms[comm_id] = open_comm(
|
362
|
+
comm_container=self, content=content, buffers=buffers
|
363
|
+
)
|
364
|
+
|
365
|
+
def comm_msg(self, content: dict, buffers: Sequence[bytes]) -> None:
|
366
|
+
"""Respond to a Comm message from the kernel."""
|
367
|
+
comm_id = str(content.get("comm_id"))
|
368
|
+
if comm := self.comms.get(comm_id):
|
369
|
+
comm.process_data(content.get("data", {}), buffers)
|
370
|
+
|
371
|
+
def comm_close(self, content: dict, buffers: Sequence[bytes]) -> None:
|
372
|
+
"""Close a notebook Comm."""
|
373
|
+
comm_id = content.get("comm_id")
|
374
|
+
if comm_id in self.comms:
|
375
|
+
del self.comms[comm_id]
|
376
|
+
|
377
|
+
def lsp_open_handler(self, lsp: LspClient) -> None:
|
378
|
+
"""Tell the LSP we opened a file."""
|
379
|
+
lsp.open_doc(
|
380
|
+
path=self.path, language=self.language, text=self.current_input.buffer.text
|
381
|
+
)
|
382
|
+
|
383
|
+
def lsp_change_handler(self, lsp: LspClient) -> None:
|
384
|
+
"""Tell the LSP server a file has changed."""
|
385
|
+
lsp.change_doc(
|
386
|
+
path=self.path,
|
387
|
+
language=self.language,
|
388
|
+
text=self.current_input.buffer.text,
|
389
|
+
)
|
390
|
+
|
391
|
+
def lsp_before_save_handler(self, lsp: LspClient) -> None:
|
392
|
+
"""Tell the the LSP we are about to save a document."""
|
393
|
+
lsp.will_save_doc(self.path)
|
394
|
+
|
395
|
+
def lsp_after_save_handler(self, lsp: LspClient) -> None:
|
396
|
+
"""Tell the the LSP we saved a document."""
|
397
|
+
lsp.save_doc(self.path, text=self.current_input.buffer.text)
|
398
|
+
|
399
|
+
def lsp_close_handler(self, lsp: LspClient) -> None:
|
400
|
+
"""Tell the LSP we opened a file."""
|
401
|
+
lsp.close_doc(path=self.path)
|
402
|
+
|
403
|
+
def lsp_update_diagnostics(self, lsp: LspClient) -> None:
|
404
|
+
"""Process a new diagnostic report from the LSP."""
|
405
|
+
if (diagnostics := lsp.reports.pop(self.path.as_uri(), None)) is not None:
|
406
|
+
self.reports[lsp] = Report.from_lsp(self.current_input.text, diagnostics)
|
407
|
+
self.app.invalidate()
|
408
|
+
|
409
|
+
def report(self) -> Report:
|
410
|
+
"""Return the current diagnostic reports."""
|
411
|
+
return Report.from_reports(*self.reports.values())
|
412
|
+
|
413
|
+
# ################################### Commands ####################################
|
414
|
+
|
415
|
+
@staticmethod
|
416
|
+
@add_cmd(filter=kernel_tab_has_focus)
|
417
|
+
def _change_kernel() -> None:
|
418
|
+
"""Change the notebook's kernel."""
|
419
|
+
if isinstance(kt := get_app().tab, KernelTab):
|
420
|
+
kt.change_kernel()
|
euporie/core/tabs/notebook.py
CHANGED
@@ -1,4 +1,4 @@
|
|
1
|
-
"""Contain the
|
1
|
+
"""Contain the base class for a notebook tabs."""
|
2
2
|
|
3
3
|
from __future__ import annotations
|
4
4
|
|
@@ -9,15 +9,14 @@ from functools import partial
|
|
9
9
|
from typing import TYPE_CHECKING
|
10
10
|
|
11
11
|
import nbformat
|
12
|
-
from prompt_toolkit.filters import Never
|
12
|
+
from prompt_toolkit.filters import Never
|
13
13
|
|
14
14
|
from euporie.core.comm.registry import open_comm
|
15
15
|
from euporie.core.commands import get_cmd
|
16
|
-
from euporie.core.
|
17
|
-
from euporie.core.kernel import MsgCallbacks
|
16
|
+
from euporie.core.io import edit_in_editor
|
17
|
+
from euporie.core.kernel.client import MsgCallbacks
|
18
18
|
from euporie.core.path import UntitledPath
|
19
|
-
from euporie.core.tabs.
|
20
|
-
from euporie.core.terminal import edit_in_editor
|
19
|
+
from euporie.core.tabs.kernel import KernelTab
|
21
20
|
from euporie.core.widgets.cell import Cell, get_cell_id
|
22
21
|
|
23
22
|
try:
|
@@ -34,9 +33,9 @@ if TYPE_CHECKING:
|
|
34
33
|
from prompt_toolkit.filters import Filter
|
35
34
|
from prompt_toolkit.layout.containers import AnyContainer
|
36
35
|
|
37
|
-
from euporie.core.app import BaseApp
|
36
|
+
from euporie.core.app.app import BaseApp
|
38
37
|
from euporie.core.comm.base import Comm
|
39
|
-
from euporie.core.kernel import Kernel
|
38
|
+
from euporie.core.kernel.client import Kernel
|
40
39
|
from euporie.core.lsp import LspClient
|
41
40
|
from euporie.core.widgets.inputs import KernelInput
|
42
41
|
|
@@ -75,7 +74,7 @@ class BaseNotebook(KernelTab, metaclass=ABCMeta):
|
|
75
74
|
prompt, password
|
76
75
|
),
|
77
76
|
"set_execution_count": lambda n: self.cell.set_execution_count(n),
|
78
|
-
"add_output":
|
77
|
+
"add_output": self.new_output_default,
|
79
78
|
"clear_output": lambda wait: self.cell.clear_output(wait),
|
80
79
|
"set_metadata": lambda path, data: self.cell.set_metadata(path, data),
|
81
80
|
"set_status": self.set_status,
|
@@ -107,21 +106,22 @@ class BaseNotebook(KernelTab, metaclass=ABCMeta):
|
|
107
106
|
|
108
107
|
def pre_init_kernel(self) -> None:
|
109
108
|
"""Run stuff before the kernel is loaded."""
|
109
|
+
super().pre_init_kernel()
|
110
110
|
# Load notebook file
|
111
111
|
self.load()
|
112
112
|
|
113
113
|
def post_init_kernel(self) -> None:
|
114
114
|
"""Load the notebook container after the kernel has been loaded."""
|
115
|
+
super().post_init_kernel()
|
116
|
+
|
115
117
|
# Replace the tab's container
|
116
118
|
prev = self.container
|
117
119
|
self.container = self.load_container()
|
118
120
|
self.loaded = True
|
119
|
-
|
120
121
|
# Update the focus if the old container had focus
|
121
122
|
if self.app.layout.has_focus(prev):
|
122
123
|
self.focus()
|
123
124
|
|
124
|
-
self.app.invalidate()
|
125
125
|
# Load widgets
|
126
126
|
self.load_widgets_from_metadata()
|
127
127
|
|
@@ -306,9 +306,11 @@ class BaseNotebook(KernelTab, metaclass=ABCMeta):
|
|
306
306
|
log.debug("Saving notebook..")
|
307
307
|
self.saving = True
|
308
308
|
self.app.invalidate()
|
309
|
-
|
309
|
+
# Ensure parent path exists
|
310
|
+
parent = self.path.parent
|
311
|
+
parent.mkdir(exist_ok=True, parents=True)
|
310
312
|
# Save to a temp file, then replace the original
|
311
|
-
temp_path =
|
313
|
+
temp_path = parent / f".{self.path.stem}.tmp{self.path.suffix}"
|
312
314
|
log.debug("Using temporary file %s", temp_path.name)
|
313
315
|
try:
|
314
316
|
open_file = temp_path.open("w")
|
@@ -385,6 +387,11 @@ class BaseNotebook(KernelTab, metaclass=ABCMeta):
|
|
385
387
|
done=cell.ran,
|
386
388
|
)
|
387
389
|
|
390
|
+
def new_output_default(self, output_json: dict[str, Any], own: bool) -> None:
|
391
|
+
"""Add a new output without a cell to the currently selected cell."""
|
392
|
+
if self.app.config.show_remote_outputs:
|
393
|
+
self.cell.add_output(output_json, own)
|
394
|
+
|
388
395
|
def load_widgets_from_metadata(self) -> None:
|
389
396
|
"""Load widgets from state saved in notebook metadata."""
|
390
397
|
for comm_id, comm_data in (
|
@@ -455,44 +462,3 @@ class BaseNotebook(KernelTab, metaclass=ABCMeta):
|
|
455
462
|
def lsp_update_diagnostics(self, lsp: LspClient) -> None:
|
456
463
|
"""Process a new diagnostic report from the LSP."""
|
457
464
|
# Do nothing, these are handled by cells
|
458
|
-
|
459
|
-
# ################################### Settings ####################################
|
460
|
-
|
461
|
-
add_setting(
|
462
|
-
name="save_widget_state",
|
463
|
-
flags=["--save-widget-state"],
|
464
|
-
type_=bool,
|
465
|
-
help_="Save a notebook's widget state in the notebook metadata",
|
466
|
-
default=True,
|
467
|
-
description="""
|
468
|
-
When set to ``True``, the state of any widgets in the current notebook will
|
469
|
-
be saves in the notebook's metadata. This enables widgets to be displayed
|
470
|
-
when the notebook is re-opened without having to re-run the notebook.
|
471
|
-
""",
|
472
|
-
)
|
473
|
-
|
474
|
-
add_setting(
|
475
|
-
name="max_notebook_width",
|
476
|
-
flags=["--max-notebook-width"],
|
477
|
-
type_=int,
|
478
|
-
help_="Maximum width of notebooks",
|
479
|
-
default=120,
|
480
|
-
schema={
|
481
|
-
"minimum": 1,
|
482
|
-
},
|
483
|
-
description="""
|
484
|
-
The maximum width at which to display a notebook.
|
485
|
-
""",
|
486
|
-
)
|
487
|
-
|
488
|
-
add_setting(
|
489
|
-
name="expand",
|
490
|
-
flags=["--expand"],
|
491
|
-
type_=bool,
|
492
|
-
help_="Use the full width to display notebooks",
|
493
|
-
default=False,
|
494
|
-
description="""
|
495
|
-
Whether the notebook page should expand to fill the available width
|
496
|
-
""",
|
497
|
-
cmd_filter=~buffer_has_focus,
|
498
|
-
)
|
euporie/core/utils.py
CHANGED
@@ -3,14 +3,18 @@
|
|
3
3
|
from __future__ import annotations
|
4
4
|
|
5
5
|
import contextvars
|
6
|
+
from collections.abc import Sequence
|
7
|
+
from functools import cache
|
6
8
|
from itertools import chain
|
7
9
|
from threading import Thread
|
8
|
-
from typing import TYPE_CHECKING,
|
10
|
+
from typing import TYPE_CHECKING, TypeVar, overload
|
9
11
|
|
10
12
|
from prompt_toolkit.mouse_events import MouseButton, MouseEventType
|
11
13
|
|
12
14
|
if TYPE_CHECKING:
|
13
|
-
from
|
15
|
+
from collections.abc import Iterable
|
16
|
+
from types import ModuleType
|
17
|
+
from typing import Any, Callable
|
14
18
|
|
15
19
|
from prompt_toolkit.key_binding.key_bindings import NotImplementedOrNone
|
16
20
|
from prompt_toolkit.layout.mouse_handlers import MouseHandler
|
@@ -32,12 +36,10 @@ class ChainedList(Sequence[T]):
|
|
32
36
|
return list(chain.from_iterable(self.lists))
|
33
37
|
|
34
38
|
@overload
|
35
|
-
def __getitem__(self, i: int) -> T:
|
36
|
-
...
|
39
|
+
def __getitem__(self, i: int) -> T: ...
|
37
40
|
|
38
41
|
@overload
|
39
|
-
def __getitem__(self, i: slice) -> list[T]:
|
40
|
-
...
|
42
|
+
def __getitem__(self, i: slice) -> list[T]: ...
|
41
43
|
|
42
44
|
def __getitem__(self, i):
|
43
45
|
"""Get an item from the chained lists."""
|
@@ -89,3 +91,93 @@ def run_in_thread_with_context(
|
|
89
91
|
kwargs=kwargs,
|
90
92
|
daemon=daemon,
|
91
93
|
).start()
|
94
|
+
|
95
|
+
|
96
|
+
@cache
|
97
|
+
def root_module(name: str) -> ModuleType:
|
98
|
+
"""Find and load the root module of a given module name by traversing up the module hierarchy.
|
99
|
+
|
100
|
+
This function walks up the module hierarchy until it finds the topmost parent module
|
101
|
+
that has a valid location. It uses Python's importlib machinery to inspect module
|
102
|
+
specifications and load modules.
|
103
|
+
|
104
|
+
Args:
|
105
|
+
name: The name of the module to find the root for (e.g., 'package.subpackage.module')
|
106
|
+
|
107
|
+
Returns:
|
108
|
+
The loaded root module object
|
109
|
+
|
110
|
+
Example:
|
111
|
+
>>> root = root_module("django.contrib.admin")
|
112
|
+
>>> print(root.__name__)
|
113
|
+
'django'
|
114
|
+
|
115
|
+
Note:
|
116
|
+
The function is cached using lru_cache to improve performance for repeated lookups.
|
117
|
+
The function handles both regular packages and frozen modules.
|
118
|
+
"""
|
119
|
+
from importlib.util import find_spec, module_from_spec
|
120
|
+
|
121
|
+
if spec := find_spec(name):
|
122
|
+
while True:
|
123
|
+
if spec.name == spec.parent:
|
124
|
+
try:
|
125
|
+
parent = find_spec("..", spec.parent)
|
126
|
+
except ImportError:
|
127
|
+
break
|
128
|
+
elif spec.parent is not None:
|
129
|
+
parent = find_spec(spec.parent)
|
130
|
+
if (spec and not spec.parent) or (parent and not parent.has_location):
|
131
|
+
break
|
132
|
+
if parent:
|
133
|
+
spec = parent
|
134
|
+
if spec.loader:
|
135
|
+
module = module_from_spec(spec)
|
136
|
+
spec.loader.exec_module(module)
|
137
|
+
return module
|
138
|
+
raise ModuleNotFoundError(name=name)
|
139
|
+
|
140
|
+
|
141
|
+
@cache
|
142
|
+
def import_submodules(root: ModuleType, names: tuple[str]) -> list[ModuleType]:
|
143
|
+
"""Import all submodules with a specific name within a root module's package hierarchy.
|
144
|
+
|
145
|
+
This function walks through all packages under the given root module and imports
|
146
|
+
any submodules that match the specified name. It handles various module types
|
147
|
+
including regular packages, single file modules, and frozen modules.
|
148
|
+
|
149
|
+
Args:
|
150
|
+
root: The root module object to search within
|
151
|
+
names: The specific submodule name to search for
|
152
|
+
|
153
|
+
Returns:
|
154
|
+
A list of imported module objects matching the specified name
|
155
|
+
|
156
|
+
Example:
|
157
|
+
>>> root = import_module("django")
|
158
|
+
>>> admin_modules = import_submodules(root, "admin")
|
159
|
+
>>> print([m.__name__ for m in admin_modules])
|
160
|
+
['django.contrib.admin', 'django.contrib.gis.admin']
|
161
|
+
|
162
|
+
Note:
|
163
|
+
- The function is cached using lru_cache to improve performance for repeated imports
|
164
|
+
- For packages, it searches through __path__
|
165
|
+
- For single file modules, it uses __file__
|
166
|
+
- For frozen modules, it uses the module specification's origin
|
167
|
+
"""
|
168
|
+
from importlib import import_module
|
169
|
+
from pkgutil import walk_packages
|
170
|
+
|
171
|
+
if hasattr(root, "__path__"):
|
172
|
+
path = root.__path__
|
173
|
+
elif hasattr(root, "__file__"):
|
174
|
+
path = [root.__file__] if root.__file__ else []
|
175
|
+
else:
|
176
|
+
# For frozen modules, we need to create a special path
|
177
|
+
spec = root.__spec__
|
178
|
+
path = [spec.origin] if spec and spec.origin else []
|
179
|
+
return [
|
180
|
+
import_module(module_name)
|
181
|
+
for _loader, module_name, _is_pkg in walk_packages(path, f"{root.__name__}.")
|
182
|
+
if module_name.rpartition(".")[2] in names
|
183
|
+
]
|
euporie/core/validation.py
CHANGED
@@ -9,7 +9,7 @@ from prompt_toolkit.validation import ValidationError, Validator
|
|
9
9
|
if TYPE_CHECKING:
|
10
10
|
from prompt_toolkit.document import Document
|
11
11
|
|
12
|
-
from euporie.core.kernel import Kernel
|
12
|
+
from euporie.core.kernel.client import Kernel
|
13
13
|
|
14
14
|
|
15
15
|
class KernelValidator(Validator):
|