adapter.py
369 lines
| 11.3 KiB
| text/x-python
|
PythonLexer
MinRK
|
r16693 | """Adapters for IPython msg spec versions.""" | ||
# Copyright (c) IPython Development Team. | ||||
# Distributed under the terms of the Modified BSD License. | ||||
import json | ||||
MinRK
|
r16698 | from IPython.core.release import kernel_protocol_version_info | ||
MinRK
|
r16693 | from IPython.utils.tokenutil import token_at_cursor | ||
def code_to_line(code, cursor_pos): | ||||
"""Turn a multiline code block and cursor position into a single line | ||||
and new cursor position. | ||||
Thomas Kluyver
|
r16815 | For adapting ``complete_`` and ``object_info_request``. | ||
MinRK
|
r16693 | """ | ||
Thomas Kluyver
|
r18195 | if not code: | ||
return "", 0 | ||||
MinRK
|
r16693 | for line in code.splitlines(True): | ||
n = len(line) | ||||
if cursor_pos > n: | ||||
cursor_pos -= n | ||||
else: | ||||
break | ||||
return line, cursor_pos | ||||
class Adapter(object): | ||||
"""Base class for adapting messages | ||||
Override message_type(msg) methods to create adapters. | ||||
""" | ||||
msg_type_map = {} | ||||
def update_header(self, msg): | ||||
return msg | ||||
def update_metadata(self, msg): | ||||
return msg | ||||
def update_msg_type(self, msg): | ||||
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 | ||||
Matthias BUSSONNIER
|
r16800 | def handle_reply_status_error(self, msg): | ||
MinRK
|
r16698 | """This will be called *instead of* the regular handler | ||
on any reply with status != ok | ||||
""" | ||||
return msg | ||||
MinRK
|
r16693 | def __call__(self, msg): | ||
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 | ||||
MinRK
|
r16698 | |||
# handle status=error replies separately (no change, at present) | ||||
MinRK
|
r16695 | if msg['content'].get('status', None) in {'error', 'aborted'}: | ||
MinRK
|
r16698 | return self.handle_reply_status_error(msg) | ||
MinRK
|
r16693 | return handler(msg) | ||
def _version_str_to_list(version): | ||||
"""convert a version string to a list of ints | ||||
non-int segments are excluded | ||||
""" | ||||
v = [] | ||||
MinRK
|
r16695 | for part in version.split('.'): | ||
MinRK
|
r16693 | 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): | ||||
msg['header'].pop('version', None) | ||||
return msg | ||||
# shell channel | ||||
def kernel_info_reply(self, msg): | ||||
Min RK
|
r19022 | v4c = {} | ||
MinRK
|
r16693 | content = msg['content'] | ||
for key in ('language_version', 'protocol_version'): | ||||
if key in content: | ||||
Min RK
|
r19022 | v4c[key] = _version_str_to_list(content[key]) | ||
if content.get('implementation', '') == 'ipython' \ | ||||
MinRK
|
r16693 | and 'implementation_version' in content: | ||
Min RK
|
r19022 | 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 | ||||
MinRK
|
r16693 | return msg | ||
def execute_request(self, msg): | ||||
content = msg['content'] | ||||
content.setdefault('user_variables', []) | ||||
return msg | ||||
def execute_reply(self, msg): | ||||
content = msg['content'] | ||||
content.setdefault('user_variables', {}) | ||||
# TODO: handle payloads | ||||
return msg | ||||
def complete_request(self, msg): | ||||
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 | ||||
MinRK
|
r16695 | new_content['block'] = None | ||
MinRK
|
r16693 | new_content['cursor_pos'] = cursor_pos | ||
return msg | ||||
def complete_reply(self, msg): | ||||
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): | ||||
content = msg['content'] | ||||
code = content['code'] | ||||
cursor_pos = content['cursor_pos'] | ||||
MinRK
|
r16695 | line, _ = code_to_line(code, cursor_pos) | ||
MinRK
|
r16693 | |||
MinRK
|
r16698 | new_content = msg['content'] = {} | ||
MinRK
|
r16695 | new_content['oname'] = token_at_cursor(code, cursor_pos) | ||
MinRK
|
r16693 | new_content['detail_level'] = content['detail_level'] | ||
return msg | ||||
def object_info_reply(self, msg): | ||||
"""inspect_reply can't be easily backward compatible""" | ||||
MinRK
|
r18423 | msg['content'] = {'found' : False, 'oname' : 'unknown'} | ||
MinRK
|
r16693 | return msg | ||
# iopub channel | ||||
MinRK
|
r18104 | def stream(self, msg): | ||
content = msg['content'] | ||||
content['data'] = content.pop('text') | ||||
return msg | ||||
MinRK
|
r16693 | def display_data(self, msg): | ||
content = msg['content'] | ||||
content.setdefault("source", "display") | ||||
MinRK
|
r16695 | data = content['data'] | ||
if 'application/json' in data: | ||||
try: | ||||
data['application/json'] = json.dumps(data['application/json']) | ||||
except Exception: | ||||
# warn? | ||||
pass | ||||
MinRK
|
r16693 | return msg | ||
# stdin channel | ||||
def input_request(self, msg): | ||||
msg['content'].pop('password', None) | ||||
return msg | ||||
class V4toV5(Adapter): | ||||
"""Convert msg spec V4 to V5""" | ||||
MinRK
|
r16698 | version = '5.0' | ||
MinRK
|
r16693 | |||
# invert message renames above | ||||
msg_type_map = {v:k for k,v in V5toV4.msg_type_map.items()} | ||||
def update_header(self, msg): | ||||
msg['header']['version'] = self.version | ||||
return msg | ||||
# shell channel | ||||
def kernel_info_reply(self, msg): | ||||
content = msg['content'] | ||||
Min RK
|
r19022 | for key in ('protocol_version', 'ipython_version'): | ||
MinRK
|
r16693 | if key in content: | ||
Min RK
|
r19022 | content[key] = '.'.join(map(str, content[key])) | ||
content.setdefault('protocol_version', '4.1') | ||||
MinRK
|
r16693 | |||
if content['language'].startswith('python') and 'ipython_version' in content: | ||||
content['implementation'] = 'ipython' | ||||
content['implementation_version'] = content.pop('ipython_version') | ||||
Min RK
|
r19022 | 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) | ||||
MinRK
|
r16693 | content['banner'] = '' | ||
return msg | ||||
def execute_request(self, msg): | ||||
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): | ||||
content = msg['content'] | ||||
user_expressions = content.setdefault('user_expressions', {}) | ||||
user_variables = content.pop('user_variables', {}) | ||||
if user_variables: | ||||
user_expressions.update(user_variables) | ||||
Thomas Kluyver
|
r17995 | |||
# 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') | ||||
MinRK
|
r16693 | return msg | ||
def complete_request(self, msg): | ||||
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): | ||||
MinRK
|
r16698 | # complete_reply needs more context than we have to get cursor_start and end. | ||
# use special value of `-1` to indicate to frontend that it should be at | ||||
# the current cursor position. | ||||
MinRK
|
r16695 | content = msg['content'] | ||
new_content = msg['content'] = {'status' : 'ok'} | ||||
MinRK
|
r16698 | new_content['matches'] = content['matches'] | ||
new_content['cursor_start'] = -len(content['matched_text']) | ||||
new_content['cursor_end'] = None | ||||
MinRK
|
r16695 | new_content['metadata'] = {} | ||
MinRK
|
r16693 | return msg | ||
def inspect_request(self, msg): | ||||
content = msg['content'] | ||||
MinRK
|
r16695 | name = content['oname'] | ||
MinRK
|
r16693 | |||
new_content = msg['content'] = {} | ||||
new_content['code'] = name | ||||
MinRK
|
r16695 | new_content['cursor_pos'] = len(name) | ||
MinRK
|
r16693 | new_content['detail_level'] = content['detail_level'] | ||
return msg | ||||
def inspect_reply(self, msg): | ||||
"""inspect_reply can't be easily backward compatible""" | ||||
MinRK
|
r16695 | content = msg['content'] | ||
new_content = msg['content'] = {'status' : 'ok'} | ||||
found = new_content['found'] = content['found'] | ||||
MinRK
|
r18423 | new_content['name'] = content['oname'] | ||
MinRK
|
r16695 | 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) | ||||
MinRK
|
r16693 | return msg | ||
# iopub channel | ||||
MinRK
|
r18104 | def stream(self, msg): | ||
content = msg['content'] | ||||
content['text'] = content.pop('data') | ||||
return msg | ||||
MinRK
|
r16693 | def display_data(self, msg): | ||
content = msg['content'] | ||||
content.pop("source", None) | ||||
data = content['data'] | ||||
if 'application/json' in data: | ||||
MinRK
|
r16695 | try: | ||
data['application/json'] = json.loads(data['application/json']) | ||||
except Exception: | ||||
# warn? | ||||
pass | ||||
MinRK
|
r16693 | return msg | ||
# stdin channel | ||||
def input_request(self, msg): | ||||
msg['content'].setdefault('password', False) | ||||
return msg | ||||
def adapt(msg, to_version=kernel_protocol_version_info[0]): | ||||
"""Adapt a single message to a target version | ||||
Parameters | ||||
---------- | ||||
msg : dict | ||||
An IPython message. | ||||
to_version : int, optional | ||||
The target major version. | ||||
If unspecified, adapt to the current version for IPython. | ||||
Returns | ||||
------- | ||||
msg : dict | ||||
An IPython message appropriate in the new version. | ||||
""" | ||||
header = msg['header'] | ||||
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(), | ||||
Matthias BUSSONNIER
|
r16800 | } | ||