euporie 2.8.5__py3-none-any.whl → 2.8.7__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 (74) hide show
  1. euporie/console/app.py +2 -0
  2. euporie/console/tabs/console.py +27 -17
  3. euporie/core/__init__.py +2 -2
  4. euporie/core/__main__.py +2 -2
  5. euporie/core/_settings.py +7 -2
  6. euporie/core/app/_commands.py +20 -12
  7. euporie/core/app/_settings.py +34 -4
  8. euporie/core/app/app.py +31 -18
  9. euporie/core/bars/command.py +53 -27
  10. euporie/core/bars/search.py +43 -2
  11. euporie/core/border.py +7 -2
  12. euporie/core/comm/base.py +2 -2
  13. euporie/core/comm/ipywidgets.py +3 -3
  14. euporie/core/commands.py +44 -24
  15. euporie/core/completion.py +14 -6
  16. euporie/core/convert/datum.py +7 -7
  17. euporie/core/data_structures.py +20 -1
  18. euporie/core/filters.py +40 -9
  19. euporie/core/format.py +2 -3
  20. euporie/core/ft/html.py +47 -40
  21. euporie/core/graphics.py +199 -31
  22. euporie/core/history.py +15 -5
  23. euporie/core/inspection.py +16 -9
  24. euporie/core/kernel/__init__.py +53 -1
  25. euporie/core/kernel/base.py +571 -0
  26. euporie/core/kernel/{client.py → jupyter.py} +173 -430
  27. euporie/core/kernel/{manager.py → jupyter_manager.py} +4 -3
  28. euporie/core/kernel/local.py +694 -0
  29. euporie/core/key_binding/bindings/basic.py +6 -3
  30. euporie/core/keys.py +26 -25
  31. euporie/core/layout/cache.py +31 -7
  32. euporie/core/layout/containers.py +88 -13
  33. euporie/core/layout/scroll.py +69 -170
  34. euporie/core/log.py +2 -5
  35. euporie/core/path.py +61 -13
  36. euporie/core/style.py +2 -1
  37. euporie/core/suggest.py +155 -74
  38. euporie/core/tabs/__init__.py +12 -4
  39. euporie/core/tabs/_commands.py +76 -0
  40. euporie/core/tabs/_settings.py +16 -0
  41. euporie/core/tabs/base.py +89 -9
  42. euporie/core/tabs/kernel.py +83 -38
  43. euporie/core/tabs/notebook.py +28 -76
  44. euporie/core/utils.py +2 -19
  45. euporie/core/validation.py +8 -8
  46. euporie/core/widgets/_settings.py +19 -2
  47. euporie/core/widgets/cell.py +32 -32
  48. euporie/core/widgets/cell_outputs.py +10 -1
  49. euporie/core/widgets/dialog.py +60 -76
  50. euporie/core/widgets/display.py +2 -2
  51. euporie/core/widgets/forms.py +71 -59
  52. euporie/core/widgets/inputs.py +7 -4
  53. euporie/core/widgets/layout.py +281 -93
  54. euporie/core/widgets/menu.py +56 -16
  55. euporie/core/widgets/palette.py +3 -1
  56. euporie/core/widgets/tree.py +86 -76
  57. euporie/notebook/app.py +35 -16
  58. euporie/notebook/tabs/display.py +2 -2
  59. euporie/notebook/tabs/edit.py +11 -46
  60. euporie/notebook/tabs/json.py +8 -4
  61. euporie/notebook/tabs/notebook.py +26 -8
  62. euporie/preview/tabs/notebook.py +17 -13
  63. euporie/web/__init__.py +1 -0
  64. euporie/web/tabs/__init__.py +14 -0
  65. euporie/web/tabs/web.py +30 -5
  66. euporie/web/widgets/__init__.py +1 -0
  67. euporie/web/widgets/webview.py +5 -4
  68. {euporie-2.8.5.dist-info → euporie-2.8.7.dist-info}/METADATA +4 -2
  69. {euporie-2.8.5.dist-info → euporie-2.8.7.dist-info}/RECORD +74 -68
  70. {euporie-2.8.5.dist-info → euporie-2.8.7.dist-info}/entry_points.txt +1 -1
  71. {euporie-2.8.5.dist-info → euporie-2.8.7.dist-info}/licenses/LICENSE +1 -1
  72. {euporie-2.8.5.data → euporie-2.8.7.data}/data/share/applications/euporie-console.desktop +0 -0
  73. {euporie-2.8.5.data → euporie-2.8.7.data}/data/share/applications/euporie-notebook.desktop +0 -0
  74. {euporie-2.8.5.dist-info → euporie-2.8.7.dist-info}/WHEEL +0 -0
@@ -6,11 +6,11 @@ import asyncio
6
6
  import logging
7
7
  from abc import ABCMeta
8
8
  from collections import deque
9
- from functools import partial
9
+ from functools import lru_cache, partial
10
10
  from typing import TYPE_CHECKING
11
11
  from weakref import WeakKeyDictionary
12
12
 
13
- from prompt_toolkit.auto_suggest import DummyAutoSuggest
13
+ from prompt_toolkit.auto_suggest import DummyAutoSuggest, DynamicAutoSuggest
14
14
  from prompt_toolkit.completion.base import (
15
15
  DynamicCompleter,
16
16
  _MergedCompleter,
@@ -30,10 +30,9 @@ from euporie.core.inspection import (
30
30
  KernelInspector,
31
31
  LspInspector,
32
32
  )
33
- from euporie.core.kernel.client import Kernel, MsgCallbacks
34
- from euporie.core.suggest import HistoryAutoSuggest
33
+ from euporie.core.kernel import list_kernels
34
+ from euporie.core.kernel.base import NoKernel
35
35
  from euporie.core.tabs.base import Tab
36
- from euporie.core.utils import run_in_thread_with_context
37
36
 
38
37
  if TYPE_CHECKING:
39
38
  from collections.abc import Sequence
@@ -48,16 +47,33 @@ if TYPE_CHECKING:
48
47
  from euporie.core.comm.base import Comm
49
48
  from euporie.core.format import Formatter
50
49
  from euporie.core.inspection import Inspector
50
+ from euporie.core.kernel.base import BaseKernel, KernelFactory, MsgCallbacks
51
51
  from euporie.core.lsp import LspClient
52
52
  from euporie.core.widgets.inputs import KernelInput
53
53
 
54
54
  log = logging.getLogger(__name__)
55
55
 
56
56
 
57
+ @lru_cache
58
+ def autosuggest_factory(kind: str, history: History) -> AutoSuggest:
59
+ """Generate autosuggesters."""
60
+ if kind == "smart":
61
+ from euporie.core.suggest import SmartHistoryAutoSuggest
62
+
63
+ return SmartHistoryAutoSuggest(history)
64
+ elif kind == "simple":
65
+ from euporie.core.suggest import SimpleHistoryAutoSuggest
66
+
67
+ return SimpleHistoryAutoSuggest(history)
68
+ else:
69
+ from prompt_toolkit.auto_suggest import DummyAutoSuggest
70
+
71
+ return DummyAutoSuggest()
72
+
73
+
57
74
  class KernelTab(Tab, metaclass=ABCMeta):
58
75
  """A Tab which connects to a kernel."""
59
76
 
60
- kernel: Kernel
61
77
  kernel_language: str
62
78
  _metadata: dict[str, Any]
63
79
  bg_init = False
@@ -69,7 +85,7 @@ class KernelTab(Tab, metaclass=ABCMeta):
69
85
  self,
70
86
  app: BaseApp,
71
87
  path: Path | None = None,
72
- kernel: Kernel | None = None,
88
+ kernel: BaseKernel | None = None,
73
89
  comms: dict[str, Comm] | None = None,
74
90
  use_kernel_history: bool = False,
75
91
  connection_file: Path | None = None,
@@ -78,6 +94,7 @@ class KernelTab(Tab, metaclass=ABCMeta):
78
94
  # Init tab
79
95
  super().__init__(app, path)
80
96
 
97
+ self.kernel: BaseKernel = NoKernel(self)
81
98
  self.lsps: list[LspClient] = []
82
99
  self.history: History = DummyHistory()
83
100
  self.inspectors: list[Inspector] = []
@@ -97,8 +114,8 @@ class KernelTab(Tab, metaclass=ABCMeta):
97
114
 
98
115
  if self.bg_init:
99
116
  # Load kernel in a background thread
100
- run_in_thread_with_context(
101
- partial(
117
+ app.create_background_task(
118
+ asyncio.to_thread(
102
119
  self.init_kernel, kernel, comms, use_kernel_history, connection_file
103
120
  )
104
121
  )
@@ -183,10 +200,12 @@ class KernelTab(Tab, metaclass=ABCMeta):
183
200
 
184
201
  def post_init_kernel(self) -> None:
185
202
  """Run stuff after the kernel is loaded."""
203
+ if not isinstance(self.kernel, NoKernel):
204
+ self.metadata["kernelspec"] = self.kernel.spec
186
205
 
187
206
  def init_kernel(
188
207
  self,
189
- kernel: Kernel | None = None,
208
+ kernel: BaseKernel | None = None,
190
209
  comms: dict[str, Comm] | None = None,
191
210
  use_kernel_history: bool = False,
192
211
  connection_file: Path | None = None,
@@ -200,20 +219,47 @@ class KernelTab(Tab, metaclass=ABCMeta):
200
219
  self.kernel = kernel
201
220
  self.kernel.default_callbacks = self.default_callbacks
202
221
  else:
203
- self.kernel = Kernel(
222
+ from euporie.core.kernel import list_kernels
223
+
224
+ kernel_infos = list_kernels()
225
+ kernel_name = self.kernel_name or self.app.config.kernel_name
226
+ for info in kernel_infos:
227
+ if info.name == kernel_name:
228
+ factory = info.factory
229
+ break
230
+ else:
231
+ msg = (
232
+ f"Kernel '{self.kernel_display_name}' not found"
233
+ if self.kernel_name
234
+ else "No kernel selected"
235
+ )
236
+ self.change_kernel(msg=msg, startup=True)
237
+ return
238
+ self.kernel = factory(
204
239
  kernel_tab=self,
205
240
  allow_stdin=self.allow_stdin,
206
241
  default_callbacks=self.default_callbacks,
207
- connection_file=connection_file,
242
+ **(
243
+ {"connection_file": connection_file}
244
+ if connection_file is not None
245
+ else {}
246
+ ),
208
247
  )
248
+
209
249
  self.comms = comms or {} # The client-side comm states
210
- self.completers.append(KernelCompleter(self.kernel))
211
- self.inspectors.append(KernelInspector(self.kernel))
250
+ self.completers.append(KernelCompleter(lambda: self.kernel))
251
+ self.inspectors.append(KernelInspector(lambda: self.kernel))
212
252
  self.use_kernel_history = use_kernel_history
213
253
  self.history = (
214
- KernelHistory(self.kernel) if use_kernel_history else InMemoryHistory()
254
+ KernelHistory(lambda: self.kernel)
255
+ if use_kernel_history
256
+ else InMemoryHistory()
215
257
  )
216
- self.suggester = HistoryAutoSuggest(self.history)
258
+
259
+ def _get_suggester() -> AutoSuggest | None:
260
+ return autosuggest_factory(self.app.config.autosuggest, self.history)
261
+
262
+ self.suggester = DynamicAutoSuggest(_get_suggester)
217
263
 
218
264
  self.app.create_background_task(self.load_lsps())
219
265
 
@@ -221,7 +267,7 @@ class KernelTab(Tab, metaclass=ABCMeta):
221
267
 
222
268
  def close(self, cb: Callable | None = None) -> None:
223
269
  """Shut down kernel when tab is closed."""
224
- if hasattr(self, "kernel"):
270
+ if self.kernel is not None:
225
271
  self.kernel.shutdown()
226
272
  super().close(cb)
227
273
 
@@ -233,6 +279,7 @@ class KernelTab(Tab, metaclass=ABCMeta):
233
279
  """Restart the current `Notebook`'s kernel."""
234
280
 
235
281
  def _cb(result: dict[str, Any]) -> None:
282
+ self.kernel_started()
236
283
  if callable(cb):
237
284
  cb()
238
285
 
@@ -246,18 +293,8 @@ class KernelTab(Tab, metaclass=ABCMeta):
246
293
 
247
294
  def kernel_started(self, result: dict[str, Any] | None = None) -> None:
248
295
  """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":
296
+ # Set kernel spec in metadata
297
+ if self.kernel.status == "error":
261
298
  self.report_kernel_error(self.kernel.error)
262
299
 
263
300
  else:
@@ -301,9 +338,7 @@ class KernelTab(Tab, metaclass=ABCMeta):
301
338
  @property
302
339
  def kernel_name(self) -> str:
303
340
  """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
- )
341
+ return self.metadata.get("kernelspec", {}).get("name", "")
307
342
 
308
343
  @kernel_name.setter
309
344
  def kernel_name(self, value: str) -> None:
@@ -338,22 +373,32 @@ class KernelTab(Tab, metaclass=ABCMeta):
338
373
 
339
374
  def change_kernel(self, msg: str | None = None, startup: bool = False) -> None:
340
375
  """Prompt the user to select a new kernel."""
341
- kernel_specs = self.kernel.specs
376
+ kernel_infos = list_kernels()
342
377
 
343
378
  # Warn the user if no kernels are installed
344
- if not kernel_specs:
379
+ if not kernel_infos:
345
380
  if startup and "no-kernels" in self.app.dialogs:
346
381
  self.app.dialogs["no-kernels"].show()
347
382
  return
348
383
 
349
384
  # 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)))
385
+ if startup and len(kernel_infos) == 1:
386
+ self.switch_kernel(next(iter(kernel_infos)).factory)
352
387
  return
353
388
 
354
- self.app.dialogs["change-kernel"].show(
355
- tab=self, message=msg, kernel_specs=kernel_specs
389
+ # Prompt user to select a kernel
390
+ self.app.dialogs["change-kernel"].show(tab=self, message=msg)
391
+
392
+ def switch_kernel(self, factory: KernelFactory) -> None:
393
+ """Shut down the current kernel and change to another."""
394
+ if (old_kernel := self.kernel) is not None:
395
+ old_kernel.shutdown(wait=True)
396
+ kernel = factory(
397
+ kernel_tab=self,
398
+ default_callbacks=self.default_callbacks,
399
+ allow_stdin=self.allow_stdin,
356
400
  )
401
+ self.init_kernel(kernel)
357
402
 
358
403
  def comm_open(self, content: dict, buffers: Sequence[bytes]) -> None:
359
404
  """Register a new kernel Comm object in the notebook."""
@@ -12,10 +12,8 @@ import nbformat
12
12
  from prompt_toolkit.filters import Never
13
13
 
14
14
  from euporie.core.comm.registry import open_comm
15
- from euporie.core.commands import get_cmd
16
15
  from euporie.core.io import edit_in_editor
17
- from euporie.core.kernel.client import MsgCallbacks
18
- from euporie.core.path import UntitledPath
16
+ from euporie.core.kernel.base import MsgCallbacks
19
17
  from euporie.core.tabs.kernel import KernelTab
20
18
  from euporie.core.widgets.cell import Cell, get_cell_id
21
19
 
@@ -27,15 +25,17 @@ except ModuleNotFoundError:
27
25
  from nbformat import write as write_nb
28
26
 
29
27
  if TYPE_CHECKING:
28
+ from collections.abc import Sequence
30
29
  from pathlib import Path
31
30
  from typing import Any, Callable
32
31
 
33
32
  from prompt_toolkit.filters import Filter
34
33
  from prompt_toolkit.layout.containers import AnyContainer
34
+ from prompt_toolkit.layout.controls import BufferControl
35
35
 
36
36
  from euporie.core.app.app import BaseApp
37
37
  from euporie.core.comm.base import Comm
38
- from euporie.core.kernel.client import Kernel
38
+ from euporie.core.kernel.base import BaseKernel
39
39
  from euporie.core.lsp import LspClient
40
40
  from euporie.core.widgets.inputs import KernelInput
41
41
 
@@ -52,7 +52,7 @@ class BaseNotebook(KernelTab, metaclass=ABCMeta):
52
52
  self,
53
53
  app: BaseApp,
54
54
  path: Path | None = None,
55
- kernel: Kernel | None = None,
55
+ kernel: BaseKernel | None = None,
56
56
  comms: dict[str, Comm] | None = None,
57
57
  use_kernel_history: bool = False,
58
58
  json: dict[str, Any] | None = None,
@@ -92,6 +92,9 @@ class BaseNotebook(KernelTab, metaclass=ABCMeta):
92
92
  app, path, kernel=kernel, comms=comms, use_kernel_history=use_kernel_history
93
93
  )
94
94
 
95
+ # Load notebook file
96
+ self.container = self.load_container()
97
+
95
98
  # Tab stuff
96
99
 
97
100
  def reset(self) -> None:
@@ -104,24 +107,10 @@ class BaseNotebook(KernelTab, metaclass=ABCMeta):
104
107
 
105
108
  # KernelTab stuff
106
109
 
107
- def pre_init_kernel(self) -> None:
108
- """Run stuff before the kernel is loaded."""
109
- super().pre_init_kernel()
110
- # Load notebook file
111
- self.load()
112
-
113
110
  def post_init_kernel(self) -> None:
114
111
  """Load the notebook container after the kernel has been loaded."""
115
112
  super().post_init_kernel()
116
113
 
117
- # Replace the tab's container
118
- prev = self.container
119
- self.container = self.load_container()
120
- self.loaded = True
121
- # Update the focus if the old container had focus
122
- if self.app.layout.has_focus(prev):
123
- self.focus()
124
-
125
114
  # Load widgets
126
115
  self.load_widgets_from_metadata()
127
116
 
@@ -179,8 +168,7 @@ class BaseNotebook(KernelTab, metaclass=ABCMeta):
179
168
 
180
169
  @abstractmethod
181
170
  def load_container(self) -> AnyContainer:
182
- """Abcract method for loading the notebook's main container."""
183
- ...
171
+ """Absract method for loading the notebook's main container."""
184
172
 
185
173
  @abstractproperty
186
174
  def cell(self) -> Cell:
@@ -270,23 +258,20 @@ class BaseNotebook(KernelTab, metaclass=ABCMeta):
270
258
  if self.dirty and (unsaved := self.app.dialogs.get("unsaved")):
271
259
  unsaved.show(
272
260
  tab=self,
273
- cb=cb,
261
+ cb=partial(super().close, cb),
274
262
  )
275
263
  else:
276
264
  super().close(cb)
277
265
 
278
- def save(self, path: Path | None = None, cb: Callable | None = None) -> None:
266
+ def write_file(self, path: Path) -> None:
279
267
  """Write the notebook's JSON to the current notebook's file.
280
268
 
281
269
  Additionally save the widget state to the notebook metadata.
282
270
 
283
271
  Args:
284
- path: An optional new path at which to save the tab
285
- cb: A callback to run if after saving the notebook.
272
+ path: An path at which to save the file
286
273
 
287
274
  """
288
- if path is not None:
289
- self.path = path
290
275
  if self.app.config.save_widget_state:
291
276
  self.json.setdefault("metadata", {})["widgets"] = {
292
277
  "application/vnd.jupyter.widget-state+json": {
@@ -299,58 +284,21 @@ class BaseNotebook(KernelTab, metaclass=ABCMeta):
299
284
  },
300
285
  }
301
286
  }
302
- if self.path is None or isinstance(self.path, UntitledPath):
303
- if dialog := self.app.dialogs.get("save-as"):
304
- dialog.show(tab=self, cb=cb)
305
- else:
306
- log.debug("Saving notebook..")
307
- self.saving = True
308
- self.app.invalidate()
309
- # Ensure parent path exists
310
- parent = self.path.parent
311
- parent.mkdir(exist_ok=True, parents=True)
312
- # Save to a temp file, then replace the original
313
- temp_path = parent / f".{self.path.stem}.tmp{self.path.suffix}"
314
- log.debug("Using temporary file %s", temp_path.name)
287
+ with path.open("w") as open_file:
315
288
  try:
316
- open_file = temp_path.open("w")
317
- except NotImplementedError:
318
- get_cmd("save-as").run()
319
- else:
289
+ write_nb(nb=nbformat.from_dict(self.json), fp=open_file)
290
+ except AssertionError:
320
291
  try:
321
- try:
322
- write_nb(nb=nbformat.from_dict(self.json), fp=open_file)
323
- except AssertionError:
324
- try:
325
- # Jupytext requires a filename if we don't give it a format
326
- write_nb(nb=nbformat.from_dict(self.json), fp=temp_path)
327
- except Exception:
328
- # Jupytext requires a format if the path has no extension
329
- # We just use ipynb as the default format
330
- write_nb(
331
- nb=nbformat.from_dict(self.json),
332
- fp=open_file,
333
- fmt="ipynb",
334
- )
292
+ # Jupytext requires a filename if we don't give it a format
293
+ write_nb(nb=nbformat.from_dict(self.json), fp=path)
335
294
  except Exception:
336
- log.exception("An error occurred while saving the file")
337
- if dialog := self.app.dialogs.get("save-as"):
338
- dialog.show(tab=self, cb=cb)
339
- else:
340
- open_file.close()
341
- try:
342
- temp_path.rename(self.path)
343
- except Exception:
344
- if dialog := self.app.dialogs.get("save-as"):
345
- dialog.show(tab=self, cb=cb)
346
- else:
347
- self.dirty = False
348
- self.saving = False
349
- self.app.invalidate()
350
- log.debug("Notebook saved")
351
- # Run the callback
352
- if callable(cb):
353
- cb()
295
+ # Jupytext requires a format if the path has no extension
296
+ # We just use ipynb as the default format
297
+ write_nb(
298
+ nb=nbformat.from_dict(self.json),
299
+ fp=open_file,
300
+ fmt="ipynb",
301
+ )
354
302
 
355
303
  def run_cell(
356
304
  self,
@@ -462,3 +410,7 @@ class BaseNotebook(KernelTab, metaclass=ABCMeta):
462
410
  def lsp_update_diagnostics(self, lsp: LspClient) -> None:
463
411
  """Process a new diagnostic report from the LSP."""
464
412
  # Do nothing, these are handled by cells
413
+
414
+ def __pt_searchables__(self) -> Sequence[BufferControl]:
415
+ """Return list of cell input buffer controls for searching."""
416
+ return [cell.input_box.control for cell in self.rendered_cells()]
euporie/core/utils.py CHANGED
@@ -1,12 +1,10 @@
1
- """Miscellaneou utility classes."""
1
+ """Miscellaneous utility classes."""
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
- import contextvars
6
5
  from collections.abc import Sequence
7
6
  from functools import cache
8
7
  from itertools import chain
9
- from threading import Thread
10
8
  from typing import TYPE_CHECKING, TypeVar, overload
11
9
 
12
10
  from prompt_toolkit.mouse_events import MouseButton, MouseEventType
@@ -14,7 +12,7 @@ from prompt_toolkit.mouse_events import MouseButton, MouseEventType
14
12
  if TYPE_CHECKING:
15
13
  from collections.abc import Iterable
16
14
  from types import ModuleType
17
- from typing import Any, Callable
15
+ from typing import Callable
18
16
 
19
17
  from prompt_toolkit.key_binding.key_bindings import NotImplementedOrNone
20
18
  from prompt_toolkit.layout.mouse_handlers import MouseHandler
@@ -78,21 +76,6 @@ def on_click(func: Callable) -> MouseHandler:
78
76
  return _mouse_handler
79
77
 
80
78
 
81
- def run_in_thread_with_context(
82
- func: Callable, *args: Any, daemon: bool = True, **kwargs: Any
83
- ) -> None:
84
- """Run a function in an thread, but make sure it uses the same contextvars.
85
-
86
- This is required so that the function will see the right application.
87
- """
88
- Thread(
89
- target=contextvars.copy_context().run,
90
- args=(func, *args),
91
- kwargs=kwargs,
92
- daemon=daemon,
93
- ).start()
94
-
95
-
96
79
  @cache
97
80
  def root_module(name: str) -> ModuleType:
98
81
  """Find and load the root module of a given module name by traversing up the module hierarchy.
@@ -9,29 +9,29 @@ 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.client import Kernel
12
+ from euporie.core.kernel.base import BaseKernel
13
13
 
14
14
 
15
15
  class KernelValidator(Validator):
16
16
  """Validate kernel input using a kernel code completeness call."""
17
17
 
18
- def __init__(self, kernel: Kernel) -> None:
18
+ def __init__(self, kernel: BaseKernel) -> None:
19
19
  """Initialize the validator."""
20
20
  self.kernel = kernel
21
21
 
22
22
  def validate(self, document: Document) -> None:
23
23
  """Validate the input synchronously."""
24
- completeness_status = self.kernel.is_complete(
25
- code=document.text, wait=True
26
- ).get("status", "unknown")
24
+ completeness_status = self.kernel.is_complete(source=document.text).get(
25
+ "status", "unknown"
26
+ )
27
27
  if completeness_status == "incomplete":
28
28
  raise ValidationError
29
29
 
30
30
  async def validate_async(self, document: Document) -> None:
31
31
  """Return a `Future` which is set when the validation is ready."""
32
- completeness_status = (await self.kernel.is_complete_(code=document.text)).get(
33
- "status", "unknown"
34
- )
32
+ completeness_status = (
33
+ await self.kernel.is_complete_async(source=document.text)
34
+ ).get("status", "unknown")
35
35
  if completeness_status == "incomplete":
36
36
  raise ValidationError
37
37
  return
@@ -22,6 +22,22 @@ add_setting(
22
22
  cmd_filter=~buffer_has_focus,
23
23
  )
24
24
 
25
+ add_setting(
26
+ name="text_output_limit",
27
+ group="euporie.core.widgets.cell_outputs",
28
+ flags=["--text-output-limit"],
29
+ type_=int,
30
+ help_="Limit the amount of cell text output",
31
+ default=1_000_000,
32
+ schema={
33
+ "minimum": 0,
34
+ },
35
+ description="""
36
+ Limit the number of text characters in interactive cell text output to this value.
37
+ Use ``0`` to allow any amount of characters.
38
+ """,
39
+ )
40
+
25
41
  # euporie,core.widgets.file_browser:FileBrowser
26
42
 
27
43
  add_setting(
@@ -86,9 +102,10 @@ add_setting(
86
102
  name="autosuggest",
87
103
  group="euporie.core.widgets.inputs",
88
104
  flags=["--autosuggest"],
89
- type_=bool,
105
+ type_=str,
106
+ choices=["smart", "simple", "none"],
90
107
  help_="Provide line completion suggestions",
91
- default=True,
108
+ default="smart",
92
109
  description="""
93
110
  Whether to automatically suggestion line content while typing in code cells.
94
111
  """,
@@ -6,7 +6,7 @@ import asyncio
6
6
  import logging
7
7
  import os
8
8
  import weakref
9
- from functools import partial
9
+ from functools import lru_cache, partial
10
10
  from pathlib import Path
11
11
  from typing import TYPE_CHECKING, cast
12
12
  from weakref import WeakKeyDictionary
@@ -50,6 +50,7 @@ if TYPE_CHECKING:
50
50
  from prompt_toolkit.completion.base import Completer
51
51
  from prompt_toolkit.formatted_text.base import StyleAndTextTuples
52
52
 
53
+ from euporie.core.border import GridStyle
53
54
  from euporie.core.format import Formatter
54
55
  from euporie.core.inspection import Inspector
55
56
  from euporie.core.lsp import LspClient
@@ -79,6 +80,18 @@ def get_cell_id(cell_json: dict) -> str:
79
80
  return cell_id
80
81
 
81
82
 
83
+ @lru_cache(maxsize=32)
84
+ def _get_border_style(
85
+ selected: bool, focused: bool, show_borders: bool, multi_selected: bool
86
+ ) -> GridStyle:
87
+ """Get the border style grid based on cell state."""
88
+ if not (show_borders or selected):
89
+ return NoLine.grid
90
+ if focused and multi_selected:
91
+ return ThickLine.outer
92
+ return ThinLine.outer
93
+
94
+
82
95
  class Cell:
83
96
  """A kernel_tab cell element.
84
97
 
@@ -86,8 +99,6 @@ class Cell:
86
99
  focused.
87
100
  """
88
101
 
89
- input_box: KernelInput
90
-
91
102
  def __init__(
92
103
  self, index: int, json: dict, kernel_tab: BaseNotebook, is_new: bool = False
93
104
  ) -> None:
@@ -158,6 +169,14 @@ class Cell:
158
169
  """Update cell json when the input buffer has been edited."""
159
170
  weak_self._set_input(buf.text)
160
171
  weak_self.kernel_tab.dirty = True
172
+ weak_self.on_change()
173
+ # Re-render markdown cells when edited outside of edit mode
174
+ if (
175
+ weak_self.cell_type == "markdown"
176
+ and not weak_self.kernel_tab.in_edit_mode()
177
+ ):
178
+ weak_self.output_area.json = weak_self.output_json
179
+ weak_self.refresh()
161
180
 
162
181
  def on_cursor_position_changed(buf: Buffer) -> None:
163
182
  """Respond to cursor movements."""
@@ -172,7 +191,7 @@ class Cell:
172
191
  # Now we generate the main container used to represent a kernel_tab cell
173
192
 
174
193
  source_hidden = Condition(
175
- lambda: weak_self.json["metadata"]
194
+ lambda: weak_self.json.get("metadata", {})
176
195
  .get("jupyter", {})
177
196
  .get("source_hidden", False)
178
197
  )
@@ -199,42 +218,22 @@ class Cell:
199
218
  )
200
219
  self.input_box.buffer.name = self.cell_type
201
220
 
202
- self.input_box.buffer.on_text_changed += lambda buf: weak_self.on_change()
203
-
204
221
  def border_char(name: str) -> Callable[..., str]:
205
222
  """Return a function which returns the cell border character to display."""
206
223
 
207
224
  def _inner() -> str:
208
- grid = NoLine.grid
209
- if weak_self and (
210
- weak_self.kernel_tab.app.config.show_cell_borders
211
- or weak_self.selected
212
- ):
213
- if weak_self.focused and multiple_cells_selected():
214
- grid = ThickLine.outer
215
- else:
216
- grid = ThinLine.outer
225
+ if not weak_self:
226
+ return " "
227
+ grid = _get_border_style(
228
+ weak_self.selected,
229
+ weak_self.focused,
230
+ weak_self.kernel_tab.app.config.show_cell_borders,
231
+ multiple_cells_selected(),
232
+ )
217
233
  return getattr(grid, name.upper())
218
234
 
219
235
  return _inner
220
236
 
221
- # @lru_cache(maxsize=None)
222
- # def _cell_border_char(
223
- # name: str,
224
- # show_cell_borders: bool,
225
- # focused: bool,
226
- # selected: bool,
227
- # multiple_cells_selected: bool,
228
- # ) -> str:
229
- # if show_cell_borders or selected:
230
- # if focused and multiple_cells_selected:
231
- # grid = ThickLine.outer
232
- # else:
233
- # grid = ThinLine.outer
234
- # else:
235
- # grid = NoLine.grid
236
- # return getattr(grid, name.upper())
237
-
238
237
  self.control = Window(
239
238
  FormattedTextControl(
240
239
  border_char("TOP_LEFT"),
@@ -764,6 +763,7 @@ class Cell:
764
763
  def set_execution_count(self, n: int) -> None:
765
764
  """Set the execution count of the cell."""
766
765
  self.json["execution_count"] = n
766
+ self.refresh()
767
767
 
768
768
  def add_output(self, output_json: dict[str, Any], own: bool) -> None:
769
769
  """Add a new output to the cell."""