##// END OF EJS Templates
Adapt msgspec v4 pager payloads to msgspec v5 format
Thomas Kluyver -
Show More
@@ -1,336 +1,344 b''
1 1 """Adapters for IPython msg spec versions."""
2 2
3 3 # Copyright (c) IPython Development Team.
4 4 # Distributed under the terms of the Modified BSD License.
5 5
6 6 import json
7 7
8 8 from IPython.core.release import kernel_protocol_version_info
9 9 from IPython.utils.tokenutil import token_at_cursor
10 10
11 11
12 12 def code_to_line(code, cursor_pos):
13 13 """Turn a multiline code block and cursor position into a single line
14 14 and new cursor position.
15 15
16 16 For adapting ``complete_`` and ``object_info_request``.
17 17 """
18 18 for line in code.splitlines(True):
19 19 n = len(line)
20 20 if cursor_pos > n:
21 21 cursor_pos -= n
22 22 else:
23 23 break
24 24 return line, cursor_pos
25 25
26 26
27 27 class Adapter(object):
28 28 """Base class for adapting messages
29 29
30 30 Override message_type(msg) methods to create adapters.
31 31 """
32 32
33 33 msg_type_map = {}
34 34
35 35 def update_header(self, msg):
36 36 return msg
37 37
38 38 def update_metadata(self, msg):
39 39 return msg
40 40
41 41 def update_msg_type(self, msg):
42 42 header = msg['header']
43 43 msg_type = header['msg_type']
44 44 if msg_type in self.msg_type_map:
45 45 msg['msg_type'] = header['msg_type'] = self.msg_type_map[msg_type]
46 46 return msg
47 47
48 48 def handle_reply_status_error(self, msg):
49 49 """This will be called *instead of* the regular handler
50 50
51 51 on any reply with status != ok
52 52 """
53 53 return msg
54 54
55 55 def __call__(self, msg):
56 56 msg = self.update_header(msg)
57 57 msg = self.update_metadata(msg)
58 58 msg = self.update_msg_type(msg)
59 59 header = msg['header']
60 60
61 61 handler = getattr(self, header['msg_type'], None)
62 62 if handler is None:
63 63 return msg
64 64
65 65 # handle status=error replies separately (no change, at present)
66 66 if msg['content'].get('status', None) in {'error', 'aborted'}:
67 67 return self.handle_reply_status_error(msg)
68 68 return handler(msg)
69 69
70 70 def _version_str_to_list(version):
71 71 """convert a version string to a list of ints
72 72
73 73 non-int segments are excluded
74 74 """
75 75 v = []
76 76 for part in version.split('.'):
77 77 try:
78 78 v.append(int(part))
79 79 except ValueError:
80 80 pass
81 81 return v
82 82
83 83 class V5toV4(Adapter):
84 84 """Adapt msg protocol v5 to v4"""
85 85
86 86 version = '4.1'
87 87
88 88 msg_type_map = {
89 89 'execute_result' : 'pyout',
90 90 'execute_input' : 'pyin',
91 91 'error' : 'pyerr',
92 92 'inspect_request' : 'object_info_request',
93 93 'inspect_reply' : 'object_info_reply',
94 94 }
95 95
96 96 def update_header(self, msg):
97 97 msg['header'].pop('version', None)
98 98 return msg
99 99
100 100 # shell channel
101 101
102 102 def kernel_info_reply(self, msg):
103 103 content = msg['content']
104 104 content.pop('banner', None)
105 105 for key in ('language_version', 'protocol_version'):
106 106 if key in content:
107 107 content[key] = _version_str_to_list(content[key])
108 108 if content.pop('implementation', '') == 'ipython' \
109 109 and 'implementation_version' in content:
110 110 content['ipython_version'] = content.pop('implmentation_version')
111 111 content.pop('implementation_version', None)
112 112 content.setdefault("implmentation", content['language'])
113 113 return msg
114 114
115 115 def execute_request(self, msg):
116 116 content = msg['content']
117 117 content.setdefault('user_variables', [])
118 118 return msg
119 119
120 120 def execute_reply(self, msg):
121 121 content = msg['content']
122 122 content.setdefault('user_variables', {})
123 123 # TODO: handle payloads
124 124 return msg
125 125
126 126 def complete_request(self, msg):
127 127 content = msg['content']
128 128 code = content['code']
129 129 cursor_pos = content['cursor_pos']
130 130 line, cursor_pos = code_to_line(code, cursor_pos)
131 131
132 132 new_content = msg['content'] = {}
133 133 new_content['text'] = ''
134 134 new_content['line'] = line
135 135 new_content['block'] = None
136 136 new_content['cursor_pos'] = cursor_pos
137 137 return msg
138 138
139 139 def complete_reply(self, msg):
140 140 content = msg['content']
141 141 cursor_start = content.pop('cursor_start')
142 142 cursor_end = content.pop('cursor_end')
143 143 match_len = cursor_end - cursor_start
144 144 content['matched_text'] = content['matches'][0][:match_len]
145 145 content.pop('metadata', None)
146 146 return msg
147 147
148 148 def object_info_request(self, msg):
149 149 content = msg['content']
150 150 code = content['code']
151 151 cursor_pos = content['cursor_pos']
152 152 line, _ = code_to_line(code, cursor_pos)
153 153
154 154 new_content = msg['content'] = {}
155 155 new_content['oname'] = token_at_cursor(code, cursor_pos)
156 156 new_content['detail_level'] = content['detail_level']
157 157 return msg
158 158
159 159 def object_info_reply(self, msg):
160 160 """inspect_reply can't be easily backward compatible"""
161 161 msg['content'] = {'found' : False, 'name' : 'unknown'}
162 162 return msg
163 163
164 164 # iopub channel
165 165
166 166 def display_data(self, msg):
167 167 content = msg['content']
168 168 content.setdefault("source", "display")
169 169 data = content['data']
170 170 if 'application/json' in data:
171 171 try:
172 172 data['application/json'] = json.dumps(data['application/json'])
173 173 except Exception:
174 174 # warn?
175 175 pass
176 176 return msg
177 177
178 178 # stdin channel
179 179
180 180 def input_request(self, msg):
181 181 msg['content'].pop('password', None)
182 182 return msg
183 183
184 184
185 185 class V4toV5(Adapter):
186 186 """Convert msg spec V4 to V5"""
187 187 version = '5.0'
188 188
189 189 # invert message renames above
190 190 msg_type_map = {v:k for k,v in V5toV4.msg_type_map.items()}
191 191
192 192 def update_header(self, msg):
193 193 msg['header']['version'] = self.version
194 194 return msg
195 195
196 196 # shell channel
197 197
198 198 def kernel_info_reply(self, msg):
199 199 content = msg['content']
200 200 for key in ('language_version', 'protocol_version', 'ipython_version'):
201 201 if key in content:
202 202 content[key] = ".".join(map(str, content[key]))
203 203
204 204 if content['language'].startswith('python') and 'ipython_version' in content:
205 205 content['implementation'] = 'ipython'
206 206 content['implementation_version'] = content.pop('ipython_version')
207 207
208 208 content['banner'] = ''
209 209 return msg
210 210
211 211 def execute_request(self, msg):
212 212 content = msg['content']
213 213 user_variables = content.pop('user_variables', [])
214 214 user_expressions = content.setdefault('user_expressions', {})
215 215 for v in user_variables:
216 216 user_expressions[v] = v
217 217 return msg
218 218
219 219 def execute_reply(self, msg):
220 220 content = msg['content']
221 221 user_expressions = content.setdefault('user_expressions', {})
222 222 user_variables = content.pop('user_variables', {})
223 223 if user_variables:
224 224 user_expressions.update(user_variables)
225
226 # Pager payloads became a mime bundle
227 for payload in content.get('payload', []):
228 if payload.get('source', None) == 'page' and ('text' in payload):
229 if 'data' not in payload:
230 payload['data'] = {}
231 payload['data']['text/plain'] = payload.pop('text')
232
225 233 return msg
226 234
227 235 def complete_request(self, msg):
228 236 old_content = msg['content']
229 237
230 238 new_content = msg['content'] = {}
231 239 new_content['code'] = old_content['line']
232 240 new_content['cursor_pos'] = old_content['cursor_pos']
233 241 return msg
234 242
235 243 def complete_reply(self, msg):
236 244 # complete_reply needs more context than we have to get cursor_start and end.
237 245 # use special value of `-1` to indicate to frontend that it should be at
238 246 # the current cursor position.
239 247 content = msg['content']
240 248 new_content = msg['content'] = {'status' : 'ok'}
241 249 new_content['matches'] = content['matches']
242 250 new_content['cursor_start'] = -len(content['matched_text'])
243 251 new_content['cursor_end'] = None
244 252 new_content['metadata'] = {}
245 253 return msg
246 254
247 255 def inspect_request(self, msg):
248 256 content = msg['content']
249 257 name = content['oname']
250 258
251 259 new_content = msg['content'] = {}
252 260 new_content['code'] = name
253 261 new_content['cursor_pos'] = len(name)
254 262 new_content['detail_level'] = content['detail_level']
255 263 return msg
256 264
257 265 def inspect_reply(self, msg):
258 266 """inspect_reply can't be easily backward compatible"""
259 267 content = msg['content']
260 268 new_content = msg['content'] = {'status' : 'ok'}
261 269 found = new_content['found'] = content['found']
262 270 new_content['name'] = content['name']
263 271 new_content['data'] = data = {}
264 272 new_content['metadata'] = {}
265 273 if found:
266 274 lines = []
267 275 for key in ('call_def', 'init_definition', 'definition'):
268 276 if content.get(key, False):
269 277 lines.append(content[key])
270 278 break
271 279 for key in ('call_docstring', 'init_docstring', 'docstring'):
272 280 if content.get(key, False):
273 281 lines.append(content[key])
274 282 break
275 283 if not lines:
276 284 lines.append("<empty docstring>")
277 285 data['text/plain'] = '\n'.join(lines)
278 286 return msg
279 287
280 288 # iopub channel
281 289
282 290 def display_data(self, msg):
283 291 content = msg['content']
284 292 content.pop("source", None)
285 293 data = content['data']
286 294 if 'application/json' in data:
287 295 try:
288 296 data['application/json'] = json.loads(data['application/json'])
289 297 except Exception:
290 298 # warn?
291 299 pass
292 300 return msg
293 301
294 302 # stdin channel
295 303
296 304 def input_request(self, msg):
297 305 msg['content'].setdefault('password', False)
298 306 return msg
299 307
300 308
301 309
302 310 def adapt(msg, to_version=kernel_protocol_version_info[0]):
303 311 """Adapt a single message to a target version
304 312
305 313 Parameters
306 314 ----------
307 315
308 316 msg : dict
309 317 An IPython message.
310 318 to_version : int, optional
311 319 The target major version.
312 320 If unspecified, adapt to the current version for IPython.
313 321
314 322 Returns
315 323 -------
316 324
317 325 msg : dict
318 326 An IPython message appropriate in the new version.
319 327 """
320 328 header = msg['header']
321 329 if 'version' in header:
322 330 from_version = int(header['version'].split('.')[0])
323 331 else:
324 332 # assume last version before adding the key to the header
325 333 from_version = 4
326 334 adapter = adapters.get((from_version, to_version), None)
327 335 if adapter is None:
328 336 return msg
329 337 return adapter(msg)
330 338
331 339
332 340 # one adapter per major version from,to
333 341 adapters = {
334 342 (5,4) : V5toV4(),
335 343 (4,5) : V4toV5(),
336 344 }
@@ -1,314 +1,330 b''
1 1 """Tests for adapting IPython msg spec versions"""
2 2
3 3 # Copyright (c) IPython Development Team.
4 4 # Distributed under the terms of the Modified BSD License.
5 5
6 6 import copy
7 7 import json
8 8 from unittest import TestCase
9 9 import nose.tools as nt
10 10
11 11 from IPython.kernel.adapter import adapt, V4toV5, V5toV4
12 12 from IPython.kernel.zmq.session import Session
13 13
14 14
15 15 def test_default_version():
16 16 s = Session()
17 17 msg = s.msg("msg_type")
18 18 msg['header'].pop('version')
19 19 original = copy.deepcopy(msg)
20 20 adapted = adapt(original)
21 21 nt.assert_equal(adapted['header']['version'], V4toV5.version)
22 22
23 23
24 24 class AdapterTest(TestCase):
25 25
26 26 def setUp(self):
27 27 self.session = Session()
28 28
29 29 def adapt(self, msg, version=None):
30 30 original = copy.deepcopy(msg)
31 31 adapted = adapt(msg, version or self.to_version)
32 32 return original, adapted
33 33
34 34 def check_header(self, msg):
35 35 pass
36 36
37 37
38 38 class V4toV5TestCase(AdapterTest):
39 39 from_version = 4
40 40 to_version = 5
41 41
42 42 def msg(self, msg_type, content):
43 43 """Create a v4 msg (same as v5, minus version header)"""
44 44 msg = self.session.msg(msg_type, content)
45 45 msg['header'].pop('version')
46 46 return msg
47 47
48 48 def test_same_version(self):
49 49 msg = self.msg("execute_result",
50 50 content={'status' : 'ok'}
51 51 )
52 52 original, adapted = self.adapt(msg, self.from_version)
53 53
54 54 self.assertEqual(original, adapted)
55 55
56 56 def test_no_adapt(self):
57 57 msg = self.msg("input_reply", {'value' : 'some text'})
58 58 v4, v5 = self.adapt(msg)
59 59 self.assertEqual(v5['header']['version'], V4toV5.version)
60 60 v5['header'].pop('version')
61 61 self.assertEqual(v4, v5)
62 62
63 63 def test_rename_type(self):
64 64 for v5_type, v4_type in [
65 65 ('execute_result', 'pyout'),
66 66 ('execute_input', 'pyin'),
67 67 ('error', 'pyerr'),
68 68 ]:
69 69 msg = self.msg(v4_type, {'key' : 'value'})
70 70 v4, v5 = self.adapt(msg)
71 71 self.assertEqual(v5['header']['version'], V4toV5.version)
72 72 self.assertEqual(v5['header']['msg_type'], v5_type)
73 73 self.assertEqual(v4['content'], v5['content'])
74 74
75 75 def test_execute_request(self):
76 76 msg = self.msg("execute_request", {
77 77 'code' : 'a=5',
78 78 'silent' : False,
79 79 'user_expressions' : {'a' : 'apple'},
80 80 'user_variables' : ['b'],
81 81 })
82 82 v4, v5 = self.adapt(msg)
83 83 self.assertEqual(v4['header']['msg_type'], v5['header']['msg_type'])
84 84 v4c = v4['content']
85 85 v5c = v5['content']
86 86 self.assertEqual(v5c['user_expressions'], {'a' : 'apple', 'b': 'b'})
87 87 self.assertNotIn('user_variables', v5c)
88 88 self.assertEqual(v5c['code'], v4c['code'])
89
90 def test_execute_reply(self):
91 msg = self.msg("execute_reply", {
92 'status': 'ok',
93 'execution_count': 7,
94 'user_variables': {'a': 1},
95 'user_expressions': {'a+a': 2},
96 'payload': [{'source':'page', 'text':'blah'}]
97 })
98 v4, v5 = self.adapt(msg)
99 v5c = v5['content']
100 self.assertNotIn('user_variables', v5c)
101 self.assertEqual(v5c['user_expressions'], {'a': 1, 'a+a': 2})
102 self.assertEqual(v5c['payload'], [{'source': 'page',
103 'data': {'text/plain': 'blah'}}
104 ])
89 105
90 106 def test_complete_request(self):
91 107 msg = self.msg("complete_request", {
92 108 'text' : 'a.is',
93 109 'line' : 'foo = a.is',
94 110 'block' : None,
95 111 'cursor_pos' : 10,
96 112 })
97 113 v4, v5 = self.adapt(msg)
98 114 v4c = v4['content']
99 115 v5c = v5['content']
100 116 for key in ('text', 'line', 'block'):
101 117 self.assertNotIn(key, v5c)
102 118 self.assertEqual(v5c['cursor_pos'], v4c['cursor_pos'])
103 119 self.assertEqual(v5c['code'], v4c['line'])
104 120
105 121 def test_complete_reply(self):
106 122 msg = self.msg("complete_reply", {
107 123 'matched_text' : 'a.is',
108 124 'matches' : ['a.isalnum',
109 125 'a.isalpha',
110 126 'a.isdigit',
111 127 'a.islower',
112 128 ],
113 129 })
114 130 v4, v5 = self.adapt(msg)
115 131 v4c = v4['content']
116 132 v5c = v5['content']
117 133
118 134 self.assertEqual(v5c['matches'], v4c['matches'])
119 135 self.assertEqual(v5c['metadata'], {})
120 136 self.assertEqual(v5c['cursor_start'], -4)
121 137 self.assertEqual(v5c['cursor_end'], None)
122 138
123 139 def test_object_info_request(self):
124 140 msg = self.msg("object_info_request", {
125 141 'oname' : 'foo',
126 142 'detail_level' : 1,
127 143 })
128 144 v4, v5 = self.adapt(msg)
129 145 self.assertEqual(v5['header']['msg_type'], 'inspect_request')
130 146 v4c = v4['content']
131 147 v5c = v5['content']
132 148 self.assertEqual(v5c['code'], v4c['oname'])
133 149 self.assertEqual(v5c['cursor_pos'], len(v4c['oname']))
134 150 self.assertEqual(v5c['detail_level'], v4c['detail_level'])
135 151
136 152 def test_object_info_reply(self):
137 153 msg = self.msg("object_info_reply", {
138 154 'name' : 'foo',
139 155 'found' : True,
140 156 'status' : 'ok',
141 157 'definition' : 'foo(a=5)',
142 158 'docstring' : "the docstring",
143 159 })
144 160 v4, v5 = self.adapt(msg)
145 161 self.assertEqual(v5['header']['msg_type'], 'inspect_reply')
146 162 v4c = v4['content']
147 163 v5c = v5['content']
148 164 self.assertEqual(sorted(v5c), [ 'data', 'found', 'metadata', 'name', 'status'])
149 165 text = v5c['data']['text/plain']
150 166 self.assertEqual(text, '\n'.join([v4c['definition'], v4c['docstring']]))
151 167
152 168 # iopub channel
153 169
154 170 def test_display_data(self):
155 171 jsondata = dict(a=5)
156 172 msg = self.msg("display_data", {
157 173 'data' : {
158 174 'text/plain' : 'some text',
159 175 'application/json' : json.dumps(jsondata)
160 176 },
161 177 'metadata' : {'text/plain' : { 'key' : 'value' }},
162 178 })
163 179 v4, v5 = self.adapt(msg)
164 180 v4c = v4['content']
165 181 v5c = v5['content']
166 182 self.assertEqual(v5c['metadata'], v4c['metadata'])
167 183 self.assertEqual(v5c['data']['text/plain'], v4c['data']['text/plain'])
168 184 self.assertEqual(v5c['data']['application/json'], jsondata)
169 185
170 186 # stdin channel
171 187
172 188 def test_input_request(self):
173 189 msg = self.msg('input_request', {'prompt': "$>"})
174 190 v4, v5 = self.adapt(msg)
175 191 self.assertEqual(v5['content']['prompt'], v4['content']['prompt'])
176 192 self.assertFalse(v5['content']['password'])
177 193
178 194
179 195 class V5toV4TestCase(AdapterTest):
180 196 from_version = 5
181 197 to_version = 4
182 198
183 199 def msg(self, msg_type, content):
184 200 return self.session.msg(msg_type, content)
185 201
186 202 def test_same_version(self):
187 203 msg = self.msg("execute_result",
188 204 content={'status' : 'ok'}
189 205 )
190 206 original, adapted = self.adapt(msg, self.from_version)
191 207
192 208 self.assertEqual(original, adapted)
193 209
194 210 def test_no_adapt(self):
195 211 msg = self.msg("input_reply", {'value' : 'some text'})
196 212 v5, v4 = self.adapt(msg)
197 213 self.assertNotIn('version', v4['header'])
198 214 v5['header'].pop('version')
199 215 self.assertEqual(v4, v5)
200 216
201 217 def test_rename_type(self):
202 218 for v5_type, v4_type in [
203 219 ('execute_result', 'pyout'),
204 220 ('execute_input', 'pyin'),
205 221 ('error', 'pyerr'),
206 222 ]:
207 223 msg = self.msg(v5_type, {'key' : 'value'})
208 224 v5, v4 = self.adapt(msg)
209 225 self.assertEqual(v4['header']['msg_type'], v4_type)
210 226 nt.assert_not_in('version', v4['header'])
211 227 self.assertEqual(v4['content'], v5['content'])
212 228
213 229 def test_execute_request(self):
214 230 msg = self.msg("execute_request", {
215 231 'code' : 'a=5',
216 232 'silent' : False,
217 233 'user_expressions' : {'a' : 'apple'},
218 234 })
219 235 v5, v4 = self.adapt(msg)
220 236 self.assertEqual(v4['header']['msg_type'], v5['header']['msg_type'])
221 237 v4c = v4['content']
222 238 v5c = v5['content']
223 239 self.assertEqual(v4c['user_variables'], [])
224 240 self.assertEqual(v5c['code'], v4c['code'])
225 241
226 242 def test_complete_request(self):
227 243 msg = self.msg("complete_request", {
228 244 'code' : 'def foo():\n'
229 245 ' a.is\n'
230 246 'foo()',
231 247 'cursor_pos': 19,
232 248 })
233 249 v5, v4 = self.adapt(msg)
234 250 v4c = v4['content']
235 251 v5c = v5['content']
236 252 self.assertNotIn('code', v4c)
237 253 self.assertEqual(v4c['line'], v5c['code'].splitlines(True)[1])
238 254 self.assertEqual(v4c['cursor_pos'], 8)
239 255 self.assertEqual(v4c['text'], '')
240 256 self.assertEqual(v4c['block'], None)
241 257
242 258 def test_complete_reply(self):
243 259 msg = self.msg("complete_reply", {
244 260 'cursor_start' : 10,
245 261 'cursor_end' : 14,
246 262 'matches' : ['a.isalnum',
247 263 'a.isalpha',
248 264 'a.isdigit',
249 265 'a.islower',
250 266 ],
251 267 'metadata' : {},
252 268 })
253 269 v5, v4 = self.adapt(msg)
254 270 v4c = v4['content']
255 271 v5c = v5['content']
256 272 self.assertEqual(v4c['matched_text'], 'a.is')
257 273 self.assertEqual(v4c['matches'], v5c['matches'])
258 274
259 275 def test_inspect_request(self):
260 276 msg = self.msg("inspect_request", {
261 277 'code' : 'def foo():\n'
262 278 ' apple\n'
263 279 'bar()',
264 280 'cursor_pos': 18,
265 281 'detail_level' : 1,
266 282 })
267 283 v5, v4 = self.adapt(msg)
268 284 self.assertEqual(v4['header']['msg_type'], 'object_info_request')
269 285 v4c = v4['content']
270 286 v5c = v5['content']
271 287 self.assertEqual(v4c['oname'], 'apple')
272 288 self.assertEqual(v5c['detail_level'], v4c['detail_level'])
273 289
274 290 def test_inspect_reply(self):
275 291 msg = self.msg("inspect_reply", {
276 292 'name' : 'foo',
277 293 'found' : True,
278 294 'data' : {'text/plain' : 'some text'},
279 295 'metadata' : {},
280 296 })
281 297 v5, v4 = self.adapt(msg)
282 298 self.assertEqual(v4['header']['msg_type'], 'object_info_reply')
283 299 v4c = v4['content']
284 300 v5c = v5['content']
285 301 self.assertEqual(sorted(v4c), ['found', 'name'])
286 302 self.assertEqual(v4c['found'], False)
287 303
288 304 # iopub channel
289 305
290 306 def test_display_data(self):
291 307 jsondata = dict(a=5)
292 308 msg = self.msg("display_data", {
293 309 'data' : {
294 310 'text/plain' : 'some text',
295 311 'application/json' : jsondata,
296 312 },
297 313 'metadata' : {'text/plain' : { 'key' : 'value' }},
298 314 })
299 315 v5, v4 = self.adapt(msg)
300 316 v4c = v4['content']
301 317 v5c = v5['content']
302 318 self.assertEqual(v5c['metadata'], v4c['metadata'])
303 319 self.assertEqual(v5c['data']['text/plain'], v4c['data']['text/plain'])
304 320 self.assertEqual(v4c['data']['application/json'], json.dumps(jsondata))
305 321
306 322 # stdin channel
307 323
308 324 def test_input_request(self):
309 325 msg = self.msg('input_request', {'prompt': "$>", 'password' : True})
310 326 v5, v4 = self.adapt(msg)
311 327 self.assertEqual(v5['content']['prompt'], v4['content']['prompt'])
312 328 self.assertNotIn('password', v4['content'])
313 329
314 330
General Comments 0
You need to be logged in to leave comments. Login now