##// END OF EJS Templates
Make top-level metadata dictionary not optional.
Jason Grout -
Show More
@@ -1,390 +1,390 b''
1 1 //----------------------------------------------------------------------------
2 2 // Copyright (C) 2008-2011 The IPython Development Team
3 3 //
4 4 // Distributed under the terms of the BSD License. The full license is in
5 5 // the file COPYING, distributed as part of this software.
6 6 //----------------------------------------------------------------------------
7 7
8 8 //============================================================================
9 9 // Kernel
10 10 //============================================================================
11 11
12 12 var IPython = (function (IPython) {
13 13
14 14 var utils = IPython.utils;
15 15
16 16 // Initialization and connection.
17 17
18 18 var Kernel = function (base_url) {
19 19 this.kernel_id = null;
20 20 this.shell_channel = null;
21 21 this.iopub_channel = null;
22 22 this.base_url = base_url;
23 23 this.running = false;
24 24 this.username = "username";
25 25 this.session_id = utils.uuid();
26 26 this._msg_callbacks = {};
27 27
28 28 if (typeof(WebSocket) !== 'undefined') {
29 29 this.WebSocket = WebSocket;
30 30 } else if (typeof(MozWebSocket) !== 'undefined') {
31 31 this.WebSocket = MozWebSocket;
32 32 } else {
33 33 alert('Your browser does not have WebSocket support, please try Chrome, Safari or Firefox β‰₯ 6. Firefox 4 and 5 are also supported by you have to enable WebSockets in about:config.');
34 34 };
35 35 };
36 36
37 37
38 38 Kernel.prototype._get_msg = function (msg_type, content) {
39 39 var msg = {
40 40 header : {
41 41 msg_id : utils.uuid(),
42 42 username : this.username,
43 43 session : this.session_id,
44 44 msg_type : msg_type
45 45 },
46 46 content : content,
47 47 parent_header : {}
48 48 };
49 49 return msg;
50 50 };
51 51
52 52 Kernel.prototype.start = function (notebook_id) {
53 53 var that = this;
54 54 if (!this.running) {
55 55 var qs = $.param({notebook:notebook_id});
56 56 var url = this.base_url + '?' + qs;
57 57 $.post(url,
58 58 $.proxy(that._kernel_started,that),
59 59 'json'
60 60 );
61 61 };
62 62 };
63 63
64 64
65 65 Kernel.prototype.restart = function () {
66 66 $([IPython.events]).trigger('status_restarting.Kernel');
67 67 var that = this;
68 68 if (this.running) {
69 69 this.stop_channels();
70 70 var url = this.kernel_url + "/restart";
71 71 $.post(url,
72 72 $.proxy(that._kernel_started, that),
73 73 'json'
74 74 );
75 75 };
76 76 };
77 77
78 78
79 79 Kernel.prototype._kernel_started = function (json) {
80 80 console.log("Kernel started: ", json.kernel_id);
81 81 this.running = true;
82 82 this.kernel_id = json.kernel_id;
83 83 this.ws_url = json.ws_url;
84 84 this.kernel_url = this.base_url + "/" + this.kernel_id;
85 85 this.start_channels();
86 86 this.shell_channel.onmessage = $.proxy(this._handle_shell_reply,this);
87 87 this.iopub_channel.onmessage = $.proxy(this._handle_iopub_reply,this);
88 88 };
89 89
90 90
91 91 Kernel.prototype._websocket_closed = function(ws_url, early){
92 92 var msg;
93 93 var parent_item = $('body');
94 94 if (early) {
95 95 msg = "Websocket connection to " + ws_url + " could not be established." +
96 96 " You will NOT be able to run code." +
97 97 " Your browser may not be compatible with the websocket version in the server," +
98 98 " or if the url does not look right, there could be an error in the" +
99 99 " server's configuration.";
100 100 } else {
101 101 IPython.notification_widget.set_message('Reconnecting Websockets', 1000);
102 102 this.start_channels();
103 103 return;
104 104 }
105 105 var dialog = $('<div/>');
106 106 dialog.html(msg);
107 107 parent_item.append(dialog);
108 108 dialog.dialog({
109 109 resizable: false,
110 110 modal: true,
111 111 title: "Websocket closed",
112 112 closeText: "",
113 113 close: function(event, ui) {$(this).dialog('destroy').remove();},
114 114 buttons : {
115 115 "OK": function () {
116 116 $(this).dialog('close');
117 117 }
118 118 }
119 119 });
120 120
121 121 };
122 122
123 123 Kernel.prototype.start_channels = function () {
124 124 var that = this;
125 125 this.stop_channels();
126 126 var ws_url = this.ws_url + this.kernel_url;
127 127 console.log("Starting WS:", ws_url);
128 128 this.shell_channel = new this.WebSocket(ws_url + "/shell");
129 129 this.iopub_channel = new this.WebSocket(ws_url + "/iopub");
130 130 send_cookie = function(){
131 131 this.send(document.cookie);
132 132 };
133 133 var already_called_onclose = false; // only alert once
134 134 ws_closed_early = function(evt){
135 135 if (already_called_onclose){
136 136 return;
137 137 }
138 138 already_called_onclose = true;
139 139 if ( ! evt.wasClean ){
140 140 that._websocket_closed(ws_url, true);
141 141 }
142 142 };
143 143 ws_closed_late = function(evt){
144 144 if (already_called_onclose){
145 145 return;
146 146 }
147 147 already_called_onclose = true;
148 148 if ( ! evt.wasClean ){
149 149 that._websocket_closed(ws_url, false);
150 150 }
151 151 };
152 152 this.shell_channel.onopen = send_cookie;
153 153 this.shell_channel.onclose = ws_closed_early;
154 154 this.iopub_channel.onopen = send_cookie;
155 155 this.iopub_channel.onclose = ws_closed_early;
156 156 // switch from early-close to late-close message after 1s
157 157 setTimeout(function(){
158 158 that.shell_channel.onclose = ws_closed_late;
159 159 that.iopub_channel.onclose = ws_closed_late;
160 160 }, 1000);
161 161 };
162 162
163 163
164 164 Kernel.prototype.stop_channels = function () {
165 165 if (this.shell_channel !== null) {
166 166 this.shell_channel.onclose = function (evt) {};
167 167 this.shell_channel.close();
168 168 this.shell_channel = null;
169 169 };
170 170 if (this.iopub_channel !== null) {
171 171 this.iopub_channel.onclose = function (evt) {};
172 172 this.iopub_channel.close();
173 173 this.iopub_channel = null;
174 174 };
175 175 };
176 176
177 177 // Main public methods.
178 178
179 179 Kernel.prototype.object_info_request = function (objname, callbacks) {
180 180 // When calling this method pass a callbacks structure of the form:
181 181 //
182 182 // callbacks = {
183 183 // 'object_info_reply': object_into_reply_callback
184 184 // }
185 185 //
186 186 // The object_info_reply_callback will be passed the content object of the
187 187 // object_into_reply message documented here:
188 188 //
189 189 // http://ipython.org/ipython-doc/dev/development/messaging.html#object-information
190 190 if(typeof(objname)!=null && objname!=null)
191 191 {
192 192 var content = {
193 193 oname : objname.toString(),
194 194 };
195 195 var msg = this._get_msg("object_info_request", content);
196 196 this.shell_channel.send(JSON.stringify(msg));
197 197 this.set_callbacks_for_msg(msg.header.msg_id, callbacks);
198 198 return msg.header.msg_id;
199 199 }
200 200 return;
201 201 }
202 202
203 203 Kernel.prototype.execute = function (code, callbacks, options) {
204 204 // The options object should contain the options for the execute call. Its default
205 205 // values are:
206 206 //
207 207 // options = {
208 208 // silent : true,
209 209 // user_variables : [],
210 210 // user_expressions : {},
211 211 // allow_stdin : false
212 212 // }
213 213 //
214 214 // When calling this method pass a callbacks structure of the form:
215 215 //
216 216 // callbacks = {
217 217 // 'execute_reply': execute_reply_callback,
218 218 // 'output': output_callback,
219 219 // 'clear_output': clear_output_callback,
220 220 // 'set_next_input': set_next_input_callback
221 221 // }
222 222 //
223 223 // The execute_reply_callback will be passed the content and metadata objects of the execute_reply
224 224 // message documented here:
225 225 //
226 226 // http://ipython.org/ipython-doc/dev/development/messaging.html#execute
227 227 //
228 228 // The output_callback will be passed msg_type ('stream','display_data','pyout','pyerr')
229 229 // of the output and the content and metadata objects of the PUB/SUB channel that contains the
230 230 // output:
231 231 //
232 232 // http://ipython.org/ipython-doc/dev/development/messaging.html#messages-on-the-pub-sub-socket
233 233 //
234 234 // The clear_output_callback will be passed a content object that contains
235 235 // stdout, stderr and other fields that are booleans, as well as the metadata object.
236 236 //
237 237 // The set_next_input_callback will be passed the text that should become the next
238 238 // input cell.
239 239
240 240 var content = {
241 241 code : code,
242 242 silent : true,
243 243 user_variables : [],
244 244 user_expressions : {},
245 245 allow_stdin : false
246 246 };
247 247 $.extend(true, content, options)
248 248 var msg = this._get_msg("execute_request", content);
249 249 this.shell_channel.send(JSON.stringify(msg));
250 250 this.set_callbacks_for_msg(msg.header.msg_id, callbacks);
251 251 return msg.header.msg_id;
252 252 };
253 253
254 254
255 255 Kernel.prototype.complete = function (line, cursor_pos, callbacks) {
256 256 // When calling this method pass a callbacks structure of the form:
257 257 //
258 258 // callbacks = {
259 259 // 'complete_reply': complete_reply_callback
260 260 // }
261 261 //
262 262 // The complete_reply_callback will be passed the content object of the
263 263 // complete_reply message documented here:
264 264 //
265 265 // http://ipython.org/ipython-doc/dev/development/messaging.html#complete
266 266 callbacks = callbacks || {};
267 267 var content = {
268 268 text : '',
269 269 line : line,
270 270 cursor_pos : cursor_pos
271 271 };
272 272 var msg = this._get_msg("complete_request", content);
273 273 this.shell_channel.send(JSON.stringify(msg));
274 274 this.set_callbacks_for_msg(msg.header.msg_id, callbacks);
275 275 return msg.header.msg_id;
276 276 };
277 277
278 278
279 279 Kernel.prototype.interrupt = function () {
280 280 if (this.running) {
281 281 $([IPython.events]).trigger('status_interrupting.Kernel');
282 282 $.post(this.kernel_url + "/interrupt");
283 283 };
284 284 };
285 285
286 286
287 287 Kernel.prototype.kill = function () {
288 288 if (this.running) {
289 289 this.running = false;
290 290 var settings = {
291 291 cache : false,
292 292 type : "DELETE"
293 293 };
294 294 $.ajax(this.kernel_url, settings);
295 295 };
296 296 };
297 297
298 298
299 299 // Reply handlers.
300 300
301 301 Kernel.prototype.get_callbacks_for_msg = function (msg_id) {
302 302 var callbacks = this._msg_callbacks[msg_id];
303 303 return callbacks;
304 304 };
305 305
306 306
307 307 Kernel.prototype.set_callbacks_for_msg = function (msg_id, callbacks) {
308 308 this._msg_callbacks[msg_id] = callbacks || {};
309 309 }
310 310
311 311
312 312 Kernel.prototype._handle_shell_reply = function (e) {
313 313 reply = $.parseJSON(e.data);
314 314 var header = reply.header;
315 315 var content = reply.content;
316 316 var metadata = reply.metadata;
317 317 var msg_type = header.msg_type;
318 318 var callbacks = this.get_callbacks_for_msg(reply.parent_header.msg_id);
319 319 if (callbacks !== undefined) {
320 320 var cb = callbacks[msg_type];
321 321 if (cb !== undefined) {
322 322 cb(content, metadata);
323 323 }
324 324 };
325 325
326 326 if (content.payload !== undefined) {
327 327 var payload = content.payload || [];
328 328 this._handle_payload(callbacks, payload);
329 329 }
330 330 };
331 331
332 332
333 333 Kernel.prototype._handle_payload = function (callbacks, payload) {
334 334 var l = payload.length;
335 335 // Payloads are handled by triggering events because we don't want the Kernel
336 336 // to depend on the Notebook or Pager classes.
337 337 for (var i=0; i<l; i++) {
338 338 if (payload[i].source === 'IPython.zmq.page.page') {
339 339 var data = {'text':payload[i].text}
340 340 $([IPython.events]).trigger('open_with_text.Pager', data);
341 341 } else if (payload[i].source === 'IPython.zmq.zmqshell.ZMQInteractiveShell.set_next_input') {
342 342 if (callbacks.set_next_input !== undefined) {
343 343 callbacks.set_next_input(payload[i].text)
344 344 }
345 345 }
346 346 };
347 347 };
348 348
349 349
350 350 Kernel.prototype._handle_iopub_reply = function (e) {
351 351 var reply = $.parseJSON(e.data);
352 352 var content = reply.content;
353 353 var msg_type = reply.header.msg_type;
354 var metadata = reply.metadata || {};
354 var metadata = reply.metadata;
355 355 var callbacks = this.get_callbacks_for_msg(reply.parent_header.msg_id);
356 356 if (msg_type !== 'status' && callbacks === undefined) {
357 357 // Message not from one of this notebook's cells and there are no
358 358 // callbacks to handle it.
359 359 return;
360 360 }
361 361 var output_types = ['stream','display_data','pyout','pyerr'];
362 362 if (output_types.indexOf(msg_type) >= 0) {
363 363 var cb = callbacks['output'];
364 364 if (cb !== undefined) {
365 365 cb(msg_type, content, metadata);
366 366 }
367 367 } else if (msg_type === 'status') {
368 368 if (content.execution_state === 'busy') {
369 369 $([IPython.events]).trigger('status_busy.Kernel');
370 370 } else if (content.execution_state === 'idle') {
371 371 $([IPython.events]).trigger('status_idle.Kernel');
372 372 } else if (content.execution_state === 'dead') {
373 373 this.stop_channels();
374 374 $([IPython.events]).trigger('status_dead.Kernel');
375 375 };
376 376 } else if (msg_type === 'clear_output') {
377 377 var cb = callbacks['clear_output'];
378 378 if (cb !== undefined) {
379 379 cb(content, metadata);
380 380 }
381 381 };
382 382 };
383 383
384 384
385 385 IPython.Kernel = Kernel;
386 386
387 387 return IPython;
388 388
389 389 }(IPython));
390 390
@@ -1,767 +1,767 b''
1 1 """Session object for building, serializing, sending, and receiving messages in
2 2 IPython. The Session object supports serialization, HMAC signatures, and
3 3 metadata on messages.
4 4
5 5 Also defined here are utilities for working with Sessions:
6 6 * A SessionFactory to be used as a base class for configurables that work with
7 7 Sessions.
8 8 * A Message object for convenience that allows attribute-access to the msg dict.
9 9
10 10 Authors:
11 11
12 12 * Min RK
13 13 * Brian Granger
14 14 * Fernando Perez
15 15 """
16 16 #-----------------------------------------------------------------------------
17 17 # Copyright (C) 2010-2011 The IPython Development Team
18 18 #
19 19 # Distributed under the terms of the BSD License. The full license is in
20 20 # the file COPYING, distributed as part of this software.
21 21 #-----------------------------------------------------------------------------
22 22
23 23 #-----------------------------------------------------------------------------
24 24 # Imports
25 25 #-----------------------------------------------------------------------------
26 26
27 27 import hmac
28 28 import logging
29 29 import os
30 30 import pprint
31 31 import uuid
32 32 from datetime import datetime
33 33
34 34 try:
35 35 import cPickle
36 36 pickle = cPickle
37 37 except:
38 38 cPickle = None
39 39 import pickle
40 40
41 41 import zmq
42 42 from zmq.utils import jsonapi
43 43 from zmq.eventloop.ioloop import IOLoop
44 44 from zmq.eventloop.zmqstream import ZMQStream
45 45
46 46 from IPython.config.application import Application, boolean_flag
47 47 from IPython.config.configurable import Configurable, LoggingConfigurable
48 48 from IPython.utils.importstring import import_item
49 49 from IPython.utils.jsonutil import extract_dates, squash_dates, date_default
50 50 from IPython.utils.py3compat import str_to_bytes
51 51 from IPython.utils.traitlets import (CBytes, Unicode, Bool, Any, Instance, Set,
52 52 DottedObjectName, CUnicode, Dict)
53 53
54 54 #-----------------------------------------------------------------------------
55 55 # utility functions
56 56 #-----------------------------------------------------------------------------
57 57
58 58 def squash_unicode(obj):
59 59 """coerce unicode back to bytestrings."""
60 60 if isinstance(obj,dict):
61 61 for key in obj.keys():
62 62 obj[key] = squash_unicode(obj[key])
63 63 if isinstance(key, unicode):
64 64 obj[squash_unicode(key)] = obj.pop(key)
65 65 elif isinstance(obj, list):
66 66 for i,v in enumerate(obj):
67 67 obj[i] = squash_unicode(v)
68 68 elif isinstance(obj, unicode):
69 69 obj = obj.encode('utf8')
70 70 return obj
71 71
72 72 #-----------------------------------------------------------------------------
73 73 # globals and defaults
74 74 #-----------------------------------------------------------------------------
75 75
76 76
77 77 # ISO8601-ify datetime objects
78 78 json_packer = lambda obj: jsonapi.dumps(obj, default=date_default)
79 79 json_unpacker = lambda s: extract_dates(jsonapi.loads(s))
80 80
81 81 pickle_packer = lambda o: pickle.dumps(o,-1)
82 82 pickle_unpacker = pickle.loads
83 83
84 84 default_packer = json_packer
85 85 default_unpacker = json_unpacker
86 86
87 87 DELIM=b"<IDS|MSG>"
88 88
89 89
90 90 #-----------------------------------------------------------------------------
91 91 # Mixin tools for apps that use Sessions
92 92 #-----------------------------------------------------------------------------
93 93
94 94 session_aliases = dict(
95 95 ident = 'Session.session',
96 96 user = 'Session.username',
97 97 keyfile = 'Session.keyfile',
98 98 )
99 99
100 100 session_flags = {
101 101 'secure' : ({'Session' : { 'key' : str_to_bytes(str(uuid.uuid4())),
102 102 'keyfile' : '' }},
103 103 """Use HMAC digests for authentication of messages.
104 104 Setting this flag will generate a new UUID to use as the HMAC key.
105 105 """),
106 106 'no-secure' : ({'Session' : { 'key' : b'', 'keyfile' : '' }},
107 107 """Don't authenticate messages."""),
108 108 }
109 109
110 110 def default_secure(cfg):
111 111 """Set the default behavior for a config environment to be secure.
112 112
113 113 If Session.key/keyfile have not been set, set Session.key to
114 114 a new random UUID.
115 115 """
116 116
117 117 if 'Session' in cfg:
118 118 if 'key' in cfg.Session or 'keyfile' in cfg.Session:
119 119 return
120 120 # key/keyfile not specified, generate new UUID:
121 121 cfg.Session.key = str_to_bytes(str(uuid.uuid4()))
122 122
123 123
124 124 #-----------------------------------------------------------------------------
125 125 # Classes
126 126 #-----------------------------------------------------------------------------
127 127
128 128 class SessionFactory(LoggingConfigurable):
129 129 """The Base class for configurables that have a Session, Context, logger,
130 130 and IOLoop.
131 131 """
132 132
133 133 logname = Unicode('')
134 134 def _logname_changed(self, name, old, new):
135 135 self.log = logging.getLogger(new)
136 136
137 137 # not configurable:
138 138 context = Instance('zmq.Context')
139 139 def _context_default(self):
140 140 return zmq.Context.instance()
141 141
142 142 session = Instance('IPython.zmq.session.Session')
143 143
144 144 loop = Instance('zmq.eventloop.ioloop.IOLoop', allow_none=False)
145 145 def _loop_default(self):
146 146 return IOLoop.instance()
147 147
148 148 def __init__(self, **kwargs):
149 149 super(SessionFactory, self).__init__(**kwargs)
150 150
151 151 if self.session is None:
152 152 # construct the session
153 153 self.session = Session(**kwargs)
154 154
155 155
156 156 class Message(object):
157 157 """A simple message object that maps dict keys to attributes.
158 158
159 159 A Message can be created from a dict and a dict from a Message instance
160 160 simply by calling dict(msg_obj)."""
161 161
162 162 def __init__(self, msg_dict):
163 163 dct = self.__dict__
164 164 for k, v in dict(msg_dict).iteritems():
165 165 if isinstance(v, dict):
166 166 v = Message(v)
167 167 dct[k] = v
168 168
169 169 # Having this iterator lets dict(msg_obj) work out of the box.
170 170 def __iter__(self):
171 171 return iter(self.__dict__.iteritems())
172 172
173 173 def __repr__(self):
174 174 return repr(self.__dict__)
175 175
176 176 def __str__(self):
177 177 return pprint.pformat(self.__dict__)
178 178
179 179 def __contains__(self, k):
180 180 return k in self.__dict__
181 181
182 182 def __getitem__(self, k):
183 183 return self.__dict__[k]
184 184
185 185
186 186 def msg_header(msg_id, msg_type, username, session):
187 187 date = datetime.now()
188 188 return locals()
189 189
190 190 def extract_header(msg_or_header):
191 191 """Given a message or header, return the header."""
192 192 if not msg_or_header:
193 193 return {}
194 194 try:
195 195 # See if msg_or_header is the entire message.
196 196 h = msg_or_header['header']
197 197 except KeyError:
198 198 try:
199 199 # See if msg_or_header is just the header
200 200 h = msg_or_header['msg_id']
201 201 except KeyError:
202 202 raise
203 203 else:
204 204 h = msg_or_header
205 205 if not isinstance(h, dict):
206 206 h = dict(h)
207 207 return h
208 208
209 209 class Session(Configurable):
210 210 """Object for handling serialization and sending of messages.
211 211
212 212 The Session object handles building messages and sending them
213 213 with ZMQ sockets or ZMQStream objects. Objects can communicate with each
214 214 other over the network via Session objects, and only need to work with the
215 215 dict-based IPython message spec. The Session will handle
216 216 serialization/deserialization, security, and metadata.
217 217
218 218 Sessions support configurable serialiization via packer/unpacker traits,
219 219 and signing with HMAC digests via the key/keyfile traits.
220 220
221 221 Parameters
222 222 ----------
223 223
224 224 debug : bool
225 225 whether to trigger extra debugging statements
226 226 packer/unpacker : str : 'json', 'pickle' or import_string
227 227 importstrings for methods to serialize message parts. If just
228 228 'json' or 'pickle', predefined JSON and pickle packers will be used.
229 229 Otherwise, the entire importstring must be used.
230 230
231 231 The functions must accept at least valid JSON input, and output *bytes*.
232 232
233 233 For example, to use msgpack:
234 234 packer = 'msgpack.packb', unpacker='msgpack.unpackb'
235 235 pack/unpack : callables
236 236 You can also set the pack/unpack callables for serialization directly.
237 237 session : bytes
238 238 the ID of this Session object. The default is to generate a new UUID.
239 239 username : unicode
240 240 username added to message headers. The default is to ask the OS.
241 241 key : bytes
242 242 The key used to initialize an HMAC signature. If unset, messages
243 243 will not be signed or checked.
244 244 keyfile : filepath
245 245 The file containing a key. If this is set, `key` will be initialized
246 246 to the contents of the file.
247 247
248 248 """
249 249
250 250 debug=Bool(False, config=True, help="""Debug output in the Session""")
251 251
252 252 packer = DottedObjectName('json',config=True,
253 253 help="""The name of the packer for serializing messages.
254 254 Should be one of 'json', 'pickle', or an import name
255 255 for a custom callable serializer.""")
256 256 def _packer_changed(self, name, old, new):
257 257 if new.lower() == 'json':
258 258 self.pack = json_packer
259 259 self.unpack = json_unpacker
260 260 elif new.lower() == 'pickle':
261 261 self.pack = pickle_packer
262 262 self.unpack = pickle_unpacker
263 263 else:
264 264 self.pack = import_item(str(new))
265 265
266 266 unpacker = DottedObjectName('json', config=True,
267 267 help="""The name of the unpacker for unserializing messages.
268 268 Only used with custom functions for `packer`.""")
269 269 def _unpacker_changed(self, name, old, new):
270 270 if new.lower() == 'json':
271 271 self.pack = json_packer
272 272 self.unpack = json_unpacker
273 273 elif new.lower() == 'pickle':
274 274 self.pack = pickle_packer
275 275 self.unpack = pickle_unpacker
276 276 else:
277 277 self.unpack = import_item(str(new))
278 278
279 279 session = CUnicode(u'', config=True,
280 280 help="""The UUID identifying this session.""")
281 281 def _session_default(self):
282 282 u = unicode(uuid.uuid4())
283 283 self.bsession = u.encode('ascii')
284 284 return u
285 285
286 286 def _session_changed(self, name, old, new):
287 287 self.bsession = self.session.encode('ascii')
288 288
289 289 # bsession is the session as bytes
290 290 bsession = CBytes(b'')
291 291
292 292 username = Unicode(os.environ.get('USER',u'username'), config=True,
293 293 help="""Username for the Session. Default is your system username.""")
294 294
295 295 metadata = Dict({}, config=True,
296 296 help="""Metadata dictionary, which serves as the default top-level metadata dict for each message.""")
297 297
298 298 # message signature related traits:
299 299
300 300 key = CBytes(b'', config=True,
301 301 help="""execution key, for extra authentication.""")
302 302 def _key_changed(self, name, old, new):
303 303 if new:
304 304 self.auth = hmac.HMAC(new)
305 305 else:
306 306 self.auth = None
307 307 auth = Instance(hmac.HMAC)
308 308 digest_history = Set()
309 309
310 310 keyfile = Unicode('', config=True,
311 311 help="""path to file containing execution key.""")
312 312 def _keyfile_changed(self, name, old, new):
313 313 with open(new, 'rb') as f:
314 314 self.key = f.read().strip()
315 315
316 316 # serialization traits:
317 317
318 318 pack = Any(default_packer) # the actual packer function
319 319 def _pack_changed(self, name, old, new):
320 320 if not callable(new):
321 321 raise TypeError("packer must be callable, not %s"%type(new))
322 322
323 323 unpack = Any(default_unpacker) # the actual packer function
324 324 def _unpack_changed(self, name, old, new):
325 325 # unpacker is not checked - it is assumed to be
326 326 if not callable(new):
327 327 raise TypeError("unpacker must be callable, not %s"%type(new))
328 328
329 329 def __init__(self, **kwargs):
330 330 """create a Session object
331 331
332 332 Parameters
333 333 ----------
334 334
335 335 debug : bool
336 336 whether to trigger extra debugging statements
337 337 packer/unpacker : str : 'json', 'pickle' or import_string
338 338 importstrings for methods to serialize message parts. If just
339 339 'json' or 'pickle', predefined JSON and pickle packers will be used.
340 340 Otherwise, the entire importstring must be used.
341 341
342 342 The functions must accept at least valid JSON input, and output
343 343 *bytes*.
344 344
345 345 For example, to use msgpack:
346 346 packer = 'msgpack.packb', unpacker='msgpack.unpackb'
347 347 pack/unpack : callables
348 348 You can also set the pack/unpack callables for serialization
349 349 directly.
350 350 session : unicode (must be ascii)
351 351 the ID of this Session object. The default is to generate a new
352 352 UUID.
353 353 bsession : bytes
354 354 The session as bytes
355 355 username : unicode
356 356 username added to message headers. The default is to ask the OS.
357 357 key : bytes
358 358 The key used to initialize an HMAC signature. If unset, messages
359 359 will not be signed or checked.
360 360 keyfile : filepath
361 361 The file containing a key. If this is set, `key` will be
362 362 initialized to the contents of the file.
363 363 """
364 364 super(Session, self).__init__(**kwargs)
365 365 self._check_packers()
366 366 self.none = self.pack({})
367 367 # ensure self._session_default() if necessary, so bsession is defined:
368 368 self.session
369 369
370 370 @property
371 371 def msg_id(self):
372 372 """always return new uuid"""
373 373 return str(uuid.uuid4())
374 374
375 375 def _check_packers(self):
376 376 """check packers for binary data and datetime support."""
377 377 pack = self.pack
378 378 unpack = self.unpack
379 379
380 380 # check simple serialization
381 381 msg = dict(a=[1,'hi'])
382 382 try:
383 383 packed = pack(msg)
384 384 except Exception:
385 385 raise ValueError("packer could not serialize a simple message")
386 386
387 387 # ensure packed message is bytes
388 388 if not isinstance(packed, bytes):
389 389 raise ValueError("message packed to %r, but bytes are required"%type(packed))
390 390
391 391 # check that unpack is pack's inverse
392 392 try:
393 393 unpacked = unpack(packed)
394 394 except Exception:
395 395 raise ValueError("unpacker could not handle the packer's output")
396 396
397 397 # check datetime support
398 398 msg = dict(t=datetime.now())
399 399 try:
400 400 unpacked = unpack(pack(msg))
401 401 except Exception:
402 402 self.pack = lambda o: pack(squash_dates(o))
403 403 self.unpack = lambda s: extract_dates(unpack(s))
404 404
405 405 def msg_header(self, msg_type):
406 406 return msg_header(self.msg_id, msg_type, self.username, self.session)
407 407
408 408 def msg(self, msg_type, content=None, parent=None, subheader=None, header=None, metadata=None):
409 409 """Return the nested message dict.
410 410
411 411 This format is different from what is sent over the wire. The
412 412 serialize/unserialize methods converts this nested message dict to the wire
413 413 format, which is a list of message parts.
414 414 """
415 415 msg = {}
416 416 header = self.msg_header(msg_type) if header is None else header
417 417 msg['header'] = header
418 418 if subheader is not None:
419 419 msg['header'].update(subheader)
420 420 msg['msg_id'] = header['msg_id']
421 421 msg['msg_type'] = header['msg_type']
422 422 msg['parent_header'] = {} if parent is None else extract_header(parent)
423 423 msg['content'] = {} if content is None else content
424 metadata_dict = self.metadata.copy()
424 msg['metadata'] = self.metadata.copy()
425 425 if metadata is not None:
426 metadata_dict.update(metadata)
427 if metadata_dict:
428 msg['metadata'] = metadata_dict
426 msg['metadata'].update(metadata)
429 427 return msg
430 428
431 429 def sign(self, msg_list):
432 430 """Sign a message with HMAC digest. If no auth, return b''.
433 431
434 432 Parameters
435 433 ----------
436 434 msg_list : list
437 435 The [p_header,p_parent,p_content] part of the message list.
438 436 """
439 437 if self.auth is None:
440 438 return b''
441 439 h = self.auth.copy()
442 440 for m in msg_list:
443 441 h.update(m)
444 442 return str_to_bytes(h.hexdigest())
445 443
446 444 def serialize(self, msg, ident=None):
447 445 """Serialize the message components to bytes.
448 446
449 447 This is roughly the inverse of unserialize. The serialize/unserialize
450 448 methods work with full message lists, whereas pack/unpack work with
451 449 the individual message parts in the message list.
452 450
453 451 Parameters
454 452 ----------
455 453 msg : dict or Message
456 454 The nexted message dict as returned by the self.msg method.
457 455
458 456 Returns
459 457 -------
460 458 msg_list : list
461 459 The list of bytes objects to be sent with the format:
462 [ident1,ident2,...,DELIM,HMAC,p_header,p_parent,p_content,
460 [ident1,ident2,...,DELIM,HMAC,p_header,p_parent,p_metadata,p_content,
463 461 buffer1,buffer2,...]. In this list, the p_* entities are
464 462 the packed or serialized versions, so if JSON is used, these
465 463 are utf8 encoded JSON strings.
466 464 """
467 465 content = msg.get('content', {})
468 466 if content is None:
469 467 content = self.none
470 468 elif isinstance(content, dict):
471 469 content = self.pack(content)
472 470 elif isinstance(content, bytes):
473 471 # content is already packed, as in a relayed message
474 472 pass
475 473 elif isinstance(content, unicode):
476 474 # should be bytes, but JSON often spits out unicode
477 475 content = content.encode('utf8')
478 476 else:
479 477 raise TypeError("Content incorrect type: %s"%type(content))
480 478
481 479 real_message = [self.pack(msg['header']),
482 480 self.pack(msg['parent_header']),
483 content
481 self.pack(msg['metadata']),
482 content,
484 483 ]
485 484
486 485 to_send = []
487 486
488 487 if isinstance(ident, list):
489 488 # accept list of idents
490 489 to_send.extend(ident)
491 490 elif ident is not None:
492 491 to_send.append(ident)
493 492 to_send.append(DELIM)
494 493
495 494 signature = self.sign(real_message)
496 495 to_send.append(signature)
497 496
498 497 to_send.extend(real_message)
499 498
500 499 return to_send
501 500
502 501 def send(self, stream, msg_or_type, content=None, parent=None, ident=None,
503 502 buffers=None, subheader=None, track=False, header=None, metadata=None):
504 503 """Build and send a message via stream or socket.
505 504
506 505 The message format used by this function internally is as follows:
507 506
508 507 [ident1,ident2,...,DELIM,HMAC,p_header,p_parent,p_content,
509 508 buffer1,buffer2,...]
510 509
511 510 The serialize/unserialize methods convert the nested message dict into this
512 511 format.
513 512
514 513 Parameters
515 514 ----------
516 515
517 516 stream : zmq.Socket or ZMQStream
518 517 The socket-like object used to send the data.
519 518 msg_or_type : str or Message/dict
520 519 Normally, msg_or_type will be a msg_type unless a message is being
521 520 sent more than once. If a header is supplied, this can be set to
522 521 None and the msg_type will be pulled from the header.
523 522
524 523 content : dict or None
525 524 The content of the message (ignored if msg_or_type is a message).
526 525 header : dict or None
527 526 The header dict for the message (ignored if msg_to_type is a message).
528 527 parent : Message or dict or None
529 528 The parent or parent header describing the parent of this message
530 529 (ignored if msg_or_type is a message).
531 530 ident : bytes or list of bytes
532 531 The zmq.IDENTITY routing path.
533 532 subheader : dict or None
534 533 Extra header keys for this message's header (ignored if msg_or_type
535 534 is a message).
536 535 metadata : dict or None
537 536 The metadata describing the message
538 537 buffers : list or None
539 538 The already-serialized buffers to be appended to the message.
540 539 track : bool
541 540 Whether to track. Only for use with Sockets, because ZMQStream
542 541 objects cannot track messages.
543 542
544 543
545 544 Returns
546 545 -------
547 546 msg : dict
548 547 The constructed message.
549 548 (msg,tracker) : (dict, MessageTracker)
550 549 if track=True, then a 2-tuple will be returned,
551 550 the first element being the constructed
552 551 message, and the second being the MessageTracker
553 552
554 553 """
555 554
556 555 if not isinstance(stream, (zmq.Socket, ZMQStream)):
557 556 raise TypeError("stream must be Socket or ZMQStream, not %r"%type(stream))
558 557 elif track and isinstance(stream, ZMQStream):
559 558 raise TypeError("ZMQStream cannot track messages")
560 559
561 560 if isinstance(msg_or_type, (Message, dict)):
562 561 # We got a Message or message dict, not a msg_type so don't
563 562 # build a new Message.
564 563 msg = msg_or_type
565 564 else:
566 565 msg = self.msg(msg_or_type, content=content, parent=parent,
567 566 subheader=subheader, header=header, metadata=metadata)
568 567
569 568 buffers = [] if buffers is None else buffers
570 569 to_send = self.serialize(msg, ident)
571 570 flag = 0
572 571 if buffers:
573 572 flag = zmq.SNDMORE
574 573 _track = False
575 574 else:
576 575 _track=track
577 576 if track:
578 577 tracker = stream.send_multipart(to_send, flag, copy=False, track=_track)
579 578 else:
580 579 tracker = stream.send_multipart(to_send, flag, copy=False)
581 580 for b in buffers[:-1]:
582 581 stream.send(b, flag, copy=False)
583 582 if buffers:
584 583 if track:
585 584 tracker = stream.send(buffers[-1], copy=False, track=track)
586 585 else:
587 586 tracker = stream.send(buffers[-1], copy=False)
588 587
589 588 # omsg = Message(msg)
590 589 if self.debug:
591 590 pprint.pprint(msg)
592 591 pprint.pprint(to_send)
593 592 pprint.pprint(buffers)
594 593
595 594 msg['tracker'] = tracker
596 595
597 596 return msg
598 597
599 598 def send_raw(self, stream, msg_list, flags=0, copy=True, ident=None):
600 599 """Send a raw message via ident path.
601 600
602 601 This method is used to send a already serialized message.
603 602
604 603 Parameters
605 604 ----------
606 605 stream : ZMQStream or Socket
607 606 The ZMQ stream or socket to use for sending the message.
608 607 msg_list : list
609 608 The serialized list of messages to send. This only includes the
610 [p_header,p_parent,p_content,buffer1,buffer2,...] portion of
609 [p_header,p_parent,p_metadata,p_content,buffer1,buffer2,...] portion of
611 610 the message.
612 611 ident : ident or list
613 612 A single ident or a list of idents to use in sending.
614 613 """
615 614 to_send = []
616 615 if isinstance(ident, bytes):
617 616 ident = [ident]
618 617 if ident is not None:
619 618 to_send.extend(ident)
620 619
621 620 to_send.append(DELIM)
622 621 to_send.append(self.sign(msg_list))
623 622 to_send.extend(msg_list)
624 623 stream.send_multipart(msg_list, flags, copy=copy)
625 624
626 625 def recv(self, socket, mode=zmq.NOBLOCK, content=True, copy=True):
627 626 """Receive and unpack a message.
628 627
629 628 Parameters
630 629 ----------
631 630 socket : ZMQStream or Socket
632 631 The socket or stream to use in receiving.
633 632
634 633 Returns
635 634 -------
636 635 [idents], msg
637 636 [idents] is a list of idents and msg is a nested message dict of
638 637 same format as self.msg returns.
639 638 """
640 639 if isinstance(socket, ZMQStream):
641 640 socket = socket.socket
642 641 try:
643 642 msg_list = socket.recv_multipart(mode, copy=copy)
644 643 except zmq.ZMQError as e:
645 644 if e.errno == zmq.EAGAIN:
646 645 # We can convert EAGAIN to None as we know in this case
647 646 # recv_multipart won't return None.
648 647 return None,None
649 648 else:
650 649 raise
651 650 # split multipart message into identity list and message dict
652 651 # invalid large messages can cause very expensive string comparisons
653 652 idents, msg_list = self.feed_identities(msg_list, copy)
654 653 try:
655 654 return idents, self.unserialize(msg_list, content=content, copy=copy)
656 655 except Exception as e:
657 656 # TODO: handle it
658 657 raise e
659 658
660 659 def feed_identities(self, msg_list, copy=True):
661 660 """Split the identities from the rest of the message.
662 661
663 662 Feed until DELIM is reached, then return the prefix as idents and
664 663 remainder as msg_list. This is easily broken by setting an IDENT to DELIM,
665 664 but that would be silly.
666 665
667 666 Parameters
668 667 ----------
669 668 msg_list : a list of Message or bytes objects
670 669 The message to be split.
671 670 copy : bool
672 671 flag determining whether the arguments are bytes or Messages
673 672
674 673 Returns
675 674 -------
676 675 (idents, msg_list) : two lists
677 676 idents will always be a list of bytes, each of which is a ZMQ
678 677 identity. msg_list will be a list of bytes or zmq.Messages of the
679 678 form [HMAC,p_header,p_parent,p_content,buffer1,buffer2,...] and
680 679 should be unpackable/unserializable via self.unserialize at this
681 680 point.
682 681 """
683 682 if copy:
684 683 idx = msg_list.index(DELIM)
685 684 return msg_list[:idx], msg_list[idx+1:]
686 685 else:
687 686 failed = True
688 687 for idx,m in enumerate(msg_list):
689 688 if m.bytes == DELIM:
690 689 failed = False
691 690 break
692 691 if failed:
693 692 raise ValueError("DELIM not in msg_list")
694 693 idents, msg_list = msg_list[:idx], msg_list[idx+1:]
695 694 return [m.bytes for m in idents], msg_list
696 695
697 696 def unserialize(self, msg_list, content=True, copy=True):
698 697 """Unserialize a msg_list to a nested message dict.
699 698
700 699 This is roughly the inverse of serialize. The serialize/unserialize
701 700 methods work with full message lists, whereas pack/unpack work with
702 701 the individual message parts in the message list.
703 702
704 703 Parameters:
705 704 -----------
706 705 msg_list : list of bytes or Message objects
707 706 The list of message parts of the form [HMAC,p_header,p_parent,
708 p_content,buffer1,buffer2,...].
707 p_metadata,p_content,buffer1,buffer2,...].
709 708 content : bool (True)
710 709 Whether to unpack the content dict (True), or leave it packed
711 710 (False).
712 711 copy : bool (True)
713 712 Whether to return the bytes (True), or the non-copying Message
714 713 object in each place (False).
715 714
716 715 Returns
717 716 -------
718 717 msg : dict
719 718 The nested message dict with top-level keys [header, parent_header,
720 719 content, buffers].
721 720 """
722 minlen = 4
721 minlen = 5
723 722 message = {}
724 723 if not copy:
725 724 for i in range(minlen):
726 725 msg_list[i] = msg_list[i].bytes
727 726 if self.auth is not None:
728 727 signature = msg_list[0]
729 728 if not signature:
730 729 raise ValueError("Unsigned Message")
731 730 if signature in self.digest_history:
732 731 raise ValueError("Duplicate Signature: %r"%signature)
733 732 self.digest_history.add(signature)
734 733 check = self.sign(msg_list[1:4])
735 734 if not signature == check:
736 735 raise ValueError("Invalid Signature: %r"%signature)
737 736 if not len(msg_list) >= minlen:
738 737 raise TypeError("malformed message, must have at least %i elements"%minlen)
739 738 header = self.unpack(msg_list[1])
740 739 message['header'] = header
741 740 message['msg_id'] = header['msg_id']
742 741 message['msg_type'] = header['msg_type']
743 742 message['parent_header'] = self.unpack(msg_list[2])
743 message['metadata'] = self.unpack(msg_list[3])
744 744 if content:
745 message['content'] = self.unpack(msg_list[3])
745 message['content'] = self.unpack(msg_list[4])
746 746 else:
747 message['content'] = msg_list[3]
747 message['content'] = msg_list[4]
748 748
749 message['buffers'] = msg_list[4:]
749 message['buffers'] = msg_list[5:]
750 750 return message
751 751
752 752 def test_msg2obj():
753 753 am = dict(x=1)
754 754 ao = Message(am)
755 755 assert ao.x == am['x']
756 756
757 757 am['y'] = dict(z=1)
758 758 ao = Message(am)
759 759 assert ao.y.z == am['y']['z']
760 760
761 761 k1, k2 = 'y', 'z'
762 762 assert ao[k1][k2] == am[k1][k2]
763 763
764 764 am2 = dict(ao)
765 765 assert am['x'] == am2['x']
766 766 assert am['y']['z'] == am2['y']['z']
767 767
@@ -1,212 +1,218 b''
1 1 """test building messages with streamsession"""
2 2
3 3 #-------------------------------------------------------------------------------
4 4 # Copyright (C) 2011 The IPython Development Team
5 5 #
6 6 # Distributed under the terms of the BSD License. The full license is in
7 7 # the file COPYING, distributed as part of this software.
8 8 #-------------------------------------------------------------------------------
9 9
10 10 #-------------------------------------------------------------------------------
11 11 # Imports
12 12 #-------------------------------------------------------------------------------
13 13
14 14 import os
15 15 import uuid
16 16 import zmq
17 17
18 18 from zmq.tests import BaseZMQTestCase
19 19 from zmq.eventloop.zmqstream import ZMQStream
20 20
21 21 from IPython.zmq import session as ss
22 22
23 23 class SessionTestCase(BaseZMQTestCase):
24 24
25 25 def setUp(self):
26 26 BaseZMQTestCase.setUp(self)
27 27 self.session = ss.Session()
28 28
29 29
30 30 class MockSocket(zmq.Socket):
31 31
32 32 def __init__(self, *args, **kwargs):
33 33 super(MockSocket,self).__init__(*args,**kwargs)
34 34 self.data = []
35 35
36 36 def send_multipart(self, msgparts, *args, **kwargs):
37 37 self.data.extend(msgparts)
38 38
39 39 def send(self, part, *args, **kwargs):
40 40 self.data.append(part)
41 41
42 42 def recv_multipart(self, *args, **kwargs):
43 43 return self.data
44 44
45 45 class TestSession(SessionTestCase):
46 46
47 47 def test_msg(self):
48 48 """message format"""
49 49 msg = self.session.msg('execute')
50 thekeys = set('header parent_header content msg_type msg_id'.split())
50 thekeys = set('header parent_header metadata content msg_type msg_id'.split())
51 51 s = set(msg.keys())
52 52 self.assertEqual(s, thekeys)
53 53 self.assertTrue(isinstance(msg['content'],dict))
54 self.assertTrue(isinstance(msg['metadata'],dict))
54 55 self.assertTrue(isinstance(msg['header'],dict))
55 56 self.assertTrue(isinstance(msg['parent_header'],dict))
56 57 self.assertTrue(isinstance(msg['msg_id'],str))
57 58 self.assertTrue(isinstance(msg['msg_type'],str))
58 59 self.assertEqual(msg['header']['msg_type'], 'execute')
59 60 self.assertEqual(msg['msg_type'], 'execute')
60 61
61 62 def test_serialize(self):
62 63 msg = self.session.msg('execute', content=dict(a=10, b=1.1))
63 64 msg_list = self.session.serialize(msg, ident=b'foo')
64 65 ident, msg_list = self.session.feed_identities(msg_list)
65 66 new_msg = self.session.unserialize(msg_list)
66 67 self.assertEqual(ident[0], b'foo')
67 68 self.assertEqual(new_msg['msg_id'],msg['msg_id'])
68 69 self.assertEqual(new_msg['msg_type'],msg['msg_type'])
69 70 self.assertEqual(new_msg['header'],msg['header'])
70 71 self.assertEqual(new_msg['content'],msg['content'])
71 72 self.assertEqual(new_msg['parent_header'],msg['parent_header'])
73 self.assertEqual(new_msg['metadata'],msg['metadata'])
72 74 # ensure floats don't come out as Decimal:
73 75 self.assertEqual(type(new_msg['content']['b']),type(new_msg['content']['b']))
74 76
75 77 def test_send(self):
76 78 socket = MockSocket(zmq.Context.instance(),zmq.PAIR)
77 79
78 80 msg = self.session.msg('execute', content=dict(a=10))
79 81 self.session.send(socket, msg, ident=b'foo', buffers=[b'bar'])
80 82 ident, msg_list = self.session.feed_identities(socket.data)
81 83 new_msg = self.session.unserialize(msg_list)
82 84 self.assertEqual(ident[0], b'foo')
83 85 self.assertEqual(new_msg['msg_id'],msg['msg_id'])
84 86 self.assertEqual(new_msg['msg_type'],msg['msg_type'])
85 87 self.assertEqual(new_msg['header'],msg['header'])
86 88 self.assertEqual(new_msg['content'],msg['content'])
87 89 self.assertEqual(new_msg['parent_header'],msg['parent_header'])
90 self.assertEqual(new_msg['metadata'],msg['metadata'])
88 91 self.assertEqual(new_msg['buffers'],[b'bar'])
89 92
90 93 socket.data = []
91 94
92 95 content = msg['content']
93 96 header = msg['header']
94 97 parent = msg['parent_header']
98 metadata = msg['metadata']
95 99 msg_type = header['msg_type']
96 100 self.session.send(socket, None, content=content, parent=parent,
97 header=header, ident=b'foo', buffers=[b'bar'])
101 header=header, metadata=metadata, ident=b'foo', buffers=[b'bar'])
98 102 ident, msg_list = self.session.feed_identities(socket.data)
99 103 new_msg = self.session.unserialize(msg_list)
100 104 self.assertEqual(ident[0], b'foo')
101 105 self.assertEqual(new_msg['msg_id'],msg['msg_id'])
102 106 self.assertEqual(new_msg['msg_type'],msg['msg_type'])
103 107 self.assertEqual(new_msg['header'],msg['header'])
104 108 self.assertEqual(new_msg['content'],msg['content'])
109 self.assertEqual(new_msg['metadata'],msg['metadata'])
105 110 self.assertEqual(new_msg['parent_header'],msg['parent_header'])
106 111 self.assertEqual(new_msg['buffers'],[b'bar'])
107 112
108 113 socket.data = []
109 114
110 115 self.session.send(socket, msg, ident=b'foo', buffers=[b'bar'])
111 116 ident, new_msg = self.session.recv(socket)
112 117 self.assertEqual(ident[0], b'foo')
113 118 self.assertEqual(new_msg['msg_id'],msg['msg_id'])
114 119 self.assertEqual(new_msg['msg_type'],msg['msg_type'])
115 120 self.assertEqual(new_msg['header'],msg['header'])
116 121 self.assertEqual(new_msg['content'],msg['content'])
122 self.assertEqual(new_msg['metadata'],msg['metadata'])
117 123 self.assertEqual(new_msg['parent_header'],msg['parent_header'])
118 124 self.assertEqual(new_msg['buffers'],[b'bar'])
119 125
120 126 socket.close()
121 127
122 128 def test_args(self):
123 129 """initialization arguments for Session"""
124 130 s = self.session
125 131 self.assertTrue(s.pack is ss.default_packer)
126 132 self.assertTrue(s.unpack is ss.default_unpacker)
127 133 self.assertEqual(s.username, os.environ.get('USER', u'username'))
128 134
129 135 s = ss.Session()
130 136 self.assertEqual(s.username, os.environ.get('USER', u'username'))
131 137
132 138 self.assertRaises(TypeError, ss.Session, pack='hi')
133 139 self.assertRaises(TypeError, ss.Session, unpack='hi')
134 140 u = str(uuid.uuid4())
135 141 s = ss.Session(username=u'carrot', session=u)
136 142 self.assertEqual(s.session, u)
137 143 self.assertEqual(s.username, u'carrot')
138 144
139 145 def test_tracking(self):
140 146 """test tracking messages"""
141 147 a,b = self.create_bound_pair(zmq.PAIR, zmq.PAIR)
142 148 s = self.session
143 149 stream = ZMQStream(a)
144 150 msg = s.send(a, 'hello', track=False)
145 151 self.assertTrue(msg['tracker'] is None)
146 152 msg = s.send(a, 'hello', track=True)
147 153 self.assertTrue(isinstance(msg['tracker'], zmq.MessageTracker))
148 154 M = zmq.Message(b'hi there', track=True)
149 155 msg = s.send(a, 'hello', buffers=[M], track=True)
150 156 t = msg['tracker']
151 157 self.assertTrue(isinstance(t, zmq.MessageTracker))
152 158 self.assertRaises(zmq.NotDone, t.wait, .1)
153 159 del M
154 160 t.wait(1) # this will raise
155 161
156 162
157 163 # def test_rekey(self):
158 164 # """rekeying dict around json str keys"""
159 165 # d = {'0': uuid.uuid4(), 0:uuid.uuid4()}
160 166 # self.assertRaises(KeyError, ss.rekey, d)
161 167 #
162 168 # d = {'0': uuid.uuid4(), 1:uuid.uuid4(), 'asdf':uuid.uuid4()}
163 169 # d2 = {0:d['0'],1:d[1],'asdf':d['asdf']}
164 170 # rd = ss.rekey(d)
165 171 # self.assertEqual(d2,rd)
166 172 #
167 173 # d = {'1.5':uuid.uuid4(),'1':uuid.uuid4()}
168 174 # d2 = {1.5:d['1.5'],1:d['1']}
169 175 # rd = ss.rekey(d)
170 176 # self.assertEqual(d2,rd)
171 177 #
172 178 # d = {'1.0':uuid.uuid4(),'1':uuid.uuid4()}
173 179 # self.assertRaises(KeyError, ss.rekey, d)
174 180 #
175 181 def test_unique_msg_ids(self):
176 182 """test that messages receive unique ids"""
177 183 ids = set()
178 184 for i in range(2**12):
179 185 h = self.session.msg_header('test')
180 186 msg_id = h['msg_id']
181 187 self.assertTrue(msg_id not in ids)
182 188 ids.add(msg_id)
183 189
184 190 def test_feed_identities(self):
185 191 """scrub the front for zmq IDENTITIES"""
186 192 theids = "engine client other".split()
187 193 content = dict(code='whoda',stuff=object())
188 194 themsg = self.session.msg('execute',content=content)
189 195 pmsg = theids
190 196
191 197 def test_session_id(self):
192 198 session = ss.Session()
193 199 # get bs before us
194 200 bs = session.bsession
195 201 us = session.session
196 202 self.assertEqual(us.encode('ascii'), bs)
197 203 session = ss.Session()
198 204 # get us before bs
199 205 us = session.session
200 206 bs = session.bsession
201 207 self.assertEqual(us.encode('ascii'), bs)
202 208 # change propagates:
203 209 session.session = 'something else'
204 210 bs = session.bsession
205 211 us = session.session
206 212 self.assertEqual(us.encode('ascii'), bs)
207 213 session = ss.Session(session='stuff')
208 214 # get us before bs
209 215 self.assertEqual(session.bsession, session.session.encode('ascii'))
210 216 self.assertEqual(b'stuff', session.bsession)
211 217
212 218
@@ -1,959 +1,959 b''
1 1 .. _messaging:
2 2
3 3 ======================
4 4 Messaging in IPython
5 5 ======================
6 6
7 7
8 8 Introduction
9 9 ============
10 10
11 11 This document explains the basic communications design and messaging
12 12 specification for how the various IPython objects interact over a network
13 13 transport. The current implementation uses the ZeroMQ_ library for messaging
14 14 within and between hosts.
15 15
16 16 .. Note::
17 17
18 18 This document should be considered the authoritative description of the
19 19 IPython messaging protocol, and all developers are strongly encouraged to
20 20 keep it updated as the implementation evolves, so that we have a single
21 21 common reference for all protocol details.
22 22
23 23 The basic design is explained in the following diagram:
24 24
25 25 .. image:: figs/frontend-kernel.png
26 26 :width: 450px
27 27 :alt: IPython kernel/frontend messaging architecture.
28 28 :align: center
29 29 :target: ../_images/frontend-kernel.png
30 30
31 31 A single kernel can be simultaneously connected to one or more frontends. The
32 32 kernel has three sockets that serve the following functions:
33 33
34 34 1. stdin: this ROUTER socket is connected to all frontends, and it allows
35 35 the kernel to request input from the active frontend when :func:`raw_input` is called.
36 36 The frontend that executed the code has a DEALER socket that acts as a 'virtual keyboard'
37 37 for the kernel while this communication is happening (illustrated in the
38 38 figure by the black outline around the central keyboard). In practice,
39 39 frontends may display such kernel requests using a special input widget or
40 40 otherwise indicating that the user is to type input for the kernel instead
41 41 of normal commands in the frontend.
42 42
43 43 2. Shell: this single ROUTER socket allows multiple incoming connections from
44 44 frontends, and this is the socket where requests for code execution, object
45 45 information, prompts, etc. are made to the kernel by any frontend. The
46 46 communication on this socket is a sequence of request/reply actions from
47 47 each frontend and the kernel.
48 48
49 49 3. IOPub: this socket is the 'broadcast channel' where the kernel publishes all
50 50 side effects (stdout, stderr, etc.) as well as the requests coming from any
51 51 client over the shell socket and its own requests on the stdin socket. There
52 52 are a number of actions in Python which generate side effects: :func:`print`
53 53 writes to ``sys.stdout``, errors generate tracebacks, etc. Additionally, in
54 54 a multi-client scenario, we want all frontends to be able to know what each
55 55 other has sent to the kernel (this can be useful in collaborative scenarios,
56 56 for example). This socket allows both side effects and the information
57 57 about communications taking place with one client over the shell channel
58 58 to be made available to all clients in a uniform manner.
59 59
60 60 All messages are tagged with enough information (details below) for clients
61 61 to know which messages come from their own interaction with the kernel and
62 62 which ones are from other clients, so they can display each type
63 63 appropriately.
64 64
65 65 The actual format of the messages allowed on each of these channels is
66 66 specified below. Messages are dicts of dicts with string keys and values that
67 67 are reasonably representable in JSON. Our current implementation uses JSON
68 68 explicitly as its message format, but this shouldn't be considered a permanent
69 69 feature. As we've discovered that JSON has non-trivial performance issues due
70 70 to excessive copying, we may in the future move to a pure pickle-based raw
71 71 message format. However, it should be possible to easily convert from the raw
72 72 objects to JSON, since we may have non-python clients (e.g. a web frontend).
73 73 As long as it's easy to make a JSON version of the objects that is a faithful
74 74 representation of all the data, we can communicate with such clients.
75 75
76 76 .. Note::
77 77
78 78 Not all of these have yet been fully fleshed out, but the key ones are, see
79 79 kernel and frontend files for actual implementation details.
80 80
81 81 General Message Format
82 82 ======================
83 83
84 84 A message is defined by the following four-dictionary structure::
85 85
86 86 {
87 87 # The message header contains a pair of unique identifiers for the
88 88 # originating session and the actual message id, in addition to the
89 89 # username for the process that generated the message. This is useful in
90 90 # collaborative settings where multiple users may be interacting with the
91 91 # same kernel simultaneously, so that frontends can label the various
92 92 # messages in a meaningful way.
93 93 'header' : {
94 94 'msg_id' : uuid,
95 95 'username' : str,
96 96 'session' : uuid
97 97 # All recognized message type strings are listed below.
98 98 'msg_type' : str,
99 99 },
100 100
101 101 # In a chain of messages, the header from the parent is copied so that
102 102 # clients can track where messages come from.
103 103 'parent_header' : dict,
104 104
105 105 # The actual content of the message must be a dict, whose structure
106 106 # depends on the message type.
107 107 'content' : dict,
108 108
109 # Any metadata associated with the message; this dictionary is optional.
109 # Any metadata associated with the message.
110 110 'metadata' : dict,
111 111 }
112 112
113 113
114 114 Python functional API
115 115 =====================
116 116
117 117 As messages are dicts, they map naturally to a ``func(**kw)`` call form. We
118 118 should develop, at a few key points, functional forms of all the requests that
119 119 take arguments in this manner and automatically construct the necessary dict
120 120 for sending.
121 121
122 122 In addition, the Python implementation of the message specification extends
123 123 messages upon deserialization to the following form for convenience::
124 124
125 125 {
126 126 'header' : dict,
127 127 # The msg's unique identifier and type are always stored in the header,
128 128 # but the Python implementation copies them to the top level.
129 129 'msg_id' : uuid,
130 130 'msg_type' : str,
131 131 'parent_header' : dict,
132 132 'content' : dict,
133 'metadata' : dict, # optional
133 'metadata' : dict,
134 134 }
135 135
136 136 All messages sent to or received by any IPython process should have this
137 137 extended structure.
138 138
139 139
140 140 Messages on the shell ROUTER/DEALER sockets
141 141 ===========================================
142 142
143 143 .. _execute:
144 144
145 145 Execute
146 146 -------
147 147
148 148 This message type is used by frontends to ask the kernel to execute code on
149 149 behalf of the user, in a namespace reserved to the user's variables (and thus
150 150 separate from the kernel's own internal code and variables).
151 151
152 152 Message type: ``execute_request``::
153 153
154 154 content = {
155 155 # Source code to be executed by the kernel, one or more lines.
156 156 'code' : str,
157 157
158 158 # A boolean flag which, if True, signals the kernel to execute
159 159 # this code as quietly as possible. This means that the kernel
160 160 # will compile the code with 'exec' instead of 'single' (so
161 161 # sys.displayhook will not fire), and will *not*:
162 162 # - broadcast exceptions on the PUB socket
163 163 # - do any logging
164 164 # - populate any history
165 165 #
166 166 # The default is False.
167 167 'silent' : bool,
168 168
169 169 # A list of variable names from the user's namespace to be retrieved. What
170 170 # returns is a JSON string of the variable's repr(), not a python object.
171 171 'user_variables' : list,
172 172
173 173 # Similarly, a dict mapping names to expressions to be evaluated in the
174 174 # user's dict.
175 175 'user_expressions' : dict,
176 176
177 177 # Some frontends (e.g. the Notebook) do not support stdin requests. If
178 178 # raw_input is called from code executed from such a frontend, a
179 179 # StdinNotImplementedError will be raised.
180 180 'allow_stdin' : True,
181 181
182 182 }
183 183
184 184 The ``code`` field contains a single string (possibly multiline). The kernel
185 185 is responsible for splitting this into one or more independent execution blocks
186 186 and deciding whether to compile these in 'single' or 'exec' mode (see below for
187 187 detailed execution semantics).
188 188
189 189 The ``user_`` fields deserve a detailed explanation. In the past, IPython had
190 190 the notion of a prompt string that allowed arbitrary code to be evaluated, and
191 191 this was put to good use by many in creating prompts that displayed system
192 192 status, path information, and even more esoteric uses like remote instrument
193 193 status aqcuired over the network. But now that IPython has a clean separation
194 194 between the kernel and the clients, the kernel has no prompt knowledge; prompts
195 195 are a frontend-side feature, and it should be even possible for different
196 196 frontends to display different prompts while interacting with the same kernel.
197 197
198 198 The kernel now provides the ability to retrieve data from the user's namespace
199 199 after the execution of the main ``code``, thanks to two fields in the
200 200 ``execute_request`` message:
201 201
202 202 - ``user_variables``: If only variables from the user's namespace are needed, a
203 203 list of variable names can be passed and a dict with these names as keys and
204 204 their :func:`repr()` as values will be returned.
205 205
206 206 - ``user_expressions``: For more complex expressions that require function
207 207 evaluations, a dict can be provided with string keys and arbitrary python
208 208 expressions as values. The return message will contain also a dict with the
209 209 same keys and the :func:`repr()` of the evaluated expressions as value.
210 210
211 211 With this information, frontends can display any status information they wish
212 212 in the form that best suits each frontend (a status line, a popup, inline for a
213 213 terminal, etc).
214 214
215 215 .. Note::
216 216
217 217 In order to obtain the current execution counter for the purposes of
218 218 displaying input prompts, frontends simply make an execution request with an
219 219 empty code string and ``silent=True``.
220 220
221 221 Execution semantics
222 222 ~~~~~~~~~~~~~~~~~~~
223 223
224 224 When the silent flag is false, the execution of use code consists of the
225 225 following phases (in silent mode, only the ``code`` field is executed):
226 226
227 227 1. Run the ``pre_runcode_hook``.
228 228
229 229 2. Execute the ``code`` field, see below for details.
230 230
231 231 3. If #2 succeeds, compute ``user_variables`` and ``user_expressions`` are
232 232 computed. This ensures that any error in the latter don't harm the main
233 233 code execution.
234 234
235 235 4. Call any method registered with :meth:`register_post_execute`.
236 236
237 237 .. warning::
238 238
239 239 The API for running code before/after the main code block is likely to
240 240 change soon. Both the ``pre_runcode_hook`` and the
241 241 :meth:`register_post_execute` are susceptible to modification, as we find a
242 242 consistent model for both.
243 243
244 244 To understand how the ``code`` field is executed, one must know that Python
245 245 code can be compiled in one of three modes (controlled by the ``mode`` argument
246 246 to the :func:`compile` builtin):
247 247
248 248 *single*
249 249 Valid for a single interactive statement (though the source can contain
250 250 multiple lines, such as a for loop). When compiled in this mode, the
251 251 generated bytecode contains special instructions that trigger the calling of
252 252 :func:`sys.displayhook` for any expression in the block that returns a value.
253 253 This means that a single statement can actually produce multiple calls to
254 254 :func:`sys.displayhook`, if for example it contains a loop where each
255 255 iteration computes an unassigned expression would generate 10 calls::
256 256
257 257 for i in range(10):
258 258 i**2
259 259
260 260 *exec*
261 261 An arbitrary amount of source code, this is how modules are compiled.
262 262 :func:`sys.displayhook` is *never* implicitly called.
263 263
264 264 *eval*
265 265 A single expression that returns a value. :func:`sys.displayhook` is *never*
266 266 implicitly called.
267 267
268 268
269 269 The ``code`` field is split into individual blocks each of which is valid for
270 270 execution in 'single' mode, and then:
271 271
272 272 - If there is only a single block: it is executed in 'single' mode.
273 273
274 274 - If there is more than one block:
275 275
276 276 * if the last one is a single line long, run all but the last in 'exec' mode
277 277 and the very last one in 'single' mode. This makes it easy to type simple
278 278 expressions at the end to see computed values.
279 279
280 280 * if the last one is no more than two lines long, run all but the last in
281 281 'exec' mode and the very last one in 'single' mode. This makes it easy to
282 282 type simple expressions at the end to see computed values. - otherwise
283 283 (last one is also multiline), run all in 'exec' mode
284 284
285 285 * otherwise (last one is also multiline), run all in 'exec' mode as a single
286 286 unit.
287 287
288 288 Any error in retrieving the ``user_variables`` or evaluating the
289 289 ``user_expressions`` will result in a simple error message in the return fields
290 290 of the form::
291 291
292 292 [ERROR] ExceptionType: Exception message
293 293
294 294 The user can simply send the same variable name or expression for evaluation to
295 295 see a regular traceback.
296 296
297 297 Errors in any registered post_execute functions are also reported similarly,
298 298 and the failing function is removed from the post_execution set so that it does
299 299 not continue triggering failures.
300 300
301 301 Upon completion of the execution request, the kernel *always* sends a reply,
302 302 with a status code indicating what happened and additional data depending on
303 303 the outcome. See :ref:`below <execution_results>` for the possible return
304 304 codes and associated data.
305 305
306 306
307 307 Execution counter (old prompt number)
308 308 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
309 309
310 310 The kernel has a single, monotonically increasing counter of all execution
311 311 requests that are made with ``silent=False``. This counter is used to populate
312 312 the ``In[n]``, ``Out[n]`` and ``_n`` variables, so clients will likely want to
313 313 display it in some form to the user, which will typically (but not necessarily)
314 314 be done in the prompts. The value of this counter will be returned as the
315 315 ``execution_count`` field of all ``execute_reply`` messages.
316 316
317 317 .. _execution_results:
318 318
319 319 Execution results
320 320 ~~~~~~~~~~~~~~~~~
321 321
322 322 Message type: ``execute_reply``::
323 323
324 324 content = {
325 325 # One of: 'ok' OR 'error' OR 'abort'
326 326 'status' : str,
327 327
328 328 # The global kernel counter that increases by one with each non-silent
329 329 # executed request. This will typically be used by clients to display
330 330 # prompt numbers to the user. If the request was a silent one, this will
331 331 # be the current value of the counter in the kernel.
332 332 'execution_count' : int,
333 333 }
334 334
335 335 When status is 'ok', the following extra fields are present::
336 336
337 337 {
338 338 # 'payload' will be a list of payload dicts.
339 339 # Each execution payload is a dict with string keys that may have been
340 340 # produced by the code being executed. It is retrieved by the kernel at
341 341 # the end of the execution and sent back to the front end, which can take
342 342 # action on it as needed. See main text for further details.
343 343 'payload' : list(dict),
344 344
345 345 # Results for the user_variables and user_expressions.
346 346 'user_variables' : dict,
347 347 'user_expressions' : dict,
348 348 }
349 349
350 350 .. admonition:: Execution payloads
351 351
352 352 The notion of an 'execution payload' is different from a return value of a
353 353 given set of code, which normally is just displayed on the pyout stream
354 354 through the PUB socket. The idea of a payload is to allow special types of
355 355 code, typically magics, to populate a data container in the IPython kernel
356 356 that will be shipped back to the caller via this channel. The kernel
357 357 has an API for this in the PayloadManager::
358 358
359 359 ip.payload_manager.write_payload(payload_dict)
360 360
361 361 which appends a dictionary to the list of payloads.
362 362
363 363
364 364 When status is 'error', the following extra fields are present::
365 365
366 366 {
367 367 'ename' : str, # Exception name, as a string
368 368 'evalue' : str, # Exception value, as a string
369 369
370 370 # The traceback will contain a list of frames, represented each as a
371 371 # string. For now we'll stick to the existing design of ultraTB, which
372 372 # controls exception level of detail statefully. But eventually we'll
373 373 # want to grow into a model where more information is collected and
374 374 # packed into the traceback object, with clients deciding how little or
375 375 # how much of it to unpack. But for now, let's start with a simple list
376 376 # of strings, since that requires only minimal changes to ultratb as
377 377 # written.
378 378 'traceback' : list,
379 379 }
380 380
381 381
382 382 When status is 'abort', there are for now no additional data fields. This
383 383 happens when the kernel was interrupted by a signal.
384 384
385 385 Kernel attribute access
386 386 -----------------------
387 387
388 388 .. warning::
389 389
390 390 This part of the messaging spec is not actually implemented in the kernel
391 391 yet.
392 392
393 393 While this protocol does not specify full RPC access to arbitrary methods of
394 394 the kernel object, the kernel does allow read (and in some cases write) access
395 395 to certain attributes.
396 396
397 397 The policy for which attributes can be read is: any attribute of the kernel, or
398 398 its sub-objects, that belongs to a :class:`Configurable` object and has been
399 399 declared at the class-level with Traits validation, is in principle accessible
400 400 as long as its name does not begin with a leading underscore. The attribute
401 401 itself will have metadata indicating whether it allows remote read and/or write
402 402 access. The message spec follows for attribute read and write requests.
403 403
404 404 Message type: ``getattr_request``::
405 405
406 406 content = {
407 407 # The (possibly dotted) name of the attribute
408 408 'name' : str,
409 409 }
410 410
411 411 When a ``getattr_request`` fails, there are two possible error types:
412 412
413 413 - AttributeError: this type of error was raised when trying to access the
414 414 given name by the kernel itself. This means that the attribute likely
415 415 doesn't exist.
416 416
417 417 - AccessError: the attribute exists but its value is not readable remotely.
418 418
419 419
420 420 Message type: ``getattr_reply``::
421 421
422 422 content = {
423 423 # One of ['ok', 'AttributeError', 'AccessError'].
424 424 'status' : str,
425 425 # If status is 'ok', a JSON object.
426 426 'value' : object,
427 427 }
428 428
429 429 Message type: ``setattr_request``::
430 430
431 431 content = {
432 432 # The (possibly dotted) name of the attribute
433 433 'name' : str,
434 434
435 435 # A JSON-encoded object, that will be validated by the Traits
436 436 # information in the kernel
437 437 'value' : object,
438 438 }
439 439
440 440 When a ``setattr_request`` fails, there are also two possible error types with
441 441 similar meanings as those of the ``getattr_request`` case, but for writing.
442 442
443 443 Message type: ``setattr_reply``::
444 444
445 445 content = {
446 446 # One of ['ok', 'AttributeError', 'AccessError'].
447 447 'status' : str,
448 448 }
449 449
450 450
451 451
452 452 Object information
453 453 ------------------
454 454
455 455 One of IPython's most used capabilities is the introspection of Python objects
456 456 in the user's namespace, typically invoked via the ``?`` and ``??`` characters
457 457 (which in reality are shorthands for the ``%pinfo`` magic). This is used often
458 458 enough that it warrants an explicit message type, especially because frontends
459 459 may want to get object information in response to user keystrokes (like Tab or
460 460 F1) besides from the user explicitly typing code like ``x??``.
461 461
462 462 Message type: ``object_info_request``::
463 463
464 464 content = {
465 465 # The (possibly dotted) name of the object to be searched in all
466 466 # relevant namespaces
467 467 'name' : str,
468 468
469 469 # The level of detail desired. The default (0) is equivalent to typing
470 470 # 'x?' at the prompt, 1 is equivalent to 'x??'.
471 471 'detail_level' : int,
472 472 }
473 473
474 474 The returned information will be a dictionary with keys very similar to the
475 475 field names that IPython prints at the terminal.
476 476
477 477 Message type: ``object_info_reply``::
478 478
479 479 content = {
480 480 # The name the object was requested under
481 481 'name' : str,
482 482
483 483 # Boolean flag indicating whether the named object was found or not. If
484 484 # it's false, all other fields will be empty.
485 485 'found' : bool,
486 486
487 487 # Flags for magics and system aliases
488 488 'ismagic' : bool,
489 489 'isalias' : bool,
490 490
491 491 # The name of the namespace where the object was found ('builtin',
492 492 # 'magics', 'alias', 'interactive', etc.)
493 493 'namespace' : str,
494 494
495 495 # The type name will be type.__name__ for normal Python objects, but it
496 496 # can also be a string like 'Magic function' or 'System alias'
497 497 'type_name' : str,
498 498
499 499 # The string form of the object, possibly truncated for length if
500 500 # detail_level is 0
501 501 'string_form' : str,
502 502
503 503 # For objects with a __class__ attribute this will be set
504 504 'base_class' : str,
505 505
506 506 # For objects with a __len__ attribute this will be set
507 507 'length' : int,
508 508
509 509 # If the object is a function, class or method whose file we can find,
510 510 # we give its full path
511 511 'file' : str,
512 512
513 513 # For pure Python callable objects, we can reconstruct the object
514 514 # definition line which provides its call signature. For convenience this
515 515 # is returned as a single 'definition' field, but below the raw parts that
516 516 # compose it are also returned as the argspec field.
517 517 'definition' : str,
518 518
519 519 # The individual parts that together form the definition string. Clients
520 520 # with rich display capabilities may use this to provide a richer and more
521 521 # precise representation of the definition line (e.g. by highlighting
522 522 # arguments based on the user's cursor position). For non-callable
523 523 # objects, this field is empty.
524 524 'argspec' : { # The names of all the arguments
525 525 args : list,
526 526 # The name of the varargs (*args), if any
527 527 varargs : str,
528 528 # The name of the varkw (**kw), if any
529 529 varkw : str,
530 530 # The values (as strings) of all default arguments. Note
531 531 # that these must be matched *in reverse* with the 'args'
532 532 # list above, since the first positional args have no default
533 533 # value at all.
534 534 defaults : list,
535 535 },
536 536
537 537 # For instances, provide the constructor signature (the definition of
538 538 # the __init__ method):
539 539 'init_definition' : str,
540 540
541 541 # Docstrings: for any object (function, method, module, package) with a
542 542 # docstring, we show it. But in addition, we may provide additional
543 543 # docstrings. For example, for instances we will show the constructor
544 544 # and class docstrings as well, if available.
545 545 'docstring' : str,
546 546
547 547 # For instances, provide the constructor and class docstrings
548 548 'init_docstring' : str,
549 549 'class_docstring' : str,
550 550
551 551 # If it's a callable object whose call method has a separate docstring and
552 552 # definition line:
553 553 'call_def' : str,
554 554 'call_docstring' : str,
555 555
556 556 # If detail_level was 1, we also try to find the source code that
557 557 # defines the object, if possible. The string 'None' will indicate
558 558 # that no source was found.
559 559 'source' : str,
560 560 }
561 561
562 562
563 563 Complete
564 564 --------
565 565
566 566 Message type: ``complete_request``::
567 567
568 568 content = {
569 569 # The text to be completed, such as 'a.is'
570 570 'text' : str,
571 571
572 572 # The full line, such as 'print a.is'. This allows completers to
573 573 # make decisions that may require information about more than just the
574 574 # current word.
575 575 'line' : str,
576 576
577 577 # The entire block of text where the line is. This may be useful in the
578 578 # case of multiline completions where more context may be needed. Note: if
579 579 # in practice this field proves unnecessary, remove it to lighten the
580 580 # messages.
581 581
582 582 'block' : str,
583 583
584 584 # The position of the cursor where the user hit 'TAB' on the line.
585 585 'cursor_pos' : int,
586 586 }
587 587
588 588 Message type: ``complete_reply``::
589 589
590 590 content = {
591 591 # The list of all matches to the completion request, such as
592 592 # ['a.isalnum', 'a.isalpha'] for the above example.
593 593 'matches' : list
594 594 }
595 595
596 596
597 597 History
598 598 -------
599 599
600 600 For clients to explicitly request history from a kernel. The kernel has all
601 601 the actual execution history stored in a single location, so clients can
602 602 request it from the kernel when needed.
603 603
604 604 Message type: ``history_request``::
605 605
606 606 content = {
607 607
608 608 # If True, also return output history in the resulting dict.
609 609 'output' : bool,
610 610
611 611 # If True, return the raw input history, else the transformed input.
612 612 'raw' : bool,
613 613
614 614 # So far, this can be 'range', 'tail' or 'search'.
615 615 'hist_access_type' : str,
616 616
617 617 # If hist_access_type is 'range', get a range of input cells. session can
618 618 # be a positive session number, or a negative number to count back from
619 619 # the current session.
620 620 'session' : int,
621 621 # start and stop are line numbers within that session.
622 622 'start' : int,
623 623 'stop' : int,
624 624
625 625 # If hist_access_type is 'tail', get the last n cells.
626 626 'n' : int,
627 627
628 628 # If hist_access_type is 'search', get cells matching the specified glob
629 629 # pattern (with * and ? as wildcards).
630 630 'pattern' : str,
631 631
632 632 }
633 633
634 634 Message type: ``history_reply``::
635 635
636 636 content = {
637 637 # A list of 3 tuples, either:
638 638 # (session, line_number, input) or
639 639 # (session, line_number, (input, output)),
640 640 # depending on whether output was False or True, respectively.
641 641 'history' : list,
642 642 }
643 643
644 644
645 645 Connect
646 646 -------
647 647
648 648 When a client connects to the request/reply socket of the kernel, it can issue
649 649 a connect request to get basic information about the kernel, such as the ports
650 650 the other ZeroMQ sockets are listening on. This allows clients to only have
651 651 to know about a single port (the shell channel) to connect to a kernel.
652 652
653 653 Message type: ``connect_request``::
654 654
655 655 content = {
656 656 }
657 657
658 658 Message type: ``connect_reply``::
659 659
660 660 content = {
661 661 'shell_port' : int # The port the shell ROUTER socket is listening on.
662 662 'iopub_port' : int # The port the PUB socket is listening on.
663 663 'stdin_port' : int # The port the stdin ROUTER socket is listening on.
664 664 'hb_port' : int # The port the heartbeat socket is listening on.
665 665 }
666 666
667 667
668 668
669 669 Kernel shutdown
670 670 ---------------
671 671
672 672 The clients can request the kernel to shut itself down; this is used in
673 673 multiple cases:
674 674
675 675 - when the user chooses to close the client application via a menu or window
676 676 control.
677 677 - when the user types 'exit' or 'quit' (or their uppercase magic equivalents).
678 678 - when the user chooses a GUI method (like the 'Ctrl-C' shortcut in the
679 679 IPythonQt client) to force a kernel restart to get a clean kernel without
680 680 losing client-side state like history or inlined figures.
681 681
682 682 The client sends a shutdown request to the kernel, and once it receives the
683 683 reply message (which is otherwise empty), it can assume that the kernel has
684 684 completed shutdown safely.
685 685
686 686 Upon their own shutdown, client applications will typically execute a last
687 687 minute sanity check and forcefully terminate any kernel that is still alive, to
688 688 avoid leaving stray processes in the user's machine.
689 689
690 690 For both shutdown request and reply, there is no actual content that needs to
691 691 be sent, so the content dict is empty.
692 692
693 693 Message type: ``shutdown_request``::
694 694
695 695 content = {
696 696 'restart' : bool # whether the shutdown is final, or precedes a restart
697 697 }
698 698
699 699 Message type: ``shutdown_reply``::
700 700
701 701 content = {
702 702 'restart' : bool # whether the shutdown is final, or precedes a restart
703 703 }
704 704
705 705 .. Note::
706 706
707 707 When the clients detect a dead kernel thanks to inactivity on the heartbeat
708 708 socket, they simply send a forceful process termination signal, since a dead
709 709 process is unlikely to respond in any useful way to messages.
710 710
711 711
712 712 Messages on the PUB/SUB socket
713 713 ==============================
714 714
715 715 Streams (stdout, stderr, etc)
716 716 ------------------------------
717 717
718 718 Message type: ``stream``::
719 719
720 720 content = {
721 721 # The name of the stream is one of 'stdin', 'stdout', 'stderr'
722 722 'name' : str,
723 723
724 724 # The data is an arbitrary string to be written to that stream
725 725 'data' : str,
726 726 }
727 727
728 728 When a kernel receives a raw_input call, it should also broadcast it on the pub
729 729 socket with the names 'stdin' and 'stdin_reply'. This will allow other clients
730 730 to monitor/display kernel interactions and possibly replay them to their user
731 731 or otherwise expose them.
732 732
733 733 Display Data
734 734 ------------
735 735
736 736 This type of message is used to bring back data that should be diplayed (text,
737 737 html, svg, etc.) in the frontends. This data is published to all frontends.
738 738 Each message can have multiple representations of the data; it is up to the
739 739 frontend to decide which to use and how. A single message should contain all
740 740 possible representations of the same information. Each representation should
741 741 be a JSON'able data structure, and should be a valid MIME type.
742 742
743 743 Some questions remain about this design:
744 744
745 745 * Do we use this message type for pyout/displayhook? Probably not, because
746 746 the displayhook also has to handle the Out prompt display. On the other hand
747 747 we could put that information into the metadata secion.
748 748
749 749 Message type: ``display_data``::
750 750
751 751 content = {
752 752
753 753 # Who create the data
754 754 'source' : str,
755 755
756 756 # The data dict contains key/value pairs, where the kids are MIME
757 757 # types and the values are the raw data of the representation in that
758 758 # format. The data dict must minimally contain the ``text/plain``
759 759 # MIME type which is used as a backup representation.
760 760 'data' : dict,
761 761
762 762 # Any metadata that describes the data
763 763 'metadata' : dict
764 764 }
765 765
766 766 Python inputs
767 767 -------------
768 768
769 769 These messages are the re-broadcast of the ``execute_request``.
770 770
771 771 Message type: ``pyin``::
772 772
773 773 content = {
774 774 'code' : str, # Source code to be executed, one or more lines
775 775
776 776 # The counter for this execution is also provided so that clients can
777 777 # display it, since IPython automatically creates variables called _iN
778 778 # (for input prompt In[N]).
779 779 'execution_count' : int
780 780 }
781 781
782 782 Python outputs
783 783 --------------
784 784
785 785 When Python produces output from code that has been compiled in with the
786 786 'single' flag to :func:`compile`, any expression that produces a value (such as
787 787 ``1+1``) is passed to ``sys.displayhook``, which is a callable that can do with
788 788 this value whatever it wants. The default behavior of ``sys.displayhook`` in
789 789 the Python interactive prompt is to print to ``sys.stdout`` the :func:`repr` of
790 790 the value as long as it is not ``None`` (which isn't printed at all). In our
791 791 case, the kernel instantiates as ``sys.displayhook`` an object which has
792 792 similar behavior, but which instead of printing to stdout, broadcasts these
793 793 values as ``pyout`` messages for clients to display appropriately.
794 794
795 795 IPython's displayhook can handle multiple simultaneous formats depending on its
796 796 configuration. The default pretty-printed repr text is always given with the
797 797 ``data`` entry in this message. Any other formats are provided in the
798 798 ``extra_formats`` list. Frontends are free to display any or all of these
799 799 according to its capabilities. ``extra_formats`` list contains 3-tuples of an ID
800 800 string, a type string, and the data. The ID is unique to the formatter
801 801 implementation that created the data. Frontends will typically ignore the ID
802 802 unless if it has requested a particular formatter. The type string tells the
803 803 frontend how to interpret the data. It is often, but not always a MIME type.
804 804 Frontends should ignore types that it does not understand. The data itself is
805 805 any JSON object and depends on the format. It is often, but not always a string.
806 806
807 807 Message type: ``pyout``::
808 808
809 809 content = {
810 810
811 811 # The counter for this execution is also provided so that clients can
812 812 # display it, since IPython automatically creates variables called _N
813 813 # (for prompt N).
814 814 'execution_count' : int,
815 815
816 816 # The data dict contains key/value pairs, where the kids are MIME
817 817 # types and the values are the raw data of the representation in that
818 818 # format. The data dict must minimally contain the ``text/plain``
819 819 # MIME type which is used as a backup representation.
820 820 'data' : dict,
821 821
822 822 }
823 823
824 824 Python errors
825 825 -------------
826 826
827 827 When an error occurs during code execution
828 828
829 829 Message type: ``pyerr``::
830 830
831 831 content = {
832 832 # Similar content to the execute_reply messages for the 'error' case,
833 833 # except the 'status' field is omitted.
834 834 }
835 835
836 836 Kernel status
837 837 -------------
838 838
839 839 This message type is used by frontends to monitor the status of the kernel.
840 840
841 841 Message type: ``status``::
842 842
843 843 content = {
844 844 # When the kernel starts to execute code, it will enter the 'busy'
845 845 # state and when it finishes, it will enter the 'idle' state.
846 846 execution_state : ('busy', 'idle')
847 847 }
848 848
849 849 Kernel crashes
850 850 --------------
851 851
852 852 When the kernel has an unexpected exception, caught by the last-resort
853 853 sys.excepthook, we should broadcast the crash handler's output before exiting.
854 854 This will allow clients to notice that a kernel died, inform the user and
855 855 propose further actions.
856 856
857 857 Message type: ``crash``::
858 858
859 859 content = {
860 860 # Similarly to the 'error' case for execute_reply messages, this will
861 861 # contain ename, etype and traceback fields.
862 862
863 863 # An additional field with supplementary information such as where to
864 864 # send the crash message
865 865 'info' : str,
866 866 }
867 867
868 868
869 869 Future ideas
870 870 ------------
871 871
872 872 Other potential message types, currently unimplemented, listed below as ideas.
873 873
874 874 Message type: ``file``::
875 875
876 876 content = {
877 877 'path' : 'cool.jpg',
878 878 'mimetype' : str,
879 879 'data' : str,
880 880 }
881 881
882 882
883 883 Messages on the stdin ROUTER/DEALER sockets
884 884 ===========================================
885 885
886 886 This is a socket where the request/reply pattern goes in the opposite direction:
887 887 from the kernel to a *single* frontend, and its purpose is to allow
888 888 ``raw_input`` and similar operations that read from ``sys.stdin`` on the kernel
889 889 to be fulfilled by the client. The request should be made to the frontend that
890 890 made the execution request that prompted ``raw_input`` to be called. For now we
891 891 will keep these messages as simple as possible, since they only mean to convey
892 892 the ``raw_input(prompt)`` call.
893 893
894 894 Message type: ``input_request``::
895 895
896 896 content = { 'prompt' : str }
897 897
898 898 Message type: ``input_reply``::
899 899
900 900 content = { 'value' : str }
901 901
902 902 .. Note::
903 903
904 904 We do not explicitly try to forward the raw ``sys.stdin`` object, because in
905 905 practice the kernel should behave like an interactive program. When a
906 906 program is opened on the console, the keyboard effectively takes over the
907 907 ``stdin`` file descriptor, and it can't be used for raw reading anymore.
908 908 Since the IPython kernel effectively behaves like a console program (albeit
909 909 one whose "keyboard" is actually living in a separate process and
910 910 transported over the zmq connection), raw ``stdin`` isn't expected to be
911 911 available.
912 912
913 913
914 914 Heartbeat for kernels
915 915 =====================
916 916
917 917 Initially we had considered using messages like those above over ZMQ for a
918 918 kernel 'heartbeat' (a way to detect quickly and reliably whether a kernel is
919 919 alive at all, even if it may be busy executing user code). But this has the
920 920 problem that if the kernel is locked inside extension code, it wouldn't execute
921 921 the python heartbeat code. But it turns out that we can implement a basic
922 922 heartbeat with pure ZMQ, without using any Python messaging at all.
923 923
924 924 The monitor sends out a single zmq message (right now, it is a str of the
925 925 monitor's lifetime in seconds), and gets the same message right back, prefixed
926 926 with the zmq identity of the DEALER socket in the heartbeat process. This can be
927 927 a uuid, or even a full message, but there doesn't seem to be a need for packing
928 928 up a message when the sender and receiver are the exact same Python object.
929 929
930 930 The model is this::
931 931
932 932 monitor.send(str(self.lifetime)) # '1.2345678910'
933 933
934 934 and the monitor receives some number of messages of the form::
935 935
936 936 ['uuid-abcd-dead-beef', '1.2345678910']
937 937
938 938 where the first part is the zmq.IDENTITY of the heart's DEALER on the engine, and
939 939 the rest is the message sent by the monitor. No Python code ever has any
940 940 access to the message between the monitor's send, and the monitor's recv.
941 941
942 942
943 943 ToDo
944 944 ====
945 945
946 946 Missing things include:
947 947
948 948 * Important: finish thinking through the payload concept and API.
949 949
950 950 * Important: ensure that we have a good solution for magics like %edit. It's
951 951 likely that with the payload concept we can build a full solution, but not
952 952 100% clear yet.
953 953
954 954 * Finishing the details of the heartbeat protocol.
955 955
956 956 * Signal handling: specify what kind of information kernel should broadcast (or
957 957 not) when it receives signals.
958 958
959 959 .. include:: ../links.rst
General Comments 0
You need to be logged in to leave comments. Login now