euporie 2.8.6__py3-none-any.whl → 2.8.8__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 (65) 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/app/_commands.py +4 -21
  5. euporie/core/app/app.py +13 -7
  6. euporie/core/bars/command.py +9 -6
  7. euporie/core/bars/search.py +43 -2
  8. euporie/core/border.py +7 -2
  9. euporie/core/comm/base.py +2 -2
  10. euporie/core/comm/ipywidgets.py +3 -3
  11. euporie/core/commands.py +44 -8
  12. euporie/core/completion.py +14 -6
  13. euporie/core/convert/datum.py +7 -7
  14. euporie/core/data_structures.py +20 -1
  15. euporie/core/filters.py +8 -0
  16. euporie/core/ft/html.py +47 -40
  17. euporie/core/graphics.py +11 -3
  18. euporie/core/history.py +15 -5
  19. euporie/core/inspection.py +16 -9
  20. euporie/core/io.py +1 -1
  21. euporie/core/kernel/__init__.py +53 -1
  22. euporie/core/kernel/base.py +571 -0
  23. euporie/core/kernel/{client.py → jupyter.py} +173 -430
  24. euporie/core/kernel/{manager.py → jupyter_manager.py} +4 -3
  25. euporie/core/kernel/local.py +694 -0
  26. euporie/core/key_binding/bindings/basic.py +6 -3
  27. euporie/core/keys.py +26 -25
  28. euporie/core/layout/cache.py +31 -7
  29. euporie/core/layout/containers.py +88 -13
  30. euporie/core/layout/scroll.py +45 -148
  31. euporie/core/log.py +1 -1
  32. euporie/core/style.py +2 -1
  33. euporie/core/suggest.py +155 -74
  34. euporie/core/tabs/__init__.py +10 -0
  35. euporie/core/tabs/_commands.py +76 -0
  36. euporie/core/tabs/_settings.py +16 -0
  37. euporie/core/tabs/base.py +22 -8
  38. euporie/core/tabs/kernel.py +81 -35
  39. euporie/core/tabs/notebook.py +14 -22
  40. euporie/core/utils.py +1 -1
  41. euporie/core/validation.py +8 -8
  42. euporie/core/widgets/_settings.py +19 -2
  43. euporie/core/widgets/cell.py +31 -31
  44. euporie/core/widgets/cell_outputs.py +10 -1
  45. euporie/core/widgets/dialog.py +30 -75
  46. euporie/core/widgets/forms.py +71 -59
  47. euporie/core/widgets/inputs.py +7 -4
  48. euporie/core/widgets/layout.py +281 -93
  49. euporie/core/widgets/menu.py +55 -15
  50. euporie/core/widgets/palette.py +3 -1
  51. euporie/core/widgets/tree.py +86 -76
  52. euporie/notebook/app.py +35 -16
  53. euporie/notebook/tabs/edit.py +4 -4
  54. euporie/notebook/tabs/json.py +6 -2
  55. euporie/notebook/tabs/notebook.py +26 -8
  56. euporie/preview/tabs/notebook.py +17 -13
  57. euporie/web/tabs/web.py +22 -3
  58. euporie/web/widgets/webview.py +3 -0
  59. {euporie-2.8.6.dist-info → euporie-2.8.8.dist-info}/METADATA +1 -1
  60. {euporie-2.8.6.dist-info → euporie-2.8.8.dist-info}/RECORD +65 -62
  61. {euporie-2.8.6.dist-info → euporie-2.8.8.dist-info}/entry_points.txt +1 -1
  62. {euporie-2.8.6.dist-info → euporie-2.8.8.dist-info}/licenses/LICENSE +1 -1
  63. {euporie-2.8.6.data → euporie-2.8.8.data}/data/share/applications/euporie-console.desktop +0 -0
  64. {euporie-2.8.6.data → euporie-2.8.8.data}/data/share/applications/euporie-notebook.desktop +0 -0
  65. {euporie-2.8.6.dist-info → euporie-2.8.8.dist-info}/WHEEL +0 -0
@@ -3,23 +3,24 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  import asyncio
6
- import concurrent.futures
7
6
  import logging
8
7
  import os
9
- import threading
10
8
  from collections import defaultdict
9
+ from functools import partial
11
10
  from subprocess import PIPE, STDOUT # S404 - Security implications considered
12
- from typing import TYPE_CHECKING, TypedDict
11
+ from typing import TYPE_CHECKING, cast
13
12
  from uuid import uuid4
14
13
 
15
14
  from upath import UPath
16
15
 
16
+ from euporie.core.kernel.base import BaseKernel, KernelInfo, MsgCallbacks
17
+
17
18
  if TYPE_CHECKING:
18
- from collections.abc import Coroutine
19
19
  from pathlib import Path
20
- from typing import Any, Callable
20
+ from typing import Any, Callable, Unpack
21
21
 
22
22
  from jupyter_client import KernelClient
23
+ from jupyter_client.kernelspec import KernelSpecManager
23
24
 
24
25
  from euporie.core.tabs.kernel import KernelTab
25
26
 
@@ -27,175 +28,112 @@ if TYPE_CHECKING:
27
28
  log = logging.getLogger(__name__)
28
29
 
29
30
 
30
- class MsgCallbacks(TypedDict, total=False):
31
- """Typed dictionary for named message callbacks."""
32
-
33
- get_input: Callable[[str, bool], None] | None
34
- set_execution_count: Callable[[int], None] | None
35
- add_output: Callable[[dict[str, Any], bool], None] | None
36
- add_input: Callable[[dict[str, Any], bool], None] | None
37
- clear_output: Callable[[bool], None] | None
38
- done: Callable[[dict[str, Any]], None] | None
39
- set_metadata: Callable[[tuple[str, ...], Any], None] | None
40
- set_status: Callable[[str], None] | None
41
- set_kernel_info: Callable[[dict[str, Any]], None] | None
42
- completeness_status: Callable[[dict[str, Any]], None] | None
43
- dead: Callable[[], None] | None
44
- # Payloads
45
- page: Callable[[list[dict], int], None] | None
46
- set_next_input: Callable[[str, bool], None] | None
47
- edit_magic: Callable[[str, int], None] | None
48
- ask_exit: Callable[[bool], None] | None
49
-
50
-
51
- class Kernel:
31
+ class JupyterKernel(BaseKernel):
52
32
  """Run a notebook kernel and communicates with it asynchronously.
53
33
 
54
34
  Has the ability to run itself in it's own thread.
55
35
  """
56
36
 
57
- _CLIENT_ID = f"euporie-{os.getpid()}"
37
+ _client_id = f"euporie-{os.getpid()}"
38
+ _spec_manager: KernelSpecManager
39
+
40
+ @classmethod
41
+ def variants(cls) -> list[KernelInfo]:
42
+ """Return available kernel specifications."""
43
+ from jupyter_core.paths import jupyter_runtime_dir
44
+
45
+ try:
46
+ manager = cls._spec_manager
47
+ except AttributeError:
48
+ from jupyter_client.kernelspec import KernelSpecManager
49
+ from jupyter_core.paths import jupyter_path
50
+
51
+ manager = cls._spec_manager = KernelSpecManager()
52
+ # Set the kernel folder list to prevent the default method from running.
53
+ # This prevents the kernel spec manager from loading IPython, just for the
54
+ # purpose of adding the depreciated :file:`.ipython/kernels` folder to the list
55
+ # of kernel search paths. Without this, having IPython installed causes a
56
+ # import race condition error where IPython was imported in the main thread for
57
+ # displaying LaTeX and in the kernel thread to discover kernel paths.
58
+ # Also this speeds up launch since importing IPython is pretty slow.
59
+ manager.kernel_dirs = jupyter_path("kernels")
60
+
61
+ return [
62
+ KernelInfo(
63
+ name=name,
64
+ display_name=info.get("spec", {}).get("display_name", name),
65
+ factory=partial(cls, kernel_name=name),
66
+ kind="new",
67
+ type=cls,
68
+ )
69
+ for name, info in manager.get_all_specs().items()
70
+ ] + [
71
+ KernelInfo(
72
+ name=path.name,
73
+ display_name=path.name,
74
+ factory=partial(cls, connection_file=path),
75
+ kind="existing",
76
+ type=cls,
77
+ )
78
+ for path in UPath(jupyter_runtime_dir()).glob("kernel-*.json")
79
+ ]
58
80
 
59
81
  def __init__(
60
82
  self,
61
83
  kernel_tab: KernelTab,
62
- threaded: bool = True,
63
- allow_stdin: bool = False,
64
84
  default_callbacks: MsgCallbacks | None = None,
85
+ allow_stdin: bool = False,
86
+ *,
87
+ kernel_name: str | None = None,
65
88
  connection_file: Path | None = None,
89
+ **kwargs: Any,
66
90
  ) -> None:
67
- """Call when the :py:class:`Kernel` is initialized.
91
+ """Initialize the JupyterKernel.
68
92
 
69
93
  Args:
70
94
  kernel_tab: The notebook this kernel belongs to
71
- threaded: If :py:const:`True`, run kernel communication in a separate thread
72
95
  allow_stdin: Whether the kernel is allowed to request input
73
96
  default_callbacks: The default callbacks to use on receipt of a message
74
- connection_file: Path to a file from which to load or to hwich to save
97
+ kernel_name: Name of the Jupyter kernel to launch
98
+ connection_file: Path to a file from which to load or to which to save
75
99
  kernel connection information
76
-
100
+ kwargs: Additional key-word arguments
77
101
  """
78
- from jupyter_core.paths import jupyter_path
102
+ super().__init__(
103
+ kernel_tab=kernel_tab,
104
+ allow_stdin=allow_stdin,
105
+ default_callbacks=default_callbacks,
106
+ )
79
107
 
80
- from euporie.core.kernel.manager import (
108
+ from euporie.core.kernel.jupyter_manager import (
81
109
  EuporieKernelManager,
82
110
  set_default_provisioner,
83
111
  )
84
112
 
85
113
  set_default_provisioner()
86
114
 
87
- self.threaded = threaded
88
- if threaded:
89
- self.loop = asyncio.new_event_loop()
90
- self.thread = threading.Thread(target=self._setup_loop)
91
- self.thread.daemon = True
92
- self.thread.start()
93
- else:
94
- self.loop = asyncio.get_event_loop()
115
+ if kernel_name is None and connection_file is not None:
116
+ import json
117
+
118
+ try:
119
+ connection_info = json.loads(connection_file.read_text())
120
+ except json.decoder.JSONDecodeError:
121
+ connection_info = {}
122
+ kernel_name = connection_info.get("kernel_name", "python3")
95
123
 
96
- self.allow_stdin = allow_stdin
124
+ if kernel_name is None and connection_file is None:
125
+ raise ValueError("Must provide `kernel_name` or `connection_file`")
97
126
 
98
- self.kernel_tab = kernel_tab
99
127
  self.connection_file = connection_file
128
+ self.km = EuporieKernelManager(kernel_name=kernel_name)
100
129
  self.kc: KernelClient | None = None
101
- self.km = EuporieKernelManager(
102
- kernel_name=str(kernel_tab.kernel_name),
103
- )
104
- self._status = "stopped"
105
- self.error: Exception | None = None
106
- self.dead = False
107
130
  self.monitor_task: asyncio.Task | None = None
108
-
109
- self.coros: dict[str, concurrent.futures.Future] = {}
110
131
  self.poll_tasks: list[asyncio.Task] = []
111
-
112
- self.default_callbacks = MsgCallbacks(
113
- {
114
- "get_input": None,
115
- "set_execution_count": None,
116
- "add_output": None,
117
- "clear_output": None,
118
- "done": None,
119
- "set_metadata": None,
120
- "set_status": None,
121
- }
122
- )
123
- if default_callbacks is not None:
124
- self.default_callbacks.update(default_callbacks)
125
-
126
132
  self.msg_id_callbacks: dict[str, MsgCallbacks] = defaultdict(
127
133
  # Return a copy of the default callbacks
128
134
  lambda: MsgCallbacks(dict(self.default_callbacks)) # type: ignore # mypy #8890
129
135
  )
130
136
 
131
- # Set the kernel folder list to prevent the default method from running.
132
- # This prevents the kernel spec manager from loading IPython, just for the
133
- # purpose of adding the depreciated :file:`.ipython/kernels` folder to the list
134
- # of kernel search paths. Without this, having IPython installed causes a
135
- # import race condition error where IPython was imported in the main thread for
136
- # displaying LaTeX and in the kernel thread to discover kernel paths.
137
- # Also this speeds up launch since importing IPython is pretty slow.
138
- self.km.kernel_spec_manager.kernel_dirs = jupyter_path("kernels")
139
-
140
- def _setup_loop(self) -> None:
141
- """Set the current loop the the kernel's event loop.
142
-
143
- This method is intended to be run in the kernel thread.
144
- """
145
- asyncio.set_event_loop(self.loop)
146
- self.status_change_event = asyncio.Event()
147
- self.loop.run_forever()
148
-
149
- def _aodo(
150
- self,
151
- coro: Coroutine,
152
- wait: bool = False,
153
- callback: Callable | None = None,
154
- timeout: int | float | None = None,
155
- single: bool = False,
156
- ) -> Any:
157
- """Schedule a coroutine in the kernel's event loop.
158
-
159
- Optionally waits for the results (blocking the main thread). Optionally
160
- schedules a callback to run when the coroutine has completed or timed out.
161
-
162
- Args:
163
- coro: The coroutine to run
164
- wait: If :py:const:`True`, block until the kernel has started
165
- callback: A function to run when the coroutine completes. The result from
166
- the coroutine will be passed as an argument
167
- timeout: The number of seconds to allow the coroutine to run if waiting
168
- single: If :py:const:`True`, any futures for previous instances of the
169
- coroutine will be cancelled
170
-
171
- Returns:
172
- The result of the coroutine
173
-
174
- """
175
- future = asyncio.run_coroutine_threadsafe(coro, self.loop)
176
-
177
- # Cancel previous future instances if required
178
- if single and self.coros.get(coro.__name__):
179
- self.coros[coro.__name__].cancel()
180
- self.coros[coro.__name__] = future
181
-
182
- if wait:
183
- result = None
184
- try:
185
- result = future.result(timeout)
186
- except concurrent.futures.TimeoutError:
187
- log.error("Operation '%s' timed out", coro)
188
- future.cancel()
189
- finally:
190
- if callable(callback):
191
- callback(result)
192
- return result
193
- else:
194
- if callable(callback):
195
- future.add_done_callback(lambda f: callback(f.result()))
196
- return None
197
- return None
198
-
199
137
  def _set_living_status(self, alive: bool) -> None:
200
138
  """Set the life status of the kernel."""
201
139
  if not alive:
@@ -214,7 +152,7 @@ class Kernel:
214
152
  await asyncio.sleep(1)
215
153
  # Check kernel is alive - use client rather than manager if we have one
216
154
  # as we could be connected to a kernel not started by the manager
217
- if self.kc:
155
+ if self.kc and self.status != "starting":
218
156
  alive = await self.kc._async_is_alive()
219
157
  self._set_living_status(alive)
220
158
  # Stop the timer if the kernel is dead
@@ -223,35 +161,6 @@ class Kernel:
223
161
  else:
224
162
  break
225
163
 
226
- @property
227
- def status(self) -> str:
228
- """Retrieve the current kernel status.
229
-
230
- Returns:
231
- The kernel status
232
-
233
- """
234
- return self._status
235
-
236
- @status.setter
237
- def status(self, value: str) -> None:
238
- """Set the kernel status."""
239
- self.status_change_event.set()
240
- self._status = value
241
- self.status_change_event.clear()
242
-
243
- def wait_for_status(self, status: str = "idle") -> None:
244
- """Block until the kernel reaches a given status value."""
245
- if self.status != status:
246
-
247
- async def _wait() -> None:
248
- while self.status != status:
249
- await asyncio.wait_for(
250
- self.status_change_event.wait(), timeout=None
251
- )
252
-
253
- self._aodo(_wait(), wait=True)
254
-
255
164
  @property
256
165
  def missing(self) -> bool:
257
166
  """Return True if the requested kernel is not found."""
@@ -272,12 +181,7 @@ class Kernel:
272
181
  else:
273
182
  return None
274
183
 
275
- @property
276
- def specs(self) -> dict[str, dict]:
277
- """Return a list of available kernelspecs."""
278
- return self.km.kernel_spec_manager.get_all_specs()
279
-
280
- async def stop_(self, cb: Callable[[], Any] | None = None) -> None:
184
+ async def stop_async(self, cb: Callable[[], Any] | None = None) -> None:
281
185
  """Stop the kernel asynchronously."""
282
186
  for task in self.poll_tasks:
283
187
  task.cancel()
@@ -287,7 +191,7 @@ class Kernel:
287
191
  await self.km.shutdown_kernel()
288
192
  log.debug("Kernel %s shutdown", self.id)
289
193
 
290
- async def start_(self) -> None:
194
+ async def start_async(self) -> None:
291
195
  """Start the kernel asynchronously and set its status."""
292
196
  from jupyter_core.paths import jupyter_runtime_dir
293
197
 
@@ -340,9 +244,19 @@ class Kernel:
340
244
  self.kc = self.km.client()
341
245
  break
342
246
 
343
- await self.post_start_()
247
+ await self.post_start()
344
248
 
345
- async def post_start_(self) -> None:
249
+ @property
250
+ def spec(self) -> dict[str, str]:
251
+ """The kernelspec metadata for the current kernel instance."""
252
+ assert self.km.kernel_spec is not None
253
+ return {
254
+ "name": self.km.kernel_name,
255
+ "display_name": self.km.kernel_spec.display_name,
256
+ "language": self.km.kernel_spec.language,
257
+ }
258
+
259
+ async def post_start(self) -> None:
346
260
  """Wait for the kernel to become ready."""
347
261
  from jupyter_client.kernelspec import NoSuchKernel
348
262
 
@@ -353,34 +267,30 @@ class Kernel:
353
267
  self.status = "error"
354
268
  log.error("Selected kernel '%s' not registered", self.km.kernel_name)
355
269
  else:
356
- if ks is not None:
357
- self.kernel_tab.metadata["kernelspec"] = {
358
- "name": self.km.kernel_name,
359
- "display_name": ks.display_name,
360
- "language": ks.language,
361
- }
362
-
363
- if self.kc is not None:
364
- log.debug("Waiting for kernel to become ready")
365
- try:
366
- await self.kc._async_wait_for_ready(timeout=10)
367
- except RuntimeError as e:
368
- log.error("Error connecting to kernel")
369
- self.error = e
370
- self.status = "error"
371
- else:
372
- log.debug("Kernel %s ready", self.id)
373
- self.status = "idle"
374
- self.error = None
375
- self.poll_tasks = [
376
- asyncio.create_task(self.poll("shell")),
377
- asyncio.create_task(self.poll("iopub")),
378
- asyncio.create_task(self.poll("stdin")),
379
- ]
380
- self.dead = False
381
-
382
- # Set username so we can identify our own messages
383
- self.kc.session.username = self._CLIENT_ID
270
+ if ks is not None and self.kc is not None:
271
+ log.debug("Waiting for kernel to become ready")
272
+ try:
273
+ await self.kc._async_wait_for_ready(timeout=30)
274
+ except RuntimeError as e:
275
+ log.error("Error connecting to kernel")
276
+ self.error = e
277
+ self.status = "error"
278
+ else:
279
+ log.debug("Kernel %s ready", self.id)
280
+ self.status = "idle"
281
+ self.error = None
282
+ self.poll_tasks = [
283
+ asyncio.create_task(self.poll("shell")),
284
+ asyncio.create_task(self.poll("iopub")),
285
+ asyncio.create_task(self.poll("stdin")),
286
+ ]
287
+ self.dead = False
288
+
289
+ # Set username so we can identify our own messages
290
+ self.kc.session.username = self._client_id
291
+
292
+ # Send empty execution request to get current execution count
293
+ self.kc.execute("", store_history=False, silent=True, allow_stdin=False)
384
294
 
385
295
  # Start monitoring the kernel status
386
296
  if self.monitor_task is not None:
@@ -411,14 +321,7 @@ class Kernel:
411
321
  log.debug("Imported `ipykernel` to prevent import deadlock")
412
322
  except ImportError:
413
323
  pass
414
-
415
- # Start the kernel
416
- self._aodo(
417
- self.start_(),
418
- timeout=timeout,
419
- wait=wait,
420
- callback=cb,
421
- )
324
+ super().start(cb, wait, timeout)
422
325
 
423
326
  async def poll(self, channel: str) -> None:
424
327
  """Poll for messages on a channel, and signal when they arrive.
@@ -434,7 +337,7 @@ class Kernel:
434
337
  rsp = await msg_getter_coro()
435
338
  # Run msg type handler
436
339
  msg_type = rsp.get("header", {}).get("msg_type")
437
- own = rsp.get("parent_header", {}).get("username") == self._CLIENT_ID
340
+ own = rsp.get("parent_header", {}).get("username") == self._client_id
438
341
  if callable(handler := getattr(self, f"on_{channel}_{msg_type}", None)):
439
342
  handler(rsp, own)
440
343
  else:
@@ -490,7 +393,9 @@ class Kernel:
490
393
  rsp["header"]["date"].isoformat(),
491
394
  )
492
395
 
493
- if (execution_count := content.get("execution_count")) and callable(
396
+ if (
397
+ (execution_count := content.get("execution_count")) is not None
398
+ ) and callable(
494
399
  set_execution_count := self.msg_id_callbacks[msg_id]["set_execution_count"]
495
400
  ):
496
401
  set_execution_count(execution_count)
@@ -573,6 +478,14 @@ class Kernel:
573
478
  ):
574
479
  completeness_status(rsp.get("content", {}))
575
480
 
481
+ def on_iopub_shutdown_reply(self, rsp: dict[str, Any], own: bool) -> None:
482
+ """Handle iopub shutdown reply messages."""
483
+ if not rsp.get("content", {}).get("restart"):
484
+ # Stop monitoring the kernel
485
+ if self.monitor_task is not None:
486
+ self.monitor_task.cancel()
487
+ self._set_living_status(False)
488
+
576
489
  def on_iopub_status(self, rsp: dict[str, Any], own: bool) -> None:
577
490
  """Call callbacks for an iopub status response."""
578
491
  msg_id = rsp.get("parent_header", {}).get("msg_id")
@@ -741,22 +654,10 @@ class Kernel:
741
654
  log.debug("Cannot run cell because kernel has not started")
742
655
  # TODO - queue cells for execution
743
656
  else:
744
- self._aodo(
745
- self.run_(source, **callbacks),
746
- wait=wait,
747
- callback=callback,
748
- )
657
+ super().run(source, wait, callback, **callbacks)
749
658
 
750
- async def run_(
751
- self,
752
- source: str,
753
- get_input: Callable[[str, bool], None] | None = None,
754
- set_execution_count: Callable[[int], None] | None = None,
755
- add_output: Callable[[dict[str, Any], bool], None] | None = None,
756
- clear_output: Callable[[bool], None] | None = None,
757
- done: Callable[[dict[str, Any]], None] | None = None,
758
- set_metadata: Callable[[tuple[str, ...], Any], None] | None = None,
759
- set_status: Callable[[str], None] | None = None,
659
+ async def run_async(
660
+ self, source: str, **local_callbacks: Unpack[MsgCallbacks]
760
661
  ) -> None:
761
662
  """Run the code cell and and set the response callbacks, optionally waiting."""
762
663
  if self.kc is None:
@@ -768,25 +669,23 @@ class Kernel:
768
669
  allow_stdin=self.allow_stdin,
769
670
  )
770
671
 
771
- def wrapped_done(content: dict[str, Any]) -> None:
772
- """Set the event after the ``done`` callback has completed."""
773
- # Run the original callback
774
- if callable(done):
775
- done(content)
776
- # Set the event
777
- event.set()
672
+ if done := local_callbacks.get("done"):
673
+
674
+ def wrapped_done(content: dict[str, Any]) -> None:
675
+ """Set the event after the ``done`` callback has completed."""
676
+ # Run the original callback
677
+ if callable(done):
678
+ done(content)
679
+ # Set the event
680
+ event.set()
681
+
682
+ local_callbacks["done"] = wrapped_done
778
683
 
779
- callbacks = {
780
- "get_input": get_input,
781
- "set_execution_count": set_execution_count,
782
- "add_output": add_output,
783
- "clear_output": clear_output,
784
- "set_metadata": set_metadata,
785
- "set_status": set_status,
786
- "done": wrapped_done,
787
- }
788
684
  self.msg_id_callbacks[msg_id].update(
789
- MsgCallbacks(filter(lambda x: x[1] is not None, callbacks.items())) # type: ignore # mypy #8890
685
+ cast(
686
+ "MsgCallbacks",
687
+ {k: v for k, v in local_callbacks.items() if v is not None},
688
+ )
790
689
  )
791
690
  # Wait for "done" callback to be called
792
691
  try:
@@ -827,8 +726,8 @@ class Kernel:
827
726
  if self.kc is not None:
828
727
  self.kc.comm_info(target_name=target_name)
829
728
 
830
- async def complete_(
831
- self, code: str, cursor_pos: int, timeout: int = 60
729
+ async def complete_async(
730
+ self, source: str, cursor_pos: int, timeout: int = 60
832
731
  ) -> list[dict]:
833
732
  """Request code completions from the kernel, asynchronously."""
834
733
  results: list[dict] = []
@@ -866,7 +765,7 @@ class Kernel:
866
765
  )
867
766
  event.set()
868
767
 
869
- msg_id = self.kc.complete(code, cursor_pos)
768
+ msg_id = self.kc.complete(source, cursor_pos)
870
769
  self.msg_id_callbacks[msg_id].update({"done": process_complete_reply})
871
770
 
872
771
  try:
@@ -876,32 +775,12 @@ class Kernel:
876
775
 
877
776
  return results
878
777
 
879
- def complete(self, code: str, cursor_pos: int) -> list[dict]:
880
- """Request code completions from the kernel.
881
-
882
- Args:
883
- code: The code string to retrieve completions for
884
- cursor_pos: The position of the cursor in the code string
885
-
886
- Returns:
887
- A list of dictionaries defining completion entries. The dictionaries
888
- contain ``text`` (the completion text), ``start_position`` (the stating
889
- position of the completion text), and optionally ``display_meta``
890
- (a string containing additional data about the completion type)
891
-
892
- """
893
- return self._aodo(
894
- self.complete_(code, cursor_pos),
895
- wait=True,
896
- single=True,
897
- )
898
-
899
- async def history_(
778
+ async def history_async(
900
779
  self,
901
780
  pattern: str = "",
902
781
  n: int = 1,
903
782
  hist_access_type: str = "search",
904
- timeout: int = 1,
783
+ timeout: int = 60,
905
784
  ) -> list[tuple[int, int, str]] | None:
906
785
  """Retrieve history from the kernel asynchronously."""
907
786
  await asyncio.sleep(0.1) # Add a tiny timeout so we don't spam the kernel
@@ -933,29 +812,9 @@ class Kernel:
933
812
 
934
813
  return results
935
814
 
936
- def history(
937
- self, pattern: str = "", n: int = 1, hist_access_type: str = "search"
938
- ) -> list[tuple[int, int, str]] | None:
939
- """Retrieve history from the kernel.
940
-
941
- Args:
942
- pattern: The pattern to search for
943
- n: the number of history items to return
944
- hist_access_type: How to access the history ('range', 'tail' or 'search')
945
-
946
- Returns:
947
- A list of history items, consisting of tuples (session, line_number, input)
948
-
949
- """
950
- return self._aodo(
951
- self.history_(pattern, n, hist_access_type),
952
- wait=True,
953
- single=True,
954
- )
955
-
956
- async def inspect_(
815
+ async def inspect_async(
957
816
  self,
958
- code: str,
817
+ source: str,
959
818
  cursor_pos: int,
960
819
  detail_level: int = 0,
961
820
  timeout: int = 2,
@@ -978,7 +837,9 @@ class Kernel:
978
837
  event.set()
979
838
 
980
839
  log.debug("Requesting contextual help from the kernel")
981
- msg_id = self.kc.inspect(code, cursor_pos=cursor_pos, detail_level=detail_level)
840
+ msg_id = self.kc.inspect(
841
+ source, cursor_pos=cursor_pos, detail_level=detail_level
842
+ )
982
843
  self.msg_id_callbacks[msg_id].update({"done": process_inspect_reply})
983
844
 
984
845
  try:
@@ -988,35 +849,9 @@ class Kernel:
988
849
 
989
850
  return result
990
851
 
991
- def inspect(
852
+ async def is_complete_async(
992
853
  self,
993
- code: str,
994
- cursor_pos: int,
995
- callback: Callable[[dict[str, Any]], None] | None = None,
996
- ) -> str:
997
- """Request code inspection from the kernel.
998
-
999
- Args:
1000
- code: The code string to retrieve completions for
1001
- cursor_pos: The position of the cursor in the code string
1002
- callback: A function to run when the inspection result arrives. The result
1003
- is passed as an argument.
1004
-
1005
- Returns:
1006
- A string containing useful information about the code at the current cursor
1007
- position
1008
-
1009
- """
1010
- return self._aodo(
1011
- self.inspect_(code, cursor_pos),
1012
- wait=False,
1013
- callback=callback,
1014
- single=True,
1015
- )
1016
-
1017
- async def is_complete_(
1018
- self,
1019
- code: str,
854
+ source: str,
1020
855
  timeout: int | float = 0.1,
1021
856
  ) -> dict[str, Any]:
1022
857
  """Ask the kernel to determine if code is complete asynchronously."""
@@ -1032,7 +867,7 @@ class Kernel:
1032
867
  result.update(content)
1033
868
  event.set()
1034
869
 
1035
- msg_id = self.kc.is_complete(code)
870
+ msg_id = self.kc.is_complete(source)
1036
871
  self.msg_id_callbacks[msg_id].update(
1037
872
  {"completeness_status": process_is_complete_reply}
1038
873
  )
@@ -1044,31 +879,10 @@ class Kernel:
1044
879
 
1045
880
  return result
1046
881
 
1047
- def is_complete(
1048
- self,
1049
- code: str,
1050
- timeout: int | float = 0.1,
1051
- wait: bool = False,
1052
- callback: Callable[[dict[str, Any]], None] | None = None,
1053
- ) -> dict[str, Any]:
1054
- """Request code completeness status from the kernel.
1055
-
1056
- Args:
1057
- code: The code string to check the completeness status of
1058
- timeout: How long to wait for a kernel response
1059
- wait: Whether to wait for the response
1060
- callback: A function to run when the inspection result arrives. The result
1061
- is passed as an argument.
1062
-
1063
- Returns:
1064
- A string describing the completeness status
1065
-
1066
- """
1067
- return self._aodo(
1068
- self.is_complete_(code, timeout),
1069
- wait=wait,
1070
- callback=callback,
1071
- )
882
+ def input(self, text: str) -> None:
883
+ """Send input to the kernel."""
884
+ if self.kc:
885
+ self.kc.input(text)
1072
886
 
1073
887
  def interrupt(self) -> None:
1074
888
  """Interrupt the kernel.
@@ -1083,63 +897,20 @@ class Kernel:
1083
897
  log.debug("Interrupting kernel %s", self.id)
1084
898
  KernelManager.interrupt_kernel(self.km)
1085
899
 
1086
- async def restart_(self) -> None:
900
+ async def restart_async(self) -> None:
1087
901
  """Restart the kernel asyncchronously."""
1088
- self.dead = True
1089
902
  log.debug("Restarting kernel `%s`", self.id)
903
+ # Cancel polling tasks
904
+ for task in self.poll_tasks:
905
+ task.cancel()
1090
906
  self.error = None
1091
907
  self.status = "starting"
1092
- await self.km.restart_kernel(now=True)
908
+ try:
909
+ await self.km.restart_kernel(now=True)
910
+ await self.post_start()
911
+ except asyncio.exceptions.InvalidStateError:
912
+ await self.start_async()
1093
913
  log.debug("Kernel %s restarted", self.id)
1094
- await self.post_start_()
1095
-
1096
- def restart(self, wait: bool = False, cb: Callable | None = None) -> None:
1097
- """Restart the current kernel."""
1098
- self._aodo(
1099
- self.restart_(),
1100
- wait=wait,
1101
- callback=cb,
1102
- )
1103
-
1104
- def change(
1105
- self,
1106
- name: str | None,
1107
- connection_file: Path | None = None,
1108
- cb: Callable | None = None,
1109
- ) -> None:
1110
- """Change the kernel.
1111
-
1112
- Args:
1113
- name: The name of the kernel to change to
1114
- connection_file: The path to the connection file to use
1115
- cb: Callback to run once restarted
1116
-
1117
- """
1118
- from euporie.core.kernel.manager import EuporieKernelManager
1119
-
1120
- self.connection_file = connection_file
1121
- self.status = "starting"
1122
-
1123
- # Update the tab's kernel spec
1124
- spec = self.specs.get(name or "", {}).get("spec", {})
1125
- self.kernel_tab.metadata["kernelspec"] = {
1126
- "name": name,
1127
- "display_name": spec.get("display_name", ""),
1128
- "language": spec.get("language", ""),
1129
- }
1130
-
1131
- # Stop the old kernel
1132
- if self.km.has_kernel:
1133
- self.stop()
1134
-
1135
- # Create a new kernel manager instance
1136
- del self.km
1137
- kwargs = {} if name is None else {"kernel_name": name}
1138
- self.km = EuporieKernelManager(**kwargs)
1139
- self.error = None
1140
-
1141
- # Start the kernel
1142
- self.start(cb=cb)
1143
914
 
1144
915
  def stop(self, cb: Callable | None = None, wait: bool = False) -> None:
1145
916
  """Stop the current kernel.
@@ -1155,17 +926,9 @@ class Kernel:
1155
926
  if callable(cb):
1156
927
  cb()
1157
928
  else:
1158
- log.debug("Stopping kernel %s (wait=%s)", self.id, wait)
1159
- # This helps us leave a little earlier
1160
- if not wait:
1161
- self.interrupt()
1162
- self._aodo(
1163
- self.stop_(),
1164
- callback=cb,
1165
- wait=wait,
1166
- )
929
+ super().stop(cb, wait)
1167
930
 
1168
- async def shutdown_(self) -> None:
931
+ async def shutdown_async(self) -> None:
1169
932
  """Shut down the kernel and close the event loop if running in a thread."""
1170
933
  # Stop monitoring the kernel
1171
934
  if self.monitor_task is not None:
@@ -1175,23 +938,3 @@ class Kernel:
1175
938
  # Stop kernel
1176
939
  if self.km.has_kernel:
1177
940
  await self.km.shutdown_kernel(now=True)
1178
- # Stop event loop
1179
- if self.threaded:
1180
- self.loop.stop()
1181
-
1182
- def shutdown(self, wait: bool = False) -> None:
1183
- """Shutdown the kernel and close the kernel's thread.
1184
-
1185
- This is intended to be run when the notebook is closed: the
1186
- :py:class:`~euporie.core.tabs.notebook.Kernel` cannot be restarted after this.
1187
-
1188
- Args:
1189
- wait: Whether to block until shutdown completes
1190
-
1191
- """
1192
- self._aodo(
1193
- self.shutdown_(),
1194
- wait=wait,
1195
- )
1196
- if self.threaded:
1197
- self.thread.join(timeout=5)