ipkernel.py 7.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203
  1. """An in-process kernel"""
  2. # Copyright (c) IPython Development Team.
  3. # Distributed under the terms of the Modified BSD License.
  4. from __future__ import annotations
  5. import logging
  6. import sys
  7. from contextlib import contextmanager
  8. from IPython.core.interactiveshell import InteractiveShellABC
  9. from traitlets import Any, Enum, Instance, List, Type, default
  10. from ipykernel.ipkernel import IPythonKernel
  11. from ipykernel.jsonutil import json_clean
  12. from ipykernel.zmqshell import ZMQInteractiveShell
  13. from ..iostream import BackgroundSocket, IOPubThread, OutStream
  14. from .constants import INPROCESS_KEY
  15. from .socket import DummySocket
  16. # -----------------------------------------------------------------------------
  17. # Main kernel class
  18. # -----------------------------------------------------------------------------
  19. class InProcessKernel(IPythonKernel):
  20. """An in-process kernel."""
  21. # -------------------------------------------------------------------------
  22. # InProcessKernel interface
  23. # -------------------------------------------------------------------------
  24. # The frontends connected to this kernel.
  25. frontends = List(Instance("ipykernel.inprocess.client.InProcessKernelClient", allow_none=True))
  26. # The GUI environment that the kernel is running under. This need not be
  27. # specified for the normal operation for the kernel, but is required for
  28. # IPython's GUI support (including pylab). The default is 'inline' because
  29. # it is safe under all GUI toolkits.
  30. gui = Enum(("tk", "gtk", "wx", "qt", "qt4", "inline"), default_value="inline")
  31. raw_input_str = Any()
  32. stdout = Any()
  33. stderr = Any()
  34. # -------------------------------------------------------------------------
  35. # Kernel interface
  36. # -------------------------------------------------------------------------
  37. shell_class = Type(allow_none=True) # type:ignore[assignment]
  38. _underlying_iopub_socket = Instance(DummySocket, ())
  39. iopub_thread: IOPubThread = Instance(IOPubThread) # type:ignore[assignment]
  40. shell_stream = Instance(DummySocket, ()) # type:ignore[assignment]
  41. @default("iopub_thread")
  42. def _default_iopub_thread(self):
  43. thread = IOPubThread(self._underlying_iopub_socket, session=self.session)
  44. thread.start()
  45. return thread
  46. iopub_socket: BackgroundSocket = Instance(BackgroundSocket) # type:ignore[assignment]
  47. @default("iopub_socket")
  48. def _default_iopub_socket(self):
  49. return self.iopub_thread.background_socket
  50. stdin_socket = Instance(DummySocket, ())
  51. def __init__(self, **traits):
  52. """Initialize the kernel."""
  53. super().__init__(**traits)
  54. self._underlying_iopub_socket.observe(self._io_dispatch, names=["message_sent"])
  55. if self.shell:
  56. self.shell.kernel = self
  57. async def execute_request(self, stream, ident, parent):
  58. """Override for temporary IO redirection."""
  59. with self._redirected_io():
  60. await super().execute_request(stream, ident, parent)
  61. def start(self):
  62. """Override registration of dispatchers for streams."""
  63. if self.shell:
  64. self.shell.exit_now = False
  65. def _abort_queues(self, subshell_id: str | None = ...):
  66. """The in-process kernel doesn't abort requests."""
  67. def _input_request(self, prompt, ident, parent, password=False):
  68. # Flush output before making the request.
  69. self.raw_input_str = None
  70. if sys.stdout is not None:
  71. sys.stdout.flush()
  72. if sys.stderr is not None:
  73. sys.stderr.flush()
  74. # Send the input request.
  75. content = json_clean(dict(prompt=prompt, password=password))
  76. assert self.session is not None
  77. msg = self.session.msg("input_request", content, parent)
  78. for frontend in self.frontends:
  79. assert frontend is not None
  80. if frontend.session.session == parent["header"]["session"]:
  81. frontend.stdin_channel.call_handlers(msg)
  82. break
  83. else:
  84. logging.error("No frontend found for raw_input request")
  85. return ""
  86. # Await a response.
  87. while self.raw_input_str is None:
  88. frontend.stdin_channel.process_events()
  89. return self.raw_input_str # type:ignore[unreachable]
  90. # -------------------------------------------------------------------------
  91. # Protected interface
  92. # -------------------------------------------------------------------------
  93. @contextmanager
  94. def _redirected_io(self):
  95. """Temporarily redirect IO to the kernel."""
  96. sys_stdout, sys_stderr = sys.stdout, sys.stderr
  97. try:
  98. sys.stdout, sys.stderr = self.stdout, self.stderr
  99. yield
  100. finally:
  101. sys.stdout, sys.stderr = sys_stdout, sys_stderr
  102. # ------ Trait change handlers --------------------------------------------
  103. def _io_dispatch(self, change):
  104. """Called when a message is sent to the IO socket."""
  105. assert self.iopub_socket.io_thread is not None
  106. assert self.session is not None
  107. _ident, msg = self.session.recv(self.iopub_socket.io_thread.socket, copy=False)
  108. for frontend in self.frontends:
  109. assert frontend is not None
  110. frontend.iopub_channel.call_handlers(msg)
  111. # ------ Trait initializers -----------------------------------------------
  112. @default("log")
  113. def _default_log(self):
  114. return logging.getLogger(__name__)
  115. @default("session")
  116. def _default_session(self):
  117. from jupyter_client.session import Session
  118. return Session(parent=self, key=INPROCESS_KEY)
  119. @default("shell_class")
  120. def _default_shell_class(self):
  121. return InProcessInteractiveShell
  122. @default("stdout")
  123. def _default_stdout(self):
  124. return OutStream(self.session, self.iopub_thread, "stdout", watchfd=False)
  125. @default("stderr")
  126. def _default_stderr(self):
  127. return OutStream(self.session, self.iopub_thread, "stderr", watchfd=False)
  128. # -----------------------------------------------------------------------------
  129. # Interactive shell subclass
  130. # -----------------------------------------------------------------------------
  131. class InProcessInteractiveShell(ZMQInteractiveShell):
  132. """An in-process interactive shell."""
  133. kernel: InProcessKernel = Instance(
  134. "ipykernel.inprocess.ipkernel.InProcessKernel", allow_none=True
  135. ) # type:ignore[assignment]
  136. # -------------------------------------------------------------------------
  137. # InteractiveShell interface
  138. # -------------------------------------------------------------------------
  139. def enable_gui(self, gui=None):
  140. """Enable GUI integration for the kernel."""
  141. if not gui:
  142. gui = self.kernel.gui
  143. self.active_eventloop = gui
  144. def enable_matplotlib(self, gui=None):
  145. """Enable matplotlib integration for the kernel."""
  146. if not gui:
  147. gui = self.kernel.gui
  148. return super().enable_matplotlib(gui)
  149. def enable_pylab(self, gui=None, import_all=True):
  150. """Activate pylab support at runtime."""
  151. if not gui:
  152. gui = self.kernel.gui
  153. return super().enable_pylab(gui, import_all)
  154. InteractiveShellABC.register(InProcessInteractiveShell)