euporie 2.8.6__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 (64) 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/kernel/__init__.py +53 -1
  21. euporie/core/kernel/base.py +571 -0
  22. euporie/core/kernel/{client.py → jupyter.py} +173 -430
  23. euporie/core/kernel/{manager.py → jupyter_manager.py} +4 -3
  24. euporie/core/kernel/local.py +694 -0
  25. euporie/core/key_binding/bindings/basic.py +6 -3
  26. euporie/core/keys.py +26 -25
  27. euporie/core/layout/cache.py +31 -7
  28. euporie/core/layout/containers.py +88 -13
  29. euporie/core/layout/scroll.py +45 -148
  30. euporie/core/log.py +1 -1
  31. euporie/core/style.py +2 -1
  32. euporie/core/suggest.py +155 -74
  33. euporie/core/tabs/__init__.py +10 -0
  34. euporie/core/tabs/_commands.py +76 -0
  35. euporie/core/tabs/_settings.py +16 -0
  36. euporie/core/tabs/base.py +22 -8
  37. euporie/core/tabs/kernel.py +81 -35
  38. euporie/core/tabs/notebook.py +14 -22
  39. euporie/core/utils.py +1 -1
  40. euporie/core/validation.py +8 -8
  41. euporie/core/widgets/_settings.py +19 -2
  42. euporie/core/widgets/cell.py +31 -31
  43. euporie/core/widgets/cell_outputs.py +10 -1
  44. euporie/core/widgets/dialog.py +30 -75
  45. euporie/core/widgets/forms.py +71 -59
  46. euporie/core/widgets/inputs.py +7 -4
  47. euporie/core/widgets/layout.py +281 -93
  48. euporie/core/widgets/menu.py +55 -15
  49. euporie/core/widgets/palette.py +3 -1
  50. euporie/core/widgets/tree.py +86 -76
  51. euporie/notebook/app.py +35 -16
  52. euporie/notebook/tabs/edit.py +4 -4
  53. euporie/notebook/tabs/json.py +6 -2
  54. euporie/notebook/tabs/notebook.py +26 -8
  55. euporie/preview/tabs/notebook.py +17 -13
  56. euporie/web/tabs/web.py +22 -3
  57. euporie/web/widgets/webview.py +3 -0
  58. {euporie-2.8.6.dist-info → euporie-2.8.7.dist-info}/METADATA +1 -1
  59. {euporie-2.8.6.dist-info → euporie-2.8.7.dist-info}/RECORD +64 -61
  60. {euporie-2.8.6.dist-info → euporie-2.8.7.dist-info}/entry_points.txt +1 -1
  61. {euporie-2.8.6.dist-info → euporie-2.8.7.dist-info}/licenses/LICENSE +1 -1
  62. {euporie-2.8.6.data → euporie-2.8.7.data}/data/share/applications/euporie-console.desktop +0 -0
  63. {euporie-2.8.6.data → euporie-2.8.7.data}/data/share/applications/euporie-notebook.desktop +0 -0
  64. {euporie-2.8.6.dist-info → euporie-2.8.7.dist-info}/WHEEL +0 -0
euporie/core/ft/html.py CHANGED
@@ -39,6 +39,7 @@ from euporie.core.border import (
39
39
  LowerLeftHalfDottedLine,
40
40
  LowerLeftHalfLine,
41
41
  NoLine,
42
+ RoundedLine,
42
43
  ThickDoubleDashedLine,
43
44
  ThickLine,
44
45
  ThickQuadrupleDashedLine,
@@ -558,7 +559,7 @@ def css_dimension(
558
559
  digits = ""
559
560
  i = 0
560
561
  try:
561
- while (c := value[i]) in "0123456789.":
562
+ while (c := value[i]) in "-0123456789.":
562
563
  digits += c
563
564
  i += 1
564
565
  except IndexError:
@@ -924,19 +925,15 @@ class Theme(Mapping):
924
925
  available_height: int,
925
926
  ) -> None:
926
927
  """Set the space available to the element for rendering."""
927
- if self.theme["position"] == "fixed":
928
+ if self.theme["position"] in {"fixed"}:
928
929
  # Space is given by position
929
- position = self.position
930
930
  dom = self.element.dom
931
931
  assert dom.width is not None
932
932
  assert dom.height is not None
933
+ position = self.position
933
934
  self.available_width = (dom.width - position.right) - position.left
934
935
  self.available_height = (dom.height - position.bottom) - position.top
935
936
 
936
- # elif parent_theme := self.parent_theme:
937
- # self.available_width = parent_theme.content_width
938
- # self.available_height = parent_theme.content_height
939
-
940
937
  else:
941
938
  self.available_width = available_width
942
939
  self.available_height = available_height
@@ -1527,6 +1524,7 @@ class Theme(Mapping):
1527
1524
  # Replace the margin on the parent
1528
1525
  if (
1529
1526
  (first_child := element.first_child_element)
1527
+ and first_child.theme.in_flow
1530
1528
  and first_child.prev_node_in_flow is None
1531
1529
  and not self.border_visibility.top
1532
1530
  and not self.padding.top
@@ -1541,6 +1539,7 @@ class Theme(Mapping):
1541
1539
  values["top"] = max(child_theme.base_margin.top, values["top"])
1542
1540
  if (
1543
1541
  (last_child := element.last_child_element)
1542
+ and last_child.theme.in_flow
1544
1543
  and last_child.next_node_in_flow is None
1545
1544
  and not self.padding.bottom
1546
1545
  and not self.border_visibility.bottom
@@ -1671,6 +1670,10 @@ class Theme(Mapping):
1671
1670
  NoLine,
1672
1671
  )
1673
1672
 
1673
+ # TODO - parse border_radius properly and check for corner radii
1674
+ if output[direction] == ThinLine and self.theme.get("border_radius"):
1675
+ output[direction] = RoundedLine
1676
+
1674
1677
  return DiLineStyle(**output)
1675
1678
 
1676
1679
  @cached_property
@@ -1857,7 +1860,11 @@ class Theme(Mapping):
1857
1860
  """The position of an element with a relative, absolute or fixed position."""
1858
1861
  # TODO - calculate position based on top, left, bottom,right, width, height
1859
1862
  soup_theme = self.element.dom.soup.theme
1860
- return DiInt(
1863
+ position = DiInt(0, 0, 0, 0)
1864
+ # if self.parent_theme is not None:
1865
+ # position += self.parent_theme.position
1866
+ position += self.base_margin
1867
+ position += DiInt(
1861
1868
  top=round(
1862
1869
  css_dimension(
1863
1870
  self.theme["top"],
@@ -1891,6 +1898,7 @@ class Theme(Mapping):
1891
1898
  or 0
1892
1899
  ),
1893
1900
  )
1901
+ return position
1894
1902
 
1895
1903
  @cached_property
1896
1904
  def anchors(self) -> DiBool:
@@ -1912,10 +1920,7 @@ class Theme(Mapping):
1912
1920
  and not self.preformatted
1913
1921
  and not element.text
1914
1922
  )
1915
- or (
1916
- self.theme["position"] == "absolute"
1917
- and try_eval(self.theme["opacity"]) == 0
1918
- )
1923
+ or (self.theme["position"] == "absolute" and self.hidden)
1919
1924
  )
1920
1925
 
1921
1926
  @cached_property
@@ -3746,19 +3751,20 @@ class HTML:
3746
3751
  ) -> StyleAndTextTuples:
3747
3752
  """Render a Node."""
3748
3753
  # Update the element theme with the available space
3749
- element.theme.update_space(available_width, available_height)
3754
+ theme = element.theme
3755
+ theme.update_space(available_width, available_height)
3750
3756
 
3751
3757
  # Render the contents
3752
- if element.theme.d_table:
3758
+ if theme.d_table:
3753
3759
  render_func = self.render_table_content
3754
3760
 
3755
- elif element.theme.d_list_item:
3761
+ elif theme.d_list_item:
3756
3762
  render_func = self.render_list_item_content
3757
3763
 
3758
- elif element.theme.d_grid:
3764
+ elif theme.d_grid:
3759
3765
  render_func = self.render_grid_content
3760
3766
 
3761
- elif element.theme.latex:
3767
+ elif theme.latex:
3762
3768
  render_func = self.render_latex_content
3763
3769
 
3764
3770
  else:
@@ -4471,8 +4477,6 @@ class HTML:
4471
4477
  float_lines_right: list[StyleAndTextTuples] = []
4472
4478
  float_width_right = 0
4473
4479
 
4474
- content_width = parent_theme.content_width
4475
-
4476
4480
  new_line: StyleAndTextTuples = []
4477
4481
 
4478
4482
  def flush() -> None:
@@ -4607,7 +4611,7 @@ class HTML:
4607
4611
  # from each active float
4608
4612
  if (
4609
4613
  new_line
4610
- and (content_width - float_width_left - float_width_right)
4614
+ and (available_width - float_width_left - float_width_right)
4611
4615
  - left
4612
4616
  - token_width
4613
4617
  < 0
@@ -4633,7 +4637,7 @@ class HTML:
4633
4637
  fillvalue=empty,
4634
4638
  ):
4635
4639
  line_width = (
4636
- content_width
4640
+ available_width
4637
4641
  - fragment_list_width(ft_left)
4638
4642
  - fragment_list_width(ft_right)
4639
4643
  )
@@ -4724,7 +4728,7 @@ class HTML:
4724
4728
  fragment_list_width(float_lines_left[0]) if float_lines_left else 0
4725
4729
  )
4726
4730
  line_width = (
4727
- content_width
4731
+ available_width
4728
4732
  - fragment_list_width(ft_left)
4729
4733
  - fragment_list_width(ft_right)
4730
4734
  )
@@ -4844,7 +4848,7 @@ class HTML:
4844
4848
  placeholder="",
4845
4849
  )
4846
4850
 
4847
- # # Fill space around block elements so they fill the content width
4851
+ # Fill space around block elements so they fill the content width
4848
4852
  if ft and ((fill and d_blocky and not theme.d_table) or d_inline_block):
4849
4853
  pad_width = None
4850
4854
  if d_blocky:
@@ -4929,7 +4933,6 @@ class HTML:
4929
4933
  parent_style = parent_theme.style if parent_theme else ""
4930
4934
 
4931
4935
  # Render the margin
4932
- # if d_blocky and (alignment := theme.block_align) != FormattedTextAlign.LEFT:
4933
4936
  if (alignment := theme.block_align) != FormattedTextAlign.LEFT:
4934
4937
  # Center block contents if margin_left and margin_right are "auto"
4935
4938
  ft = align(
@@ -4949,22 +4952,26 @@ class HTML:
4949
4952
  padding_style=parent_style,
4950
4953
  )
4951
4954
 
4952
- # Apply mouse handler to links
4953
- if (
4954
- (parent := element.parent)
4955
- and parent.name == "a"
4956
- and callable(handler := self.mouse_handler)
4957
- and (href := parent.attrs.get("href"))
4958
- ):
4959
- element.attrs["_link_path"] = self.base.joinuri(href)
4960
- element.attrs["title"] = parent.attrs.get("title")
4961
- ft = cast(
4962
- "StyleAndTextTuples",
4963
- [
4964
- (style, text, *(rest or [partial(handler, element)]))
4965
- for style, text, *rest in ft
4966
- ],
4967
- )
4955
+ # Apply mouse handler to elements with href, title, alt
4956
+ if callable(handler := self.mouse_handler):
4957
+ attrs = element.attrs
4958
+ # Inline elements inherit from parents
4959
+ if d_inline and (parent := element.parent):
4960
+ p_attrs = parent.attrs
4961
+ attrs.setdefault("href", p_attrs.get("href"))
4962
+ attrs.setdefault("title", p_attrs.get("title"))
4963
+ attrs.setdefault("alt", p_attrs.get("alt"))
4964
+ # Resolve link paths
4965
+ if href := attrs.get("href"):
4966
+ attrs["_link_path"] = self.base.joinuri(href)
4967
+ if {"title", "alt", "href"} & set(attrs):
4968
+ ft = cast(
4969
+ "StyleAndTextTuples",
4970
+ [
4971
+ (style, text, *(rest or [partial(handler, element)]))
4972
+ for style, text, *rest in ft
4973
+ ],
4974
+ )
4968
4975
 
4969
4976
  return ft
4970
4977
 
euporie/core/graphics.py CHANGED
@@ -722,6 +722,12 @@ class KittyUnicodeGraphicControl(BaseKittyGraphicControl):
722
722
  # Calculate the size and cropping bbox at which we want to display the graphic
723
723
  cols = floor(d_cols * ratio)
724
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
+ )
725
731
  if not self.loaded:
726
732
  self.load(cols=cols, rows=rows, bbox=DiInt(0, 0, 0, 0))
727
733
 
@@ -745,11 +751,13 @@ class KittyUnicodeGraphicControl(BaseKittyGraphicControl):
745
751
  ft: StyleAndTextTuples = []
746
752
 
747
753
  # Generate placeholder grid
748
- col_start = bbox.left
749
- col_stop = cols - bbox.right
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
750
758
  placeholder = self.PLACEHOLDER
751
759
  diacritics = self.DIACRITICS
752
- for row in range(bbox.top, rows - bbox.bottom):
760
+ for row in range(row_start, row_stop):
753
761
  for col in range(col_start, col_stop):
754
762
  ft.extend(
755
763
  [
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}")