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.
		
		
		
		
		
			
		
			
				
	
	
		
			432 lines
		
	
	
		
			14 KiB
		
	
	
	
		
			Python
		
	
			
		
		
	
	
			432 lines
		
	
	
		
			14 KiB
		
	
	
	
		
			Python
		
	
"""Adapters for Jupyter msg spec versions."""
 | 
						|
# Copyright (c) Jupyter Development Team.
 | 
						|
# Distributed under the terms of the Modified BSD License.
 | 
						|
import json
 | 
						|
import re
 | 
						|
from typing import Any, Dict, List, Tuple
 | 
						|
 | 
						|
from ._version import protocol_version_info
 | 
						|
 | 
						|
 | 
						|
def code_to_line(code: str, cursor_pos: int) -> Tuple[str, int]:
 | 
						|
    """Turn a multiline code block and cursor position into a single line
 | 
						|
    and new cursor position.
 | 
						|
 | 
						|
    For adapting ``complete_`` and ``object_info_request``.
 | 
						|
    """
 | 
						|
    if not code:
 | 
						|
        return "", 0
 | 
						|
    for line in code.splitlines(True):
 | 
						|
        n = len(line)
 | 
						|
        if cursor_pos > n:
 | 
						|
            cursor_pos -= n
 | 
						|
        else:
 | 
						|
            break
 | 
						|
    return line, cursor_pos
 | 
						|
 | 
						|
 | 
						|
_match_bracket = re.compile(r"\([^\(\)]+\)", re.UNICODE)
 | 
						|
_end_bracket = re.compile(r"\([^\(]*$", re.UNICODE)
 | 
						|
_identifier = re.compile(r"[a-z_][0-9a-z._]*", re.I | re.UNICODE)
 | 
						|
 | 
						|
 | 
						|
def extract_oname_v4(code: str, cursor_pos: int) -> str:
 | 
						|
    """Reimplement token-finding logic from IPython 2.x javascript
 | 
						|
 | 
						|
    for adapting object_info_request from v5 to v4
 | 
						|
    """
 | 
						|
 | 
						|
    line, _ = code_to_line(code, cursor_pos)
 | 
						|
 | 
						|
    oldline = line
 | 
						|
    line = _match_bracket.sub("", line)
 | 
						|
    while oldline != line:
 | 
						|
        oldline = line
 | 
						|
        line = _match_bracket.sub("", line)
 | 
						|
 | 
						|
    # remove everything after last open bracket
 | 
						|
    line = _end_bracket.sub("", line)
 | 
						|
    matches = _identifier.findall(line)
 | 
						|
    if matches:
 | 
						|
        return matches[-1]
 | 
						|
    else:
 | 
						|
        return ""
 | 
						|
 | 
						|
 | 
						|
class Adapter:
 | 
						|
    """Base class for adapting messages
 | 
						|
 | 
						|
    Override message_type(msg) methods to create adapters.
 | 
						|
    """
 | 
						|
 | 
						|
    msg_type_map: Dict[str, str] = {}
 | 
						|
 | 
						|
    def update_header(self, msg: Dict[str, Any]) -> Dict[str, Any]:
 | 
						|
        """Update the header."""
 | 
						|
        return msg
 | 
						|
 | 
						|
    def update_metadata(self, msg: Dict[str, Any]) -> Dict[str, Any]:
 | 
						|
        """Update the metadata."""
 | 
						|
        return msg
 | 
						|
 | 
						|
    def update_msg_type(self, msg: Dict[str, Any]) -> Dict[str, Any]:
 | 
						|
        """Update the message type."""
 | 
						|
        header = msg["header"]
 | 
						|
        msg_type = header["msg_type"]
 | 
						|
        if msg_type in self.msg_type_map:
 | 
						|
            msg["msg_type"] = header["msg_type"] = self.msg_type_map[msg_type]
 | 
						|
        return msg
 | 
						|
 | 
						|
    def handle_reply_status_error(self, msg: Dict[str, Any]) -> Dict[str, Any]:
 | 
						|
        """This will be called *instead of* the regular handler
 | 
						|
 | 
						|
        on any reply with status != ok
 | 
						|
        """
 | 
						|
        return msg
 | 
						|
 | 
						|
    def __call__(self, msg: Dict[str, Any]) -> Dict[str, Any]:
 | 
						|
        msg = self.update_header(msg)
 | 
						|
        msg = self.update_metadata(msg)
 | 
						|
        msg = self.update_msg_type(msg)
 | 
						|
        header = msg["header"]
 | 
						|
 | 
						|
        handler = getattr(self, header["msg_type"], None)
 | 
						|
        if handler is None:
 | 
						|
            return msg
 | 
						|
 | 
						|
        # handle status=error replies separately (no change, at present)
 | 
						|
        if msg["content"].get("status", None) in {"error", "aborted"}:
 | 
						|
            return self.handle_reply_status_error(msg)
 | 
						|
        return handler(msg)
 | 
						|
 | 
						|
 | 
						|
def _version_str_to_list(version: str) -> List[int]:
 | 
						|
    """convert a version string to a list of ints
 | 
						|
 | 
						|
    non-int segments are excluded
 | 
						|
    """
 | 
						|
    v = []
 | 
						|
    for part in version.split("."):
 | 
						|
        try:
 | 
						|
            v.append(int(part))
 | 
						|
        except ValueError:
 | 
						|
            pass
 | 
						|
    return v
 | 
						|
 | 
						|
 | 
						|
class V5toV4(Adapter):
 | 
						|
    """Adapt msg protocol v5 to v4"""
 | 
						|
 | 
						|
    version = "4.1"
 | 
						|
 | 
						|
    msg_type_map = {
 | 
						|
        "execute_result": "pyout",
 | 
						|
        "execute_input": "pyin",
 | 
						|
        "error": "pyerr",
 | 
						|
        "inspect_request": "object_info_request",
 | 
						|
        "inspect_reply": "object_info_reply",
 | 
						|
    }
 | 
						|
 | 
						|
    def update_header(self, msg: Dict[str, Any]) -> Dict[str, Any]:
 | 
						|
        """Update the header."""
 | 
						|
        msg["header"].pop("version", None)
 | 
						|
        msg["parent_header"].pop("version", None)
 | 
						|
        return msg
 | 
						|
 | 
						|
    # shell channel
 | 
						|
 | 
						|
    def kernel_info_reply(self, msg: Dict[str, Any]) -> Dict[str, Any]:
 | 
						|
        """Handle a kernel info reply."""
 | 
						|
        v4c = {}
 | 
						|
        content = msg["content"]
 | 
						|
        for key in ("language_version", "protocol_version"):
 | 
						|
            if key in content:
 | 
						|
                v4c[key] = _version_str_to_list(content[key])
 | 
						|
        if content.get("implementation", "") == "ipython" and "implementation_version" in content:
 | 
						|
            v4c["ipython_version"] = _version_str_to_list(content["implementation_version"])
 | 
						|
        language_info = content.get("language_info", {})
 | 
						|
        language = language_info.get("name", "")
 | 
						|
        v4c.setdefault("language", language)
 | 
						|
        if "version" in language_info:
 | 
						|
            v4c.setdefault("language_version", _version_str_to_list(language_info["version"]))
 | 
						|
        msg["content"] = v4c
 | 
						|
        return msg
 | 
						|
 | 
						|
    def execute_request(self, msg: Dict[str, Any]) -> Dict[str, Any]:
 | 
						|
        """Handle an execute request."""
 | 
						|
        content = msg["content"]
 | 
						|
        content.setdefault("user_variables", [])
 | 
						|
        return msg
 | 
						|
 | 
						|
    def execute_reply(self, msg: Dict[str, Any]) -> Dict[str, Any]:
 | 
						|
        """Handle an execute reply."""
 | 
						|
        content = msg["content"]
 | 
						|
        content.setdefault("user_variables", {})
 | 
						|
        # TODO: handle payloads
 | 
						|
        return msg
 | 
						|
 | 
						|
    def complete_request(self, msg: Dict[str, Any]) -> Dict[str, Any]:
 | 
						|
        """Handle a complete request."""
 | 
						|
        content = msg["content"]
 | 
						|
        code = content["code"]
 | 
						|
        cursor_pos = content["cursor_pos"]
 | 
						|
        line, cursor_pos = code_to_line(code, cursor_pos)
 | 
						|
 | 
						|
        new_content = msg["content"] = {}
 | 
						|
        new_content["text"] = ""
 | 
						|
        new_content["line"] = line
 | 
						|
        new_content["block"] = None
 | 
						|
        new_content["cursor_pos"] = cursor_pos
 | 
						|
        return msg
 | 
						|
 | 
						|
    def complete_reply(self, msg: Dict[str, Any]) -> Dict[str, Any]:
 | 
						|
        """Handle a complete reply."""
 | 
						|
        content = msg["content"]
 | 
						|
        cursor_start = content.pop("cursor_start")
 | 
						|
        cursor_end = content.pop("cursor_end")
 | 
						|
        match_len = cursor_end - cursor_start
 | 
						|
        content["matched_text"] = content["matches"][0][:match_len]
 | 
						|
        content.pop("metadata", None)
 | 
						|
        return msg
 | 
						|
 | 
						|
    def object_info_request(self, msg: Dict[str, Any]) -> Dict[str, Any]:
 | 
						|
        """Handle an object info request."""
 | 
						|
        content = msg["content"]
 | 
						|
        code = content["code"]
 | 
						|
        cursor_pos = content["cursor_pos"]
 | 
						|
        line, _ = code_to_line(code, cursor_pos)
 | 
						|
 | 
						|
        new_content = msg["content"] = {}
 | 
						|
        new_content["oname"] = extract_oname_v4(code, cursor_pos)
 | 
						|
        new_content["detail_level"] = content["detail_level"]
 | 
						|
        return msg
 | 
						|
 | 
						|
    def object_info_reply(self, msg: Dict[str, Any]) -> Dict[str, Any]:
 | 
						|
        """inspect_reply can't be easily backward compatible"""
 | 
						|
        msg["content"] = {"found": False, "oname": "unknown"}
 | 
						|
        return msg
 | 
						|
 | 
						|
    # iopub channel
 | 
						|
 | 
						|
    def stream(self, msg: Dict[str, Any]) -> Dict[str, Any]:
 | 
						|
        """Handle a stream message."""
 | 
						|
        content = msg["content"]
 | 
						|
        content["data"] = content.pop("text")
 | 
						|
        return msg
 | 
						|
 | 
						|
    def display_data(self, msg: Dict[str, Any]) -> Dict[str, Any]:
 | 
						|
        """Handle a display data message."""
 | 
						|
        content = msg["content"]
 | 
						|
        content.setdefault("source", "display")
 | 
						|
        data = content["data"]
 | 
						|
        if "application/json" in data:
 | 
						|
            try:
 | 
						|
                data["application/json"] = json.dumps(data["application/json"])
 | 
						|
            except Exception:
 | 
						|
                # warn?
 | 
						|
                pass
 | 
						|
        return msg
 | 
						|
 | 
						|
    # stdin channel
 | 
						|
 | 
						|
    def input_request(self, msg: Dict[str, Any]) -> Dict[str, Any]:
 | 
						|
        """Handle an input request."""
 | 
						|
        msg["content"].pop("password", None)
 | 
						|
        return msg
 | 
						|
 | 
						|
 | 
						|
class V4toV5(Adapter):
 | 
						|
    """Convert msg spec V4 to V5"""
 | 
						|
 | 
						|
    version = "5.0"
 | 
						|
 | 
						|
    # invert message renames above
 | 
						|
    msg_type_map = {v: k for k, v in V5toV4.msg_type_map.items()}
 | 
						|
 | 
						|
    def update_header(self, msg: Dict[str, Any]) -> Dict[str, Any]:
 | 
						|
        """Update the header."""
 | 
						|
        msg["header"]["version"] = self.version
 | 
						|
        if msg["parent_header"]:
 | 
						|
            msg["parent_header"]["version"] = self.version
 | 
						|
        return msg
 | 
						|
 | 
						|
    # shell channel
 | 
						|
 | 
						|
    def kernel_info_reply(self, msg: Dict[str, Any]) -> Dict[str, Any]:
 | 
						|
        """Handle a kernel info reply."""
 | 
						|
        content = msg["content"]
 | 
						|
        for key in ("protocol_version", "ipython_version"):
 | 
						|
            if key in content:
 | 
						|
                content[key] = ".".join(map(str, content[key]))
 | 
						|
 | 
						|
        content.setdefault("protocol_version", "4.1")
 | 
						|
 | 
						|
        if content["language"].startswith("python") and "ipython_version" in content:
 | 
						|
            content["implementation"] = "ipython"
 | 
						|
            content["implementation_version"] = content.pop("ipython_version")
 | 
						|
 | 
						|
        language = content.pop("language")
 | 
						|
        language_info = content.setdefault("language_info", {})
 | 
						|
        language_info.setdefault("name", language)
 | 
						|
        if "language_version" in content:
 | 
						|
            language_version = ".".join(map(str, content.pop("language_version")))
 | 
						|
            language_info.setdefault("version", language_version)
 | 
						|
 | 
						|
        content["banner"] = ""
 | 
						|
        return msg
 | 
						|
 | 
						|
    def execute_request(self, msg: Dict[str, Any]) -> Dict[str, Any]:
 | 
						|
        """Handle an execute request."""
 | 
						|
        content = msg["content"]
 | 
						|
        user_variables = content.pop("user_variables", [])
 | 
						|
        user_expressions = content.setdefault("user_expressions", {})
 | 
						|
        for v in user_variables:
 | 
						|
            user_expressions[v] = v
 | 
						|
        return msg
 | 
						|
 | 
						|
    def execute_reply(self, msg: Dict[str, Any]) -> Dict[str, Any]:
 | 
						|
        """Handle an execute reply."""
 | 
						|
        content = msg["content"]
 | 
						|
        user_expressions = content.setdefault("user_expressions", {})
 | 
						|
        user_variables = content.pop("user_variables", {})
 | 
						|
        if user_variables:
 | 
						|
            user_expressions.update(user_variables)
 | 
						|
 | 
						|
        # Pager payloads became a mime bundle
 | 
						|
        for payload in content.get("payload", []):
 | 
						|
            if payload.get("source", None) == "page" and ("text" in payload):
 | 
						|
                if "data" not in payload:
 | 
						|
                    payload["data"] = {}
 | 
						|
                payload["data"]["text/plain"] = payload.pop("text")
 | 
						|
 | 
						|
        return msg
 | 
						|
 | 
						|
    def complete_request(self, msg: Dict[str, Any]) -> Dict[str, Any]:
 | 
						|
        """Handle a complete request."""
 | 
						|
        old_content = msg["content"]
 | 
						|
 | 
						|
        new_content = msg["content"] = {}
 | 
						|
        new_content["code"] = old_content["line"]
 | 
						|
        new_content["cursor_pos"] = old_content["cursor_pos"]
 | 
						|
        return msg
 | 
						|
 | 
						|
    def complete_reply(self, msg: Dict[str, Any]) -> Dict[str, Any]:
 | 
						|
        """Handle a complete reply."""
 | 
						|
        # complete_reply needs more context than we have to get cursor_start and end.
 | 
						|
        # use special end=null to indicate current cursor position and negative offset
 | 
						|
        # for start relative to the cursor.
 | 
						|
        # start=None indicates that start == end (accounts for no -0).
 | 
						|
        content = msg["content"]
 | 
						|
        new_content = msg["content"] = {"status": "ok"}
 | 
						|
        new_content["matches"] = content["matches"]
 | 
						|
        if content["matched_text"]:
 | 
						|
            new_content["cursor_start"] = -len(content["matched_text"])
 | 
						|
        else:
 | 
						|
            # no -0, use None to indicate that start == end
 | 
						|
            new_content["cursor_start"] = None
 | 
						|
        new_content["cursor_end"] = None
 | 
						|
        new_content["metadata"] = {}
 | 
						|
        return msg
 | 
						|
 | 
						|
    def inspect_request(self, msg: Dict[str, Any]) -> Dict[str, Any]:
 | 
						|
        """Handle an inspect request."""
 | 
						|
        content = msg["content"]
 | 
						|
        name = content["oname"]
 | 
						|
 | 
						|
        new_content = msg["content"] = {}
 | 
						|
        new_content["code"] = name
 | 
						|
        new_content["cursor_pos"] = len(name)
 | 
						|
        new_content["detail_level"] = content["detail_level"]
 | 
						|
        return msg
 | 
						|
 | 
						|
    def inspect_reply(self, msg: Dict[str, Any]) -> Dict[str, Any]:
 | 
						|
        """inspect_reply can't be easily backward compatible"""
 | 
						|
        content = msg["content"]
 | 
						|
        new_content = msg["content"] = {"status": "ok"}
 | 
						|
        found = new_content["found"] = content["found"]
 | 
						|
        new_content["data"] = data = {}
 | 
						|
        new_content["metadata"] = {}
 | 
						|
        if found:
 | 
						|
            lines = []
 | 
						|
            for key in ("call_def", "init_definition", "definition"):
 | 
						|
                if content.get(key, False):
 | 
						|
                    lines.append(content[key])
 | 
						|
                    break
 | 
						|
            for key in ("call_docstring", "init_docstring", "docstring"):
 | 
						|
                if content.get(key, False):
 | 
						|
                    lines.append(content[key])
 | 
						|
                    break
 | 
						|
            if not lines:
 | 
						|
                lines.append("<empty docstring>")
 | 
						|
            data["text/plain"] = "\n".join(lines)
 | 
						|
        return msg
 | 
						|
 | 
						|
    # iopub channel
 | 
						|
 | 
						|
    def stream(self, msg: Dict[str, Any]) -> Dict[str, Any]:
 | 
						|
        """Handle a stream message."""
 | 
						|
        content = msg["content"]
 | 
						|
        content["text"] = content.pop("data")
 | 
						|
        return msg
 | 
						|
 | 
						|
    def display_data(self, msg: Dict[str, Any]) -> Dict[str, Any]:
 | 
						|
        """Handle display data."""
 | 
						|
        content = msg["content"]
 | 
						|
        content.pop("source", None)
 | 
						|
        data = content["data"]
 | 
						|
        if "application/json" in data:
 | 
						|
            try:
 | 
						|
                data["application/json"] = json.loads(data["application/json"])
 | 
						|
            except Exception:
 | 
						|
                # warn?
 | 
						|
                pass
 | 
						|
        return msg
 | 
						|
 | 
						|
    # stdin channel
 | 
						|
 | 
						|
    def input_request(self, msg: Dict[str, Any]) -> Dict[str, Any]:
 | 
						|
        """Handle an input request."""
 | 
						|
        msg["content"].setdefault("password", False)
 | 
						|
        return msg
 | 
						|
 | 
						|
 | 
						|
def adapt(msg: Dict[str, Any], to_version: int = protocol_version_info[0]) -> Dict[str, Any]:
 | 
						|
    """Adapt a single message to a target version
 | 
						|
 | 
						|
    Parameters
 | 
						|
    ----------
 | 
						|
 | 
						|
    msg : dict
 | 
						|
        A Jupyter message.
 | 
						|
    to_version : int, optional
 | 
						|
        The target major version.
 | 
						|
        If unspecified, adapt to the current version.
 | 
						|
 | 
						|
    Returns
 | 
						|
    -------
 | 
						|
 | 
						|
    msg : dict
 | 
						|
        A Jupyter message appropriate in the new version.
 | 
						|
    """
 | 
						|
    from .session import utcnow
 | 
						|
 | 
						|
    header = msg["header"]
 | 
						|
    if "date" not in header:
 | 
						|
        header["date"] = utcnow()
 | 
						|
    if "version" in header:
 | 
						|
        from_version = int(header["version"].split(".")[0])
 | 
						|
    else:
 | 
						|
        # assume last version before adding the key to the header
 | 
						|
        from_version = 4
 | 
						|
    adapter = adapters.get((from_version, to_version), None)
 | 
						|
    if adapter is None:
 | 
						|
        return msg
 | 
						|
    return adapter(msg)
 | 
						|
 | 
						|
 | 
						|
# one adapter per major version from,to
 | 
						|
adapters = {
 | 
						|
    (5, 4): V5toV4(),
 | 
						|
    (4, 5): V4toV5(),
 | 
						|
}
 |