1"""Adapters for Jupyter msg spec versions.""" 2 3# Copyright (c) Jupyter Development Team. 4# Distributed under the terms of the Modified BSD License. 5 6import re 7import json 8 9from jupyter_client import protocol_version_info 10 11def code_to_line(code, cursor_pos): 12 """Turn a multiline code block and cursor position into a single line 13 and new cursor position. 14 15 For adapting ``complete_`` and ``object_info_request``. 16 """ 17 if not code: 18 return "", 0 19 for line in code.splitlines(True): 20 n = len(line) 21 if cursor_pos > n: 22 cursor_pos -= n 23 else: 24 break 25 return line, cursor_pos 26 27 28_match_bracket = re.compile(r'\([^\(\)]+\)', re.UNICODE) 29_end_bracket = re.compile(r'\([^\(]*$', re.UNICODE) 30_identifier = re.compile(r'[a-z_][0-9a-z._]*', re.I|re.UNICODE) 31 32def extract_oname_v4(code, cursor_pos): 33 """Reimplement token-finding logic from IPython 2.x javascript 34 35 for adapting object_info_request from v5 to v4 36 """ 37 38 line, _ = code_to_line(code, cursor_pos) 39 40 oldline = line 41 line = _match_bracket.sub('', line) 42 while oldline != line: 43 oldline = line 44 line = _match_bracket.sub('', line) 45 46 # remove everything after last open bracket 47 line = _end_bracket.sub('', line) 48 matches = _identifier.findall(line) 49 if matches: 50 return matches[-1] 51 else: 52 return '' 53 54 55class Adapter(object): 56 """Base class for adapting messages 57 58 Override message_type(msg) methods to create adapters. 59 """ 60 61 msg_type_map = {} 62 63 def update_header(self, msg): 64 return msg 65 66 def update_metadata(self, msg): 67 return msg 68 69 def update_msg_type(self, msg): 70 header = msg['header'] 71 msg_type = header['msg_type'] 72 if msg_type in self.msg_type_map: 73 msg['msg_type'] = header['msg_type'] = self.msg_type_map[msg_type] 74 return msg 75 76 def handle_reply_status_error(self, msg): 77 """This will be called *instead of* the regular handler 78 79 on any reply with status != ok 80 """ 81 return msg 82 83 def __call__(self, msg): 84 msg = self.update_header(msg) 85 msg = self.update_metadata(msg) 86 msg = self.update_msg_type(msg) 87 header = msg['header'] 88 89 handler = getattr(self, header['msg_type'], None) 90 if handler is None: 91 return msg 92 93 # handle status=error replies separately (no change, at present) 94 if msg['content'].get('status', None) in {'error', 'aborted'}: 95 return self.handle_reply_status_error(msg) 96 return handler(msg) 97 98def _version_str_to_list(version): 99 """convert a version string to a list of ints 100 101 non-int segments are excluded 102 """ 103 v = [] 104 for part in version.split('.'): 105 try: 106 v.append(int(part)) 107 except ValueError: 108 pass 109 return v 110 111class V5toV4(Adapter): 112 """Adapt msg protocol v5 to v4""" 113 114 version = '4.1' 115 116 msg_type_map = { 117 'execute_result' : 'pyout', 118 'execute_input' : 'pyin', 119 'error' : 'pyerr', 120 'inspect_request' : 'object_info_request', 121 'inspect_reply' : 'object_info_reply', 122 } 123 124 def update_header(self, msg): 125 msg['header'].pop('version', None) 126 msg['parent_header'].pop('version', None) 127 return msg 128 129 # shell channel 130 131 def kernel_info_reply(self, msg): 132 v4c = {} 133 content = msg['content'] 134 for key in ('language_version', 'protocol_version'): 135 if key in content: 136 v4c[key] = _version_str_to_list(content[key]) 137 if content.get('implementation', '') == 'ipython' \ 138 and 'implementation_version' in content: 139 v4c['ipython_version'] = _version_str_to_list(content['implementation_version']) 140 language_info = content.get('language_info', {}) 141 language = language_info.get('name', '') 142 v4c.setdefault('language', language) 143 if 'version' in language_info: 144 v4c.setdefault('language_version', _version_str_to_list(language_info['version'])) 145 msg['content'] = v4c 146 return msg 147 148 def execute_request(self, msg): 149 content = msg['content'] 150 content.setdefault('user_variables', []) 151 return msg 152 153 def execute_reply(self, msg): 154 content = msg['content'] 155 content.setdefault('user_variables', {}) 156 # TODO: handle payloads 157 return msg 158 159 def complete_request(self, msg): 160 content = msg['content'] 161 code = content['code'] 162 cursor_pos = content['cursor_pos'] 163 line, cursor_pos = code_to_line(code, cursor_pos) 164 165 new_content = msg['content'] = {} 166 new_content['text'] = '' 167 new_content['line'] = line 168 new_content['block'] = None 169 new_content['cursor_pos'] = cursor_pos 170 return msg 171 172 def complete_reply(self, msg): 173 content = msg['content'] 174 cursor_start = content.pop('cursor_start') 175 cursor_end = content.pop('cursor_end') 176 match_len = cursor_end - cursor_start 177 content['matched_text'] = content['matches'][0][:match_len] 178 content.pop('metadata', None) 179 return msg 180 181 def object_info_request(self, msg): 182 content = msg['content'] 183 code = content['code'] 184 cursor_pos = content['cursor_pos'] 185 line, _ = code_to_line(code, cursor_pos) 186 187 new_content = msg['content'] = {} 188 new_content['oname'] = extract_oname_v4(code, cursor_pos) 189 new_content['detail_level'] = content['detail_level'] 190 return msg 191 192 def object_info_reply(self, msg): 193 """inspect_reply can't be easily backward compatible""" 194 msg['content'] = {'found' : False, 'oname' : 'unknown'} 195 return msg 196 197 # iopub channel 198 199 def stream(self, msg): 200 content = msg['content'] 201 content['data'] = content.pop('text') 202 return msg 203 204 def display_data(self, msg): 205 content = msg['content'] 206 content.setdefault("source", "display") 207 data = content['data'] 208 if 'application/json' in data: 209 try: 210 data['application/json'] = json.dumps(data['application/json']) 211 except Exception: 212 # warn? 213 pass 214 return msg 215 216 # stdin channel 217 218 def input_request(self, msg): 219 msg['content'].pop('password', None) 220 return msg 221 222 223class V4toV5(Adapter): 224 """Convert msg spec V4 to V5""" 225 version = '5.0' 226 227 # invert message renames above 228 msg_type_map = {v:k for k,v in V5toV4.msg_type_map.items()} 229 230 def update_header(self, msg): 231 msg['header']['version'] = self.version 232 if msg['parent_header']: 233 msg['parent_header']['version'] = self.version 234 return msg 235 236 # shell channel 237 238 def kernel_info_reply(self, msg): 239 content = msg['content'] 240 for key in ('protocol_version', 'ipython_version'): 241 if key in content: 242 content[key] = '.'.join(map(str, content[key])) 243 244 content.setdefault('protocol_version', '4.1') 245 246 if content['language'].startswith('python') and 'ipython_version' in content: 247 content['implementation'] = 'ipython' 248 content['implementation_version'] = content.pop('ipython_version') 249 250 language = content.pop('language') 251 language_info = content.setdefault('language_info', {}) 252 language_info.setdefault('name', language) 253 if 'language_version' in content: 254 language_version = '.'.join(map(str, content.pop('language_version'))) 255 language_info.setdefault('version', language_version) 256 257 content['banner'] = '' 258 return msg 259 260 def execute_request(self, msg): 261 content = msg['content'] 262 user_variables = content.pop('user_variables', []) 263 user_expressions = content.setdefault('user_expressions', {}) 264 for v in user_variables: 265 user_expressions[v] = v 266 return msg 267 268 def execute_reply(self, msg): 269 content = msg['content'] 270 user_expressions = content.setdefault('user_expressions', {}) 271 user_variables = content.pop('user_variables', {}) 272 if user_variables: 273 user_expressions.update(user_variables) 274 275 # Pager payloads became a mime bundle 276 for payload in content.get('payload', []): 277 if payload.get('source', None) == 'page' and ('text' in payload): 278 if 'data' not in payload: 279 payload['data'] = {} 280 payload['data']['text/plain'] = payload.pop('text') 281 282 return msg 283 284 def complete_request(self, msg): 285 old_content = msg['content'] 286 287 new_content = msg['content'] = {} 288 new_content['code'] = old_content['line'] 289 new_content['cursor_pos'] = old_content['cursor_pos'] 290 return msg 291 292 def complete_reply(self, msg): 293 # complete_reply needs more context than we have to get cursor_start and end. 294 # use special end=null to indicate current cursor position and negative offset 295 # for start relative to the cursor. 296 # start=None indicates that start == end (accounts for no -0). 297 content = msg['content'] 298 new_content = msg['content'] = {'status' : 'ok'} 299 new_content['matches'] = content['matches'] 300 if content['matched_text']: 301 new_content['cursor_start'] = -len(content['matched_text']) 302 else: 303 # no -0, use None to indicate that start == end 304 new_content['cursor_start'] = None 305 new_content['cursor_end'] = None 306 new_content['metadata'] = {} 307 return msg 308 309 def inspect_request(self, msg): 310 content = msg['content'] 311 name = content['oname'] 312 313 new_content = msg['content'] = {} 314 new_content['code'] = name 315 new_content['cursor_pos'] = len(name) 316 new_content['detail_level'] = content['detail_level'] 317 return msg 318 319 def inspect_reply(self, msg): 320 """inspect_reply can't be easily backward compatible""" 321 content = msg['content'] 322 new_content = msg['content'] = {'status' : 'ok'} 323 found = new_content['found'] = content['found'] 324 new_content['data'] = data = {} 325 new_content['metadata'] = {} 326 if found: 327 lines = [] 328 for key in ('call_def', 'init_definition', 'definition'): 329 if content.get(key, False): 330 lines.append(content[key]) 331 break 332 for key in ('call_docstring', 'init_docstring', 'docstring'): 333 if content.get(key, False): 334 lines.append(content[key]) 335 break 336 if not lines: 337 lines.append("<empty docstring>") 338 data['text/plain'] = '\n'.join(lines) 339 return msg 340 341 # iopub channel 342 343 def stream(self, msg): 344 content = msg['content'] 345 content['text'] = content.pop('data') 346 return msg 347 348 def display_data(self, msg): 349 content = msg['content'] 350 content.pop("source", None) 351 data = content['data'] 352 if 'application/json' in data: 353 try: 354 data['application/json'] = json.loads(data['application/json']) 355 except Exception: 356 # warn? 357 pass 358 return msg 359 360 # stdin channel 361 362 def input_request(self, msg): 363 msg['content'].setdefault('password', False) 364 return msg 365 366 367 368def adapt(msg, to_version=protocol_version_info[0]): 369 """Adapt a single message to a target version 370 371 Parameters 372 ---------- 373 374 msg : dict 375 A Jupyter message. 376 to_version : int, optional 377 The target major version. 378 If unspecified, adapt to the current version. 379 380 Returns 381 ------- 382 383 msg : dict 384 A Jupyter message appropriate in the new version. 385 """ 386 from .session import utcnow 387 header = msg['header'] 388 if 'date' not in header: 389 header['date'] = utcnow() 390 if 'version' in header: 391 from_version = int(header['version'].split('.')[0]) 392 else: 393 # assume last version before adding the key to the header 394 from_version = 4 395 adapter = adapters.get((from_version, to_version), None) 396 if adapter is None: 397 return msg 398 return adapter(msg) 399 400 401# one adapter per major version from,to 402adapters = { 403 (5,4) : V5toV4(), 404 (4,5) : V4toV5(), 405} 406