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.
Files changed (129) 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 +267 -147
  5. euporie/core/__init__.py +1 -9
  6. euporie/core/__main__.py +31 -5
  7. euporie/core/_settings.py +104 -0
  8. euporie/core/app/__init__.py +3 -0
  9. euporie/core/app/_commands.py +70 -0
  10. euporie/core/app/_settings.py +427 -0
  11. euporie/core/{app.py → app/app.py} +214 -572
  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 +182 -0
  19. euporie/core/bars/menu.py +258 -0
  20. euporie/core/{widgets → bars}/search.py +154 -57
  21. euporie/core/{widgets → bars}/status.py +9 -26
  22. euporie/core/clipboard.py +19 -80
  23. euporie/core/comm/base.py +8 -6
  24. euporie/core/comm/ipywidgets.py +21 -12
  25. euporie/core/comm/registry.py +2 -1
  26. euporie/core/commands.py +11 -5
  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 +131 -60
  31. euporie/core/convert/formats/__init__.py +31 -0
  32. euporie/core/convert/formats/ansi.py +46 -30
  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 +11 -8
  39. euporie/core/convert/utils.py +50 -23
  40. euporie/core/diagnostics.py +2 -2
  41. euporie/core/filters.py +72 -82
  42. euporie/core/format.py +13 -2
  43. euporie/core/ft/ansi.py +1 -1
  44. euporie/core/ft/html.py +36 -36
  45. euporie/core/ft/table.py +1 -3
  46. euporie/core/ft/utils.py +4 -1
  47. euporie/core/graphics.py +216 -124
  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} +100 -139
  53. euporie/core/kernel/manager.py +114 -0
  54. euporie/core/key_binding/bindings/__init__.py +2 -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 +5 -7
  58. euporie/core/key_binding/bindings/mouse.py +26 -24
  59. euporie/core/key_binding/bindings/terminal.py +193 -0
  60. euporie/core/key_binding/bindings/vi.py +46 -0
  61. euporie/core/key_binding/key_processor.py +43 -2
  62. euporie/core/key_binding/registry.py +2 -0
  63. euporie/core/key_binding/utils.py +22 -2
  64. euporie/core/keys.py +7156 -93
  65. euporie/core/layout/cache.py +35 -25
  66. euporie/core/layout/containers.py +280 -74
  67. euporie/core/layout/decor.py +5 -5
  68. euporie/core/layout/mouse.py +1 -1
  69. euporie/core/layout/print.py +16 -3
  70. euporie/core/layout/scroll.py +26 -28
  71. euporie/core/log.py +75 -60
  72. euporie/core/lsp.py +118 -24
  73. euporie/core/margins.py +60 -31
  74. euporie/core/path.py +2 -1
  75. euporie/core/renderer.py +58 -17
  76. euporie/core/style.py +60 -40
  77. euporie/core/suggest.py +103 -85
  78. euporie/core/tabs/__init__.py +34 -0
  79. euporie/core/tabs/_settings.py +113 -0
  80. euporie/core/tabs/base.py +11 -435
  81. euporie/core/tabs/kernel.py +420 -0
  82. euporie/core/tabs/notebook.py +20 -54
  83. euporie/core/utils.py +98 -6
  84. euporie/core/validation.py +1 -1
  85. euporie/core/widgets/_settings.py +188 -0
  86. euporie/core/widgets/cell.py +90 -158
  87. euporie/core/widgets/cell_outputs.py +25 -36
  88. euporie/core/widgets/decor.py +11 -41
  89. euporie/core/widgets/dialog.py +55 -44
  90. euporie/core/widgets/display.py +27 -24
  91. euporie/core/widgets/file_browser.py +5 -26
  92. euporie/core/widgets/forms.py +16 -12
  93. euporie/core/widgets/inputs.py +37 -81
  94. euporie/core/widgets/layout.py +7 -6
  95. euporie/core/widgets/logo.py +49 -0
  96. euporie/core/widgets/menu.py +13 -11
  97. euporie/core/widgets/pager.py +8 -11
  98. euporie/core/widgets/palette.py +6 -6
  99. euporie/hub/app.py +52 -31
  100. euporie/notebook/_commands.py +24 -0
  101. euporie/notebook/_settings.py +107 -0
  102. euporie/notebook/app.py +109 -210
  103. euporie/notebook/filters.py +1 -1
  104. euporie/notebook/tabs/__init__.py +46 -7
  105. euporie/notebook/tabs/_commands.py +714 -0
  106. euporie/notebook/tabs/_settings.py +32 -0
  107. euporie/notebook/tabs/display.py +2 -2
  108. euporie/notebook/tabs/edit.py +12 -7
  109. euporie/notebook/tabs/json.py +3 -3
  110. euporie/notebook/tabs/log.py +1 -18
  111. euporie/notebook/tabs/notebook.py +21 -674
  112. euporie/notebook/widgets/_commands.py +11 -0
  113. euporie/notebook/widgets/_settings.py +19 -0
  114. euporie/notebook/widgets/side_bar.py +14 -34
  115. euporie/preview/_settings.py +104 -0
  116. euporie/preview/app.py +8 -30
  117. euporie/preview/tabs/notebook.py +15 -86
  118. euporie/web/tabs/web.py +4 -6
  119. euporie/web/widgets/webview.py +5 -12
  120. {euporie-2.8.1.dist-info → euporie-2.8.5.dist-info}/METADATA +11 -15
  121. euporie-2.8.5.dist-info/RECORD +172 -0
  122. {euporie-2.8.1.dist-info → euporie-2.8.5.dist-info}/WHEEL +1 -1
  123. {euporie-2.8.1.dist-info → euporie-2.8.5.dist-info}/entry_points.txt +2 -2
  124. {euporie-2.8.1.dist-info → euporie-2.8.5.dist-info}/licenses/LICENSE +1 -1
  125. euporie/core/launch.py +0 -59
  126. euporie/core/terminal.py +0 -527
  127. euporie-2.8.1.dist-info/RECORD +0 -146
  128. {euporie-2.8.1.data → euporie-2.8.5.data}/data/share/applications/euporie-console.desktop +0 -0
  129. {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()
@@ -1,4 +1,4 @@
1
- """Contain the main class for a notebook file."""
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, buffer_has_focus
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.config import add_setting
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.base import KernelTab
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": lambda output_json: self.cell.add_output(output_json),
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 = self.path.parent / f".{self.path.stem}.tmp{self.path.suffix}"
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, Sequence, TypeVar, overload
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 typing import Any, Callable, Iterable
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
+ ]
@@ -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):