You cannot select more than 25 topics
			Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
		
		
		
		
		
			
		
			
				
	
	
		
			828 lines
		
	
	
		
			30 KiB
		
	
	
	
		
			Python
		
	
			
		
		
	
	
			828 lines
		
	
	
		
			30 KiB
		
	
	
	
		
			Python
		
	
"""Base class to manage the interaction with a running kernel"""
 | 
						|
# Copyright (c) Jupyter Development Team.
 | 
						|
# Distributed under the terms of the Modified BSD License.
 | 
						|
import asyncio
 | 
						|
import inspect
 | 
						|
import sys
 | 
						|
import time
 | 
						|
import typing as t
 | 
						|
from functools import partial
 | 
						|
from getpass import getpass
 | 
						|
from queue import Empty
 | 
						|
 | 
						|
import zmq.asyncio
 | 
						|
from jupyter_core.utils import ensure_async
 | 
						|
from traitlets import Any, Bool, Instance, Type
 | 
						|
 | 
						|
from .channels import major_protocol_version
 | 
						|
from .channelsabc import ChannelABC, HBChannelABC
 | 
						|
from .clientabc import KernelClientABC
 | 
						|
from .connect import ConnectionFileMixin
 | 
						|
from .session import Session
 | 
						|
 | 
						|
# some utilities to validate message structure, these might get moved elsewhere
 | 
						|
# if they prove to have more generic utility
 | 
						|
 | 
						|
 | 
						|
def validate_string_dict(dct: t.Dict[str, str]) -> None:
 | 
						|
    """Validate that the input is a dict with string keys and values.
 | 
						|
 | 
						|
    Raises ValueError if not."""
 | 
						|
    for k, v in dct.items():
 | 
						|
        if not isinstance(k, str):
 | 
						|
            raise ValueError("key %r in dict must be a string" % k)
 | 
						|
        if not isinstance(v, str):
 | 
						|
            raise ValueError("value %r in dict must be a string" % v)
 | 
						|
 | 
						|
 | 
						|
def reqrep(wrapped: t.Callable, meth: t.Callable, channel: str = "shell") -> t.Callable:
 | 
						|
    wrapped = wrapped(meth, channel)
 | 
						|
    if not meth.__doc__:
 | 
						|
        # python -OO removes docstrings,
 | 
						|
        # so don't bother building the wrapped docstring
 | 
						|
        return wrapped
 | 
						|
 | 
						|
    basedoc, _ = meth.__doc__.split("Returns\n", 1)
 | 
						|
    parts = [basedoc.strip()]
 | 
						|
    if "Parameters" not in basedoc:
 | 
						|
        parts.append(
 | 
						|
            """
 | 
						|
        Parameters
 | 
						|
        ----------
 | 
						|
        """
 | 
						|
        )
 | 
						|
    parts.append(
 | 
						|
        """
 | 
						|
        reply: bool (default: False)
 | 
						|
            Whether to wait for and return reply
 | 
						|
        timeout: float or None (default: None)
 | 
						|
            Timeout to use when waiting for a reply
 | 
						|
 | 
						|
        Returns
 | 
						|
        -------
 | 
						|
        msg_id: str
 | 
						|
            The msg_id of the request sent, if reply=False (default)
 | 
						|
        reply: dict
 | 
						|
            The reply message for this request, if reply=True
 | 
						|
    """
 | 
						|
    )
 | 
						|
    wrapped.__doc__ = "\n".join(parts)
 | 
						|
    return wrapped
 | 
						|
 | 
						|
 | 
						|
class KernelClient(ConnectionFileMixin):
 | 
						|
    """Communicates with a single kernel on any host via zmq channels.
 | 
						|
 | 
						|
    There are five channels associated with each kernel:
 | 
						|
 | 
						|
    * shell: for request/reply calls to the kernel.
 | 
						|
    * iopub: for the kernel to publish results to frontends.
 | 
						|
    * hb: for monitoring the kernel's heartbeat.
 | 
						|
    * stdin: for frontends to reply to raw_input calls in the kernel.
 | 
						|
    * control: for kernel management calls to the kernel.
 | 
						|
 | 
						|
    The messages that can be sent on these channels are exposed as methods of the
 | 
						|
    client (KernelClient.execute, complete, history, etc.). These methods only
 | 
						|
    send the message, they don't wait for a reply. To get results, use e.g.
 | 
						|
    :meth:`get_shell_msg` to fetch messages from the shell channel.
 | 
						|
    """
 | 
						|
 | 
						|
    # The PyZMQ Context to use for communication with the kernel.
 | 
						|
    context = Instance(zmq.Context)
 | 
						|
 | 
						|
    _created_context = Bool(False)
 | 
						|
 | 
						|
    def _context_default(self) -> zmq.Context:
 | 
						|
        self._created_context = True
 | 
						|
        return zmq.Context()
 | 
						|
 | 
						|
    # The classes to use for the various channels
 | 
						|
    shell_channel_class = Type(ChannelABC)
 | 
						|
    iopub_channel_class = Type(ChannelABC)
 | 
						|
    stdin_channel_class = Type(ChannelABC)
 | 
						|
    hb_channel_class = Type(HBChannelABC)
 | 
						|
    control_channel_class = Type(ChannelABC)
 | 
						|
 | 
						|
    # Protected traits
 | 
						|
    _shell_channel = Any()
 | 
						|
    _iopub_channel = Any()
 | 
						|
    _stdin_channel = Any()
 | 
						|
    _hb_channel = Any()
 | 
						|
    _control_channel = Any()
 | 
						|
 | 
						|
    # flag for whether execute requests should be allowed to call raw_input:
 | 
						|
    allow_stdin: bool = True
 | 
						|
 | 
						|
    def __del__(self) -> None:
 | 
						|
        """Handle garbage collection.  Destroy context if applicable."""
 | 
						|
        if (
 | 
						|
            self._created_context
 | 
						|
            and self.context is not None  # type:ignore[redundant-expr]
 | 
						|
            and not self.context.closed
 | 
						|
        ):
 | 
						|
            if self.channels_running:
 | 
						|
                if self.log:
 | 
						|
                    self.log.warning("Could not destroy zmq context for %s", self)
 | 
						|
            else:
 | 
						|
                if self.log:
 | 
						|
                    self.log.debug("Destroying zmq context for %s", self)
 | 
						|
                self.context.destroy()
 | 
						|
        try:
 | 
						|
            super_del = super().__del__  # type:ignore[misc]
 | 
						|
        except AttributeError:
 | 
						|
            pass
 | 
						|
        else:
 | 
						|
            super_del()
 | 
						|
 | 
						|
    # --------------------------------------------------------------------------
 | 
						|
    # Channel proxy methods
 | 
						|
    # --------------------------------------------------------------------------
 | 
						|
 | 
						|
    async def _async_get_shell_msg(self, *args: t.Any, **kwargs: t.Any) -> t.Dict[str, t.Any]:
 | 
						|
        """Get a message from the shell channel"""
 | 
						|
        return await ensure_async(self.shell_channel.get_msg(*args, **kwargs))
 | 
						|
 | 
						|
    async def _async_get_iopub_msg(self, *args: t.Any, **kwargs: t.Any) -> t.Dict[str, t.Any]:
 | 
						|
        """Get a message from the iopub channel"""
 | 
						|
        return await ensure_async(self.iopub_channel.get_msg(*args, **kwargs))
 | 
						|
 | 
						|
    async def _async_get_stdin_msg(self, *args: t.Any, **kwargs: t.Any) -> t.Dict[str, t.Any]:
 | 
						|
        """Get a message from the stdin channel"""
 | 
						|
        return await ensure_async(self.stdin_channel.get_msg(*args, **kwargs))
 | 
						|
 | 
						|
    async def _async_get_control_msg(self, *args: t.Any, **kwargs: t.Any) -> t.Dict[str, t.Any]:
 | 
						|
        """Get a message from the control channel"""
 | 
						|
        return await ensure_async(self.control_channel.get_msg(*args, **kwargs))
 | 
						|
 | 
						|
    async def _async_wait_for_ready(self, timeout: t.Optional[float] = None) -> None:
 | 
						|
        """Waits for a response when a client is blocked
 | 
						|
 | 
						|
        - Sets future time for timeout
 | 
						|
        - Blocks on shell channel until a message is received
 | 
						|
        - Exit if the kernel has died
 | 
						|
        - If client times out before receiving a message from the kernel, send RuntimeError
 | 
						|
        - Flush the IOPub channel
 | 
						|
        """
 | 
						|
        if timeout is None:
 | 
						|
            timeout = float("inf")
 | 
						|
        abs_timeout = time.time() + timeout
 | 
						|
 | 
						|
        from .manager import KernelManager
 | 
						|
 | 
						|
        if not isinstance(self.parent, KernelManager):
 | 
						|
            # This Client was not created by a KernelManager,
 | 
						|
            # so wait for kernel to become responsive to heartbeats
 | 
						|
            # before checking for kernel_info reply
 | 
						|
            while not await self._async_is_alive():
 | 
						|
                if time.time() > abs_timeout:
 | 
						|
                    raise RuntimeError(
 | 
						|
                        "Kernel didn't respond to heartbeats in %d seconds and timed out" % timeout
 | 
						|
                    )
 | 
						|
                await asyncio.sleep(0.2)
 | 
						|
 | 
						|
        # Wait for kernel info reply on shell channel
 | 
						|
        while True:
 | 
						|
            self.kernel_info()
 | 
						|
            try:
 | 
						|
                msg = await ensure_async(self.shell_channel.get_msg(timeout=1))
 | 
						|
            except Empty:
 | 
						|
                pass
 | 
						|
            else:
 | 
						|
                if msg["msg_type"] == "kernel_info_reply":
 | 
						|
                    # Checking that IOPub is connected. If it is not connected, start over.
 | 
						|
                    try:
 | 
						|
                        await ensure_async(self.iopub_channel.get_msg(timeout=0.2))
 | 
						|
                    except Empty:
 | 
						|
                        pass
 | 
						|
                    else:
 | 
						|
                        self._handle_kernel_info_reply(msg)
 | 
						|
                        break
 | 
						|
 | 
						|
            if not await self._async_is_alive():
 | 
						|
                msg = "Kernel died before replying to kernel_info"
 | 
						|
                raise RuntimeError(msg)
 | 
						|
 | 
						|
            # Check if current time is ready check time plus timeout
 | 
						|
            if time.time() > abs_timeout:
 | 
						|
                raise RuntimeError("Kernel didn't respond in %d seconds" % timeout)
 | 
						|
 | 
						|
        # Flush IOPub channel
 | 
						|
        while True:
 | 
						|
            try:
 | 
						|
                msg = await ensure_async(self.iopub_channel.get_msg(timeout=0.2))
 | 
						|
            except Empty:
 | 
						|
                break
 | 
						|
 | 
						|
    async def _async_recv_reply(
 | 
						|
        self, msg_id: str, timeout: t.Optional[float] = None, channel: str = "shell"
 | 
						|
    ) -> t.Dict[str, t.Any]:
 | 
						|
        """Receive and return the reply for a given request"""
 | 
						|
        if timeout is not None:
 | 
						|
            deadline = time.monotonic() + timeout
 | 
						|
        while True:
 | 
						|
            if timeout is not None:
 | 
						|
                timeout = max(0, deadline - time.monotonic())
 | 
						|
            try:
 | 
						|
                if channel == "control":
 | 
						|
                    reply = await self._async_get_control_msg(timeout=timeout)
 | 
						|
                else:
 | 
						|
                    reply = await self._async_get_shell_msg(timeout=timeout)
 | 
						|
            except Empty as e:
 | 
						|
                msg = "Timeout waiting for reply"
 | 
						|
                raise TimeoutError(msg) from e
 | 
						|
            if reply["parent_header"].get("msg_id") != msg_id:
 | 
						|
                # not my reply, someone may have forgotten to retrieve theirs
 | 
						|
                continue
 | 
						|
            return reply
 | 
						|
 | 
						|
    async def _stdin_hook_default(self, msg: t.Dict[str, t.Any]) -> None:
 | 
						|
        """Handle an input request"""
 | 
						|
        content = msg["content"]
 | 
						|
        prompt = getpass if content.get("password", False) else input
 | 
						|
 | 
						|
        try:
 | 
						|
            raw_data = prompt(content["prompt"])  # type:ignore[operator]
 | 
						|
        except EOFError:
 | 
						|
            # turn EOFError into EOF character
 | 
						|
            raw_data = "\x04"
 | 
						|
        except KeyboardInterrupt:
 | 
						|
            sys.stdout.write("\n")
 | 
						|
            return
 | 
						|
 | 
						|
        # only send stdin reply if there *was not* another request
 | 
						|
        # or execution finished while we were reading.
 | 
						|
        if not (await self.stdin_channel.msg_ready() or await self.shell_channel.msg_ready()):
 | 
						|
            self.input(raw_data)
 | 
						|
 | 
						|
    def _output_hook_default(self, msg: t.Dict[str, t.Any]) -> None:
 | 
						|
        """Default hook for redisplaying plain-text output"""
 | 
						|
        msg_type = msg["header"]["msg_type"]
 | 
						|
        content = msg["content"]
 | 
						|
        if msg_type == "stream":
 | 
						|
            stream = getattr(sys, content["name"])
 | 
						|
            stream.write(content["text"])
 | 
						|
        elif msg_type in ("display_data", "execute_result"):
 | 
						|
            sys.stdout.write(content["data"].get("text/plain", ""))
 | 
						|
        elif msg_type == "error":
 | 
						|
            sys.stderr.write("\n".join(content["traceback"]))
 | 
						|
 | 
						|
    def _output_hook_kernel(
 | 
						|
        self,
 | 
						|
        session: Session,
 | 
						|
        socket: zmq.sugar.socket.Socket,
 | 
						|
        parent_header: t.Any,
 | 
						|
        msg: t.Dict[str, t.Any],
 | 
						|
    ) -> None:
 | 
						|
        """Output hook when running inside an IPython kernel
 | 
						|
 | 
						|
        adds rich output support.
 | 
						|
        """
 | 
						|
        msg_type = msg["header"]["msg_type"]
 | 
						|
        if msg_type in ("display_data", "execute_result", "error"):
 | 
						|
            session.send(socket, msg_type, msg["content"], parent=parent_header)
 | 
						|
        else:
 | 
						|
            self._output_hook_default(msg)
 | 
						|
 | 
						|
    # --------------------------------------------------------------------------
 | 
						|
    # Channel management methods
 | 
						|
    # --------------------------------------------------------------------------
 | 
						|
 | 
						|
    def start_channels(
 | 
						|
        self,
 | 
						|
        shell: bool = True,
 | 
						|
        iopub: bool = True,
 | 
						|
        stdin: bool = True,
 | 
						|
        hb: bool = True,
 | 
						|
        control: bool = True,
 | 
						|
    ) -> None:
 | 
						|
        """Starts the channels for this kernel.
 | 
						|
 | 
						|
        This will create the channels if they do not exist and then start
 | 
						|
        them (their activity runs in a thread). If port numbers of 0 are
 | 
						|
        being used (random ports) then you must first call
 | 
						|
        :meth:`start_kernel`. If the channels have been stopped and you
 | 
						|
        call this, :class:`RuntimeError` will be raised.
 | 
						|
        """
 | 
						|
        if iopub:
 | 
						|
            self.iopub_channel.start()
 | 
						|
        if shell:
 | 
						|
            self.shell_channel.start()
 | 
						|
        if stdin:
 | 
						|
            self.stdin_channel.start()
 | 
						|
            self.allow_stdin = True
 | 
						|
        else:
 | 
						|
            self.allow_stdin = False
 | 
						|
        if hb:
 | 
						|
            self.hb_channel.start()
 | 
						|
        if control:
 | 
						|
            self.control_channel.start()
 | 
						|
 | 
						|
    def stop_channels(self) -> None:
 | 
						|
        """Stops all the running channels for this kernel.
 | 
						|
 | 
						|
        This stops their event loops and joins their threads.
 | 
						|
        """
 | 
						|
        if self.shell_channel.is_alive():
 | 
						|
            self.shell_channel.stop()
 | 
						|
        if self.iopub_channel.is_alive():
 | 
						|
            self.iopub_channel.stop()
 | 
						|
        if self.stdin_channel.is_alive():
 | 
						|
            self.stdin_channel.stop()
 | 
						|
        if self.hb_channel.is_alive():
 | 
						|
            self.hb_channel.stop()
 | 
						|
        if self.control_channel.is_alive():
 | 
						|
            self.control_channel.stop()
 | 
						|
 | 
						|
    @property
 | 
						|
    def channels_running(self) -> bool:
 | 
						|
        """Are any of the channels created and running?"""
 | 
						|
        return (
 | 
						|
            (self._shell_channel and self.shell_channel.is_alive())
 | 
						|
            or (self._iopub_channel and self.iopub_channel.is_alive())
 | 
						|
            or (self._stdin_channel and self.stdin_channel.is_alive())
 | 
						|
            or (self._hb_channel and self.hb_channel.is_alive())
 | 
						|
            or (self._control_channel and self.control_channel.is_alive())
 | 
						|
        )
 | 
						|
 | 
						|
    ioloop = None  # Overridden in subclasses that use pyzmq event loop
 | 
						|
 | 
						|
    @property
 | 
						|
    def shell_channel(self) -> t.Any:
 | 
						|
        """Get the shell channel object for this kernel."""
 | 
						|
        if self._shell_channel is None:
 | 
						|
            url = self._make_url("shell")
 | 
						|
            self.log.debug("connecting shell channel to %s", url)
 | 
						|
            socket = self.connect_shell(identity=self.session.bsession)
 | 
						|
            self._shell_channel = self.shell_channel_class(  # type:ignore[call-arg,abstract]
 | 
						|
                socket, self.session, self.ioloop
 | 
						|
            )
 | 
						|
        return self._shell_channel
 | 
						|
 | 
						|
    @property
 | 
						|
    def iopub_channel(self) -> t.Any:
 | 
						|
        """Get the iopub channel object for this kernel."""
 | 
						|
        if self._iopub_channel is None:
 | 
						|
            url = self._make_url("iopub")
 | 
						|
            self.log.debug("connecting iopub channel to %s", url)
 | 
						|
            socket = self.connect_iopub()
 | 
						|
            self._iopub_channel = self.iopub_channel_class(  # type:ignore[call-arg,abstract]
 | 
						|
                socket, self.session, self.ioloop
 | 
						|
            )
 | 
						|
        return self._iopub_channel
 | 
						|
 | 
						|
    @property
 | 
						|
    def stdin_channel(self) -> t.Any:
 | 
						|
        """Get the stdin channel object for this kernel."""
 | 
						|
        if self._stdin_channel is None:
 | 
						|
            url = self._make_url("stdin")
 | 
						|
            self.log.debug("connecting stdin channel to %s", url)
 | 
						|
            socket = self.connect_stdin(identity=self.session.bsession)
 | 
						|
            self._stdin_channel = self.stdin_channel_class(  # type:ignore[call-arg,abstract]
 | 
						|
                socket, self.session, self.ioloop
 | 
						|
            )
 | 
						|
        return self._stdin_channel
 | 
						|
 | 
						|
    @property
 | 
						|
    def hb_channel(self) -> t.Any:
 | 
						|
        """Get the hb channel object for this kernel."""
 | 
						|
        if self._hb_channel is None:
 | 
						|
            url = self._make_url("hb")
 | 
						|
            self.log.debug("connecting heartbeat channel to %s", url)
 | 
						|
            self._hb_channel = self.hb_channel_class(  # type:ignore[call-arg,abstract]
 | 
						|
                self.context, self.session, url
 | 
						|
            )
 | 
						|
        return self._hb_channel
 | 
						|
 | 
						|
    @property
 | 
						|
    def control_channel(self) -> t.Any:
 | 
						|
        """Get the control channel object for this kernel."""
 | 
						|
        if self._control_channel is None:
 | 
						|
            url = self._make_url("control")
 | 
						|
            self.log.debug("connecting control channel to %s", url)
 | 
						|
            socket = self.connect_control(identity=self.session.bsession)
 | 
						|
            self._control_channel = self.control_channel_class(  # type:ignore[call-arg,abstract]
 | 
						|
                socket, self.session, self.ioloop
 | 
						|
            )
 | 
						|
        return self._control_channel
 | 
						|
 | 
						|
    async def _async_is_alive(self) -> bool:
 | 
						|
        """Is the kernel process still running?"""
 | 
						|
        from .manager import KernelManager
 | 
						|
 | 
						|
        if isinstance(self.parent, KernelManager):
 | 
						|
            # This KernelClient was created by a KernelManager,
 | 
						|
            # we can ask the parent KernelManager:
 | 
						|
            return await self.parent._async_is_alive()
 | 
						|
        if self._hb_channel is not None:
 | 
						|
            # We don't have access to the KernelManager,
 | 
						|
            # so we use the heartbeat.
 | 
						|
            return self._hb_channel.is_beating()
 | 
						|
        # no heartbeat and not local, we can't tell if it's running,
 | 
						|
        # so naively return True
 | 
						|
        return True
 | 
						|
 | 
						|
    async def _async_execute_interactive(
 | 
						|
        self,
 | 
						|
        code: str,
 | 
						|
        silent: bool = False,
 | 
						|
        store_history: bool = True,
 | 
						|
        user_expressions: t.Optional[t.Dict[str, t.Any]] = None,
 | 
						|
        allow_stdin: t.Optional[bool] = None,
 | 
						|
        stop_on_error: bool = True,
 | 
						|
        timeout: t.Optional[float] = None,
 | 
						|
        output_hook: t.Optional[t.Callable] = None,
 | 
						|
        stdin_hook: t.Optional[t.Callable] = None,
 | 
						|
    ) -> t.Dict[str, t.Any]:
 | 
						|
        """Execute code in the kernel interactively
 | 
						|
 | 
						|
        Output will be redisplayed, and stdin prompts will be relayed as well.
 | 
						|
        If an IPython kernel is detected, rich output will be displayed.
 | 
						|
 | 
						|
        You can pass a custom output_hook callable that will be called
 | 
						|
        with every IOPub message that is produced instead of the default redisplay.
 | 
						|
 | 
						|
        .. versionadded:: 5.0
 | 
						|
 | 
						|
        Parameters
 | 
						|
        ----------
 | 
						|
        code : str
 | 
						|
            A string of code in the kernel's language.
 | 
						|
 | 
						|
        silent : bool, optional (default False)
 | 
						|
            If set, the kernel will execute the code as quietly possible, and
 | 
						|
            will force store_history to be False.
 | 
						|
 | 
						|
        store_history : bool, optional (default True)
 | 
						|
            If set, the kernel will store command history.  This is forced
 | 
						|
            to be False if silent is True.
 | 
						|
 | 
						|
        user_expressions : dict, optional
 | 
						|
            A dict mapping names to expressions to be evaluated in the user's
 | 
						|
            dict. The expression values are returned as strings formatted using
 | 
						|
            :func:`repr`.
 | 
						|
 | 
						|
        allow_stdin : bool, optional (default self.allow_stdin)
 | 
						|
            Flag for whether the kernel can send stdin requests to frontends.
 | 
						|
 | 
						|
            Some frontends (e.g. the Notebook) do not support stdin requests.
 | 
						|
            If raw_input is called from code executed from such a frontend, a
 | 
						|
            StdinNotImplementedError will be raised.
 | 
						|
 | 
						|
        stop_on_error: bool, optional (default True)
 | 
						|
            Flag whether to abort the execution queue, if an exception is encountered.
 | 
						|
 | 
						|
        timeout: float or None (default: None)
 | 
						|
            Timeout to use when waiting for a reply
 | 
						|
 | 
						|
        output_hook: callable(msg)
 | 
						|
            Function to be called with output messages.
 | 
						|
            If not specified, output will be redisplayed.
 | 
						|
 | 
						|
        stdin_hook: callable(msg)
 | 
						|
            Function or awaitable to be called with stdin_request messages.
 | 
						|
            If not specified, input/getpass will be called.
 | 
						|
 | 
						|
        Returns
 | 
						|
        -------
 | 
						|
        reply: dict
 | 
						|
            The reply message for this request
 | 
						|
        """
 | 
						|
        if not self.iopub_channel.is_alive():
 | 
						|
            emsg = "IOPub channel must be running to receive output"
 | 
						|
            raise RuntimeError(emsg)
 | 
						|
        if allow_stdin is None:
 | 
						|
            allow_stdin = self.allow_stdin
 | 
						|
        if allow_stdin and not self.stdin_channel.is_alive():
 | 
						|
            emsg = "stdin channel must be running to allow input"
 | 
						|
            raise RuntimeError(emsg)
 | 
						|
        msg_id = await ensure_async(
 | 
						|
            self.execute(
 | 
						|
                code,
 | 
						|
                silent=silent,
 | 
						|
                store_history=store_history,
 | 
						|
                user_expressions=user_expressions,
 | 
						|
                allow_stdin=allow_stdin,
 | 
						|
                stop_on_error=stop_on_error,
 | 
						|
            )
 | 
						|
        )
 | 
						|
        if stdin_hook is None:
 | 
						|
            stdin_hook = self._stdin_hook_default
 | 
						|
        # detect IPython kernel
 | 
						|
        if output_hook is None and "IPython" in sys.modules:
 | 
						|
            from IPython import get_ipython
 | 
						|
 | 
						|
            ip = get_ipython()  # type:ignore[no-untyped-call]
 | 
						|
            in_kernel = getattr(ip, "kernel", False)
 | 
						|
            if in_kernel:
 | 
						|
                output_hook = partial(
 | 
						|
                    self._output_hook_kernel,
 | 
						|
                    ip.display_pub.session,
 | 
						|
                    ip.display_pub.pub_socket,
 | 
						|
                    ip.display_pub.parent_header,
 | 
						|
                )
 | 
						|
        if output_hook is None:
 | 
						|
            # default: redisplay plain-text outputs
 | 
						|
            output_hook = self._output_hook_default
 | 
						|
 | 
						|
        # set deadline based on timeout
 | 
						|
        if timeout is not None:
 | 
						|
            deadline = time.monotonic() + timeout
 | 
						|
        else:
 | 
						|
            timeout_ms = None
 | 
						|
 | 
						|
        poller = zmq.asyncio.Poller()
 | 
						|
        iopub_socket = self.iopub_channel.socket
 | 
						|
        poller.register(iopub_socket, zmq.POLLIN)
 | 
						|
        if allow_stdin:
 | 
						|
            stdin_socket = self.stdin_channel.socket
 | 
						|
            poller.register(stdin_socket, zmq.POLLIN)
 | 
						|
        else:
 | 
						|
            stdin_socket = None
 | 
						|
 | 
						|
        # wait for output and redisplay it
 | 
						|
        while True:
 | 
						|
            if timeout is not None:
 | 
						|
                timeout = max(0, deadline - time.monotonic())
 | 
						|
                timeout_ms = int(1000 * timeout)
 | 
						|
            events = dict(await poller.poll(timeout_ms))
 | 
						|
            if not events:
 | 
						|
                emsg = "Timeout waiting for output"
 | 
						|
                raise TimeoutError(emsg)
 | 
						|
            if stdin_socket in events:
 | 
						|
                req = await ensure_async(self.stdin_channel.get_msg(timeout=0))
 | 
						|
                res = stdin_hook(req)
 | 
						|
                if inspect.isawaitable(res):
 | 
						|
                    await res
 | 
						|
                continue
 | 
						|
            if iopub_socket not in events:
 | 
						|
                continue
 | 
						|
 | 
						|
            msg = await ensure_async(self.iopub_channel.get_msg(timeout=0))
 | 
						|
 | 
						|
            if msg["parent_header"].get("msg_id") != msg_id:
 | 
						|
                # not from my request
 | 
						|
                continue
 | 
						|
            output_hook(msg)
 | 
						|
 | 
						|
            # stop on idle
 | 
						|
            if (
 | 
						|
                msg["header"]["msg_type"] == "status"
 | 
						|
                and msg["content"]["execution_state"] == "idle"
 | 
						|
            ):
 | 
						|
                break
 | 
						|
 | 
						|
        # output is done, get the reply
 | 
						|
        if timeout is not None:
 | 
						|
            timeout = max(0, deadline - time.monotonic())
 | 
						|
        return await self._async_recv_reply(msg_id, timeout=timeout)
 | 
						|
 | 
						|
    # Methods to send specific messages on channels
 | 
						|
    def execute(
 | 
						|
        self,
 | 
						|
        code: str,
 | 
						|
        silent: bool = False,
 | 
						|
        store_history: bool = True,
 | 
						|
        user_expressions: t.Optional[t.Dict[str, t.Any]] = None,
 | 
						|
        allow_stdin: t.Optional[bool] = None,
 | 
						|
        stop_on_error: bool = True,
 | 
						|
    ) -> str:
 | 
						|
        """Execute code in the kernel.
 | 
						|
 | 
						|
        Parameters
 | 
						|
        ----------
 | 
						|
        code : str
 | 
						|
            A string of code in the kernel's language.
 | 
						|
 | 
						|
        silent : bool, optional (default False)
 | 
						|
            If set, the kernel will execute the code as quietly possible, and
 | 
						|
            will force store_history to be False.
 | 
						|
 | 
						|
        store_history : bool, optional (default True)
 | 
						|
            If set, the kernel will store command history.  This is forced
 | 
						|
            to be False if silent is True.
 | 
						|
 | 
						|
        user_expressions : dict, optional
 | 
						|
            A dict mapping names to expressions to be evaluated in the user's
 | 
						|
            dict. The expression values are returned as strings formatted using
 | 
						|
            :func:`repr`.
 | 
						|
 | 
						|
        allow_stdin : bool, optional (default self.allow_stdin)
 | 
						|
            Flag for whether the kernel can send stdin requests to frontends.
 | 
						|
 | 
						|
            Some frontends (e.g. the Notebook) do not support stdin requests.
 | 
						|
            If raw_input is called from code executed from such a frontend, a
 | 
						|
            StdinNotImplementedError will be raised.
 | 
						|
 | 
						|
        stop_on_error: bool, optional (default True)
 | 
						|
            Flag whether to abort the execution queue, if an exception is encountered.
 | 
						|
 | 
						|
        Returns
 | 
						|
        -------
 | 
						|
        The msg_id of the message sent.
 | 
						|
        """
 | 
						|
        if user_expressions is None:
 | 
						|
            user_expressions = {}
 | 
						|
        if allow_stdin is None:
 | 
						|
            allow_stdin = self.allow_stdin
 | 
						|
 | 
						|
        # Don't waste network traffic if inputs are invalid
 | 
						|
        if not isinstance(code, str):
 | 
						|
            raise ValueError("code %r must be a string" % code)
 | 
						|
        validate_string_dict(user_expressions)
 | 
						|
 | 
						|
        # Create class for content/msg creation. Related to, but possibly
 | 
						|
        # not in Session.
 | 
						|
        content = {
 | 
						|
            "code": code,
 | 
						|
            "silent": silent,
 | 
						|
            "store_history": store_history,
 | 
						|
            "user_expressions": user_expressions,
 | 
						|
            "allow_stdin": allow_stdin,
 | 
						|
            "stop_on_error": stop_on_error,
 | 
						|
        }
 | 
						|
        msg = self.session.msg("execute_request", content)
 | 
						|
        self.shell_channel.send(msg)
 | 
						|
        return msg["header"]["msg_id"]
 | 
						|
 | 
						|
    def complete(self, code: str, cursor_pos: t.Optional[int] = None) -> str:
 | 
						|
        """Tab complete text in the kernel's namespace.
 | 
						|
 | 
						|
        Parameters
 | 
						|
        ----------
 | 
						|
        code : str
 | 
						|
            The context in which completion is requested.
 | 
						|
            Can be anything between a variable name and an entire cell.
 | 
						|
        cursor_pos : int, optional
 | 
						|
            The position of the cursor in the block of code where the completion was requested.
 | 
						|
            Default: ``len(code)``
 | 
						|
 | 
						|
        Returns
 | 
						|
        -------
 | 
						|
        The msg_id of the message sent.
 | 
						|
        """
 | 
						|
        if cursor_pos is None:
 | 
						|
            cursor_pos = len(code)
 | 
						|
        content = {"code": code, "cursor_pos": cursor_pos}
 | 
						|
        msg = self.session.msg("complete_request", content)
 | 
						|
        self.shell_channel.send(msg)
 | 
						|
        return msg["header"]["msg_id"]
 | 
						|
 | 
						|
    def inspect(self, code: str, cursor_pos: t.Optional[int] = None, detail_level: int = 0) -> str:
 | 
						|
        """Get metadata information about an object in the kernel's namespace.
 | 
						|
 | 
						|
        It is up to the kernel to determine the appropriate object to inspect.
 | 
						|
 | 
						|
        Parameters
 | 
						|
        ----------
 | 
						|
        code : str
 | 
						|
            The context in which info is requested.
 | 
						|
            Can be anything between a variable name and an entire cell.
 | 
						|
        cursor_pos : int, optional
 | 
						|
            The position of the cursor in the block of code where the info was requested.
 | 
						|
            Default: ``len(code)``
 | 
						|
        detail_level : int, optional
 | 
						|
            The level of detail for the introspection (0-2)
 | 
						|
 | 
						|
        Returns
 | 
						|
        -------
 | 
						|
        The msg_id of the message sent.
 | 
						|
        """
 | 
						|
        if cursor_pos is None:
 | 
						|
            cursor_pos = len(code)
 | 
						|
        content = {
 | 
						|
            "code": code,
 | 
						|
            "cursor_pos": cursor_pos,
 | 
						|
            "detail_level": detail_level,
 | 
						|
        }
 | 
						|
        msg = self.session.msg("inspect_request", content)
 | 
						|
        self.shell_channel.send(msg)
 | 
						|
        return msg["header"]["msg_id"]
 | 
						|
 | 
						|
    def history(
 | 
						|
        self,
 | 
						|
        raw: bool = True,
 | 
						|
        output: bool = False,
 | 
						|
        hist_access_type: str = "range",
 | 
						|
        **kwargs: t.Any,
 | 
						|
    ) -> str:
 | 
						|
        """Get entries from the kernel's history list.
 | 
						|
 | 
						|
        Parameters
 | 
						|
        ----------
 | 
						|
        raw : bool
 | 
						|
            If True, return the raw input.
 | 
						|
        output : bool
 | 
						|
            If True, then return the output as well.
 | 
						|
        hist_access_type : str
 | 
						|
            'range' (fill in session, start and stop params), 'tail' (fill in n)
 | 
						|
             or 'search' (fill in pattern param).
 | 
						|
 | 
						|
        session : int
 | 
						|
            For a range request, the session from which to get lines. Session
 | 
						|
            numbers are positive integers; negative ones count back from the
 | 
						|
            current session.
 | 
						|
        start : int
 | 
						|
            The first line number of a history range.
 | 
						|
        stop : int
 | 
						|
            The final (excluded) line number of a history range.
 | 
						|
 | 
						|
        n : int
 | 
						|
            The number of lines of history to get for a tail request.
 | 
						|
 | 
						|
        pattern : str
 | 
						|
            The glob-syntax pattern for a search request.
 | 
						|
 | 
						|
        Returns
 | 
						|
        -------
 | 
						|
        The ID of the message sent.
 | 
						|
        """
 | 
						|
        if hist_access_type == "range":
 | 
						|
            kwargs.setdefault("session", 0)
 | 
						|
            kwargs.setdefault("start", 0)
 | 
						|
        content = dict(raw=raw, output=output, hist_access_type=hist_access_type, **kwargs)
 | 
						|
        msg = self.session.msg("history_request", content)
 | 
						|
        self.shell_channel.send(msg)
 | 
						|
        return msg["header"]["msg_id"]
 | 
						|
 | 
						|
    def kernel_info(self) -> str:
 | 
						|
        """Request kernel info
 | 
						|
 | 
						|
        Returns
 | 
						|
        -------
 | 
						|
        The msg_id of the message sent
 | 
						|
        """
 | 
						|
        msg = self.session.msg("kernel_info_request")
 | 
						|
        self.shell_channel.send(msg)
 | 
						|
        return msg["header"]["msg_id"]
 | 
						|
 | 
						|
    def comm_info(self, target_name: t.Optional[str] = None) -> str:
 | 
						|
        """Request comm info
 | 
						|
 | 
						|
        Returns
 | 
						|
        -------
 | 
						|
        The msg_id of the message sent
 | 
						|
        """
 | 
						|
        content = {} if target_name is None else {"target_name": target_name}
 | 
						|
        msg = self.session.msg("comm_info_request", content)
 | 
						|
        self.shell_channel.send(msg)
 | 
						|
        return msg["header"]["msg_id"]
 | 
						|
 | 
						|
    def _handle_kernel_info_reply(self, msg: t.Dict[str, t.Any]) -> None:
 | 
						|
        """handle kernel info reply
 | 
						|
 | 
						|
        sets protocol adaptation version. This might
 | 
						|
        be run from a separate thread.
 | 
						|
        """
 | 
						|
        adapt_version = int(msg["content"]["protocol_version"].split(".")[0])
 | 
						|
        if adapt_version != major_protocol_version:
 | 
						|
            self.session.adapt_version = adapt_version
 | 
						|
 | 
						|
    def is_complete(self, code: str) -> str:
 | 
						|
        """Ask the kernel whether some code is complete and ready to execute.
 | 
						|
 | 
						|
        Returns
 | 
						|
        -------
 | 
						|
        The ID of the message sent.
 | 
						|
        """
 | 
						|
        msg = self.session.msg("is_complete_request", {"code": code})
 | 
						|
        self.shell_channel.send(msg)
 | 
						|
        return msg["header"]["msg_id"]
 | 
						|
 | 
						|
    def input(self, string: str) -> None:
 | 
						|
        """Send a string of raw input to the kernel.
 | 
						|
 | 
						|
        This should only be called in response to the kernel sending an
 | 
						|
        ``input_request`` message on the stdin channel.
 | 
						|
 | 
						|
        Returns
 | 
						|
        -------
 | 
						|
        The ID of the message sent.
 | 
						|
        """
 | 
						|
        content = {"value": string}
 | 
						|
        msg = self.session.msg("input_reply", content)
 | 
						|
        self.stdin_channel.send(msg)
 | 
						|
 | 
						|
    def shutdown(self, restart: bool = False) -> str:
 | 
						|
        """Request an immediate kernel shutdown on the control channel.
 | 
						|
 | 
						|
        Upon receipt of the (empty) reply, client code can safely assume that
 | 
						|
        the kernel has shut down and it's safe to forcefully terminate it if
 | 
						|
        it's still alive.
 | 
						|
 | 
						|
        The kernel will send the reply via a function registered with Python's
 | 
						|
        atexit module, ensuring it's truly done as the kernel is done with all
 | 
						|
        normal operation.
 | 
						|
 | 
						|
        Returns
 | 
						|
        -------
 | 
						|
        The msg_id of the message sent
 | 
						|
        """
 | 
						|
        # Send quit message to kernel. Once we implement kernel-side setattr,
 | 
						|
        # this should probably be done that way, but for now this will do.
 | 
						|
        msg = self.session.msg("shutdown_request", {"restart": restart})
 | 
						|
        self.control_channel.send(msg)
 | 
						|
        return msg["header"]["msg_id"]
 | 
						|
 | 
						|
 | 
						|
KernelClientABC.register(KernelClient)
 |