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
@@ -0,0 +1,571 @@
1
+ """Base class for euporie kernels."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ import concurrent
7
+ import logging
8
+ import threading
9
+ from abc import ABC, abstractmethod
10
+ from typing import TYPE_CHECKING, NamedTuple, TypedDict, overload
11
+
12
+ if TYPE_CHECKING:
13
+ from collections.abc import Callable, Coroutine
14
+ from typing import Any, Literal, Protocol, TypeVar, Unpack
15
+
16
+ from euporie.core.tabs.kernel import KernelTab
17
+
18
+ class KernelFactory(Protocol):
19
+ """Type for kernel factory functions."""
20
+
21
+ def __call__(
22
+ self,
23
+ kernel_tab: KernelTab,
24
+ default_callbacks: MsgCallbacks | None = None,
25
+ allow_stdin: bool = False,
26
+ **kwargs: Any,
27
+ ) -> BaseKernel:
28
+ """Signature for creating a new Kernel instance."""
29
+
30
+ T = TypeVar("T")
31
+
32
+ log = logging.getLogger(__name__)
33
+
34
+ _THREAD: list[threading.Thread | None] = [None]
35
+ _LOOP: list[asyncio.AbstractEventLoop | None] = [None]
36
+
37
+
38
+ class KernelInfo(NamedTuple):
39
+ """Named tuple representing a launchable kernel."""
40
+
41
+ name: str
42
+ display_name: str
43
+ type: type[BaseKernel]
44
+ kind: Literal["new", "existing"]
45
+ factory: KernelFactory
46
+
47
+
48
+ class MsgCallbacks(TypedDict, total=False):
49
+ """Typed dictionary for named message callbacks."""
50
+
51
+ get_input: Callable[[str, bool], None] | None
52
+ set_execution_count: Callable[[int], None] | None
53
+ add_output: Callable[[dict[str, Any], bool], None] | None
54
+ add_input: Callable[[dict[str, Any], bool], None] | None
55
+ clear_output: Callable[[bool], None] | None
56
+ done: Callable[[dict[str, Any]], None] | None
57
+ set_metadata: Callable[[tuple[str, ...], Any], None] | None
58
+ set_status: Callable[[str], None] | None
59
+ set_kernel_info: Callable[[dict[str, Any]], None] | None
60
+ completeness_status: Callable[[dict[str, Any]], None] | None
61
+ dead: Callable[[], None] | None
62
+ # Payloads
63
+ page: Callable[[list[dict], int], None] | None
64
+ set_next_input: Callable[[str, bool], None] | None
65
+ edit_magic: Callable[[str, int], None] | None
66
+ ask_exit: Callable[[bool], None] | None
67
+
68
+
69
+ def get_loop() -> asyncio.AbstractEventLoop:
70
+ """Create or return the conversion IO loop.
71
+
72
+ The loop will be running on a separate thread.
73
+ """
74
+ if _LOOP[0] is None:
75
+ loop = asyncio.new_event_loop()
76
+ _LOOP[0] = loop
77
+ thread = threading.Thread(
78
+ target=loop.run_forever, name="EuporieKernelLoop", daemon=True
79
+ )
80
+ thread.start()
81
+ _THREAD[0] = thread
82
+ assert _LOOP[0] is not None
83
+ # Check we are not already in the conversion event loop
84
+ try:
85
+ running_loop = asyncio.get_running_loop()
86
+ except RuntimeError:
87
+ running_loop = None
88
+ if _LOOP[0] is running_loop:
89
+ raise NotImplementedError("Cannot nest event loop access")
90
+ return _LOOP[0]
91
+
92
+
93
+ class BaseKernel(ABC):
94
+ """Abstract base class for euporie kernels."""
95
+
96
+ @classmethod
97
+ def variants(cls) -> list[KernelInfo]:
98
+ """Return a list of parameterized variants of this kernel."""
99
+ return []
100
+
101
+ def __init__(
102
+ self,
103
+ kernel_tab: KernelTab,
104
+ default_callbacks: MsgCallbacks | None = None,
105
+ allow_stdin: bool = False,
106
+ **kwargs: Any,
107
+ ) -> None:
108
+ """Initialize the kernel.
109
+
110
+ Args:
111
+ kernel_tab: The notebook this kernel belongs to
112
+ allow_stdin: Whether the kernel is allowed to request input
113
+ default_callbacks: The default callbacks to use on receipt of a message
114
+ **kwargs: Additional keyword arguments passed to parent classes
115
+ """
116
+ self.loop = get_loop()
117
+ self.kernel_tab = kernel_tab
118
+ self.allow_stdin = allow_stdin
119
+ self._status = "stopped"
120
+ self.error: Exception | None = None
121
+ self.dead = False
122
+ self.status_change_event = asyncio.Event()
123
+ self.coros: dict[str, concurrent.futures.Future] = {}
124
+ self.msg_id_callbacks: dict[str, MsgCallbacks] = {}
125
+ self.threaded = False
126
+
127
+ self.default_callbacks = MsgCallbacks(
128
+ {
129
+ "get_input": None,
130
+ "set_execution_count": None,
131
+ "add_output": None,
132
+ "clear_output": None,
133
+ "done": None,
134
+ "set_metadata": None,
135
+ "set_status": None,
136
+ }
137
+ )
138
+ if default_callbacks is not None:
139
+ self.default_callbacks.update(default_callbacks)
140
+
141
+ @property
142
+ @abstractmethod
143
+ def spec(self) -> dict[str, str]:
144
+ """The kernelspec metadata for the current kernel instance."""
145
+
146
+ @overload
147
+ def _aodo(
148
+ self,
149
+ coro: Coroutine[Any, Any, T],
150
+ wait: Literal[True] = True,
151
+ callback: Callable | None = None,
152
+ timeout: int | float | None = None,
153
+ single: bool = False,
154
+ ) -> T | None: ...
155
+ @overload
156
+ def _aodo(
157
+ self,
158
+ coro: Coroutine[Any, Any, T],
159
+ wait: Literal[False] = False,
160
+ callback: Callable | None = None,
161
+ timeout: int | float | None = None,
162
+ single: bool = False,
163
+ ) -> concurrent.futures.Future: ...
164
+ @overload
165
+ def _aodo(
166
+ self,
167
+ coro: Coroutine[Any, Any, T],
168
+ wait: bool = False,
169
+ callback: Callable | None = None,
170
+ timeout: int | float | None = None,
171
+ single: bool = False,
172
+ ) -> T | None | concurrent.futures.Future: ...
173
+ def _aodo(self, coro, wait=False, callback=None, timeout=None, single=False):
174
+ """Schedule a coroutine in the kernel's event loop.
175
+
176
+ Optionally waits for the results (blocking the main thread). Optionally
177
+ schedules a callback to run when the coroutine has completed or timed out.
178
+
179
+ Args:
180
+ coro: The coroutine to run
181
+ wait: If :py:const:`True`, block until the kernel has started
182
+ callback: A function to run when the coroutine completes. The result from
183
+ the coroutine will be passed as an argument
184
+ timeout: The number of seconds to allow the coroutine to run if waiting
185
+ single: If :py:const:`True`, any futures for previous instances of the
186
+ coroutine will be cancelled
187
+
188
+ Returns:
189
+ The result of the coroutine
190
+
191
+ """
192
+ future = asyncio.run_coroutine_threadsafe(coro, self.loop)
193
+
194
+ # Cancel previous future instances if required
195
+ if single and self.coros.get(coro.__name__):
196
+ self.coros[coro.__name__].cancel()
197
+ self.coros[coro.__name__] = future
198
+
199
+ if wait:
200
+ result = None
201
+ try:
202
+ result = future.result(timeout)
203
+ except concurrent.futures.TimeoutError:
204
+ log.error("Operation '%s' timed out", coro)
205
+ future.cancel()
206
+ return result
207
+ finally:
208
+ if callable(callback):
209
+ callback(result)
210
+ return result
211
+ else:
212
+ if callable(callback):
213
+ future.add_done_callback(lambda f: callback(f.result()))
214
+ return future
215
+
216
+ @property
217
+ def status(self) -> str:
218
+ """Retrieve the current kernel status.
219
+
220
+ Returns:
221
+ The kernel status
222
+
223
+ """
224
+ return self._status
225
+
226
+ @status.setter
227
+ def status(self, value: str) -> None:
228
+ """Set the kernel status."""
229
+ self.status_change_event.set()
230
+ self._status = value
231
+ self.status_change_event.clear()
232
+
233
+ def wait_for_status(self, status: str = "idle") -> None:
234
+ """Block until the kernel reaches a given status value."""
235
+ if self.status != status:
236
+
237
+ async def _wait() -> None:
238
+ while self.status != status:
239
+ await asyncio.wait_for(
240
+ self.status_change_event.wait(), timeout=None
241
+ )
242
+
243
+ self._aodo(_wait(), wait=True)
244
+
245
+ def start(
246
+ self, cb: Callable | None = None, wait: bool = False, timeout: int = 10
247
+ ) -> None:
248
+ """Start the kernel.
249
+
250
+ Args:
251
+ cb: An optional callback to run after the kernel has started
252
+ wait: If :py:const:`True`, block until the kernel has started
253
+ timeout: How long to wait until failure is assumed
254
+
255
+ """
256
+ self._aodo(
257
+ self.start_async(),
258
+ timeout=timeout,
259
+ wait=wait,
260
+ callback=cb,
261
+ )
262
+
263
+ @abstractmethod
264
+ async def start_async(self) -> None:
265
+ """Start the kernel."""
266
+
267
+ def stop(self, cb: Callable | None = None, wait: bool = False) -> None:
268
+ """Stop the kernel."""
269
+ log.debug("Stopping kernel %s (wait=%s)", self.id, wait)
270
+ if not wait:
271
+ self.interrupt()
272
+ self._aodo(self.stop_async(), callback=cb, wait=wait)
273
+
274
+ async def stop_async(self, cb: Callable | None = None) -> None:
275
+ """Stop the kernel asynchronously."""
276
+ self.stop(cb)
277
+
278
+ def run(
279
+ self,
280
+ source: str,
281
+ wait: bool = False,
282
+ callback: Callable[..., None] | None = None,
283
+ **callbacks: Callable[..., Any],
284
+ ) -> None:
285
+ """Execute code in the kernel."""
286
+ self._aodo(
287
+ self.run_async(source, **callbacks),
288
+ wait=wait,
289
+ callback=callback,
290
+ )
291
+
292
+ @abstractmethod
293
+ async def run_async(
294
+ self, source: str, **local_callbacks: Unpack[MsgCallbacks]
295
+ ) -> None:
296
+ """Execute code in the kernel asynchronously."""
297
+
298
+ def complete(self, source: str, cursor_pos: int) -> list[dict]:
299
+ """Request code completions from the kernel.
300
+
301
+ Args:
302
+ source: The code string to retrieve completions for
303
+ cursor_pos: The position of the cursor in the code string
304
+
305
+ Returns:
306
+ A list of dictionaries defining completion entries. The dictionaries
307
+ contain ``text`` (the completion text), ``start_position`` (the stating
308
+ position of the completion text), and optionally ``display_meta``
309
+ (a string containing additional data about the completion type)
310
+
311
+ """
312
+ return (
313
+ self._aodo(
314
+ self.complete_async(source, cursor_pos),
315
+ wait=True,
316
+ single=True,
317
+ )
318
+ or []
319
+ )
320
+
321
+ @abstractmethod
322
+ async def complete_async(self, source: str, cursor_pos: int) -> list[dict]:
323
+ """Get code completions asynchronously."""
324
+
325
+ def history(
326
+ self, pattern: str = "", n: int = 1, hist_access_type: str = "search"
327
+ ) -> list[tuple[int, int, str]] | None:
328
+ """Retrieve history from the kernel.
329
+
330
+ Args:
331
+ pattern: The pattern to search for
332
+ n: the number of history items to return
333
+ hist_access_type: How to access the history ('range', 'tail' or 'search')
334
+
335
+ Returns:
336
+ A list of history items, consisting of tuples (session, line_number, input)
337
+
338
+ """
339
+ return self._aodo(
340
+ self.history_async(pattern, n, hist_access_type),
341
+ wait=True,
342
+ single=True,
343
+ )
344
+
345
+ async def history_async(
346
+ self,
347
+ pattern: str = "",
348
+ n: int = 1,
349
+ hist_access_type: str = "search",
350
+ timeout: int = 1,
351
+ ) -> list[tuple[int, int, str]] | None:
352
+ """Retrieve history from the kernel asynchronously."""
353
+ return []
354
+
355
+ def inspect(
356
+ self,
357
+ source: str,
358
+ cursor_pos: int,
359
+ detail_level: int = 0,
360
+ timeout: int = 2,
361
+ callback: Callable[[dict[str, Any]], None] | None = None,
362
+ ) -> dict[str, Any]:
363
+ """Request code inspection from the kernel.
364
+
365
+ Args:
366
+ source: The code string to retrieve completions for
367
+ cursor_pos: The position of the cursor in the code string
368
+ detail_level: Level of detail for the inspection (0-2)
369
+ timeout: Number of seconds to wait for inspection results
370
+ callback: A function to run when the inspection result arrives. The result
371
+ is passed as an argument.
372
+
373
+ Returns:
374
+ A string containing useful information about the code at the current cursor
375
+ position
376
+
377
+ """
378
+ return (
379
+ self._aodo(
380
+ self.inspect_async(source, cursor_pos, detail_level),
381
+ wait=True,
382
+ callback=callback,
383
+ single=True,
384
+ )
385
+ or {}
386
+ )
387
+
388
+ @abstractmethod
389
+ async def inspect_async(
390
+ self,
391
+ source: str,
392
+ cursor_pos: int,
393
+ detail_level: int = 0,
394
+ timeout: int = 2,
395
+ ) -> dict[str, Any]:
396
+ """Get code inspection/documentation asynchronously."""
397
+
398
+ def is_complete(
399
+ self,
400
+ source: str,
401
+ timeout: int | float = 0.1,
402
+ callback: Callable[[dict[str, Any]], None] | None = None,
403
+ ) -> dict[str, Any]:
404
+ """Request code completeness status from the kernel.
405
+
406
+ Args:
407
+ source: The code string to check the completeness status of
408
+ timeout: How long to wait for a kernel response
409
+ wait: Whether to wait for the response
410
+ callback: A function to run when the inspection result arrives. The result
411
+ is passed as an argument.
412
+
413
+ Returns:
414
+ A string describing the completeness status
415
+
416
+ """
417
+ return (
418
+ self._aodo(
419
+ self.is_complete_async(source, timeout), wait=True, callback=callback
420
+ )
421
+ or {}
422
+ )
423
+
424
+ @abstractmethod
425
+ async def is_complete_async(
426
+ self,
427
+ source: str,
428
+ timeout: int | float = 0.1,
429
+ ) -> dict[str, Any]:
430
+ """Check if code is complete asynchronously."""
431
+
432
+ @abstractmethod
433
+ def input(self, text: str) -> None:
434
+ """Send input to the kernel."""
435
+
436
+ @abstractmethod
437
+ def interrupt(self) -> None:
438
+ """Interrupt the kernel."""
439
+
440
+ def restart(self, wait: bool = False, cb: Callable | None = None) -> None:
441
+ """Restart the current kernel."""
442
+ self._aodo(
443
+ self.restart_async(),
444
+ wait=wait,
445
+ callback=cb,
446
+ )
447
+
448
+ @abstractmethod
449
+ async def restart_async(self) -> None:
450
+ """Restart the kernel asynchronously."""
451
+
452
+ def shutdown(self, wait: bool = False, cb: Callable | None = None) -> None:
453
+ """Shutdown the kernel.
454
+
455
+ This is intended to be run when the notebook is closed: the
456
+ :py:class:`~euporie.core.kernel.base.BaseKernel` cannot be restarted after this.
457
+
458
+ Args:
459
+ wait: Whether to block until shutdown completes
460
+ cb: Callback run after shutdown completes
461
+
462
+ """
463
+ self._aodo(self.shutdown_async(), wait=wait, callback=cb)
464
+
465
+ @abstractmethod
466
+ async def shutdown_async(self) -> None:
467
+ """Shutdown the kernel asynchronously."""
468
+
469
+ @property
470
+ @abstractmethod
471
+ def missing(self) -> bool:
472
+ """Return whether the kernel is missing."""
473
+
474
+ @property
475
+ @abstractmethod
476
+ def id(self) -> str | None:
477
+ """Return the kernel ID."""
478
+
479
+ @abstractmethod
480
+ def info(
481
+ self,
482
+ set_kernel_info: Callable[[dict[str, Any]], None] | None = None,
483
+ set_status: Callable[[str], None] | None = None,
484
+ ) -> None:
485
+ """Request information about the kernel."""
486
+
487
+ def kc_comm(self, comm_id: str, data: dict[str, Any]) -> str:
488
+ """By default kernels do not implement COMM communication."""
489
+ log.warning("The %s kernel does not implement COMMs", self.__class__.__name__)
490
+ return "" # TODO - raise NotImplementedError
491
+
492
+ def comm_info(self, target_name: str | None = None) -> None:
493
+ """Request information about the current comms.
494
+
495
+ Does nothing by default.
496
+ """
497
+
498
+
499
+ class NoKernel(BaseKernel):
500
+ """A `None` kernel."""
501
+
502
+ @property
503
+ def spec(self) -> dict[str, str]:
504
+ """The kernelspec metadata for the current kernel instance."""
505
+ raise NotImplementedError()
506
+
507
+ async def start_async(self) -> None:
508
+ """Start the kernel."""
509
+ raise NotImplementedError()
510
+
511
+ async def run_async(
512
+ self, source: str, **local_callbacks: Unpack[MsgCallbacks]
513
+ ) -> None:
514
+ """Execute code in the kernel asynchronously."""
515
+ raise NotImplementedError()
516
+
517
+ async def is_complete_async(
518
+ self,
519
+ source: str,
520
+ timeout: int | float = 0.1,
521
+ ) -> dict[str, Any]:
522
+ """Check if code is complete asynchronously."""
523
+ raise NotImplementedError()
524
+
525
+ async def complete_async(self, source: str, cursor_pos: int) -> list[dict]:
526
+ """Get code completions asynchronously."""
527
+ raise NotImplementedError()
528
+
529
+ async def inspect_async(
530
+ self,
531
+ source: str,
532
+ cursor_pos: int,
533
+ detail_level: int = 0,
534
+ timeout: int = 2,
535
+ ) -> dict[str, Any]:
536
+ """Get code inspection/documentation asynchronously."""
537
+ raise NotImplementedError()
538
+
539
+ def input(self, text: str) -> None:
540
+ """Send input to the kernel."""
541
+ # Do nothing
542
+
543
+ def interrupt(self) -> None:
544
+ """Interrupt the kernel."""
545
+ # Do nothing
546
+
547
+ async def restart_async(self) -> None:
548
+ """Restart the kernel asynchronously."""
549
+ # Do nothing
550
+
551
+ async def shutdown_async(self) -> None:
552
+ """Shutdown the kernel asynchronously."""
553
+ # Do nothing
554
+
555
+ @property
556
+ def missing(self) -> bool:
557
+ """Return whether the kernel is missing."""
558
+ raise NotImplementedError()
559
+
560
+ @property
561
+ def id(self) -> str | None:
562
+ """Return the kernel ID."""
563
+ raise NotImplementedError()
564
+
565
+ def info(
566
+ self,
567
+ set_kernel_info: Callable[[dict[str, Any]], None] | None = None,
568
+ set_status: Callable[[str], None] | None = None,
569
+ ) -> None:
570
+ """Request information about the kernel."""
571
+ raise NotImplementedError()