euporie 2.8.1__py3-none-any.whl → 2.8.5__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 (129) hide show
  1. euporie/console/_commands.py +143 -0
  2. euporie/console/_settings.py +58 -0
  3. euporie/console/app.py +25 -71
  4. euporie/console/tabs/console.py +267 -147
  5. euporie/core/__init__.py +1 -9
  6. euporie/core/__main__.py +31 -5
  7. euporie/core/_settings.py +104 -0
  8. euporie/core/app/__init__.py +3 -0
  9. euporie/core/app/_commands.py +70 -0
  10. euporie/core/app/_settings.py +427 -0
  11. euporie/core/{app.py → app/app.py} +214 -572
  12. euporie/core/app/base.py +51 -0
  13. euporie/core/{current.py → app/current.py} +13 -4
  14. euporie/core/app/cursor.py +35 -0
  15. euporie/core/app/dummy.py +12 -0
  16. euporie/core/app/launch.py +28 -0
  17. euporie/core/bars/__init__.py +11 -0
  18. euporie/core/bars/command.py +182 -0
  19. euporie/core/bars/menu.py +258 -0
  20. euporie/core/{widgets → bars}/search.py +154 -57
  21. euporie/core/{widgets → bars}/status.py +9 -26
  22. euporie/core/clipboard.py +19 -80
  23. euporie/core/comm/base.py +8 -6
  24. euporie/core/comm/ipywidgets.py +21 -12
  25. euporie/core/comm/registry.py +2 -1
  26. euporie/core/commands.py +11 -5
  27. euporie/core/completion.py +3 -2
  28. euporie/core/config.py +368 -341
  29. euporie/core/convert/__init__.py +0 -30
  30. euporie/core/convert/datum.py +131 -60
  31. euporie/core/convert/formats/__init__.py +31 -0
  32. euporie/core/convert/formats/ansi.py +46 -30
  33. euporie/core/convert/formats/common.py +11 -23
  34. euporie/core/convert/formats/html.py +45 -40
  35. euporie/core/convert/formats/pil.py +1 -1
  36. euporie/core/convert/formats/png.py +3 -5
  37. euporie/core/convert/formats/sixel.py +3 -3
  38. euporie/core/convert/registry.py +11 -8
  39. euporie/core/convert/utils.py +50 -23
  40. euporie/core/diagnostics.py +2 -2
  41. euporie/core/filters.py +72 -82
  42. euporie/core/format.py +13 -2
  43. euporie/core/ft/ansi.py +1 -1
  44. euporie/core/ft/html.py +36 -36
  45. euporie/core/ft/table.py +1 -3
  46. euporie/core/ft/utils.py +4 -1
  47. euporie/core/graphics.py +216 -124
  48. euporie/core/history.py +2 -2
  49. euporie/core/inspection.py +3 -2
  50. euporie/core/io.py +207 -28
  51. euporie/core/kernel/__init__.py +1 -0
  52. euporie/core/{kernel.py → kernel/client.py} +100 -139
  53. euporie/core/kernel/manager.py +114 -0
  54. euporie/core/key_binding/bindings/__init__.py +2 -8
  55. euporie/core/key_binding/bindings/basic.py +47 -7
  56. euporie/core/key_binding/bindings/completion.py +3 -8
  57. euporie/core/key_binding/bindings/micro.py +5 -7
  58. euporie/core/key_binding/bindings/mouse.py +26 -24
  59. euporie/core/key_binding/bindings/terminal.py +193 -0
  60. euporie/core/key_binding/bindings/vi.py +46 -0
  61. euporie/core/key_binding/key_processor.py +43 -2
  62. euporie/core/key_binding/registry.py +2 -0
  63. euporie/core/key_binding/utils.py +22 -2
  64. euporie/core/keys.py +7156 -93
  65. euporie/core/layout/cache.py +35 -25
  66. euporie/core/layout/containers.py +280 -74
  67. euporie/core/layout/decor.py +5 -5
  68. euporie/core/layout/mouse.py +1 -1
  69. euporie/core/layout/print.py +16 -3
  70. euporie/core/layout/scroll.py +26 -28
  71. euporie/core/log.py +75 -60
  72. euporie/core/lsp.py +118 -24
  73. euporie/core/margins.py +60 -31
  74. euporie/core/path.py +2 -1
  75. euporie/core/renderer.py +58 -17
  76. euporie/core/style.py +60 -40
  77. euporie/core/suggest.py +103 -85
  78. euporie/core/tabs/__init__.py +34 -0
  79. euporie/core/tabs/_settings.py +113 -0
  80. euporie/core/tabs/base.py +11 -435
  81. euporie/core/tabs/kernel.py +420 -0
  82. euporie/core/tabs/notebook.py +20 -54
  83. euporie/core/utils.py +98 -6
  84. euporie/core/validation.py +1 -1
  85. euporie/core/widgets/_settings.py +188 -0
  86. euporie/core/widgets/cell.py +90 -158
  87. euporie/core/widgets/cell_outputs.py +25 -36
  88. euporie/core/widgets/decor.py +11 -41
  89. euporie/core/widgets/dialog.py +55 -44
  90. euporie/core/widgets/display.py +27 -24
  91. euporie/core/widgets/file_browser.py +5 -26
  92. euporie/core/widgets/forms.py +16 -12
  93. euporie/core/widgets/inputs.py +37 -81
  94. euporie/core/widgets/layout.py +7 -6
  95. euporie/core/widgets/logo.py +49 -0
  96. euporie/core/widgets/menu.py +13 -11
  97. euporie/core/widgets/pager.py +8 -11
  98. euporie/core/widgets/palette.py +6 -6
  99. euporie/hub/app.py +52 -31
  100. euporie/notebook/_commands.py +24 -0
  101. euporie/notebook/_settings.py +107 -0
  102. euporie/notebook/app.py +109 -210
  103. euporie/notebook/filters.py +1 -1
  104. euporie/notebook/tabs/__init__.py +46 -7
  105. euporie/notebook/tabs/_commands.py +714 -0
  106. euporie/notebook/tabs/_settings.py +32 -0
  107. euporie/notebook/tabs/display.py +2 -2
  108. euporie/notebook/tabs/edit.py +12 -7
  109. euporie/notebook/tabs/json.py +3 -3
  110. euporie/notebook/tabs/log.py +1 -18
  111. euporie/notebook/tabs/notebook.py +21 -674
  112. euporie/notebook/widgets/_commands.py +11 -0
  113. euporie/notebook/widgets/_settings.py +19 -0
  114. euporie/notebook/widgets/side_bar.py +14 -34
  115. euporie/preview/_settings.py +104 -0
  116. euporie/preview/app.py +8 -30
  117. euporie/preview/tabs/notebook.py +15 -86
  118. euporie/web/tabs/web.py +4 -6
  119. euporie/web/widgets/webview.py +5 -12
  120. {euporie-2.8.1.dist-info → euporie-2.8.5.dist-info}/METADATA +11 -15
  121. euporie-2.8.5.dist-info/RECORD +172 -0
  122. {euporie-2.8.1.dist-info → euporie-2.8.5.dist-info}/WHEEL +1 -1
  123. {euporie-2.8.1.dist-info → euporie-2.8.5.dist-info}/entry_points.txt +2 -2
  124. {euporie-2.8.1.dist-info → euporie-2.8.5.dist-info}/licenses/LICENSE +1 -1
  125. euporie/core/launch.py +0 -59
  126. euporie/core/terminal.py +0 -527
  127. euporie-2.8.1.dist-info/RECORD +0 -146
  128. {euporie-2.8.1.data → euporie-2.8.5.data}/data/share/applications/euporie-console.desktop +0 -0
  129. {euporie-2.8.1.data → euporie-2.8.5.data}/data/share/applications/euporie-notebook.desktop +0 -0
@@ -6,131 +6,34 @@ import asyncio
6
6
  import concurrent.futures
7
7
  import logging
8
8
  import os
9
- import re
10
- import sys
11
9
  import threading
12
10
  from collections import defaultdict
13
11
  from subprocess import PIPE, STDOUT # S404 - Security implications considered
14
12
  from typing import TYPE_CHECKING, TypedDict
15
13
  from uuid import uuid4
16
14
 
17
- import nbformat
18
- from _frozen_importlib import _DeadlockError
19
- from jupyter_client import AsyncKernelManager, KernelManager
20
- from jupyter_client.kernelspec import NATIVE_KERNEL_NAME, NoSuchKernel
21
- from jupyter_client.provisioning import KernelProvisionerFactory as KPF
22
- from jupyter_client.provisioning.local_provisioner import LocalProvisioner
23
- from jupyter_core.paths import jupyter_path, jupyter_runtime_dir
24
15
  from upath import UPath
25
16
 
26
17
  if TYPE_CHECKING:
18
+ from collections.abc import Coroutine
27
19
  from pathlib import Path
28
- from typing import Any, Callable, Coroutine, TextIO
20
+ from typing import Any, Callable
29
21
 
30
22
  from jupyter_client import KernelClient
31
- from jupyter_client.connect import KernelConnectionInfo
32
23
 
33
- from euporie.core.comm.base import KernelTab
24
+ from euporie.core.tabs.kernel import KernelTab
34
25
 
35
26
 
36
27
  log = logging.getLogger(__name__)
37
28
 
38
29
 
39
- class LoggingLocalProvisioner(LocalProvisioner): # type:ignore[misc]
40
- """A Jupyter kernel provisionser which logs kernel output."""
41
-
42
- async def launch_kernel(
43
- self, cmd: list[str], **kwargs: Any
44
- ) -> KernelConnectionInfo:
45
- """Launch a kernel with a command."""
46
- await super().launch_kernel(cmd, **kwargs)
47
-
48
- def log_kernel_output(pipe: TextIO, log_func: Callable) -> None:
49
- try:
50
- with pipe:
51
- for line in iter(pipe.readline, ""):
52
- log_func(line.rstrip())
53
- except StopIteration:
54
- pass
55
-
56
- if self.process is not None:
57
- # Start thread to listen for kernel output
58
- threading.Thread(
59
- target=log_kernel_output,
60
- args=(self.process.stdout, log.warning),
61
- daemon=True,
62
- ).start()
63
-
64
- return self.connection_info
65
-
66
-
67
- KPF.instance().default_provisioner_name = "logging-local-provisioner"
68
-
69
-
70
- class EuporieKernelManager(AsyncKernelManager):
71
- """Kernel Manager subclass.
72
-
73
- ``jupyter_client`` replaces a plain ``python`` command with the current executable,
74
- but this is not desirable if the client is running in its own prefix (e.g. with
75
- ``pipx``). We work around this here.
76
-
77
- See https://github.com/jupyter/jupyter_client/issues/949
78
- """
79
-
80
- def format_kernel_cmd(self, extra_arguments: list[str] | None = None) -> list[str]:
81
- """Replace templated args (e.g. {connection_file})."""
82
- extra_arguments = extra_arguments or []
83
- assert self.kernel_spec is not None
84
- cmd = self.kernel_spec.argv + extra_arguments
85
-
86
- v_major, v_minor = sys.version_info[:2]
87
- if cmd and cmd[0] in {
88
- "python",
89
- f"python{v_major}",
90
- f"python{v_major}.{v_minor}",
91
- }:
92
- # If the command is `python` without an absolute path and euporie is
93
- # running in the same prefix as the kernel_spec file is located, use
94
- # sys.executable: otherwise fall back to the executable in the base prefix
95
- if (
96
- os.path.commonpath((sys.prefix, self.kernel_spec.resource_dir))
97
- == sys.prefix
98
- ):
99
- cmd[0] = sys.executable
100
- else:
101
- cmd[0] = sys._base_executable # type: ignore [attr-defined]
102
-
103
- # Make sure to use the realpath for the connection_file
104
- # On windows, when running with the store python, the connection_file path
105
- # is not usable by non python kernels because the path is being rerouted when
106
- # inside of a store app.
107
- # See this bug here: https://bugs.python.org/issue41196
108
- ns = {
109
- "connection_file": os.path.realpath(self.connection_file),
110
- "prefix": sys.prefix,
111
- }
112
-
113
- if self.kernel_spec:
114
- ns["resource_dir"] = self.kernel_spec.resource_dir
115
-
116
- if self._launch_args:
117
- ns.update({str(k): str(v) for k, v in self._launch_args.items()})
118
-
119
- pat = re.compile(r"\{([A-Za-z0-9_]+)\}")
120
-
121
- def _from_ns(match: re.Match) -> str:
122
- """Get the key out of ns if it's there, otherwise no change."""
123
- return ns.get(match.group(1), match.group())
124
-
125
- return [pat.sub(_from_ns, arg) for arg in cmd]
126
-
127
-
128
30
  class MsgCallbacks(TypedDict, total=False):
129
31
  """Typed dictionary for named message callbacks."""
130
32
 
131
33
  get_input: Callable[[str, bool], None] | None
132
34
  set_execution_count: Callable[[int], None] | None
133
- add_output: Callable[[dict[str, Any]], None] | None
35
+ add_output: Callable[[dict[str, Any], bool], None] | None
36
+ add_input: Callable[[dict[str, Any], bool], None] | None
134
37
  clear_output: Callable[[bool], None] | None
135
38
  done: Callable[[dict[str, Any]], None] | None
136
39
  set_metadata: Callable[[tuple[str, ...], Any], None] | None
@@ -151,6 +54,8 @@ class Kernel:
151
54
  Has the ability to run itself in it's own thread.
152
55
  """
153
56
 
57
+ _CLIENT_ID = f"euporie-{os.getpid()}"
58
+
154
59
  def __init__(
155
60
  self,
156
61
  kernel_tab: KernelTab,
@@ -170,6 +75,15 @@ class Kernel:
170
75
  kernel connection information
171
76
 
172
77
  """
78
+ from jupyter_core.paths import jupyter_path
79
+
80
+ from euporie.core.kernel.manager import (
81
+ EuporieKernelManager,
82
+ set_default_provisioner,
83
+ )
84
+
85
+ set_default_provisioner()
86
+
173
87
  self.threaded = threaded
174
88
  if threaded:
175
89
  self.loop = asyncio.new_event_loop()
@@ -341,6 +255,8 @@ class Kernel:
341
255
  @property
342
256
  def missing(self) -> bool:
343
257
  """Return True if the requested kernel is not found."""
258
+ from jupyter_client.kernelspec import NoSuchKernel
259
+
344
260
  try:
345
261
  self.km.kernel_spec # noqa B018
346
262
  except NoSuchKernel:
@@ -373,6 +289,8 @@ class Kernel:
373
289
 
374
290
  async def start_(self) -> None:
375
291
  """Start the kernel asynchronously and set its status."""
292
+ from jupyter_core.paths import jupyter_runtime_dir
293
+
376
294
  if self.km.kernel_name is None:
377
295
  self.status = "error"
378
296
  log.debug("Starting kernel")
@@ -401,16 +319,18 @@ class Kernel:
401
319
  # Otherwise, start a new kernel using the kernel manager
402
320
  else:
403
321
  runtime_dir.mkdir(exist_ok=True, parents=True)
404
- while True:
322
+ for attempt in range(1, 4):
405
323
  try:
406
324
  # TODO - send stdout to log
407
325
  await self.km.start_kernel(stdout=PIPE, stderr=STDOUT, text=True)
408
- except _DeadlockError:
409
- # Keep trying if we get an import deadlock
410
- await asyncio.sleep(0.1)
411
- continue
412
326
  except Exception as e:
413
- log.error("Kernel '%s' could not start", self.km.kernel_name)
327
+ log.error(
328
+ "Kernel '%s' could not start on attempt %s",
329
+ self.km.kernel_name,
330
+ attempt,
331
+ )
332
+ if attempt > 2:
333
+ continue
414
334
  self.status = "error"
415
335
  self.error = e
416
336
  else:
@@ -418,12 +338,14 @@ class Kernel:
418
338
  # Create a client for the newly started kernel
419
339
  if self.km.has_kernel:
420
340
  self.kc = self.km.client()
421
- break
341
+ break
422
342
 
423
343
  await self.post_start_()
424
344
 
425
345
  async def post_start_(self) -> None:
426
346
  """Wait for the kernel to become ready."""
347
+ from jupyter_client.kernelspec import NoSuchKernel
348
+
427
349
  try:
428
350
  ks = self.km.kernel_spec
429
351
  except NoSuchKernel as e:
@@ -457,6 +379,9 @@ class Kernel:
457
379
  ]
458
380
  self.dead = False
459
381
 
382
+ # Set username so we can identify our own messages
383
+ self.kc.session.username = self._CLIENT_ID
384
+
460
385
  # Start monitoring the kernel status
461
386
  if self.monitor_task is not None:
462
387
  self.monitor_task.cancel()
@@ -473,6 +398,8 @@ class Kernel:
473
398
  timeout: How long to wait until failure is assumed
474
399
 
475
400
  """
401
+ from jupyter_client.kernelspec import NATIVE_KERNEL_NAME
402
+
476
403
  # Attempt to import ipykernel if it is installed
477
404
  # ipykernel is imported by jupyter_client, but since starting the kernel runs
478
405
  # in another thread, we do the import here first to prevent import deadlocks,
@@ -507,22 +434,24 @@ class Kernel:
507
434
  rsp = await msg_getter_coro()
508
435
  # Run msg type handler
509
436
  msg_type = rsp.get("header", {}).get("msg_type")
437
+ own = rsp.get("parent_header", {}).get("username") == self._CLIENT_ID
510
438
  if callable(handler := getattr(self, f"on_{channel}_{msg_type}", None)):
511
- handler(rsp)
439
+ handler(rsp, own)
512
440
  else:
513
- self.on_unhandled(channel, rsp)
441
+ self.on_unhandled(channel, rsp, own)
514
442
 
515
- def on_unhandled(self, channel: str, rsp: dict[str, Any]) -> None:
443
+ def on_unhandled(self, channel: str, rsp: dict[str, Any], own: bool) -> None:
516
444
  """Report unhandled messages to the debug log."""
517
445
  log.debug(
518
- "Unhandled %s message:\nparent_id = '%s'\ntype = '%s'\ncontent='%s'",
446
+ "Unhandled %s message:\nparent_id = '%s'\ntype = '%s'\ncontent='%s'\nown: %s",
519
447
  channel,
520
448
  rsp.get("parent_header", {}).get("msg_id"),
521
449
  rsp["header"]["msg_type"],
522
450
  rsp.get("content"),
451
+ own,
523
452
  )
524
453
 
525
- def on_stdin_input_request(self, rsp: dict[str, Any]) -> None:
454
+ def on_stdin_input_request(self, rsp: dict[str, Any], own: bool) -> None:
526
455
  """Call ``get_input`` callback for a stdin input request message."""
527
456
  msg_id = rsp.get("parent_header", {}).get("msg_id")
528
457
  content = rsp.get("content", {})
@@ -532,7 +461,7 @@ class Kernel:
532
461
  content.get("password", False),
533
462
  )
534
463
 
535
- def on_shell_status(self, rsp: dict[str, Any]) -> None:
464
+ def on_shell_status(self, rsp: dict[str, Any], own: bool) -> None:
536
465
  """Call ``set_execution_count`` callback for a shell status response."""
537
466
  msg_id = rsp.get("parent_header", {}).get("msg_id")
538
467
  content = rsp.get("content", {})
@@ -548,7 +477,7 @@ class Kernel:
548
477
  ):
549
478
  set_execution_count(execution_count)
550
479
 
551
- def on_shell_execute_reply(self, rsp: dict[str, Any]) -> None:
480
+ def on_shell_execute_reply(self, rsp: dict[str, Any], own: bool) -> None:
552
481
  """Call callbacks for a shell execute reply response."""
553
482
  msg_id = rsp.get("parent_header", {}).get("msg_id")
554
483
  content = rsp.get("content", {})
@@ -575,11 +504,14 @@ class Kernel:
575
504
  if callable(
576
505
  add_output := self.msg_id_callbacks[msg_id]["add_output"]
577
506
  ) and (data := payload.get("data", {})):
507
+ import nbformat
508
+
578
509
  add_output(
579
510
  nbformat.v4.new_output(
580
511
  "execute_result",
581
512
  data=data,
582
- )
513
+ ),
514
+ own,
583
515
  )
584
516
  elif source == "set_next_input":
585
517
  if callable(
@@ -605,7 +537,7 @@ class Kernel:
605
537
  ):
606
538
  done(content)
607
539
 
608
- def on_shell_kernel_info_reply(self, rsp: dict[str, Any]) -> None:
540
+ def on_shell_kernel_info_reply(self, rsp: dict[str, Any], own: bool) -> None:
609
541
  """Call callbacks for a shell kernel info response."""
610
542
  msg_id = rsp.get("parent_header", {}).get("msg_id")
611
543
  if callable(
@@ -613,25 +545,25 @@ class Kernel:
613
545
  ):
614
546
  set_kernel_info(rsp.get("content", {}))
615
547
 
616
- def on_shell_complete_reply(self, rsp: dict[str, Any]) -> None:
548
+ def on_shell_complete_reply(self, rsp: dict[str, Any], own: bool) -> None:
617
549
  """Call callbacks for a shell completion reply response."""
618
550
  msg_id = rsp.get("parent_header", {}).get("msg_id")
619
551
  if callable(done := self.msg_id_callbacks[msg_id].get("done")):
620
552
  done(rsp.get("content", {}))
621
553
 
622
- def on_shell_history_reply(self, rsp: dict[str, Any]) -> None:
554
+ def on_shell_history_reply(self, rsp: dict[str, Any], own: bool) -> None:
623
555
  """Call callbacks for a shell history reply response."""
624
556
  msg_id = rsp.get("parent_header", {}).get("msg_id")
625
557
  if callable(done := self.msg_id_callbacks[msg_id].get("done")):
626
558
  done(rsp.get("content", {}))
627
559
 
628
- def on_shell_inspect_reply(self, rsp: dict[str, Any]) -> None:
560
+ def on_shell_inspect_reply(self, rsp: dict[str, Any], own: bool) -> None:
629
561
  """Call callbacks for a shell inspection reply response."""
630
562
  msg_id = rsp.get("parent_header", {}).get("msg_id")
631
563
  if callable(done := self.msg_id_callbacks[msg_id].get("done")):
632
564
  done(rsp.get("content", {}))
633
565
 
634
- def on_shell_is_complete_reply(self, rsp: dict[str, Any]) -> None:
566
+ def on_shell_is_complete_reply(self, rsp: dict[str, Any], own: bool) -> None:
635
567
  """Call callbacks for a shell completeness reply response."""
636
568
  msg_id = rsp.get("parent_header", {}).get("msg_id")
637
569
  if callable(
@@ -641,7 +573,7 @@ class Kernel:
641
573
  ):
642
574
  completeness_status(rsp.get("content", {}))
643
575
 
644
- def on_iopub_status(self, rsp: dict[str, Any]) -> None:
576
+ def on_iopub_status(self, rsp: dict[str, Any], own: bool) -> None:
645
577
  """Call callbacks for an iopub status response."""
646
578
  msg_id = rsp.get("parent_header", {}).get("msg_id")
647
579
  status = rsp.get("content", {}).get("execution_state")
@@ -670,9 +602,11 @@ class Kernel:
670
602
  rsp["header"]["date"].isoformat(),
671
603
  )
672
604
 
673
- def on_iopub_execute_input(self, rsp: dict[str, Any]) -> None:
605
+ def on_iopub_execute_input(self, rsp: dict[str, Any], own: bool) -> None:
674
606
  """Call callbacks for an iopub execute input response."""
675
607
  msg_id = rsp.get("parent_header", {}).get("msg_id")
608
+ content = rsp.get("content", {})
609
+
676
610
  if self.kernel_tab.app.config.record_cell_timing and callable(
677
611
  set_metadata := self.msg_id_callbacks[msg_id]["set_metadata"]
678
612
  ):
@@ -681,23 +615,42 @@ class Kernel:
681
615
  rsp["header"]["date"].isoformat(),
682
616
  )
683
617
 
684
- def on_iopub_display_data(self, rsp: dict[str, Any]) -> None:
618
+ execution_count: int | None = None
619
+ if (execution_count := content.get("execution_count")) and (
620
+ callable(
621
+ set_execution_count := self.msg_id_callbacks[msg_id][
622
+ "set_execution_count"
623
+ ]
624
+ )
625
+ ):
626
+ set_execution_count(execution_count)
627
+
628
+ if callable(add_input := self.msg_id_callbacks[msg_id].get("add_input")):
629
+ add_input(content, own)
630
+
631
+ def on_iopub_display_data(self, rsp: dict[str, Any], own: bool) -> None:
685
632
  """Call callbacks for an iopub display data response."""
686
633
  msg_id = rsp.get("parent_header", {}).get("msg_id")
687
634
  if callable(add_output := self.msg_id_callbacks[msg_id]["add_output"]):
688
- add_output(nbformat.v4.output_from_msg(rsp))
635
+ import nbformat
689
636
 
690
- def on_iopub_update_display_data(self, rsp: dict[str, Any]) -> None:
637
+ add_output(nbformat.v4.output_from_msg(rsp), own)
638
+
639
+ def on_iopub_update_display_data(self, rsp: dict[str, Any], own: bool) -> None:
691
640
  """Call callbacks for an iopub update display data response."""
692
641
  msg_id = rsp.get("parent_header", {}).get("msg_id")
693
642
  if callable(add_output := self.msg_id_callbacks[msg_id]["add_output"]):
694
- add_output(nbformat.v4.output_from_msg(rsp))
643
+ import nbformat
644
+
645
+ add_output(nbformat.v4.output_from_msg(rsp), own)
695
646
 
696
- def on_iopub_execute_result(self, rsp: dict[str, Any]) -> None:
647
+ def on_iopub_execute_result(self, rsp: dict[str, Any], own: bool) -> None:
697
648
  """Call callbacks for an iopub execute result response."""
698
649
  msg_id = rsp.get("parent_header", {}).get("msg_id")
699
650
  if callable(add_output := self.msg_id_callbacks[msg_id]["add_output"]):
700
- add_output(nbformat.v4.output_from_msg(rsp))
651
+ import nbformat
652
+
653
+ add_output(nbformat.v4.output_from_msg(rsp), own)
701
654
 
702
655
  if (execution_count := rsp.get("content", {}).get("execution_count")) and (
703
656
  callable(
@@ -708,21 +661,25 @@ class Kernel:
708
661
  ):
709
662
  set_execution_count(execution_count)
710
663
 
711
- def on_iopub_error(self, rsp: dict[str, dict[str, Any]]) -> None:
664
+ def on_iopub_error(self, rsp: dict[str, Any], own: bool) -> None:
712
665
  """Call callbacks for an iopub error response."""
713
666
  msg_id = rsp.get("parent_header", {}).get("msg_id", "")
714
667
  if callable(add_output := self.msg_id_callbacks[msg_id].get("add_output")):
715
- add_output(nbformat.v4.output_from_msg(rsp))
668
+ import nbformat
669
+
670
+ add_output(nbformat.v4.output_from_msg(rsp), own)
716
671
  if callable(done := self.msg_id_callbacks[msg_id].get("done")):
717
672
  done(rsp.get("content", {}))
718
673
 
719
- def on_iopub_stream(self, rsp: dict[str, Any]) -> None:
674
+ def on_iopub_stream(self, rsp: dict[str, Any], own: bool) -> None:
720
675
  """Call callbacks for an iopub stream response."""
721
676
  msg_id = rsp.get("parent_header", {}).get("msg_id")
722
677
  if callable(add_output := self.msg_id_callbacks[msg_id]["add_output"]):
723
- add_output(nbformat.v4.output_from_msg(rsp))
678
+ import nbformat
724
679
 
725
- def on_iopub_clear_output(self, rsp: dict[str, Any]) -> None:
680
+ add_output(nbformat.v4.output_from_msg(rsp), own)
681
+
682
+ def on_iopub_clear_output(self, rsp: dict[str, Any], own: bool) -> None:
726
683
  """Call callbacks for an iopub clear output response."""
727
684
  # Clear cell output, either now or when we get the next output
728
685
  msg_id = rsp.get("parent_header", {}).get("msg_id")
@@ -737,7 +694,7 @@ class Kernel:
737
694
  )
738
695
  '''
739
696
 
740
- def on_iopub_comm_open(self, rsp: dict[str, Any]) -> None:
697
+ def on_iopub_comm_open(self, rsp: dict[str, Any], own: bool) -> None:
741
698
  """Call callbacks for an comm open response."""
742
699
  # TODO
743
700
  # "If the target_name key is not found on the receiving side, then it should
@@ -747,13 +704,13 @@ class Kernel:
747
704
  content=rsp.get("content", {}), buffers=rsp.get("buffers", [])
748
705
  )
749
706
 
750
- def on_iopub_comm_msg(self, rsp: dict[str, Any]) -> None:
707
+ def on_iopub_comm_msg(self, rsp: dict[str, Any], own: bool) -> None:
751
708
  """Call callbacks for an iopub comm message response."""
752
709
  self.kernel_tab.comm_msg(
753
710
  content=rsp.get("content", {}), buffers=rsp.get("buffers", [])
754
711
  )
755
712
 
756
- def on_iopub_comm_close(self, rsp: dict[str, Any]) -> None:
713
+ def on_iopub_comm_close(self, rsp: dict[str, Any], own: bool) -> None:
757
714
  """Call callbacks for an iopub comm close response."""
758
715
  self.kernel_tab.comm_close(
759
716
  content=rsp.get("content", {}), buffers=rsp.get("buffers", [])
@@ -795,7 +752,7 @@ class Kernel:
795
752
  source: str,
796
753
  get_input: Callable[[str, bool], None] | None = None,
797
754
  set_execution_count: Callable[[int], None] | None = None,
798
- add_output: Callable[[dict[str, Any]], None] | None = None,
755
+ add_output: Callable[[dict[str, Any], bool], None] | None = None,
799
756
  clear_output: Callable[[bool], None] | None = None,
800
757
  done: Callable[[dict[str, Any]], None] | None = None,
801
758
  set_metadata: Callable[[tuple[str, ...], Any], None] | None = None,
@@ -1121,6 +1078,8 @@ class Kernel:
1121
1078
  kernel's event loop to finish.
1122
1079
  """
1123
1080
  if self.km.has_kernel:
1081
+ from jupyter_client import KernelManager
1082
+
1124
1083
  log.debug("Interrupting kernel %s", self.id)
1125
1084
  KernelManager.interrupt_kernel(self.km)
1126
1085
 
@@ -1156,6 +1115,8 @@ class Kernel:
1156
1115
  cb: Callback to run once restarted
1157
1116
 
1158
1117
  """
1118
+ from euporie.core.kernel.manager import EuporieKernelManager
1119
+
1159
1120
  self.connection_file = connection_file
1160
1121
  self.status = "starting"
1161
1122
 
@@ -0,0 +1,114 @@
1
+ """Contain classes relating to kernel management."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import logging
6
+ import os
7
+ import re
8
+ import sys
9
+ import threading
10
+ from typing import TYPE_CHECKING
11
+
12
+ from jupyter_client import AsyncKernelManager
13
+ from jupyter_client.provisioning.local_provisioner import LocalProvisioner
14
+
15
+ if TYPE_CHECKING:
16
+ from typing import Any, Callable, TextIO
17
+
18
+ from jupyter_client.connect import KernelConnectionInfo
19
+
20
+
21
+ log = logging.getLogger(__name__)
22
+
23
+
24
+ class LoggingLocalProvisioner(LocalProvisioner): # type:ignore[misc]
25
+ """A Jupyter kernel provisionser which logs kernel output."""
26
+
27
+ async def launch_kernel(
28
+ self, cmd: list[str], **kwargs: Any
29
+ ) -> KernelConnectionInfo:
30
+ """Launch a kernel with a command."""
31
+ await super().launch_kernel(cmd, **kwargs)
32
+
33
+ def log_kernel_output(pipe: TextIO, log_func: Callable) -> None:
34
+ try:
35
+ with pipe:
36
+ for line in iter(pipe.readline, ""):
37
+ log_func(line.rstrip())
38
+ except StopIteration:
39
+ pass
40
+
41
+ if self.process is not None:
42
+ # Start thread to listen for kernel output
43
+ threading.Thread(
44
+ target=log_kernel_output,
45
+ args=(self.process.stdout, log.warning),
46
+ daemon=True,
47
+ ).start()
48
+
49
+ return self.connection_info
50
+
51
+
52
+ def set_default_provisioner() -> None:
53
+ """Set the default kernel provisioner to euporie's logging provisioner."""
54
+ from jupyter_client.provisioning import KernelProvisionerFactory as KPF
55
+
56
+ KPF.instance().default_provisioner_name = "logging-local-provisioner"
57
+
58
+
59
+ class EuporieKernelManager(AsyncKernelManager):
60
+ """Kernel Manager subclass.
61
+
62
+ ``jupyter_client`` replaces a plain ``python`` command with the current executable,
63
+ but this is not desirable if the client is running in its own prefix (e.g. with
64
+ ``pipx``). We work around this here.
65
+
66
+ See https://github.com/jupyter/jupyter_client/issues/949
67
+ """
68
+
69
+ def format_kernel_cmd(self, extra_arguments: list[str] | None = None) -> list[str]:
70
+ """Replace templated args (e.g. {connection_file})."""
71
+ extra_arguments = extra_arguments or []
72
+ assert self.kernel_spec is not None
73
+ cmd = self.kernel_spec.argv + extra_arguments
74
+
75
+ v_major, v_minor = sys.version_info[:2]
76
+ if cmd and cmd[0] in {
77
+ "python",
78
+ f"python{v_major}",
79
+ f"python{v_major}.{v_minor}",
80
+ }:
81
+ # If the command is `python` without an absolute path and euporie is
82
+ # running in the same prefix as the kernel_spec file is located, use
83
+ # sys.executable: otherwise fall back to the executable in the base prefix
84
+ if (
85
+ os.path.commonpath((sys.prefix, self.kernel_spec.resource_dir))
86
+ == sys.prefix
87
+ ):
88
+ cmd[0] = sys.executable
89
+ else:
90
+ cmd[0] = sys._base_executable # type: ignore [attr-defined]
91
+
92
+ # Make sure to use the realpath for the connection_file
93
+ # On windows, when running with the store python, the connection_file path
94
+ # is not usable by non python kernels because the path is being rerouted when
95
+ # inside of a store app.
96
+ # See this bug here: https://bugs.python.org/issue41196
97
+ ns = {
98
+ "connection_file": os.path.realpath(self.connection_file),
99
+ "prefix": sys.prefix,
100
+ }
101
+
102
+ if self.kernel_spec:
103
+ ns["resource_dir"] = self.kernel_spec.resource_dir
104
+
105
+ if self._launch_args:
106
+ ns.update({str(k): str(v) for k, v in self._launch_args.items()})
107
+
108
+ pat = re.compile(r"\{([A-Za-z0-9_]+)\}")
109
+
110
+ def _from_ns(match: re.Match) -> str:
111
+ """Get the key out of ns if it's there, otherwise no change."""
112
+ return ns.get(match.group(1), match.group())
113
+
114
+ return [pat.sub(_from_ns, arg) for arg in cmd]
@@ -1,11 +1,5 @@
1
1
  """Define collections of generic key-bindings which do not belong to widgets."""
2
2
 
3
- from euporie.core.key_binding.bindings import (
4
- basic,
5
- completion,
6
- micro,
7
- mouse,
8
- page_navigation,
9
- )
3
+ from . import basic, completion, micro, mouse, page_navigation, vi
10
4
 
11
- __all__ = ["basic", "completion", "micro", "mouse", "page_navigation"]
5
+ __all__ = ["basic", "completion", "micro", "mouse", "page_navigation", "vi"]