adapter.py 12 KB

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