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
euporie/core/graphics.py CHANGED
@@ -376,8 +376,8 @@ class ItermGraphicControl(GraphicControl):
376
376
  return self._format_cache.get(key, render_lines)
377
377
 
378
378
 
379
- class KittyGraphicControl(GraphicControl):
380
- """A graphic control which displays images using Kitty's graphics protocol."""
379
+ class BaseKittyGraphicControl(GraphicControl):
380
+ """Base graphic control with common methods for both styles of kitty display."""
381
381
 
382
382
  _kitty_image_count: ClassVar[int] = 1
383
383
 
@@ -451,7 +451,7 @@ class KittyGraphicControl(GraphicControl):
451
451
  BoundedWritePosition(0, 0, width=cols, height=rows, bbox=bbox)
452
452
  )
453
453
  self.kitty_image_id = self._kitty_image_count
454
- KittyGraphicControl._kitty_image_count += 1
454
+ self.__class__._kitty_image_count += 1
455
455
 
456
456
  while data:
457
457
  chunk, data = data[:4096], data[4096:]
@@ -471,24 +471,6 @@ class KittyGraphicControl(GraphicControl):
471
471
  self.app.output.flush()
472
472
  self.loaded = True
473
473
 
474
- def hide_cmd(self) -> str:
475
- """Generate a command to hide the graphic."""
476
- return passthrough(
477
- self._kitty_cmd(
478
- a="d",
479
- d="i",
480
- i=self.kitty_image_id,
481
- q=1,
482
- ),
483
- self.app.config,
484
- )
485
-
486
- def hide(self) -> None:
487
- """Hide the graphic from show without deleting it."""
488
- if self.kitty_image_id > 0:
489
- self.app.output.write_raw(self.hide_cmd())
490
- self.app.output.flush()
491
-
492
474
  def delete(self) -> None:
493
475
  """Delete the graphic from the terminal."""
494
476
  if self.kitty_image_id > 0:
@@ -506,6 +488,22 @@ class KittyGraphicControl(GraphicControl):
506
488
  self.app.output.flush()
507
489
  self.loaded = False
508
490
 
491
+ def reset(self) -> None:
492
+ """Hide and delete the kitty graphic from the terminal."""
493
+ self.hide()
494
+ self.delete()
495
+ super().reset()
496
+
497
+ def close(self) -> None:
498
+ """Remove the displayed object entirely."""
499
+ super().close()
500
+ if not self.app.leave_graphics():
501
+ self.delete()
502
+
503
+
504
+ class KittyGraphicControl(BaseKittyGraphicControl):
505
+ """A graphic control which displays images using Kitty's graphics protocol."""
506
+
509
507
  def get_rendered_lines(
510
508
  self, visible_width: int, visible_height: int, wrap_lines: bool = False
511
509
  ) -> list[StyleAndTextTuples]:
@@ -616,17 +614,181 @@ class KittyGraphicControl(GraphicControl):
616
614
  )
617
615
  return self._format_cache.get(key, render_lines)
618
616
 
619
- def reset(self) -> None:
620
- """Hide and delete the kitty graphic from the terminal."""
621
- self.hide()
622
- self.delete()
623
- super().reset()
617
+ def hide_cmd(self) -> str:
618
+ """Generate a command to hide the graphic."""
619
+ return passthrough(
620
+ self._kitty_cmd(
621
+ a="d",
622
+ d="i",
623
+ i=self.kitty_image_id,
624
+ q=1,
625
+ ),
626
+ self.app.config,
627
+ )
628
+
629
+ def hide(self) -> None:
630
+ """Hide the graphic from show without deleting it."""
631
+ if self.kitty_image_id > 0:
632
+ self.app.output.write_raw(self.hide_cmd())
633
+ self.app.output.flush()
624
634
 
625
- def close(self) -> None:
626
- """Remove the displayed object entirely."""
627
- super().close()
628
- if not self.app.leave_graphics():
629
- self.delete()
635
+
636
+ class KittyUnicodeGraphicControl(BaseKittyGraphicControl):
637
+ """A graphic control which displays images using Kitty's Unicode placeholder mechanism."""
638
+
639
+ PLACEHOLDER = "\U0010eeee" # U+10EEEE placeholder character
640
+ # fmt: off
641
+ DIACRITICS = ( # Diacritics for encoding row/column numbers (0-9)
642
+ "\u0305", "\u030d", "\u030e", "\u0310", "\u0312", "\u033d", "\u033e", "\u033f",
643
+ "\u0346", "\u034a", "\u034b", "\u034c", "\u0350", "\u0351", "\u0352", "\u0357",
644
+ "\u035b", "\u0363", "\u0364", "\u0365", "\u0366", "\u0367", "\u0368", "\u0369",
645
+ "\u036a", "\u036b", "\u036c", "\u036d", "\u036e", "\u036f", "\u0483", "\u0484",
646
+ "\u0485", "\u0486", "\u0487", "\u0592", "\u0593", "\u0594", "\u0595", "\u0597",
647
+ "\u0598", "\u0599", "\u059c", "\u059d", "\u059e", "\u059f", "\u05a0", "\u05a1",
648
+ "\u05a8", "\u05a9", "\u05ab", "\u05ac", "\u05af", "\u05c4", "\u0610", "\u0611",
649
+ "\u0612", "\u0613", "\u0614", "\u0615", "\u0616", "\u0617", "\u0657", "\u0658",
650
+ "\u0659", "\u065a", "\u065b", "\u065d", "\u065e", "\u06d6", "\u06d7", "\u06d8",
651
+ "\u06d9", "\u06da", "\u06db", "\u06dc", "\u06df", "\u06e0", "\u06e1", "\u06e2",
652
+ "\u06e4", "\u06e7", "\u06e8", "\u06eb", "\u06ec", "\u0730", "\u0732", "\u0733",
653
+ "\u0735", "\u0736", "\u073a", "\u073d", "\u073f", "\u0740", "\u0741", "\u0743",
654
+ "\u0745", "\u0747", "\u0749", "\u074a", "\u07eb", "\u07ec", "\u07ed", "\u07ee",
655
+ "\u07ef", "\u07f0", "\u07f1", "\u07f3", "\u0816", "\u0817", "\u0818", "\u0819",
656
+ "\u081b", "\u081c", "\u081d", "\u081e", "\u081f", "\u0820", "\u0821", "\u0822",
657
+ "\u0823", "\u0825", "\u0826", "\u0827", "\u0829", "\u082a", "\u082b", "\u082c",
658
+ "\u082d", "\u0951", "\u0953", "\u0954", "\u0f82", "\u0f83", "\u0f86", "\u0f87",
659
+ "\u135d", "\u135e", "\u135f", "\u17dd", "\u193a", "\u1a17", "\u1a75", "\u1a76",
660
+ "\u1a77", "\u1a78", "\u1a79", "\u1a7a", "\u1a7b", "\u1a7c", "\u1b6b", "\u1b6d",
661
+ "\u1b6e", "\u1b6f", "\u1b70", "\u1b71", "\u1b72", "\u1b73", "\u1cd0", "\u1cd1",
662
+ "\u1cd2", "\u1cda", "\u1cdb", "\u1ce0", "\u1dc0", "\u1dc1", "\u1dc3", "\u1dc4",
663
+ "\u1dc5", "\u1dc6", "\u1dc7", "\u1dc8", "\u1dc9", "\u1dcb", "\u1dcc", "\u1dd1",
664
+ "\u1dd2", "\u1dd3", "\u1dd4", "\u1dd5", "\u1dd6", "\u1dd7", "\u1dd8", "\u1dd9",
665
+ "\u1dda", "\u1ddb", "\u1ddc", "\u1ddd", "\u1dde", "\u1ddf", "\u1de0", "\u1de1",
666
+ "\u1de2", "\u1de3", "\u1de4", "\u1de5", "\u1de6", "\u1dfe", "\u20d0", "\u20d1",
667
+ "\u20d4", "\u20d5", "\u20d6", "\u20d7", "\u20db", "\u20dc", "\u20e1", "\u20e7",
668
+ "\u20e9", "\u20f0", "\u2cef", "\u2cf0", "\u2cf1", "\u2de0", "\u2de1", "\u2de2",
669
+ "\u2de3", "\u2de4", "\u2de5", "\u2de6", "\u2de7", "\u2de8", "\u2de9", "\u2dea",
670
+ "\u2deb", "\u2dec", "\u2ded", "\u2dee", "\u2def", "\u2df0", "\u2df1", "\u2df2",
671
+ "\u2df3", "\u2df4", "\u2df5", "\u2df6", "\u2df7", "\u2df8", "\u2df9", "\u2dfa",
672
+ "\u2dfb", "\u2dfc", "\u2dfd", "\u2dfe", "\u2dff", "\ua66f", "\ua67c", "\ua67d",
673
+ "\ua6f0", "\ua6f1", "\ua8e0", "\ua8e1", "\ua8e2", "\ua8e3", "\ua8e4", "\ua8e5",
674
+ "\ua8e6", "\ua8e7", "\ua8e8", "\ua8e9", "\ua8ea", "\ua8eb", "\ua8ec", "\ua8ed",
675
+ "\ua8ee", "\ua8ef", "\ua8f0", "\ua8f1", "\uaab0", "\uaab2", "\uaab3", "\uaab7",
676
+ "\uaab8", "\uaabe", "\uaabf", "\uaac1", "\ufe20", "\ufe21", "\ufe22", "\ufe23",
677
+ "\ufe24", "\ufe25", "\ufe26",
678
+ "\U00010a0f", "\U00010a38", "\U0001d185", "\U0001d186", "\U0001d187",
679
+ "\U0001d188", "\U0001d189", "\U0001d1aa", "\U0001d1ab", "\U0001d1ac",
680
+ "\U0001d1ad", "\U0001d242", "\U0001d243", "\U0001d244",
681
+ )
682
+ # fmt: on
683
+
684
+ def __init__(
685
+ self,
686
+ datum: Datum,
687
+ scale: float = 0,
688
+ bbox: DiInt | None = None,
689
+ ) -> None:
690
+ """Create a new kitty graphic instance."""
691
+ super().__init__(datum, scale, bbox)
692
+ self.placements: set[tuple[int, int]] = set()
693
+
694
+ def get_rendered_lines(
695
+ self, visible_width: int, visible_height: int, wrap_lines: bool = False
696
+ ) -> list[StyleAndTextTuples]:
697
+ """Get rendered lines from the cache, or generate them."""
698
+ bbox = self.bbox
699
+
700
+ cell_size_px = self.app.cell_size_px
701
+ datum = self._datum_pad_cache[(self.datum, *cell_size_px)]
702
+ px, py = datum.pixel_size()
703
+ # Fall back to a default pixel size
704
+ px = px or 100
705
+ py = py or 100
706
+
707
+ d_cols, d_aspect = datum.cell_size()
708
+ d_rows = d_cols * d_aspect
709
+
710
+ total_available_width = visible_width + bbox.left + bbox.right
711
+ total_available_height = visible_height + bbox.top + bbox.bottom
712
+
713
+ # Scale down the graphic to fit in the available space
714
+ if d_rows > total_available_height or d_cols > total_available_width:
715
+ if d_rows / total_available_height > d_cols / total_available_width:
716
+ ratio = min(1, total_available_height / d_rows)
717
+ else:
718
+ ratio = min(1, total_available_width / d_cols)
719
+ else:
720
+ ratio = 1
721
+
722
+ # Calculate the size and cropping bbox at which we want to display the graphic
723
+ cols = floor(d_cols * ratio)
724
+ rows = ceil(cols * d_aspect)
725
+ d_bbox = DiInt(
726
+ top=self.bbox.top,
727
+ right=max(0, cols - (total_available_width - self.bbox.right)),
728
+ bottom=max(0, rows - (total_available_height - self.bbox.bottom)),
729
+ left=self.bbox.left,
730
+ )
731
+ if not self.loaded:
732
+ self.load(cols=cols, rows=rows, bbox=DiInt(0, 0, 0, 0))
733
+
734
+ # Add virtual placement for this size if required
735
+ if (cols, rows) not in self.placements:
736
+ cmd = self._kitty_cmd(
737
+ a="p", # Display a previously transmitted image
738
+ i=self.kitty_image_id,
739
+ p=1, # Placement ID
740
+ U=1, # Create a virtual placement
741
+ c=cols,
742
+ r=rows,
743
+ q=2,
744
+ )
745
+ self.app.output.write_raw(passthrough(cmd, self.app.config))
746
+ self.app.output.flush()
747
+ self.placements.add((cols, rows))
748
+
749
+ def render_lines() -> list[StyleAndTextTuples]:
750
+ """Render the lines to display in the control."""
751
+ ft: StyleAndTextTuples = []
752
+
753
+ # Generate placeholder grid
754
+ row_start = d_bbox.top
755
+ row_stop = rows - d_bbox.bottom
756
+ col_start = d_bbox.left
757
+ col_stop = cols - d_bbox.right
758
+ placeholder = self.PLACEHOLDER
759
+ diacritics = self.DIACRITICS
760
+ for row in range(row_start, row_stop):
761
+ for col in range(col_start, col_stop):
762
+ ft.extend(
763
+ [
764
+ # We set the ptk-color for the last column so the renderer
765
+ # knows to change the color back after this gets rendered.
766
+ ("fg:#888" if col == col_stop - 1 else "", " "),
767
+ (
768
+ "[ZeroWidthEscape]",
769
+ # We move the cursor back a cell before writing the
770
+ # kitty unicode char using a ZWE
771
+ "\b"
772
+ # Set the kitty graphic and placement we want to render
773
+ # by manually setting an 8-bit foregroun color.
774
+ # The placement ID is set to 1 using underline color.
775
+ f"\x1b[38;5;{self.kitty_image_id}m\x1b[58;1m"
776
+ # Writing the unicode char moves the cursor forward
777
+ # again to where the renderer expects it to be
778
+ f"{placeholder}{diacritics[row]}{diacritics[col]}",
779
+ ),
780
+ ]
781
+ )
782
+ ft.append(("", "\n"))
783
+ return list(split_lines(ft))
784
+
785
+ key = (
786
+ visible_width,
787
+ self.app.color_palette,
788
+ self.app.cell_size_px,
789
+ bbox,
790
+ )
791
+ return self._format_cache.get(key, render_lines)
630
792
 
631
793
 
632
794
  class NotVisible(Exception):
@@ -767,11 +929,17 @@ def select_graphic_control(format_: str) -> type[GraphicControl] | None:
767
929
  and (not _in_mplex or (_in_mplex and force_graphics))
768
930
  ):
769
931
  useable_graphics_controls.append(KittyGraphicControl)
932
+ useable_graphics_controls.append(KittyUnicodeGraphicControl)
770
933
  if (
771
934
  preferred_graphics_protocol == "kitty"
772
935
  and KittyGraphicControl in useable_graphics_controls
773
936
  ):
774
937
  SelectedGraphicControl = KittyGraphicControl
938
+ elif (
939
+ preferred_graphics_protocol == "kitty-unicode"
940
+ and KittyUnicodeGraphicControl in useable_graphics_controls
941
+ ):
942
+ SelectedGraphicControl = KittyUnicodeGraphicControl
775
943
  # Tmux now supports sixels (>=3.4)
776
944
  elif (app.term_graphics_sixel or force_graphics) and find_route(
777
945
  format_, "sixel"
euporie/core/history.py CHANGED
@@ -9,8 +9,9 @@ from prompt_toolkit.history import History
9
9
 
10
10
  if TYPE_CHECKING:
11
11
  from collections.abc import AsyncGenerator, Iterable
12
+ from typing import Callable
12
13
 
13
- from euporie.core.kernel.client import Kernel
14
+ from euporie.core.kernel.base import BaseKernel
14
15
 
15
16
  log = logging.getLogger(__name__)
16
17
 
@@ -18,15 +19,24 @@ log = logging.getLogger(__name__)
18
19
  class KernelHistory(History):
19
20
  """Load the kernel's command history."""
20
21
 
21
- def __init__(self, kernel: Kernel, n: int = 1000) -> None:
22
+ def __init__(
23
+ self, kernel: BaseKernel | Callable[[], BaseKernel], n: int = 1000
24
+ ) -> None:
22
25
  """Create a new instance of the kernel history loader."""
23
26
  super().__init__()
24
- self.kernel = kernel
27
+ self._kernel = kernel
25
28
  # How many items to load
26
29
  self.n = n
27
30
  self.n_loaded = 0
28
31
  self.loading = False
29
32
 
33
+ @property
34
+ def kernel(self) -> BaseKernel:
35
+ """Return the current kernel."""
36
+ if callable(self._kernel):
37
+ return self._kernel()
38
+ return self._kernel
39
+
30
40
  async def load(self) -> AsyncGenerator[str, None]:
31
41
  """Load the history and yield all entries, most recent history first.
32
42
 
@@ -39,9 +49,9 @@ class KernelHistory(History):
39
49
  Yields:
40
50
  Each history string
41
51
  """
42
- if not self.loading and not self._loaded and self.kernel.kc:
52
+ if not self.loading and not self._loaded and self.kernel:
43
53
  self.loading = True
44
- items = await self.kernel.history_(n=self.n, hist_access_type="tail")
54
+ items = await self.kernel.history_async(n=self.n, hist_access_type="tail")
45
55
  if items:
46
56
  self._loaded_strings = [item[2] for item in reversed(items)]
47
57
  # Remove sequential duplicates
@@ -13,7 +13,7 @@ if TYPE_CHECKING:
13
13
 
14
14
  from prompt_toolkit.document import Document
15
15
 
16
- from euporie.core.kernel.client import Kernel
16
+ from euporie.core.kernel.base import BaseKernel
17
17
  from euporie.core.lsp import LspClient
18
18
 
19
19
 
@@ -27,22 +27,29 @@ class Inspector(metaclass=ABCMeta):
27
27
 
28
28
 
29
29
  class KernelInspector(Inspector):
30
- """Inspector which retrieves contextual help from a Jupyter kernel."""
30
+ """Inspector which retrieves contextual help from a kernel."""
31
31
 
32
- def __init__(self, kernel: Kernel) -> None:
33
- """Initialize a new inspector which queries a Jupyter kernel."""
34
- self.kernel = kernel
32
+ def __init__(self, kernel: BaseKernel | Callable[[], BaseKernel]) -> None:
33
+ """Initialize a new inspector which queries a kernel."""
34
+ self._kernel = kernel
35
+
36
+ @property
37
+ def kernel(self) -> BaseKernel:
38
+ """Return the current kernel."""
39
+ if callable(self._kernel):
40
+ return self._kernel()
41
+ return self._kernel
35
42
 
36
43
  async def get_context(self, document: Document, auto: bool) -> dict[str, Any]:
37
44
  """Request contextual help from the kernel."""
38
- return await self.kernel.inspect_(document.text, document.cursor_position)
45
+ return await self.kernel.inspect_async(document.text, document.cursor_position)
39
46
 
40
47
 
41
48
  class LspInspector(Inspector):
42
49
  """Inspector which retrieves contextual help from a Language Server."""
43
50
 
44
51
  def __init__(self, lsp: LspClient, path: Path) -> None:
45
- """Initialize a new inspector which queries a Jupyter kernel."""
52
+ """Initialize a new inspector which queries a kernel."""
46
53
  self.lsp = lsp
47
54
  self.path = path
48
55
 
@@ -56,12 +63,12 @@ class LspInspector(Inspector):
56
63
 
57
64
 
58
65
  class FirstInspector(Inspector):
59
- """Return results of the first inspector to response."""
66
+ """Return results of the first inspector to respond."""
60
67
 
61
68
  def __init__(
62
69
  self, inspectors: Sequence[Inspector] | Callable[[], Sequence[Inspector]]
63
70
  ) -> None:
64
- """Initialize a new inspector which queries a Jupyter kernel."""
71
+ """Initialize a new inspector which queries a kernel."""
65
72
  self.inspectors = inspectors
66
73
 
67
74
  async def get_context(self, document: Document, auto: bool) -> dict[str, Any]:
@@ -1 +1,53 @@
1
- """Concerns the interaction with Jupyter kernels."""
1
+ """Concerns the interaction with kernels."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from importlib.util import find_spec
6
+ from pkgutil import resolve_name
7
+ from typing import TYPE_CHECKING
8
+
9
+ if TYPE_CHECKING:
10
+ from typing import Any, Literal
11
+
12
+ from euporie.core.kernel.base import BaseKernel, KernelInfo, MsgCallbacks
13
+ from euporie.core.tabs.kernel import KernelTab
14
+
15
+ KERNEL_REGISTRY = {
16
+ "local": "euporie.core.kernel.local:LocalPythonKernel",
17
+ }
18
+ if find_spec("jupyter_client"):
19
+ KERNEL_REGISTRY["jupyter"] = "euporie.core.kernel.jupyter:JupyterKernel"
20
+
21
+
22
+ def list_kernels() -> list[KernelInfo]:
23
+ """Get specifications for all available kernel types.
24
+
25
+ Returns:
26
+ A dictionary mapping kernel type names to their specifications.
27
+ """
28
+ return [
29
+ variant
30
+ for type_path in KERNEL_REGISTRY.values()
31
+ for variant in resolve_name(type_path).variants()
32
+ ]
33
+
34
+
35
+ def create_kernel(
36
+ type_name: Literal["jupyter", "local"],
37
+ kernel_tab: KernelTab,
38
+ default_callbacks: MsgCallbacks | None = None,
39
+ allow_stdin: bool = False,
40
+ **kwargs: Any,
41
+ ) -> BaseKernel:
42
+ """Create and return appropriate kernel instance."""
43
+ type_path = KERNEL_REGISTRY.get(type_name)
44
+ if type_path is not None:
45
+ type_class = resolve_name(type_path)
46
+ return type_class(
47
+ kernel_tab=kernel_tab,
48
+ default_callbacks=default_callbacks,
49
+ allow_stdin=allow_stdin,
50
+ **kwargs,
51
+ )
52
+ else:
53
+ raise ValueError(f"Unknown kernel type: {type_name}")