1"""Base class to manage the interaction with a running kernel"""
2
3# Copyright (c) Jupyter Development Team.
4# Distributed under the terms of the Modified BSD License.
5import asyncio
6import inspect
7import sys
8import time
9import typing as t
10from functools import partial
11from getpass import getpass
12from queue import Empty
13
14import zmq.asyncio
15from jupyter_core.utils import ensure_async
16from traitlets import Any, Bool, Instance, Type
17
18from .channels import major_protocol_version
19from .channelsabc import ChannelABC, HBChannelABC
20from .clientabc import KernelClientABC
21from .connect import ConnectionFileMixin
22from .session import Session
23
24# some utilities to validate message structure, these might get moved elsewhere
25# if they prove to have more generic utility
26
27
28def validate_string_dict(dct: t.Dict[str, str]) -> None:
29 """Validate that the input is a dict with string keys and values.
30
31 Raises ValueError if not."""
32 for k, v in dct.items():
33 if not isinstance(k, str):
34 raise ValueError("key %r in dict must be a string" % k)
35 if not isinstance(v, str):
36 raise ValueError("value %r in dict must be a string" % v)
37
38
39def reqrep(wrapped: t.Callable, meth: t.Callable, channel: str = "shell") -> t.Callable:
40 wrapped = wrapped(meth, channel)
41 if not meth.__doc__:
42 # python -OO removes docstrings,
43 # so don't bother building the wrapped docstring
44 return wrapped
45
46 basedoc, _ = meth.__doc__.split("Returns\n", 1)
47 parts = [basedoc.strip()]
48 if "Parameters" not in basedoc:
49 parts.append(
50 """
51 Parameters
52 ----------
53 """
54 )
55 parts.append(
56 """
57 reply: bool (default: False)
58 Whether to wait for and return reply
59 timeout: float or None (default: None)
60 Timeout to use when waiting for a reply
61
62 Returns
63 -------
64 msg_id: str
65 The msg_id of the request sent, if reply=False (default)
66 reply: dict
67 The reply message for this request, if reply=True
68 """
69 )
70 wrapped.__doc__ = "\n".join(parts)
71 return wrapped
72
73
74class KernelClient(ConnectionFileMixin):
75 """Communicates with a single kernel on any host via zmq channels.
76
77 There are five channels associated with each kernel:
78
79 * shell: for request/reply calls to the kernel.
80 * iopub: for the kernel to publish results to frontends.
81 * hb: for monitoring the kernel's heartbeat.
82 * stdin: for frontends to reply to raw_input calls in the kernel.
83 * control: for kernel management calls to the kernel.
84
85 The messages that can be sent on these channels are exposed as methods of the
86 client (KernelClient.execute, complete, history, etc.). These methods only
87 send the message, they don't wait for a reply. To get results, use e.g.
88 :meth:`get_shell_msg` to fetch messages from the shell channel.
89 """
90
91 # The PyZMQ Context to use for communication with the kernel.
92 context = Instance(zmq.Context)
93
94 _created_context = Bool(False)
95
96 def _context_default(self) -> zmq.Context:
97 self._created_context = True
98 return zmq.Context()
99
100 # The classes to use for the various channels
101 shell_channel_class = Type(ChannelABC)
102 iopub_channel_class = Type(ChannelABC)
103 stdin_channel_class = Type(ChannelABC)
104 hb_channel_class = Type(HBChannelABC)
105 control_channel_class = Type(ChannelABC)
106
107 # Protected traits
108 _shell_channel = Any()
109 _iopub_channel = Any()
110 _stdin_channel = Any()
111 _hb_channel = Any()
112 _control_channel = Any()
113
114 # flag for whether execute requests should be allowed to call raw_input:
115 allow_stdin: bool = True
116
117 def __del__(self) -> None:
118 """Handle garbage collection. Destroy context if applicable."""
119 if (
120 self._created_context
121 and self.context is not None # type:ignore[redundant-expr]
122 and not self.context.closed
123 ):
124 if self.channels_running:
125 if self.log:
126 self.log.warning("Could not destroy zmq context for %s", self)
127 else:
128 if self.log:
129 self.log.debug("Destroying zmq context for %s", self)
130 self.context.destroy(linger=100)
131 try:
132 super_del = super().__del__ # type:ignore[misc]
133 except AttributeError:
134 pass
135 else:
136 super_del()
137
138 # --------------------------------------------------------------------------
139 # Channel proxy methods
140 # --------------------------------------------------------------------------
141
142 async def _async_get_shell_msg(self, *args: t.Any, **kwargs: t.Any) -> t.Dict[str, t.Any]:
143 """Get a message from the shell channel"""
144 return await ensure_async(self.shell_channel.get_msg(*args, **kwargs))
145
146 async def _async_get_iopub_msg(self, *args: t.Any, **kwargs: t.Any) -> t.Dict[str, t.Any]:
147 """Get a message from the iopub channel"""
148 return await ensure_async(self.iopub_channel.get_msg(*args, **kwargs))
149
150 async def _async_get_stdin_msg(self, *args: t.Any, **kwargs: t.Any) -> t.Dict[str, t.Any]:
151 """Get a message from the stdin channel"""
152 return await ensure_async(self.stdin_channel.get_msg(*args, **kwargs))
153
154 async def _async_get_control_msg(self, *args: t.Any, **kwargs: t.Any) -> t.Dict[str, t.Any]:
155 """Get a message from the control channel"""
156 return await ensure_async(self.control_channel.get_msg(*args, **kwargs))
157
158 async def _async_wait_for_ready(self, timeout: float | None = None) -> None:
159 """Waits for a response when a client is blocked
160
161 - Sets future time for timeout
162 - Blocks on shell channel until a message is received
163 - Exit if the kernel has died
164 - If client times out before receiving a message from the kernel, send RuntimeError
165 - Flush the IOPub channel
166 """
167 if timeout is None:
168 timeout = float("inf")
169 abs_timeout = time.time() + timeout
170
171 from .manager import KernelManager
172
173 if not isinstance(self.parent, KernelManager):
174 # This Client was not created by a KernelManager,
175 # so wait for kernel to become responsive to heartbeats
176 # before checking for kernel_info reply
177 while not await self._async_is_alive():
178 if time.time() > abs_timeout:
179 raise RuntimeError(
180 "Kernel didn't respond to heartbeats in %d seconds and timed out" % timeout
181 )
182 await asyncio.sleep(0.2)
183
184 # Wait for kernel info reply on shell channel
185 while True:
186 self.kernel_info()
187 try:
188 msg = await ensure_async(self.shell_channel.get_msg(timeout=1))
189 except Empty:
190 pass
191 else:
192 if msg["msg_type"] == "kernel_info_reply":
193 # Checking that IOPub is connected. If it is not connected, start over.
194 try:
195 await ensure_async(self.iopub_channel.get_msg(timeout=0.2))
196 except Empty:
197 pass
198 else:
199 self._handle_kernel_info_reply(msg)
200 break
201
202 if not await self._async_is_alive():
203 msg = "Kernel died before replying to kernel_info"
204 raise RuntimeError(msg)
205
206 # Check if current time is ready check time plus timeout
207 if time.time() > abs_timeout:
208 raise RuntimeError("Kernel didn't respond in %d seconds" % timeout)
209
210 # Flush IOPub channel
211 while True:
212 try:
213 msg = await ensure_async(self.iopub_channel.get_msg(timeout=0.2))
214 except Empty:
215 break
216
217 async def _async_recv_reply(
218 self, msg_id: str, timeout: float | None = None, channel: str = "shell"
219 ) -> t.Dict[str, t.Any]:
220 """Receive and return the reply for a given request"""
221 if timeout is not None:
222 deadline = time.monotonic() + timeout
223 while True:
224 if timeout is not None:
225 timeout = max(0, deadline - time.monotonic())
226 try:
227 if channel == "control":
228 reply = await self._async_get_control_msg(timeout=timeout)
229 else:
230 reply = await self._async_get_shell_msg(timeout=timeout)
231 except Empty as e:
232 msg = "Timeout waiting for reply"
233 raise TimeoutError(msg) from e
234 if reply["parent_header"].get("msg_id") != msg_id:
235 # not my reply, someone may have forgotten to retrieve theirs
236 continue
237 return reply
238
239 async def _stdin_hook_default(self, msg: t.Dict[str, t.Any]) -> None:
240 """Handle an input request"""
241 content = msg["content"]
242 prompt = getpass if content.get("password", False) else input
243
244 try:
245 raw_data = prompt(content["prompt"])
246 except EOFError:
247 # turn EOFError into EOF character
248 raw_data = "\x04"
249 except KeyboardInterrupt:
250 sys.stdout.write("\n")
251 return
252
253 # only send stdin reply if there *was not* another request
254 # or execution finished while we were reading.
255 if not (await self.stdin_channel.msg_ready() or await self.shell_channel.msg_ready()):
256 self.input(raw_data)
257
258 def _output_hook_default(self, msg: t.Dict[str, t.Any]) -> None:
259 """Default hook for redisplaying plain-text output"""
260 msg_type = msg["header"]["msg_type"]
261 content = msg["content"]
262 if msg_type == "stream":
263 stream = getattr(sys, content["name"])
264 stream.write(content["text"])
265 elif msg_type in ("display_data", "execute_result"):
266 sys.stdout.write(content["data"].get("text/plain", ""))
267 elif msg_type == "error":
268 sys.stderr.write("\n".join(content["traceback"]))
269
270 def _output_hook_kernel(
271 self,
272 session: Session,
273 socket: zmq.sugar.socket.Socket,
274 parent_header: t.Any,
275 msg: t.Dict[str, t.Any],
276 ) -> None:
277 """Output hook when running inside an IPython kernel
278
279 adds rich output support.
280 """
281 msg_type = msg["header"]["msg_type"]
282 if msg_type in ("display_data", "execute_result", "error"):
283 session.send(socket, msg_type, msg["content"], parent=parent_header)
284 else:
285 self._output_hook_default(msg)
286
287 # --------------------------------------------------------------------------
288 # Channel management methods
289 # --------------------------------------------------------------------------
290
291 def start_channels(
292 self,
293 shell: bool = True,
294 iopub: bool = True,
295 stdin: bool = True,
296 hb: bool = True,
297 control: bool = True,
298 ) -> None:
299 """Starts the channels for this kernel.
300
301 This will create the channels if they do not exist and then start
302 them (their activity runs in a thread). If port numbers of 0 are
303 being used (random ports) then you must first call
304 :meth:`start_kernel`. If the channels have been stopped and you
305 call this, :class:`RuntimeError` will be raised.
306 """
307 if iopub:
308 self.iopub_channel.start()
309 if shell:
310 self.shell_channel.start()
311 if stdin:
312 self.stdin_channel.start()
313 self.allow_stdin = True
314 else:
315 self.allow_stdin = False
316 if hb:
317 self.hb_channel.start()
318 if control:
319 self.control_channel.start()
320
321 def stop_channels(self) -> None:
322 """Stops all the running channels for this kernel.
323
324 This stops their event loops and joins their threads.
325 """
326 if self.shell_channel.is_alive():
327 self.shell_channel.stop()
328 if self.iopub_channel.is_alive():
329 self.iopub_channel.stop()
330 if self.stdin_channel.is_alive():
331 self.stdin_channel.stop()
332 if self.hb_channel.is_alive():
333 self.hb_channel.stop()
334 if self.control_channel.is_alive():
335 self.control_channel.stop()
336
337 if self._created_context and not self.context.closed:
338 self.context.destroy(linger=100)
339
340 @property
341 def channels_running(self) -> bool:
342 """Are any of the channels created and running?"""
343 return (
344 (self._shell_channel and self.shell_channel.is_alive())
345 or (self._iopub_channel and self.iopub_channel.is_alive())
346 or (self._stdin_channel and self.stdin_channel.is_alive())
347 or (self._hb_channel and self.hb_channel.is_alive())
348 or (self._control_channel and self.control_channel.is_alive())
349 )
350
351 ioloop = None # Overridden in subclasses that use pyzmq event loop
352
353 @property
354 def shell_channel(self) -> t.Any:
355 """Get the shell channel object for this kernel."""
356 if self._shell_channel is None:
357 url = self._make_url("shell")
358 self.log.debug("connecting shell channel to %s", url)
359 socket = self.connect_shell(identity=self.session.bsession)
360 self._shell_channel = self.shell_channel_class( # type:ignore[call-arg,abstract]
361 socket, self.session, self.ioloop
362 )
363 return self._shell_channel
364
365 @property
366 def iopub_channel(self) -> t.Any:
367 """Get the iopub channel object for this kernel."""
368 if self._iopub_channel is None:
369 url = self._make_url("iopub")
370 self.log.debug("connecting iopub channel to %s", url)
371 socket = self.connect_iopub()
372 self._iopub_channel = self.iopub_channel_class( # type:ignore[call-arg,abstract]
373 socket, self.session, self.ioloop
374 )
375 return self._iopub_channel
376
377 @property
378 def stdin_channel(self) -> t.Any:
379 """Get the stdin channel object for this kernel."""
380 if self._stdin_channel is None:
381 url = self._make_url("stdin")
382 self.log.debug("connecting stdin channel to %s", url)
383 socket = self.connect_stdin(identity=self.session.bsession)
384 self._stdin_channel = self.stdin_channel_class( # type:ignore[call-arg,abstract]
385 socket, self.session, self.ioloop
386 )
387 return self._stdin_channel
388
389 @property
390 def hb_channel(self) -> t.Any:
391 """Get the hb channel object for this kernel."""
392 if self._hb_channel is None:
393 url = self._make_url("hb")
394 self.log.debug("connecting heartbeat channel to %s", url)
395 hb_kwargs = {}
396 if self.curve_publickey:
397 hb_kwargs["curve_serverkey"] = self.curve_publickey
398 try:
399 self._hb_channel = self.hb_channel_class( # type:ignore[call-arg,abstract]
400 self.context,
401 self.session,
402 url,
403 **hb_kwargs,
404 )
405 except TypeError as e:
406 if "curve_serverkey" in str(e):
407 msg = (
408 f"{self.hb_channel_class.__name__} does not support the "
409 "'curve_serverkey' parameter. Upgrade the heartbeat channel "
410 "class or disable CurveZMQ encryption."
411 )
412 raise RuntimeError(msg) from e
413 else:
414 raise
415 return self._hb_channel
416
417 @property
418 def control_channel(self) -> t.Any:
419 """Get the control channel object for this kernel."""
420 if self._control_channel is None:
421 url = self._make_url("control")
422 self.log.debug("connecting control channel to %s", url)
423 socket = self.connect_control(identity=self.session.bsession)
424 self._control_channel = self.control_channel_class( # type:ignore[call-arg,abstract]
425 socket, self.session, self.ioloop
426 )
427 return self._control_channel
428
429 async def _async_is_alive(self) -> bool:
430 """Is the kernel process still running?"""
431 from .manager import KernelManager
432
433 if isinstance(self.parent, KernelManager):
434 # This KernelClient was created by a KernelManager,
435 # we can ask the parent KernelManager:
436 return await self.parent._async_is_alive()
437 if self._hb_channel is not None:
438 # We don't have access to the KernelManager,
439 # so we use the heartbeat.
440 return self._hb_channel.is_beating()
441 # no heartbeat and not local, we can't tell if it's running,
442 # so naively return True
443 return True
444
445 async def _async_execute_interactive(
446 self,
447 code: str,
448 silent: bool = False,
449 store_history: bool = True,
450 user_expressions: t.Dict[str, t.Any] | None = None,
451 allow_stdin: bool | None = None,
452 stop_on_error: bool = True,
453 timeout: float | None = None,
454 output_hook: t.Callable | None = None,
455 stdin_hook: t.Callable | None = None,
456 ) -> t.Dict[str, t.Any]:
457 """Execute code in the kernel interactively
458
459 Output will be redisplayed, and stdin prompts will be relayed as well.
460 If an IPython kernel is detected, rich output will be displayed.
461
462 You can pass a custom output_hook callable that will be called
463 with every IOPub message that is produced instead of the default redisplay.
464
465 .. versionadded:: 5.0
466
467 Parameters
468 ----------
469 code : str
470 A string of code in the kernel's language.
471
472 silent : bool, optional (default False)
473 If set, the kernel will execute the code as quietly possible, and
474 will force store_history to be False.
475
476 store_history : bool, optional (default True)
477 If set, the kernel will store command history. This is forced
478 to be False if silent is True.
479
480 user_expressions : dict, optional
481 A dict mapping names to expressions to be evaluated in the user's
482 dict. The expression values are returned as strings formatted using
483 :func:`repr`.
484
485 allow_stdin : bool, optional (default self.allow_stdin)
486 Flag for whether the kernel can send stdin requests to frontends.
487
488 Some frontends (e.g. the Notebook) do not support stdin requests.
489 If raw_input is called from code executed from such a frontend, a
490 StdinNotImplementedError will be raised.
491
492 stop_on_error: bool, optional (default True)
493 Flag whether to abort the execution queue, if an exception is encountered.
494
495 timeout: float or None (default: None)
496 Timeout to use when waiting for a reply
497
498 output_hook: callable(msg)
499 Function to be called with output messages.
500 If not specified, output will be redisplayed.
501
502 stdin_hook: callable(msg)
503 Function or awaitable to be called with stdin_request messages.
504 If not specified, input/getpass will be called.
505
506 Returns
507 -------
508 reply: dict
509 The reply message for this request
510 """
511 if not self.iopub_channel.is_alive():
512 emsg = "IOPub channel must be running to receive output"
513 raise RuntimeError(emsg)
514 if allow_stdin is None:
515 allow_stdin = self.allow_stdin
516 if allow_stdin and not self.stdin_channel.is_alive():
517 emsg = "stdin channel must be running to allow input"
518 raise RuntimeError(emsg)
519 msg_id = await ensure_async(
520 self.execute(
521 code,
522 silent=silent,
523 store_history=store_history,
524 user_expressions=user_expressions,
525 allow_stdin=allow_stdin,
526 stop_on_error=stop_on_error,
527 )
528 )
529 if stdin_hook is None:
530 stdin_hook = self._stdin_hook_default
531 # detect IPython kernel
532 if output_hook is None and "IPython" in sys.modules:
533 from IPython import get_ipython
534
535 ip = get_ipython() # type:ignore[no-untyped-call]
536 in_kernel = getattr(ip, "kernel", False)
537 if in_kernel:
538 output_hook = partial(
539 self._output_hook_kernel,
540 ip.display_pub.session,
541 ip.display_pub.pub_socket,
542 ip.display_pub.parent_header,
543 )
544 if output_hook is None:
545 # default: redisplay plain-text outputs
546 output_hook = self._output_hook_default
547
548 # set deadline based on timeout
549 if timeout is not None:
550 deadline = time.monotonic() + timeout
551 else:
552 timeout_ms = None
553
554 poller = zmq.asyncio.Poller()
555 iopub_socket = self.iopub_channel.socket
556 poller.register(iopub_socket, zmq.POLLIN)
557 if allow_stdin:
558 stdin_socket = self.stdin_channel.socket
559 poller.register(stdin_socket, zmq.POLLIN)
560 else:
561 stdin_socket = None
562
563 # wait for output and redisplay it
564 while True:
565 if timeout is not None:
566 timeout = max(0, deadline - time.monotonic())
567 timeout_ms = int(1000 * timeout)
568 events = dict(await poller.poll(timeout_ms))
569 if not events:
570 emsg = "Timeout waiting for output"
571 raise TimeoutError(emsg)
572 if stdin_socket in events:
573 req = await ensure_async(self.stdin_channel.get_msg(timeout=0))
574 res = stdin_hook(req)
575 if inspect.isawaitable(res):
576 await res
577 continue
578 if iopub_socket not in events:
579 continue
580
581 msg = await ensure_async(self.iopub_channel.get_msg(timeout=0))
582
583 if msg["parent_header"].get("msg_id") != msg_id:
584 # not from my request
585 continue
586 output_hook(msg)
587
588 # stop on idle
589 if (
590 msg["header"]["msg_type"] == "status"
591 and msg["content"]["execution_state"] == "idle"
592 ):
593 break
594
595 # output is done, get the reply
596 if timeout is not None:
597 timeout = max(0, deadline - time.monotonic())
598 return await self._async_recv_reply(msg_id, timeout=timeout)
599
600 # Methods to send specific messages on channels
601 def execute(
602 self,
603 code: str,
604 silent: bool = False,
605 store_history: bool = True,
606 user_expressions: t.Dict[str, t.Any] | None = None,
607 allow_stdin: bool | None = None,
608 stop_on_error: bool = True,
609 ) -> str:
610 """Execute code in the kernel.
611
612 Parameters
613 ----------
614 code : str
615 A string of code in the kernel's language.
616
617 silent : bool, optional (default False)
618 If set, the kernel will execute the code as quietly possible, and
619 will force store_history to be False.
620
621 store_history : bool, optional (default True)
622 If set, the kernel will store command history. This is forced
623 to be False if silent is True.
624
625 user_expressions : dict, optional
626 A dict mapping names to expressions to be evaluated in the user's
627 dict. The expression values are returned as strings formatted using
628 :func:`repr`.
629
630 allow_stdin : bool, optional (default self.allow_stdin)
631 Flag for whether the kernel can send stdin requests to frontends.
632
633 Some frontends (e.g. the Notebook) do not support stdin requests.
634 If raw_input is called from code executed from such a frontend, a
635 StdinNotImplementedError will be raised.
636
637 stop_on_error: bool, optional (default True)
638 Flag whether to abort the execution queue, if an exception is encountered.
639
640 Returns
641 -------
642 The msg_id of the message sent.
643 """
644 if user_expressions is None:
645 user_expressions = {}
646 if allow_stdin is None:
647 allow_stdin = self.allow_stdin
648
649 # Don't waste network traffic if inputs are invalid
650 if not isinstance(code, str):
651 raise ValueError("code %r must be a string" % code)
652 validate_string_dict(user_expressions)
653
654 # Create class for content/msg creation. Related to, but possibly
655 # not in Session.
656 content = {
657 "code": code,
658 "silent": silent,
659 "store_history": store_history,
660 "user_expressions": user_expressions,
661 "allow_stdin": allow_stdin,
662 "stop_on_error": stop_on_error,
663 }
664 msg = self.session.msg("execute_request", content)
665 self.shell_channel.send(msg)
666 return msg["header"]["msg_id"]
667
668 def complete(self, code: str, cursor_pos: int | None = None) -> str:
669 """Tab complete text in the kernel's namespace.
670
671 Parameters
672 ----------
673 code : str
674 The context in which completion is requested.
675 Can be anything between a variable name and an entire cell.
676 cursor_pos : int, optional
677 The position of the cursor in the block of code where the completion was requested.
678 Default: ``len(code)``
679
680 Returns
681 -------
682 The msg_id of the message sent.
683 """
684 if cursor_pos is None:
685 cursor_pos = len(code)
686 content = {"code": code, "cursor_pos": cursor_pos}
687 msg = self.session.msg("complete_request", content)
688 self.shell_channel.send(msg)
689 return msg["header"]["msg_id"]
690
691 def inspect(self, code: str, cursor_pos: int | None = None, detail_level: int = 0) -> str:
692 """Get metadata information about an object in the kernel's namespace.
693
694 It is up to the kernel to determine the appropriate object to inspect.
695
696 Parameters
697 ----------
698 code : str
699 The context in which info is requested.
700 Can be anything between a variable name and an entire cell.
701 cursor_pos : int, optional
702 The position of the cursor in the block of code where the info was requested.
703 Default: ``len(code)``
704 detail_level : int, optional
705 The level of detail for the introspection (0-2)
706
707 Returns
708 -------
709 The msg_id of the message sent.
710 """
711 if cursor_pos is None:
712 cursor_pos = len(code)
713 content = {
714 "code": code,
715 "cursor_pos": cursor_pos,
716 "detail_level": detail_level,
717 }
718 msg = self.session.msg("inspect_request", content)
719 self.shell_channel.send(msg)
720 return msg["header"]["msg_id"]
721
722 def history(
723 self,
724 raw: bool = True,
725 output: bool = False,
726 hist_access_type: str = "range",
727 **kwargs: t.Any,
728 ) -> str:
729 """Get entries from the kernel's history list.
730
731 Parameters
732 ----------
733 raw : bool
734 If True, return the raw input.
735 output : bool
736 If True, then return the output as well.
737 hist_access_type : str
738 'range' (fill in session, start and stop params), 'tail' (fill in n)
739 or 'search' (fill in pattern param).
740
741 session : int
742 For a range request, the session from which to get lines. Session
743 numbers are positive integers; negative ones count back from the
744 current session.
745 start : int
746 The first line number of a history range.
747 stop : int
748 The final (excluded) line number of a history range.
749
750 n : int
751 The number of lines of history to get for a tail request.
752
753 pattern : str
754 The glob-syntax pattern for a search request.
755
756 Returns
757 -------
758 The ID of the message sent.
759 """
760 if hist_access_type == "range":
761 kwargs.setdefault("session", 0)
762 kwargs.setdefault("start", 0)
763 content = dict(raw=raw, output=output, hist_access_type=hist_access_type, **kwargs)
764 msg = self.session.msg("history_request", content)
765 self.shell_channel.send(msg)
766 return msg["header"]["msg_id"]
767
768 def kernel_info(self) -> str:
769 """Request kernel info
770
771 Returns
772 -------
773 The msg_id of the message sent
774 """
775 msg = self.session.msg("kernel_info_request")
776 self.shell_channel.send(msg)
777 return msg["header"]["msg_id"]
778
779 def comm_info(self, target_name: str | None = None) -> str:
780 """Request comm info
781
782 Returns
783 -------
784 The msg_id of the message sent
785 """
786 content = {} if target_name is None else {"target_name": target_name}
787 msg = self.session.msg("comm_info_request", content)
788 self.shell_channel.send(msg)
789 return msg["header"]["msg_id"]
790
791 def _handle_kernel_info_reply(self, msg: t.Dict[str, t.Any]) -> None:
792 """handle kernel info reply
793
794 sets protocol adaptation version. This might
795 be run from a separate thread.
796 """
797 adapt_version = int(msg["content"]["protocol_version"].split(".")[0])
798 if adapt_version != major_protocol_version:
799 self.session.adapt_version = adapt_version
800
801 def is_complete(self, code: str) -> str:
802 """Ask the kernel whether some code is complete and ready to execute.
803
804 Returns
805 -------
806 The ID of the message sent.
807 """
808 msg = self.session.msg("is_complete_request", {"code": code})
809 self.shell_channel.send(msg)
810 return msg["header"]["msg_id"]
811
812 def input(self, string: str) -> None:
813 """Send a string of raw input to the kernel.
814
815 This should only be called in response to the kernel sending an
816 ``input_request`` message on the stdin channel.
817
818 Returns
819 -------
820 The ID of the message sent.
821 """
822 content = {"value": string}
823 msg = self.session.msg("input_reply", content)
824 self.stdin_channel.send(msg)
825
826 def shutdown(self, restart: bool = False) -> str:
827 """Request an immediate kernel shutdown on the control channel.
828
829 Upon receipt of the (empty) reply, client code can safely assume that
830 the kernel has shut down and it's safe to forcefully terminate it if
831 it's still alive.
832
833 The kernel will send the reply via a function registered with Python's
834 atexit module, ensuring it's truly done as the kernel is done with all
835 normal operation.
836
837 Returns
838 -------
839 The msg_id of the message sent
840 """
841 # Send quit message to kernel. Once we implement kernel-side setattr,
842 # this should probably be done that way, but for now this will do.
843 msg = self.session.msg("shutdown_request", {"restart": restart})
844 self.control_channel.send(msg)
845 return msg["header"]["msg_id"]
846
847
848KernelClientABC.register(KernelClient)