|
"""Adapters for Jupyter msg spec versions.""" |
|
|
|
|
|
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) |
|
|
|
|
|
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 |
|
|
|
|
|
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 |
|
|
|
|
|
|
|
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", {}) |
|
|
|
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 |
|
|
|
|
|
|
|
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: |
|
|
|
pass |
|
return msg |
|
|
|
|
|
|
|
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" |
|
|
|
|
|
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 |
|
|
|
|
|
|
|
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) |
|
|
|
|
|
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.""" |
|
|
|
|
|
|
|
|
|
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: |
|
|
|
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 |
|
|
|
|
|
|
|
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: |
|
|
|
pass |
|
return msg |
|
|
|
|
|
|
|
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: |
|
|
|
from_version = 4 |
|
adapter = adapters.get((from_version, to_version), None) |
|
if adapter is None: |
|
return msg |
|
return adapter(msg) |
|
|
|
|
|
|
|
adapters = { |
|
(5, 4): V5toV4(), |
|
(4, 5): V4toV5(), |
|
} |
|
|