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