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