##// END OF EJS Templates
move language name, version into language_info...
Min RK -
Show More
@@ -1,2496 +1,2493 b''
1 // Copyright (c) IPython Development Team.
1 // Copyright (c) IPython Development Team.
2 // Distributed under the terms of the Modified BSD License.
2 // Distributed under the terms of the Modified BSD License.
3
3
4 define([
4 define([
5 'base/js/namespace',
5 'base/js/namespace',
6 'jquery',
6 'jquery',
7 'base/js/utils',
7 'base/js/utils',
8 'base/js/dialog',
8 'base/js/dialog',
9 'notebook/js/textcell',
9 'notebook/js/textcell',
10 'notebook/js/codecell',
10 'notebook/js/codecell',
11 'services/sessions/session',
11 'services/sessions/session',
12 'notebook/js/celltoolbar',
12 'notebook/js/celltoolbar',
13 'components/marked/lib/marked',
13 'components/marked/lib/marked',
14 'codemirror/lib/codemirror',
14 'codemirror/lib/codemirror',
15 'codemirror/addon/runmode/runmode',
15 'codemirror/addon/runmode/runmode',
16 'notebook/js/mathjaxutils',
16 'notebook/js/mathjaxutils',
17 'base/js/keyboard',
17 'base/js/keyboard',
18 'notebook/js/tooltip',
18 'notebook/js/tooltip',
19 'notebook/js/celltoolbarpresets/default',
19 'notebook/js/celltoolbarpresets/default',
20 'notebook/js/celltoolbarpresets/rawcell',
20 'notebook/js/celltoolbarpresets/rawcell',
21 'notebook/js/celltoolbarpresets/slideshow',
21 'notebook/js/celltoolbarpresets/slideshow',
22 'notebook/js/scrollmanager'
22 'notebook/js/scrollmanager'
23 ], function (
23 ], function (
24 IPython,
24 IPython,
25 $,
25 $,
26 utils,
26 utils,
27 dialog,
27 dialog,
28 textcell,
28 textcell,
29 codecell,
29 codecell,
30 session,
30 session,
31 celltoolbar,
31 celltoolbar,
32 marked,
32 marked,
33 CodeMirror,
33 CodeMirror,
34 runMode,
34 runMode,
35 mathjaxutils,
35 mathjaxutils,
36 keyboard,
36 keyboard,
37 tooltip,
37 tooltip,
38 default_celltoolbar,
38 default_celltoolbar,
39 rawcell_celltoolbar,
39 rawcell_celltoolbar,
40 slideshow_celltoolbar,
40 slideshow_celltoolbar,
41 scrollmanager
41 scrollmanager
42 ) {
42 ) {
43 "use strict";
43 "use strict";
44
44
45 var Notebook = function (selector, options) {
45 var Notebook = function (selector, options) {
46 // Constructor
46 // Constructor
47 //
47 //
48 // A notebook contains and manages cells.
48 // A notebook contains and manages cells.
49 //
49 //
50 // Parameters:
50 // Parameters:
51 // selector: string
51 // selector: string
52 // options: dictionary
52 // options: dictionary
53 // Dictionary of keyword arguments.
53 // Dictionary of keyword arguments.
54 // events: $(Events) instance
54 // events: $(Events) instance
55 // keyboard_manager: KeyboardManager instance
55 // keyboard_manager: KeyboardManager instance
56 // contents: Contents instance
56 // contents: Contents instance
57 // save_widget: SaveWidget instance
57 // save_widget: SaveWidget instance
58 // config: dictionary
58 // config: dictionary
59 // base_url : string
59 // base_url : string
60 // notebook_path : string
60 // notebook_path : string
61 // notebook_name : string
61 // notebook_name : string
62 this.config = utils.mergeopt(Notebook, options.config);
62 this.config = utils.mergeopt(Notebook, options.config);
63 this.base_url = options.base_url;
63 this.base_url = options.base_url;
64 this.notebook_path = options.notebook_path;
64 this.notebook_path = options.notebook_path;
65 this.notebook_name = options.notebook_name;
65 this.notebook_name = options.notebook_name;
66 this.events = options.events;
66 this.events = options.events;
67 this.keyboard_manager = options.keyboard_manager;
67 this.keyboard_manager = options.keyboard_manager;
68 this.contents = options.contents;
68 this.contents = options.contents;
69 this.save_widget = options.save_widget;
69 this.save_widget = options.save_widget;
70 this.tooltip = new tooltip.Tooltip(this.events);
70 this.tooltip = new tooltip.Tooltip(this.events);
71 this.ws_url = options.ws_url;
71 this.ws_url = options.ws_url;
72 this._session_starting = false;
72 this._session_starting = false;
73 this.default_cell_type = this.config.default_cell_type || 'code';
73 this.default_cell_type = this.config.default_cell_type || 'code';
74
74
75 // Create default scroll manager.
75 // Create default scroll manager.
76 this.scroll_manager = new scrollmanager.ScrollManager(this);
76 this.scroll_manager = new scrollmanager.ScrollManager(this);
77
77
78 // TODO: This code smells (and the other `= this` line a couple lines down)
78 // TODO: This code smells (and the other `= this` line a couple lines down)
79 // We need a better way to deal with circular instance references.
79 // We need a better way to deal with circular instance references.
80 this.keyboard_manager.notebook = this;
80 this.keyboard_manager.notebook = this;
81 this.save_widget.notebook = this;
81 this.save_widget.notebook = this;
82
82
83 mathjaxutils.init();
83 mathjaxutils.init();
84
84
85 if (marked) {
85 if (marked) {
86 marked.setOptions({
86 marked.setOptions({
87 gfm : true,
87 gfm : true,
88 tables: true,
88 tables: true,
89 // FIXME: probably want central config for CodeMirror theme when we have js config
89 // FIXME: probably want central config for CodeMirror theme when we have js config
90 langPrefix: "cm-s-ipython language-",
90 langPrefix: "cm-s-ipython language-",
91 highlight: function(code, lang, callback) {
91 highlight: function(code, lang, callback) {
92 if (!lang) {
92 if (!lang) {
93 // no language, no highlight
93 // no language, no highlight
94 if (callback) {
94 if (callback) {
95 callback(null, code);
95 callback(null, code);
96 return;
96 return;
97 } else {
97 } else {
98 return code;
98 return code;
99 }
99 }
100 }
100 }
101 utils.requireCodeMirrorMode(lang, function () {
101 utils.requireCodeMirrorMode(lang, function () {
102 var el = document.createElement("div");
102 var el = document.createElement("div");
103 var mode = CodeMirror.getMode({}, lang);
103 var mode = CodeMirror.getMode({}, lang);
104 if (!mode) {
104 if (!mode) {
105 console.log("No CodeMirror mode: " + lang);
105 console.log("No CodeMirror mode: " + lang);
106 callback(null, code);
106 callback(null, code);
107 return;
107 return;
108 }
108 }
109 try {
109 try {
110 CodeMirror.runMode(code, mode, el);
110 CodeMirror.runMode(code, mode, el);
111 callback(null, el.innerHTML);
111 callback(null, el.innerHTML);
112 } catch (err) {
112 } catch (err) {
113 console.log("Failed to highlight " + lang + " code", err);
113 console.log("Failed to highlight " + lang + " code", err);
114 callback(err, code);
114 callback(err, code);
115 }
115 }
116 }, function (err) {
116 }, function (err) {
117 console.log("No CodeMirror mode: " + lang);
117 console.log("No CodeMirror mode: " + lang);
118 callback(err, code);
118 callback(err, code);
119 });
119 });
120 }
120 }
121 });
121 });
122 }
122 }
123
123
124 this.element = $(selector);
124 this.element = $(selector);
125 this.element.scroll();
125 this.element.scroll();
126 this.element.data("notebook", this);
126 this.element.data("notebook", this);
127 this.next_prompt_number = 1;
127 this.next_prompt_number = 1;
128 this.session = null;
128 this.session = null;
129 this.kernel = null;
129 this.kernel = null;
130 this.clipboard = null;
130 this.clipboard = null;
131 this.undelete_backup = null;
131 this.undelete_backup = null;
132 this.undelete_index = null;
132 this.undelete_index = null;
133 this.undelete_below = false;
133 this.undelete_below = false;
134 this.paste_enabled = false;
134 this.paste_enabled = false;
135 this.writable = false;
135 this.writable = false;
136 // It is important to start out in command mode to match the intial mode
136 // It is important to start out in command mode to match the intial mode
137 // of the KeyboardManager.
137 // of the KeyboardManager.
138 this.mode = 'command';
138 this.mode = 'command';
139 this.set_dirty(false);
139 this.set_dirty(false);
140 this.metadata = {};
140 this.metadata = {};
141 this._checkpoint_after_save = false;
141 this._checkpoint_after_save = false;
142 this.last_checkpoint = null;
142 this.last_checkpoint = null;
143 this.checkpoints = [];
143 this.checkpoints = [];
144 this.autosave_interval = 0;
144 this.autosave_interval = 0;
145 this.autosave_timer = null;
145 this.autosave_timer = null;
146 // autosave *at most* every two minutes
146 // autosave *at most* every two minutes
147 this.minimum_autosave_interval = 120000;
147 this.minimum_autosave_interval = 120000;
148 this.notebook_name_blacklist_re = /[\/\\:]/;
148 this.notebook_name_blacklist_re = /[\/\\:]/;
149 this.nbformat = 4; // Increment this when changing the nbformat
149 this.nbformat = 4; // Increment this when changing the nbformat
150 this.nbformat_minor = 0; // Increment this when changing the nbformat
150 this.nbformat_minor = 0; // Increment this when changing the nbformat
151 this.codemirror_mode = 'ipython';
151 this.codemirror_mode = 'ipython';
152 this.create_elements();
152 this.create_elements();
153 this.bind_events();
153 this.bind_events();
154 this.kernel_selector = null;
154 this.kernel_selector = null;
155 this.dirty = null;
155 this.dirty = null;
156 this.trusted = null;
156 this.trusted = null;
157 this._fully_loaded = false;
157 this._fully_loaded = false;
158
158
159 // Trigger cell toolbar registration.
159 // Trigger cell toolbar registration.
160 default_celltoolbar.register(this);
160 default_celltoolbar.register(this);
161 rawcell_celltoolbar.register(this);
161 rawcell_celltoolbar.register(this);
162 slideshow_celltoolbar.register(this);
162 slideshow_celltoolbar.register(this);
163
163
164 // prevent assign to miss-typed properties.
164 // prevent assign to miss-typed properties.
165 Object.seal(this);
165 Object.seal(this);
166 };
166 };
167
167
168 Notebook.options_default = {
168 Notebook.options_default = {
169 // can be any cell type, or the special values of
169 // can be any cell type, or the special values of
170 // 'above', 'below', or 'selected' to get the value from another cell.
170 // 'above', 'below', or 'selected' to get the value from another cell.
171 Notebook: {
171 Notebook: {
172 default_cell_type: 'code'
172 default_cell_type: 'code'
173 }
173 }
174 };
174 };
175
175
176
176
177 /**
177 /**
178 * Create an HTML and CSS representation of the notebook.
178 * Create an HTML and CSS representation of the notebook.
179 *
179 *
180 * @method create_elements
180 * @method create_elements
181 */
181 */
182 Notebook.prototype.create_elements = function () {
182 Notebook.prototype.create_elements = function () {
183 var that = this;
183 var that = this;
184 this.element.attr('tabindex','-1');
184 this.element.attr('tabindex','-1');
185 this.container = $("<div/>").addClass("container").attr("id", "notebook-container");
185 this.container = $("<div/>").addClass("container").attr("id", "notebook-container");
186 // We add this end_space div to the end of the notebook div to:
186 // We add this end_space div to the end of the notebook div to:
187 // i) provide a margin between the last cell and the end of the notebook
187 // i) provide a margin between the last cell and the end of the notebook
188 // ii) to prevent the div from scrolling up when the last cell is being
188 // ii) to prevent the div from scrolling up when the last cell is being
189 // edited, but is too low on the page, which browsers will do automatically.
189 // edited, but is too low on the page, which browsers will do automatically.
190 var end_space = $('<div/>').addClass('end_space');
190 var end_space = $('<div/>').addClass('end_space');
191 end_space.dblclick(function (e) {
191 end_space.dblclick(function (e) {
192 var ncells = that.ncells();
192 var ncells = that.ncells();
193 that.insert_cell_below('code',ncells-1);
193 that.insert_cell_below('code',ncells-1);
194 });
194 });
195 this.element.append(this.container);
195 this.element.append(this.container);
196 this.container.append(end_space);
196 this.container.append(end_space);
197 };
197 };
198
198
199 /**
199 /**
200 * Bind JavaScript events: key presses and custom IPython events.
200 * Bind JavaScript events: key presses and custom IPython events.
201 *
201 *
202 * @method bind_events
202 * @method bind_events
203 */
203 */
204 Notebook.prototype.bind_events = function () {
204 Notebook.prototype.bind_events = function () {
205 var that = this;
205 var that = this;
206
206
207 this.events.on('set_next_input.Notebook', function (event, data) {
207 this.events.on('set_next_input.Notebook', function (event, data) {
208 var index = that.find_cell_index(data.cell);
208 var index = that.find_cell_index(data.cell);
209 var new_cell = that.insert_cell_below('code',index);
209 var new_cell = that.insert_cell_below('code',index);
210 new_cell.set_text(data.text);
210 new_cell.set_text(data.text);
211 that.dirty = true;
211 that.dirty = true;
212 });
212 });
213
213
214 this.events.on('set_dirty.Notebook', function (event, data) {
214 this.events.on('set_dirty.Notebook', function (event, data) {
215 that.dirty = data.value;
215 that.dirty = data.value;
216 });
216 });
217
217
218 this.events.on('trust_changed.Notebook', function (event, trusted) {
218 this.events.on('trust_changed.Notebook', function (event, trusted) {
219 that.trusted = trusted;
219 that.trusted = trusted;
220 });
220 });
221
221
222 this.events.on('select.Cell', function (event, data) {
222 this.events.on('select.Cell', function (event, data) {
223 var index = that.find_cell_index(data.cell);
223 var index = that.find_cell_index(data.cell);
224 that.select(index);
224 that.select(index);
225 });
225 });
226
226
227 this.events.on('edit_mode.Cell', function (event, data) {
227 this.events.on('edit_mode.Cell', function (event, data) {
228 that.handle_edit_mode(data.cell);
228 that.handle_edit_mode(data.cell);
229 });
229 });
230
230
231 this.events.on('command_mode.Cell', function (event, data) {
231 this.events.on('command_mode.Cell', function (event, data) {
232 that.handle_command_mode(data.cell);
232 that.handle_command_mode(data.cell);
233 });
233 });
234
234
235 this.events.on('spec_changed.Kernel', function(event, data) {
235 this.events.on('spec_changed.Kernel', function(event, data) {
236 that.metadata.kernelspec =
236 that.metadata.kernelspec =
237 {name: data.name, display_name: data.display_name};
237 {name: data.name, display_name: data.display_name};
238 });
238 });
239
239
240 this.events.on('kernel_ready.Kernel', function(event, data) {
240 this.events.on('kernel_ready.Kernel', function(event, data) {
241 var kinfo = data.kernel.info_reply;
241 var kinfo = data.kernel.info_reply;
242 var langinfo = kinfo.language_info || {};
242 var langinfo = kinfo.language_info || {};
243 if (!langinfo.name) {
244 langinfo.name = kinfo.language;
245 }
246 that.metadata.language_info = langinfo;
243 that.metadata.language_info = langinfo;
247 // Mode 'null' should be plain, unhighlighted text.
244 // Mode 'null' should be plain, unhighlighted text.
248 var cm_mode = langinfo.codemirror_mode || langinfo.name || 'null';
245 var cm_mode = langinfo.codemirror_mode || langinfo.name || 'null';
249 that.set_codemirror_mode(cm_mode);
246 that.set_codemirror_mode(cm_mode);
250 });
247 });
251
248
252 var collapse_time = function (time) {
249 var collapse_time = function (time) {
253 var app_height = $('#ipython-main-app').height(); // content height
250 var app_height = $('#ipython-main-app').height(); // content height
254 var splitter_height = $('div#pager_splitter').outerHeight(true);
251 var splitter_height = $('div#pager_splitter').outerHeight(true);
255 var new_height = app_height - splitter_height;
252 var new_height = app_height - splitter_height;
256 that.element.animate({height : new_height + 'px'}, time);
253 that.element.animate({height : new_height + 'px'}, time);
257 };
254 };
258
255
259 this.element.bind('collapse_pager', function (event, extrap) {
256 this.element.bind('collapse_pager', function (event, extrap) {
260 var time = (extrap !== undefined) ? ((extrap.duration !== undefined ) ? extrap.duration : 'fast') : 'fast';
257 var time = (extrap !== undefined) ? ((extrap.duration !== undefined ) ? extrap.duration : 'fast') : 'fast';
261 collapse_time(time);
258 collapse_time(time);
262 });
259 });
263
260
264 var expand_time = function (time) {
261 var expand_time = function (time) {
265 var app_height = $('#ipython-main-app').height(); // content height
262 var app_height = $('#ipython-main-app').height(); // content height
266 var splitter_height = $('div#pager_splitter').outerHeight(true);
263 var splitter_height = $('div#pager_splitter').outerHeight(true);
267 var pager_height = $('div#pager').outerHeight(true);
264 var pager_height = $('div#pager').outerHeight(true);
268 var new_height = app_height - pager_height - splitter_height;
265 var new_height = app_height - pager_height - splitter_height;
269 that.element.animate({height : new_height + 'px'}, time);
266 that.element.animate({height : new_height + 'px'}, time);
270 };
267 };
271
268
272 this.element.bind('expand_pager', function (event, extrap) {
269 this.element.bind('expand_pager', function (event, extrap) {
273 var time = (extrap !== undefined) ? ((extrap.duration !== undefined ) ? extrap.duration : 'fast') : 'fast';
270 var time = (extrap !== undefined) ? ((extrap.duration !== undefined ) ? extrap.duration : 'fast') : 'fast';
274 expand_time(time);
271 expand_time(time);
275 });
272 });
276
273
277 // Firefox 22 broke $(window).on("beforeunload")
274 // Firefox 22 broke $(window).on("beforeunload")
278 // I'm not sure why or how.
275 // I'm not sure why or how.
279 window.onbeforeunload = function (e) {
276 window.onbeforeunload = function (e) {
280 // TODO: Make killing the kernel configurable.
277 // TODO: Make killing the kernel configurable.
281 var kill_kernel = false;
278 var kill_kernel = false;
282 if (kill_kernel) {
279 if (kill_kernel) {
283 that.session.delete();
280 that.session.delete();
284 }
281 }
285 // if we are autosaving, trigger an autosave on nav-away.
282 // if we are autosaving, trigger an autosave on nav-away.
286 // still warn, because if we don't the autosave may fail.
283 // still warn, because if we don't the autosave may fail.
287 if (that.dirty) {
284 if (that.dirty) {
288 if ( that.autosave_interval ) {
285 if ( that.autosave_interval ) {
289 // schedule autosave in a timeout
286 // schedule autosave in a timeout
290 // this gives you a chance to forcefully discard changes
287 // this gives you a chance to forcefully discard changes
291 // by reloading the page if you *really* want to.
288 // by reloading the page if you *really* want to.
292 // the timer doesn't start until you *dismiss* the dialog.
289 // the timer doesn't start until you *dismiss* the dialog.
293 setTimeout(function () {
290 setTimeout(function () {
294 if (that.dirty) {
291 if (that.dirty) {
295 that.save_notebook();
292 that.save_notebook();
296 }
293 }
297 }, 1000);
294 }, 1000);
298 return "Autosave in progress, latest changes may be lost.";
295 return "Autosave in progress, latest changes may be lost.";
299 } else {
296 } else {
300 return "Unsaved changes will be lost.";
297 return "Unsaved changes will be lost.";
301 }
298 }
302 }
299 }
303 // Null is the *only* return value that will make the browser not
300 // Null is the *only* return value that will make the browser not
304 // pop up the "don't leave" dialog.
301 // pop up the "don't leave" dialog.
305 return null;
302 return null;
306 };
303 };
307 };
304 };
308
305
309 /**
306 /**
310 * Set the dirty flag, and trigger the set_dirty.Notebook event
307 * Set the dirty flag, and trigger the set_dirty.Notebook event
311 *
308 *
312 * @method set_dirty
309 * @method set_dirty
313 */
310 */
314 Notebook.prototype.set_dirty = function (value) {
311 Notebook.prototype.set_dirty = function (value) {
315 if (value === undefined) {
312 if (value === undefined) {
316 value = true;
313 value = true;
317 }
314 }
318 if (this.dirty == value) {
315 if (this.dirty == value) {
319 return;
316 return;
320 }
317 }
321 this.events.trigger('set_dirty.Notebook', {value: value});
318 this.events.trigger('set_dirty.Notebook', {value: value});
322 };
319 };
323
320
324 /**
321 /**
325 * Scroll the top of the page to a given cell.
322 * Scroll the top of the page to a given cell.
326 *
323 *
327 * @method scroll_to_cell
324 * @method scroll_to_cell
328 * @param {Number} cell_number An index of the cell to view
325 * @param {Number} cell_number An index of the cell to view
329 * @param {Number} time Animation time in milliseconds
326 * @param {Number} time Animation time in milliseconds
330 * @return {Number} Pixel offset from the top of the container
327 * @return {Number} Pixel offset from the top of the container
331 */
328 */
332 Notebook.prototype.scroll_to_cell = function (cell_number, time) {
329 Notebook.prototype.scroll_to_cell = function (cell_number, time) {
333 var cells = this.get_cells();
330 var cells = this.get_cells();
334 time = time || 0;
331 time = time || 0;
335 cell_number = Math.min(cells.length-1,cell_number);
332 cell_number = Math.min(cells.length-1,cell_number);
336 cell_number = Math.max(0 ,cell_number);
333 cell_number = Math.max(0 ,cell_number);
337 var scroll_value = cells[cell_number].element.position().top-cells[0].element.position().top ;
334 var scroll_value = cells[cell_number].element.position().top-cells[0].element.position().top ;
338 this.element.animate({scrollTop:scroll_value}, time);
335 this.element.animate({scrollTop:scroll_value}, time);
339 return scroll_value;
336 return scroll_value;
340 };
337 };
341
338
342 /**
339 /**
343 * Scroll to the bottom of the page.
340 * Scroll to the bottom of the page.
344 *
341 *
345 * @method scroll_to_bottom
342 * @method scroll_to_bottom
346 */
343 */
347 Notebook.prototype.scroll_to_bottom = function () {
344 Notebook.prototype.scroll_to_bottom = function () {
348 this.element.animate({scrollTop:this.element.get(0).scrollHeight}, 0);
345 this.element.animate({scrollTop:this.element.get(0).scrollHeight}, 0);
349 };
346 };
350
347
351 /**
348 /**
352 * Scroll to the top of the page.
349 * Scroll to the top of the page.
353 *
350 *
354 * @method scroll_to_top
351 * @method scroll_to_top
355 */
352 */
356 Notebook.prototype.scroll_to_top = function () {
353 Notebook.prototype.scroll_to_top = function () {
357 this.element.animate({scrollTop:0}, 0);
354 this.element.animate({scrollTop:0}, 0);
358 };
355 };
359
356
360 // Edit Notebook metadata
357 // Edit Notebook metadata
361
358
362 Notebook.prototype.edit_metadata = function () {
359 Notebook.prototype.edit_metadata = function () {
363 var that = this;
360 var that = this;
364 dialog.edit_metadata({
361 dialog.edit_metadata({
365 md: this.metadata,
362 md: this.metadata,
366 callback: function (md) {
363 callback: function (md) {
367 that.metadata = md;
364 that.metadata = md;
368 },
365 },
369 name: 'Notebook',
366 name: 'Notebook',
370 notebook: this,
367 notebook: this,
371 keyboard_manager: this.keyboard_manager});
368 keyboard_manager: this.keyboard_manager});
372 };
369 };
373
370
374 // Cell indexing, retrieval, etc.
371 // Cell indexing, retrieval, etc.
375
372
376 /**
373 /**
377 * Get all cell elements in the notebook.
374 * Get all cell elements in the notebook.
378 *
375 *
379 * @method get_cell_elements
376 * @method get_cell_elements
380 * @return {jQuery} A selector of all cell elements
377 * @return {jQuery} A selector of all cell elements
381 */
378 */
382 Notebook.prototype.get_cell_elements = function () {
379 Notebook.prototype.get_cell_elements = function () {
383 return this.container.children("div.cell");
380 return this.container.children("div.cell");
384 };
381 };
385
382
386 /**
383 /**
387 * Get a particular cell element.
384 * Get a particular cell element.
388 *
385 *
389 * @method get_cell_element
386 * @method get_cell_element
390 * @param {Number} index An index of a cell to select
387 * @param {Number} index An index of a cell to select
391 * @return {jQuery} A selector of the given cell.
388 * @return {jQuery} A selector of the given cell.
392 */
389 */
393 Notebook.prototype.get_cell_element = function (index) {
390 Notebook.prototype.get_cell_element = function (index) {
394 var result = null;
391 var result = null;
395 var e = this.get_cell_elements().eq(index);
392 var e = this.get_cell_elements().eq(index);
396 if (e.length !== 0) {
393 if (e.length !== 0) {
397 result = e;
394 result = e;
398 }
395 }
399 return result;
396 return result;
400 };
397 };
401
398
402 /**
399 /**
403 * Try to get a particular cell by msg_id.
400 * Try to get a particular cell by msg_id.
404 *
401 *
405 * @method get_msg_cell
402 * @method get_msg_cell
406 * @param {String} msg_id A message UUID
403 * @param {String} msg_id A message UUID
407 * @return {Cell} Cell or null if no cell was found.
404 * @return {Cell} Cell or null if no cell was found.
408 */
405 */
409 Notebook.prototype.get_msg_cell = function (msg_id) {
406 Notebook.prototype.get_msg_cell = function (msg_id) {
410 return codecell.CodeCell.msg_cells[msg_id] || null;
407 return codecell.CodeCell.msg_cells[msg_id] || null;
411 };
408 };
412
409
413 /**
410 /**
414 * Count the cells in this notebook.
411 * Count the cells in this notebook.
415 *
412 *
416 * @method ncells
413 * @method ncells
417 * @return {Number} The number of cells in this notebook
414 * @return {Number} The number of cells in this notebook
418 */
415 */
419 Notebook.prototype.ncells = function () {
416 Notebook.prototype.ncells = function () {
420 return this.get_cell_elements().length;
417 return this.get_cell_elements().length;
421 };
418 };
422
419
423 /**
420 /**
424 * Get all Cell objects in this notebook.
421 * Get all Cell objects in this notebook.
425 *
422 *
426 * @method get_cells
423 * @method get_cells
427 * @return {Array} This notebook's Cell objects
424 * @return {Array} This notebook's Cell objects
428 */
425 */
429 // TODO: we are often calling cells as cells()[i], which we should optimize
426 // TODO: we are often calling cells as cells()[i], which we should optimize
430 // to cells(i) or a new method.
427 // to cells(i) or a new method.
431 Notebook.prototype.get_cells = function () {
428 Notebook.prototype.get_cells = function () {
432 return this.get_cell_elements().toArray().map(function (e) {
429 return this.get_cell_elements().toArray().map(function (e) {
433 return $(e).data("cell");
430 return $(e).data("cell");
434 });
431 });
435 };
432 };
436
433
437 /**
434 /**
438 * Get a Cell object from this notebook.
435 * Get a Cell object from this notebook.
439 *
436 *
440 * @method get_cell
437 * @method get_cell
441 * @param {Number} index An index of a cell to retrieve
438 * @param {Number} index An index of a cell to retrieve
442 * @return {Cell} Cell or null if no cell was found.
439 * @return {Cell} Cell or null if no cell was found.
443 */
440 */
444 Notebook.prototype.get_cell = function (index) {
441 Notebook.prototype.get_cell = function (index) {
445 var result = null;
442 var result = null;
446 var ce = this.get_cell_element(index);
443 var ce = this.get_cell_element(index);
447 if (ce !== null) {
444 if (ce !== null) {
448 result = ce.data('cell');
445 result = ce.data('cell');
449 }
446 }
450 return result;
447 return result;
451 };
448 };
452
449
453 /**
450 /**
454 * Get the cell below a given cell.
451 * Get the cell below a given cell.
455 *
452 *
456 * @method get_next_cell
453 * @method get_next_cell
457 * @param {Cell} cell The provided cell
454 * @param {Cell} cell The provided cell
458 * @return {Cell} the next cell or null if no cell was found.
455 * @return {Cell} the next cell or null if no cell was found.
459 */
456 */
460 Notebook.prototype.get_next_cell = function (cell) {
457 Notebook.prototype.get_next_cell = function (cell) {
461 var result = null;
458 var result = null;
462 var index = this.find_cell_index(cell);
459 var index = this.find_cell_index(cell);
463 if (this.is_valid_cell_index(index+1)) {
460 if (this.is_valid_cell_index(index+1)) {
464 result = this.get_cell(index+1);
461 result = this.get_cell(index+1);
465 }
462 }
466 return result;
463 return result;
467 };
464 };
468
465
469 /**
466 /**
470 * Get the cell above a given cell.
467 * Get the cell above a given cell.
471 *
468 *
472 * @method get_prev_cell
469 * @method get_prev_cell
473 * @param {Cell} cell The provided cell
470 * @param {Cell} cell The provided cell
474 * @return {Cell} The previous cell or null if no cell was found.
471 * @return {Cell} The previous cell or null if no cell was found.
475 */
472 */
476 Notebook.prototype.get_prev_cell = function (cell) {
473 Notebook.prototype.get_prev_cell = function (cell) {
477 var result = null;
474 var result = null;
478 var index = this.find_cell_index(cell);
475 var index = this.find_cell_index(cell);
479 if (index !== null && index > 0) {
476 if (index !== null && index > 0) {
480 result = this.get_cell(index-1);
477 result = this.get_cell(index-1);
481 }
478 }
482 return result;
479 return result;
483 };
480 };
484
481
485 /**
482 /**
486 * Get the numeric index of a given cell.
483 * Get the numeric index of a given cell.
487 *
484 *
488 * @method find_cell_index
485 * @method find_cell_index
489 * @param {Cell} cell The provided cell
486 * @param {Cell} cell The provided cell
490 * @return {Number} The cell's numeric index or null if no cell was found.
487 * @return {Number} The cell's numeric index or null if no cell was found.
491 */
488 */
492 Notebook.prototype.find_cell_index = function (cell) {
489 Notebook.prototype.find_cell_index = function (cell) {
493 var result = null;
490 var result = null;
494 this.get_cell_elements().filter(function (index) {
491 this.get_cell_elements().filter(function (index) {
495 if ($(this).data("cell") === cell) {
492 if ($(this).data("cell") === cell) {
496 result = index;
493 result = index;
497 }
494 }
498 });
495 });
499 return result;
496 return result;
500 };
497 };
501
498
502 /**
499 /**
503 * Get a given index , or the selected index if none is provided.
500 * Get a given index , or the selected index if none is provided.
504 *
501 *
505 * @method index_or_selected
502 * @method index_or_selected
506 * @param {Number} index A cell's index
503 * @param {Number} index A cell's index
507 * @return {Number} The given index, or selected index if none is provided.
504 * @return {Number} The given index, or selected index if none is provided.
508 */
505 */
509 Notebook.prototype.index_or_selected = function (index) {
506 Notebook.prototype.index_or_selected = function (index) {
510 var i;
507 var i;
511 if (index === undefined || index === null) {
508 if (index === undefined || index === null) {
512 i = this.get_selected_index();
509 i = this.get_selected_index();
513 if (i === null) {
510 if (i === null) {
514 i = 0;
511 i = 0;
515 }
512 }
516 } else {
513 } else {
517 i = index;
514 i = index;
518 }
515 }
519 return i;
516 return i;
520 };
517 };
521
518
522 /**
519 /**
523 * Get the currently selected cell.
520 * Get the currently selected cell.
524 * @method get_selected_cell
521 * @method get_selected_cell
525 * @return {Cell} The selected cell
522 * @return {Cell} The selected cell
526 */
523 */
527 Notebook.prototype.get_selected_cell = function () {
524 Notebook.prototype.get_selected_cell = function () {
528 var index = this.get_selected_index();
525 var index = this.get_selected_index();
529 return this.get_cell(index);
526 return this.get_cell(index);
530 };
527 };
531
528
532 /**
529 /**
533 * Check whether a cell index is valid.
530 * Check whether a cell index is valid.
534 *
531 *
535 * @method is_valid_cell_index
532 * @method is_valid_cell_index
536 * @param {Number} index A cell index
533 * @param {Number} index A cell index
537 * @return True if the index is valid, false otherwise
534 * @return True if the index is valid, false otherwise
538 */
535 */
539 Notebook.prototype.is_valid_cell_index = function (index) {
536 Notebook.prototype.is_valid_cell_index = function (index) {
540 if (index !== null && index >= 0 && index < this.ncells()) {
537 if (index !== null && index >= 0 && index < this.ncells()) {
541 return true;
538 return true;
542 } else {
539 } else {
543 return false;
540 return false;
544 }
541 }
545 };
542 };
546
543
547 /**
544 /**
548 * Get the index of the currently selected cell.
545 * Get the index of the currently selected cell.
549
546
550 * @method get_selected_index
547 * @method get_selected_index
551 * @return {Number} The selected cell's numeric index
548 * @return {Number} The selected cell's numeric index
552 */
549 */
553 Notebook.prototype.get_selected_index = function () {
550 Notebook.prototype.get_selected_index = function () {
554 var result = null;
551 var result = null;
555 this.get_cell_elements().filter(function (index) {
552 this.get_cell_elements().filter(function (index) {
556 if ($(this).data("cell").selected === true) {
553 if ($(this).data("cell").selected === true) {
557 result = index;
554 result = index;
558 }
555 }
559 });
556 });
560 return result;
557 return result;
561 };
558 };
562
559
563
560
564 // Cell selection.
561 // Cell selection.
565
562
566 /**
563 /**
567 * Programmatically select a cell.
564 * Programmatically select a cell.
568 *
565 *
569 * @method select
566 * @method select
570 * @param {Number} index A cell's index
567 * @param {Number} index A cell's index
571 * @return {Notebook} This notebook
568 * @return {Notebook} This notebook
572 */
569 */
573 Notebook.prototype.select = function (index) {
570 Notebook.prototype.select = function (index) {
574 if (this.is_valid_cell_index(index)) {
571 if (this.is_valid_cell_index(index)) {
575 var sindex = this.get_selected_index();
572 var sindex = this.get_selected_index();
576 if (sindex !== null && index !== sindex) {
573 if (sindex !== null && index !== sindex) {
577 // If we are about to select a different cell, make sure we are
574 // If we are about to select a different cell, make sure we are
578 // first in command mode.
575 // first in command mode.
579 if (this.mode !== 'command') {
576 if (this.mode !== 'command') {
580 this.command_mode();
577 this.command_mode();
581 }
578 }
582 this.get_cell(sindex).unselect();
579 this.get_cell(sindex).unselect();
583 }
580 }
584 var cell = this.get_cell(index);
581 var cell = this.get_cell(index);
585 cell.select();
582 cell.select();
586 if (cell.cell_type === 'heading') {
583 if (cell.cell_type === 'heading') {
587 this.events.trigger('selected_cell_type_changed.Notebook',
584 this.events.trigger('selected_cell_type_changed.Notebook',
588 {'cell_type':cell.cell_type,level:cell.level}
585 {'cell_type':cell.cell_type,level:cell.level}
589 );
586 );
590 } else {
587 } else {
591 this.events.trigger('selected_cell_type_changed.Notebook',
588 this.events.trigger('selected_cell_type_changed.Notebook',
592 {'cell_type':cell.cell_type}
589 {'cell_type':cell.cell_type}
593 );
590 );
594 }
591 }
595 }
592 }
596 return this;
593 return this;
597 };
594 };
598
595
599 /**
596 /**
600 * Programmatically select the next cell.
597 * Programmatically select the next cell.
601 *
598 *
602 * @method select_next
599 * @method select_next
603 * @return {Notebook} This notebook
600 * @return {Notebook} This notebook
604 */
601 */
605 Notebook.prototype.select_next = function () {
602 Notebook.prototype.select_next = function () {
606 var index = this.get_selected_index();
603 var index = this.get_selected_index();
607 this.select(index+1);
604 this.select(index+1);
608 return this;
605 return this;
609 };
606 };
610
607
611 /**
608 /**
612 * Programmatically select the previous cell.
609 * Programmatically select the previous cell.
613 *
610 *
614 * @method select_prev
611 * @method select_prev
615 * @return {Notebook} This notebook
612 * @return {Notebook} This notebook
616 */
613 */
617 Notebook.prototype.select_prev = function () {
614 Notebook.prototype.select_prev = function () {
618 var index = this.get_selected_index();
615 var index = this.get_selected_index();
619 this.select(index-1);
616 this.select(index-1);
620 return this;
617 return this;
621 };
618 };
622
619
623
620
624 // Edit/Command mode
621 // Edit/Command mode
625
622
626 /**
623 /**
627 * Gets the index of the cell that is in edit mode.
624 * Gets the index of the cell that is in edit mode.
628 *
625 *
629 * @method get_edit_index
626 * @method get_edit_index
630 *
627 *
631 * @return index {int}
628 * @return index {int}
632 **/
629 **/
633 Notebook.prototype.get_edit_index = function () {
630 Notebook.prototype.get_edit_index = function () {
634 var result = null;
631 var result = null;
635 this.get_cell_elements().filter(function (index) {
632 this.get_cell_elements().filter(function (index) {
636 if ($(this).data("cell").mode === 'edit') {
633 if ($(this).data("cell").mode === 'edit') {
637 result = index;
634 result = index;
638 }
635 }
639 });
636 });
640 return result;
637 return result;
641 };
638 };
642
639
643 /**
640 /**
644 * Handle when a a cell blurs and the notebook should enter command mode.
641 * Handle when a a cell blurs and the notebook should enter command mode.
645 *
642 *
646 * @method handle_command_mode
643 * @method handle_command_mode
647 * @param [cell] {Cell} Cell to enter command mode on.
644 * @param [cell] {Cell} Cell to enter command mode on.
648 **/
645 **/
649 Notebook.prototype.handle_command_mode = function (cell) {
646 Notebook.prototype.handle_command_mode = function (cell) {
650 if (this.mode !== 'command') {
647 if (this.mode !== 'command') {
651 cell.command_mode();
648 cell.command_mode();
652 this.mode = 'command';
649 this.mode = 'command';
653 this.events.trigger('command_mode.Notebook');
650 this.events.trigger('command_mode.Notebook');
654 this.keyboard_manager.command_mode();
651 this.keyboard_manager.command_mode();
655 }
652 }
656 };
653 };
657
654
658 /**
655 /**
659 * Make the notebook enter command mode.
656 * Make the notebook enter command mode.
660 *
657 *
661 * @method command_mode
658 * @method command_mode
662 **/
659 **/
663 Notebook.prototype.command_mode = function () {
660 Notebook.prototype.command_mode = function () {
664 var cell = this.get_cell(this.get_edit_index());
661 var cell = this.get_cell(this.get_edit_index());
665 if (cell && this.mode !== 'command') {
662 if (cell && this.mode !== 'command') {
666 // We don't call cell.command_mode, but rather call cell.focus_cell()
663 // We don't call cell.command_mode, but rather call cell.focus_cell()
667 // which will blur and CM editor and trigger the call to
664 // which will blur and CM editor and trigger the call to
668 // handle_command_mode.
665 // handle_command_mode.
669 cell.focus_cell();
666 cell.focus_cell();
670 }
667 }
671 };
668 };
672
669
673 /**
670 /**
674 * Handle when a cell fires it's edit_mode event.
671 * Handle when a cell fires it's edit_mode event.
675 *
672 *
676 * @method handle_edit_mode
673 * @method handle_edit_mode
677 * @param [cell] {Cell} Cell to enter edit mode on.
674 * @param [cell] {Cell} Cell to enter edit mode on.
678 **/
675 **/
679 Notebook.prototype.handle_edit_mode = function (cell) {
676 Notebook.prototype.handle_edit_mode = function (cell) {
680 if (cell && this.mode !== 'edit') {
677 if (cell && this.mode !== 'edit') {
681 cell.edit_mode();
678 cell.edit_mode();
682 this.mode = 'edit';
679 this.mode = 'edit';
683 this.events.trigger('edit_mode.Notebook');
680 this.events.trigger('edit_mode.Notebook');
684 this.keyboard_manager.edit_mode();
681 this.keyboard_manager.edit_mode();
685 }
682 }
686 };
683 };
687
684
688 /**
685 /**
689 * Make a cell enter edit mode.
686 * Make a cell enter edit mode.
690 *
687 *
691 * @method edit_mode
688 * @method edit_mode
692 **/
689 **/
693 Notebook.prototype.edit_mode = function () {
690 Notebook.prototype.edit_mode = function () {
694 var cell = this.get_selected_cell();
691 var cell = this.get_selected_cell();
695 if (cell && this.mode !== 'edit') {
692 if (cell && this.mode !== 'edit') {
696 cell.unrender();
693 cell.unrender();
697 cell.focus_editor();
694 cell.focus_editor();
698 }
695 }
699 };
696 };
700
697
701 /**
698 /**
702 * Focus the currently selected cell.
699 * Focus the currently selected cell.
703 *
700 *
704 * @method focus_cell
701 * @method focus_cell
705 **/
702 **/
706 Notebook.prototype.focus_cell = function () {
703 Notebook.prototype.focus_cell = function () {
707 var cell = this.get_selected_cell();
704 var cell = this.get_selected_cell();
708 if (cell === null) {return;} // No cell is selected
705 if (cell === null) {return;} // No cell is selected
709 cell.focus_cell();
706 cell.focus_cell();
710 };
707 };
711
708
712 // Cell movement
709 // Cell movement
713
710
714 /**
711 /**
715 * Move given (or selected) cell up and select it.
712 * Move given (or selected) cell up and select it.
716 *
713 *
717 * @method move_cell_up
714 * @method move_cell_up
718 * @param [index] {integer} cell index
715 * @param [index] {integer} cell index
719 * @return {Notebook} This notebook
716 * @return {Notebook} This notebook
720 **/
717 **/
721 Notebook.prototype.move_cell_up = function (index) {
718 Notebook.prototype.move_cell_up = function (index) {
722 var i = this.index_or_selected(index);
719 var i = this.index_or_selected(index);
723 if (this.is_valid_cell_index(i) && i > 0) {
720 if (this.is_valid_cell_index(i) && i > 0) {
724 var pivot = this.get_cell_element(i-1);
721 var pivot = this.get_cell_element(i-1);
725 var tomove = this.get_cell_element(i);
722 var tomove = this.get_cell_element(i);
726 if (pivot !== null && tomove !== null) {
723 if (pivot !== null && tomove !== null) {
727 tomove.detach();
724 tomove.detach();
728 pivot.before(tomove);
725 pivot.before(tomove);
729 this.select(i-1);
726 this.select(i-1);
730 var cell = this.get_selected_cell();
727 var cell = this.get_selected_cell();
731 cell.focus_cell();
728 cell.focus_cell();
732 }
729 }
733 this.set_dirty(true);
730 this.set_dirty(true);
734 }
731 }
735 return this;
732 return this;
736 };
733 };
737
734
738
735
739 /**
736 /**
740 * Move given (or selected) cell down and select it
737 * Move given (or selected) cell down and select it
741 *
738 *
742 * @method move_cell_down
739 * @method move_cell_down
743 * @param [index] {integer} cell index
740 * @param [index] {integer} cell index
744 * @return {Notebook} This notebook
741 * @return {Notebook} This notebook
745 **/
742 **/
746 Notebook.prototype.move_cell_down = function (index) {
743 Notebook.prototype.move_cell_down = function (index) {
747 var i = this.index_or_selected(index);
744 var i = this.index_or_selected(index);
748 if (this.is_valid_cell_index(i) && this.is_valid_cell_index(i+1)) {
745 if (this.is_valid_cell_index(i) && this.is_valid_cell_index(i+1)) {
749 var pivot = this.get_cell_element(i+1);
746 var pivot = this.get_cell_element(i+1);
750 var tomove = this.get_cell_element(i);
747 var tomove = this.get_cell_element(i);
751 if (pivot !== null && tomove !== null) {
748 if (pivot !== null && tomove !== null) {
752 tomove.detach();
749 tomove.detach();
753 pivot.after(tomove);
750 pivot.after(tomove);
754 this.select(i+1);
751 this.select(i+1);
755 var cell = this.get_selected_cell();
752 var cell = this.get_selected_cell();
756 cell.focus_cell();
753 cell.focus_cell();
757 }
754 }
758 }
755 }
759 this.set_dirty();
756 this.set_dirty();
760 return this;
757 return this;
761 };
758 };
762
759
763
760
764 // Insertion, deletion.
761 // Insertion, deletion.
765
762
766 /**
763 /**
767 * Delete a cell from the notebook.
764 * Delete a cell from the notebook.
768 *
765 *
769 * @method delete_cell
766 * @method delete_cell
770 * @param [index] A cell's numeric index
767 * @param [index] A cell's numeric index
771 * @return {Notebook} This notebook
768 * @return {Notebook} This notebook
772 */
769 */
773 Notebook.prototype.delete_cell = function (index) {
770 Notebook.prototype.delete_cell = function (index) {
774 var i = this.index_or_selected(index);
771 var i = this.index_or_selected(index);
775 var cell = this.get_cell(i);
772 var cell = this.get_cell(i);
776 if (!cell.is_deletable()) {
773 if (!cell.is_deletable()) {
777 return this;
774 return this;
778 }
775 }
779
776
780 this.undelete_backup = cell.toJSON();
777 this.undelete_backup = cell.toJSON();
781 $('#undelete_cell').removeClass('disabled');
778 $('#undelete_cell').removeClass('disabled');
782 if (this.is_valid_cell_index(i)) {
779 if (this.is_valid_cell_index(i)) {
783 var old_ncells = this.ncells();
780 var old_ncells = this.ncells();
784 var ce = this.get_cell_element(i);
781 var ce = this.get_cell_element(i);
785 ce.remove();
782 ce.remove();
786 if (i === 0) {
783 if (i === 0) {
787 // Always make sure we have at least one cell.
784 // Always make sure we have at least one cell.
788 if (old_ncells === 1) {
785 if (old_ncells === 1) {
789 this.insert_cell_below('code');
786 this.insert_cell_below('code');
790 }
787 }
791 this.select(0);
788 this.select(0);
792 this.undelete_index = 0;
789 this.undelete_index = 0;
793 this.undelete_below = false;
790 this.undelete_below = false;
794 } else if (i === old_ncells-1 && i !== 0) {
791 } else if (i === old_ncells-1 && i !== 0) {
795 this.select(i-1);
792 this.select(i-1);
796 this.undelete_index = i - 1;
793 this.undelete_index = i - 1;
797 this.undelete_below = true;
794 this.undelete_below = true;
798 } else {
795 } else {
799 this.select(i);
796 this.select(i);
800 this.undelete_index = i;
797 this.undelete_index = i;
801 this.undelete_below = false;
798 this.undelete_below = false;
802 }
799 }
803 this.events.trigger('delete.Cell', {'cell': cell, 'index': i});
800 this.events.trigger('delete.Cell', {'cell': cell, 'index': i});
804 this.set_dirty(true);
801 this.set_dirty(true);
805 }
802 }
806 return this;
803 return this;
807 };
804 };
808
805
809 /**
806 /**
810 * Restore the most recently deleted cell.
807 * Restore the most recently deleted cell.
811 *
808 *
812 * @method undelete
809 * @method undelete
813 */
810 */
814 Notebook.prototype.undelete_cell = function() {
811 Notebook.prototype.undelete_cell = function() {
815 if (this.undelete_backup !== null && this.undelete_index !== null) {
812 if (this.undelete_backup !== null && this.undelete_index !== null) {
816 var current_index = this.get_selected_index();
813 var current_index = this.get_selected_index();
817 if (this.undelete_index < current_index) {
814 if (this.undelete_index < current_index) {
818 current_index = current_index + 1;
815 current_index = current_index + 1;
819 }
816 }
820 if (this.undelete_index >= this.ncells()) {
817 if (this.undelete_index >= this.ncells()) {
821 this.select(this.ncells() - 1);
818 this.select(this.ncells() - 1);
822 }
819 }
823 else {
820 else {
824 this.select(this.undelete_index);
821 this.select(this.undelete_index);
825 }
822 }
826 var cell_data = this.undelete_backup;
823 var cell_data = this.undelete_backup;
827 var new_cell = null;
824 var new_cell = null;
828 if (this.undelete_below) {
825 if (this.undelete_below) {
829 new_cell = this.insert_cell_below(cell_data.cell_type);
826 new_cell = this.insert_cell_below(cell_data.cell_type);
830 } else {
827 } else {
831 new_cell = this.insert_cell_above(cell_data.cell_type);
828 new_cell = this.insert_cell_above(cell_data.cell_type);
832 }
829 }
833 new_cell.fromJSON(cell_data);
830 new_cell.fromJSON(cell_data);
834 if (this.undelete_below) {
831 if (this.undelete_below) {
835 this.select(current_index+1);
832 this.select(current_index+1);
836 } else {
833 } else {
837 this.select(current_index);
834 this.select(current_index);
838 }
835 }
839 this.undelete_backup = null;
836 this.undelete_backup = null;
840 this.undelete_index = null;
837 this.undelete_index = null;
841 }
838 }
842 $('#undelete_cell').addClass('disabled');
839 $('#undelete_cell').addClass('disabled');
843 };
840 };
844
841
845 /**
842 /**
846 * Insert a cell so that after insertion the cell is at given index.
843 * Insert a cell so that after insertion the cell is at given index.
847 *
844 *
848 * If cell type is not provided, it will default to the type of the
845 * If cell type is not provided, it will default to the type of the
849 * currently active cell.
846 * currently active cell.
850 *
847 *
851 * Similar to insert_above, but index parameter is mandatory
848 * Similar to insert_above, but index parameter is mandatory
852 *
849 *
853 * Index will be brought back into the accessible range [0,n]
850 * Index will be brought back into the accessible range [0,n]
854 *
851 *
855 * @method insert_cell_at_index
852 * @method insert_cell_at_index
856 * @param [type] {string} in ['code','markdown', 'raw'], defaults to 'code'
853 * @param [type] {string} in ['code','markdown', 'raw'], defaults to 'code'
857 * @param [index] {int} a valid index where to insert cell
854 * @param [index] {int} a valid index where to insert cell
858 *
855 *
859 * @return cell {cell|null} created cell or null
856 * @return cell {cell|null} created cell or null
860 **/
857 **/
861 Notebook.prototype.insert_cell_at_index = function(type, index){
858 Notebook.prototype.insert_cell_at_index = function(type, index){
862
859
863 var ncells = this.ncells();
860 var ncells = this.ncells();
864 index = Math.min(index, ncells);
861 index = Math.min(index, ncells);
865 index = Math.max(index, 0);
862 index = Math.max(index, 0);
866 var cell = null;
863 var cell = null;
867 type = type || this.default_cell_type;
864 type = type || this.default_cell_type;
868 if (type === 'above') {
865 if (type === 'above') {
869 if (index > 0) {
866 if (index > 0) {
870 type = this.get_cell(index-1).cell_type;
867 type = this.get_cell(index-1).cell_type;
871 } else {
868 } else {
872 type = 'code';
869 type = 'code';
873 }
870 }
874 } else if (type === 'below') {
871 } else if (type === 'below') {
875 if (index < ncells) {
872 if (index < ncells) {
876 type = this.get_cell(index).cell_type;
873 type = this.get_cell(index).cell_type;
877 } else {
874 } else {
878 type = 'code';
875 type = 'code';
879 }
876 }
880 } else if (type === 'selected') {
877 } else if (type === 'selected') {
881 type = this.get_selected_cell().cell_type;
878 type = this.get_selected_cell().cell_type;
882 }
879 }
883
880
884 if (ncells === 0 || this.is_valid_cell_index(index) || index === ncells) {
881 if (ncells === 0 || this.is_valid_cell_index(index) || index === ncells) {
885 var cell_options = {
882 var cell_options = {
886 events: this.events,
883 events: this.events,
887 config: this.config,
884 config: this.config,
888 keyboard_manager: this.keyboard_manager,
885 keyboard_manager: this.keyboard_manager,
889 notebook: this,
886 notebook: this,
890 tooltip: this.tooltip
887 tooltip: this.tooltip
891 };
888 };
892 switch(type) {
889 switch(type) {
893 case 'code':
890 case 'code':
894 cell = new codecell.CodeCell(this.kernel, cell_options);
891 cell = new codecell.CodeCell(this.kernel, cell_options);
895 cell.set_input_prompt();
892 cell.set_input_prompt();
896 break;
893 break;
897 case 'markdown':
894 case 'markdown':
898 cell = new textcell.MarkdownCell(cell_options);
895 cell = new textcell.MarkdownCell(cell_options);
899 break;
896 break;
900 case 'raw':
897 case 'raw':
901 cell = new textcell.RawCell(cell_options);
898 cell = new textcell.RawCell(cell_options);
902 break;
899 break;
903 default:
900 default:
904 console.log("invalid cell type: ", type);
901 console.log("invalid cell type: ", type);
905 }
902 }
906
903
907 if(this._insert_element_at_index(cell.element,index)) {
904 if(this._insert_element_at_index(cell.element,index)) {
908 cell.render();
905 cell.render();
909 this.events.trigger('create.Cell', {'cell': cell, 'index': index});
906 this.events.trigger('create.Cell', {'cell': cell, 'index': index});
910 cell.refresh();
907 cell.refresh();
911 // We used to select the cell after we refresh it, but there
908 // We used to select the cell after we refresh it, but there
912 // are now cases were this method is called where select is
909 // are now cases were this method is called where select is
913 // not appropriate. The selection logic should be handled by the
910 // not appropriate. The selection logic should be handled by the
914 // caller of the the top level insert_cell methods.
911 // caller of the the top level insert_cell methods.
915 this.set_dirty(true);
912 this.set_dirty(true);
916 }
913 }
917 }
914 }
918 return cell;
915 return cell;
919
916
920 };
917 };
921
918
922 /**
919 /**
923 * Insert an element at given cell index.
920 * Insert an element at given cell index.
924 *
921 *
925 * @method _insert_element_at_index
922 * @method _insert_element_at_index
926 * @param element {dom_element} a cell element
923 * @param element {dom_element} a cell element
927 * @param [index] {int} a valid index where to inser cell
924 * @param [index] {int} a valid index where to inser cell
928 * @private
925 * @private
929 *
926 *
930 * return true if everything whent fine.
927 * return true if everything whent fine.
931 **/
928 **/
932 Notebook.prototype._insert_element_at_index = function(element, index){
929 Notebook.prototype._insert_element_at_index = function(element, index){
933 if (element === undefined){
930 if (element === undefined){
934 return false;
931 return false;
935 }
932 }
936
933
937 var ncells = this.ncells();
934 var ncells = this.ncells();
938
935
939 if (ncells === 0) {
936 if (ncells === 0) {
940 // special case append if empty
937 // special case append if empty
941 this.element.find('div.end_space').before(element);
938 this.element.find('div.end_space').before(element);
942 } else if ( ncells === index ) {
939 } else if ( ncells === index ) {
943 // special case append it the end, but not empty
940 // special case append it the end, but not empty
944 this.get_cell_element(index-1).after(element);
941 this.get_cell_element(index-1).after(element);
945 } else if (this.is_valid_cell_index(index)) {
942 } else if (this.is_valid_cell_index(index)) {
946 // otherwise always somewhere to append to
943 // otherwise always somewhere to append to
947 this.get_cell_element(index).before(element);
944 this.get_cell_element(index).before(element);
948 } else {
945 } else {
949 return false;
946 return false;
950 }
947 }
951
948
952 if (this.undelete_index !== null && index <= this.undelete_index) {
949 if (this.undelete_index !== null && index <= this.undelete_index) {
953 this.undelete_index = this.undelete_index + 1;
950 this.undelete_index = this.undelete_index + 1;
954 this.set_dirty(true);
951 this.set_dirty(true);
955 }
952 }
956 return true;
953 return true;
957 };
954 };
958
955
959 /**
956 /**
960 * Insert a cell of given type above given index, or at top
957 * Insert a cell of given type above given index, or at top
961 * of notebook if index smaller than 0.
958 * of notebook if index smaller than 0.
962 *
959 *
963 * default index value is the one of currently selected cell
960 * default index value is the one of currently selected cell
964 *
961 *
965 * @method insert_cell_above
962 * @method insert_cell_above
966 * @param [type] {string} cell type
963 * @param [type] {string} cell type
967 * @param [index] {integer}
964 * @param [index] {integer}
968 *
965 *
969 * @return handle to created cell or null
966 * @return handle to created cell or null
970 **/
967 **/
971 Notebook.prototype.insert_cell_above = function (type, index) {
968 Notebook.prototype.insert_cell_above = function (type, index) {
972 index = this.index_or_selected(index);
969 index = this.index_or_selected(index);
973 return this.insert_cell_at_index(type, index);
970 return this.insert_cell_at_index(type, index);
974 };
971 };
975
972
976 /**
973 /**
977 * Insert a cell of given type below given index, or at bottom
974 * Insert a cell of given type below given index, or at bottom
978 * of notebook if index greater than number of cells
975 * of notebook if index greater than number of cells
979 *
976 *
980 * default index value is the one of currently selected cell
977 * default index value is the one of currently selected cell
981 *
978 *
982 * @method insert_cell_below
979 * @method insert_cell_below
983 * @param [type] {string} cell type
980 * @param [type] {string} cell type
984 * @param [index] {integer}
981 * @param [index] {integer}
985 *
982 *
986 * @return handle to created cell or null
983 * @return handle to created cell or null
987 *
984 *
988 **/
985 **/
989 Notebook.prototype.insert_cell_below = function (type, index) {
986 Notebook.prototype.insert_cell_below = function (type, index) {
990 index = this.index_or_selected(index);
987 index = this.index_or_selected(index);
991 return this.insert_cell_at_index(type, index+1);
988 return this.insert_cell_at_index(type, index+1);
992 };
989 };
993
990
994
991
995 /**
992 /**
996 * Insert cell at end of notebook
993 * Insert cell at end of notebook
997 *
994 *
998 * @method insert_cell_at_bottom
995 * @method insert_cell_at_bottom
999 * @param {String} type cell type
996 * @param {String} type cell type
1000 *
997 *
1001 * @return the added cell; or null
998 * @return the added cell; or null
1002 **/
999 **/
1003 Notebook.prototype.insert_cell_at_bottom = function (type){
1000 Notebook.prototype.insert_cell_at_bottom = function (type){
1004 var len = this.ncells();
1001 var len = this.ncells();
1005 return this.insert_cell_below(type,len-1);
1002 return this.insert_cell_below(type,len-1);
1006 };
1003 };
1007
1004
1008 /**
1005 /**
1009 * Turn a cell into a code cell.
1006 * Turn a cell into a code cell.
1010 *
1007 *
1011 * @method to_code
1008 * @method to_code
1012 * @param {Number} [index] A cell's index
1009 * @param {Number} [index] A cell's index
1013 */
1010 */
1014 Notebook.prototype.to_code = function (index) {
1011 Notebook.prototype.to_code = function (index) {
1015 var i = this.index_or_selected(index);
1012 var i = this.index_or_selected(index);
1016 if (this.is_valid_cell_index(i)) {
1013 if (this.is_valid_cell_index(i)) {
1017 var source_cell = this.get_cell(i);
1014 var source_cell = this.get_cell(i);
1018 if (!(source_cell instanceof codecell.CodeCell)) {
1015 if (!(source_cell instanceof codecell.CodeCell)) {
1019 var target_cell = this.insert_cell_below('code',i);
1016 var target_cell = this.insert_cell_below('code',i);
1020 var text = source_cell.get_text();
1017 var text = source_cell.get_text();
1021 if (text === source_cell.placeholder) {
1018 if (text === source_cell.placeholder) {
1022 text = '';
1019 text = '';
1023 }
1020 }
1024 //metadata
1021 //metadata
1025 target_cell.metadata = source_cell.metadata;
1022 target_cell.metadata = source_cell.metadata;
1026
1023
1027 target_cell.set_text(text);
1024 target_cell.set_text(text);
1028 // make this value the starting point, so that we can only undo
1025 // make this value the starting point, so that we can only undo
1029 // to this state, instead of a blank cell
1026 // to this state, instead of a blank cell
1030 target_cell.code_mirror.clearHistory();
1027 target_cell.code_mirror.clearHistory();
1031 source_cell.element.remove();
1028 source_cell.element.remove();
1032 this.select(i);
1029 this.select(i);
1033 var cursor = source_cell.code_mirror.getCursor();
1030 var cursor = source_cell.code_mirror.getCursor();
1034 target_cell.code_mirror.setCursor(cursor);
1031 target_cell.code_mirror.setCursor(cursor);
1035 this.set_dirty(true);
1032 this.set_dirty(true);
1036 }
1033 }
1037 }
1034 }
1038 };
1035 };
1039
1036
1040 /**
1037 /**
1041 * Turn a cell into a Markdown cell.
1038 * Turn a cell into a Markdown cell.
1042 *
1039 *
1043 * @method to_markdown
1040 * @method to_markdown
1044 * @param {Number} [index] A cell's index
1041 * @param {Number} [index] A cell's index
1045 */
1042 */
1046 Notebook.prototype.to_markdown = function (index) {
1043 Notebook.prototype.to_markdown = function (index) {
1047 var i = this.index_or_selected(index);
1044 var i = this.index_or_selected(index);
1048 if (this.is_valid_cell_index(i)) {
1045 if (this.is_valid_cell_index(i)) {
1049 var source_cell = this.get_cell(i);
1046 var source_cell = this.get_cell(i);
1050
1047
1051 if (!(source_cell instanceof textcell.MarkdownCell)) {
1048 if (!(source_cell instanceof textcell.MarkdownCell)) {
1052 var target_cell = this.insert_cell_below('markdown',i);
1049 var target_cell = this.insert_cell_below('markdown',i);
1053 var text = source_cell.get_text();
1050 var text = source_cell.get_text();
1054
1051
1055 if (text === source_cell.placeholder) {
1052 if (text === source_cell.placeholder) {
1056 text = '';
1053 text = '';
1057 }
1054 }
1058 // metadata
1055 // metadata
1059 target_cell.metadata = source_cell.metadata;
1056 target_cell.metadata = source_cell.metadata;
1060 // We must show the editor before setting its contents
1057 // We must show the editor before setting its contents
1061 target_cell.unrender();
1058 target_cell.unrender();
1062 target_cell.set_text(text);
1059 target_cell.set_text(text);
1063 // make this value the starting point, so that we can only undo
1060 // make this value the starting point, so that we can only undo
1064 // to this state, instead of a blank cell
1061 // to this state, instead of a blank cell
1065 target_cell.code_mirror.clearHistory();
1062 target_cell.code_mirror.clearHistory();
1066 source_cell.element.remove();
1063 source_cell.element.remove();
1067 this.select(i);
1064 this.select(i);
1068 if ((source_cell instanceof textcell.TextCell) && source_cell.rendered) {
1065 if ((source_cell instanceof textcell.TextCell) && source_cell.rendered) {
1069 target_cell.render();
1066 target_cell.render();
1070 }
1067 }
1071 var cursor = source_cell.code_mirror.getCursor();
1068 var cursor = source_cell.code_mirror.getCursor();
1072 target_cell.code_mirror.setCursor(cursor);
1069 target_cell.code_mirror.setCursor(cursor);
1073 this.set_dirty(true);
1070 this.set_dirty(true);
1074 }
1071 }
1075 }
1072 }
1076 };
1073 };
1077
1074
1078 /**
1075 /**
1079 * Turn a cell into a raw text cell.
1076 * Turn a cell into a raw text cell.
1080 *
1077 *
1081 * @method to_raw
1078 * @method to_raw
1082 * @param {Number} [index] A cell's index
1079 * @param {Number} [index] A cell's index
1083 */
1080 */
1084 Notebook.prototype.to_raw = function (index) {
1081 Notebook.prototype.to_raw = function (index) {
1085 var i = this.index_or_selected(index);
1082 var i = this.index_or_selected(index);
1086 if (this.is_valid_cell_index(i)) {
1083 if (this.is_valid_cell_index(i)) {
1087 var target_cell = null;
1084 var target_cell = null;
1088 var source_cell = this.get_cell(i);
1085 var source_cell = this.get_cell(i);
1089
1086
1090 if (!(source_cell instanceof textcell.RawCell)) {
1087 if (!(source_cell instanceof textcell.RawCell)) {
1091 target_cell = this.insert_cell_below('raw',i);
1088 target_cell = this.insert_cell_below('raw',i);
1092 var text = source_cell.get_text();
1089 var text = source_cell.get_text();
1093 if (text === source_cell.placeholder) {
1090 if (text === source_cell.placeholder) {
1094 text = '';
1091 text = '';
1095 }
1092 }
1096 //metadata
1093 //metadata
1097 target_cell.metadata = source_cell.metadata;
1094 target_cell.metadata = source_cell.metadata;
1098 // We must show the editor before setting its contents
1095 // We must show the editor before setting its contents
1099 target_cell.unrender();
1096 target_cell.unrender();
1100 target_cell.set_text(text);
1097 target_cell.set_text(text);
1101 // make this value the starting point, so that we can only undo
1098 // make this value the starting point, so that we can only undo
1102 // to this state, instead of a blank cell
1099 // to this state, instead of a blank cell
1103 target_cell.code_mirror.clearHistory();
1100 target_cell.code_mirror.clearHistory();
1104 source_cell.element.remove();
1101 source_cell.element.remove();
1105 this.select(i);
1102 this.select(i);
1106 var cursor = source_cell.code_mirror.getCursor();
1103 var cursor = source_cell.code_mirror.getCursor();
1107 target_cell.code_mirror.setCursor(cursor);
1104 target_cell.code_mirror.setCursor(cursor);
1108 this.set_dirty(true);
1105 this.set_dirty(true);
1109 }
1106 }
1110 }
1107 }
1111 };
1108 };
1112
1109
1113 Notebook.prototype._warn_heading = function () {
1110 Notebook.prototype._warn_heading = function () {
1114 // warn about heading cells being removed
1111 // warn about heading cells being removed
1115 dialog.modal({
1112 dialog.modal({
1116 notebook: this,
1113 notebook: this,
1117 keyboard_manager: this.keyboard_manager,
1114 keyboard_manager: this.keyboard_manager,
1118 title : "Use markdown headings",
1115 title : "Use markdown headings",
1119 body : $("<p/>").text(
1116 body : $("<p/>").text(
1120 'IPython no longer uses special heading cells. ' +
1117 'IPython no longer uses special heading cells. ' +
1121 'Instead, write your headings in Markdown cells using # characters:'
1118 'Instead, write your headings in Markdown cells using # characters:'
1122 ).append($('<pre/>').text(
1119 ).append($('<pre/>').text(
1123 '## This is a level 2 heading'
1120 '## This is a level 2 heading'
1124 )),
1121 )),
1125 buttons : {
1122 buttons : {
1126 "OK" : {}
1123 "OK" : {}
1127 }
1124 }
1128 });
1125 });
1129 };
1126 };
1130
1127
1131 /**
1128 /**
1132 * Turn a cell into a markdown cell with a heading.
1129 * Turn a cell into a markdown cell with a heading.
1133 *
1130 *
1134 * @method to_heading
1131 * @method to_heading
1135 * @param {Number} [index] A cell's index
1132 * @param {Number} [index] A cell's index
1136 * @param {Number} [level] A heading level (e.g., 1 for h1)
1133 * @param {Number} [level] A heading level (e.g., 1 for h1)
1137 */
1134 */
1138 Notebook.prototype.to_heading = function (index, level) {
1135 Notebook.prototype.to_heading = function (index, level) {
1139 this.to_markdown(index);
1136 this.to_markdown(index);
1140 level = level || 1;
1137 level = level || 1;
1141 var i = this.index_or_selected(index);
1138 var i = this.index_or_selected(index);
1142 if (this.is_valid_cell_index(i)) {
1139 if (this.is_valid_cell_index(i)) {
1143 var cell = this.get_cell(i);
1140 var cell = this.get_cell(i);
1144 cell.set_heading_level(level);
1141 cell.set_heading_level(level);
1145 this.set_dirty(true);
1142 this.set_dirty(true);
1146 }
1143 }
1147 };
1144 };
1148
1145
1149
1146
1150 // Cut/Copy/Paste
1147 // Cut/Copy/Paste
1151
1148
1152 /**
1149 /**
1153 * Enable UI elements for pasting cells.
1150 * Enable UI elements for pasting cells.
1154 *
1151 *
1155 * @method enable_paste
1152 * @method enable_paste
1156 */
1153 */
1157 Notebook.prototype.enable_paste = function () {
1154 Notebook.prototype.enable_paste = function () {
1158 var that = this;
1155 var that = this;
1159 if (!this.paste_enabled) {
1156 if (!this.paste_enabled) {
1160 $('#paste_cell_replace').removeClass('disabled')
1157 $('#paste_cell_replace').removeClass('disabled')
1161 .on('click', function () {that.paste_cell_replace();});
1158 .on('click', function () {that.paste_cell_replace();});
1162 $('#paste_cell_above').removeClass('disabled')
1159 $('#paste_cell_above').removeClass('disabled')
1163 .on('click', function () {that.paste_cell_above();});
1160 .on('click', function () {that.paste_cell_above();});
1164 $('#paste_cell_below').removeClass('disabled')
1161 $('#paste_cell_below').removeClass('disabled')
1165 .on('click', function () {that.paste_cell_below();});
1162 .on('click', function () {that.paste_cell_below();});
1166 this.paste_enabled = true;
1163 this.paste_enabled = true;
1167 }
1164 }
1168 };
1165 };
1169
1166
1170 /**
1167 /**
1171 * Disable UI elements for pasting cells.
1168 * Disable UI elements for pasting cells.
1172 *
1169 *
1173 * @method disable_paste
1170 * @method disable_paste
1174 */
1171 */
1175 Notebook.prototype.disable_paste = function () {
1172 Notebook.prototype.disable_paste = function () {
1176 if (this.paste_enabled) {
1173 if (this.paste_enabled) {
1177 $('#paste_cell_replace').addClass('disabled').off('click');
1174 $('#paste_cell_replace').addClass('disabled').off('click');
1178 $('#paste_cell_above').addClass('disabled').off('click');
1175 $('#paste_cell_above').addClass('disabled').off('click');
1179 $('#paste_cell_below').addClass('disabled').off('click');
1176 $('#paste_cell_below').addClass('disabled').off('click');
1180 this.paste_enabled = false;
1177 this.paste_enabled = false;
1181 }
1178 }
1182 };
1179 };
1183
1180
1184 /**
1181 /**
1185 * Cut a cell.
1182 * Cut a cell.
1186 *
1183 *
1187 * @method cut_cell
1184 * @method cut_cell
1188 */
1185 */
1189 Notebook.prototype.cut_cell = function () {
1186 Notebook.prototype.cut_cell = function () {
1190 this.copy_cell();
1187 this.copy_cell();
1191 this.delete_cell();
1188 this.delete_cell();
1192 };
1189 };
1193
1190
1194 /**
1191 /**
1195 * Copy a cell.
1192 * Copy a cell.
1196 *
1193 *
1197 * @method copy_cell
1194 * @method copy_cell
1198 */
1195 */
1199 Notebook.prototype.copy_cell = function () {
1196 Notebook.prototype.copy_cell = function () {
1200 var cell = this.get_selected_cell();
1197 var cell = this.get_selected_cell();
1201 this.clipboard = cell.toJSON();
1198 this.clipboard = cell.toJSON();
1202 // remove undeletable status from the copied cell
1199 // remove undeletable status from the copied cell
1203 if (this.clipboard.metadata.deletable !== undefined) {
1200 if (this.clipboard.metadata.deletable !== undefined) {
1204 delete this.clipboard.metadata.deletable;
1201 delete this.clipboard.metadata.deletable;
1205 }
1202 }
1206 this.enable_paste();
1203 this.enable_paste();
1207 };
1204 };
1208
1205
1209 /**
1206 /**
1210 * Replace the selected cell with a cell in the clipboard.
1207 * Replace the selected cell with a cell in the clipboard.
1211 *
1208 *
1212 * @method paste_cell_replace
1209 * @method paste_cell_replace
1213 */
1210 */
1214 Notebook.prototype.paste_cell_replace = function () {
1211 Notebook.prototype.paste_cell_replace = function () {
1215 if (this.clipboard !== null && this.paste_enabled) {
1212 if (this.clipboard !== null && this.paste_enabled) {
1216 var cell_data = this.clipboard;
1213 var cell_data = this.clipboard;
1217 var new_cell = this.insert_cell_above(cell_data.cell_type);
1214 var new_cell = this.insert_cell_above(cell_data.cell_type);
1218 new_cell.fromJSON(cell_data);
1215 new_cell.fromJSON(cell_data);
1219 var old_cell = this.get_next_cell(new_cell);
1216 var old_cell = this.get_next_cell(new_cell);
1220 this.delete_cell(this.find_cell_index(old_cell));
1217 this.delete_cell(this.find_cell_index(old_cell));
1221 this.select(this.find_cell_index(new_cell));
1218 this.select(this.find_cell_index(new_cell));
1222 }
1219 }
1223 };
1220 };
1224
1221
1225 /**
1222 /**
1226 * Paste a cell from the clipboard above the selected cell.
1223 * Paste a cell from the clipboard above the selected cell.
1227 *
1224 *
1228 * @method paste_cell_above
1225 * @method paste_cell_above
1229 */
1226 */
1230 Notebook.prototype.paste_cell_above = function () {
1227 Notebook.prototype.paste_cell_above = function () {
1231 if (this.clipboard !== null && this.paste_enabled) {
1228 if (this.clipboard !== null && this.paste_enabled) {
1232 var cell_data = this.clipboard;
1229 var cell_data = this.clipboard;
1233 var new_cell = this.insert_cell_above(cell_data.cell_type);
1230 var new_cell = this.insert_cell_above(cell_data.cell_type);
1234 new_cell.fromJSON(cell_data);
1231 new_cell.fromJSON(cell_data);
1235 new_cell.focus_cell();
1232 new_cell.focus_cell();
1236 }
1233 }
1237 };
1234 };
1238
1235
1239 /**
1236 /**
1240 * Paste a cell from the clipboard below the selected cell.
1237 * Paste a cell from the clipboard below the selected cell.
1241 *
1238 *
1242 * @method paste_cell_below
1239 * @method paste_cell_below
1243 */
1240 */
1244 Notebook.prototype.paste_cell_below = function () {
1241 Notebook.prototype.paste_cell_below = function () {
1245 if (this.clipboard !== null && this.paste_enabled) {
1242 if (this.clipboard !== null && this.paste_enabled) {
1246 var cell_data = this.clipboard;
1243 var cell_data = this.clipboard;
1247 var new_cell = this.insert_cell_below(cell_data.cell_type);
1244 var new_cell = this.insert_cell_below(cell_data.cell_type);
1248 new_cell.fromJSON(cell_data);
1245 new_cell.fromJSON(cell_data);
1249 new_cell.focus_cell();
1246 new_cell.focus_cell();
1250 }
1247 }
1251 };
1248 };
1252
1249
1253 // Split/merge
1250 // Split/merge
1254
1251
1255 /**
1252 /**
1256 * Split the selected cell into two, at the cursor.
1253 * Split the selected cell into two, at the cursor.
1257 *
1254 *
1258 * @method split_cell
1255 * @method split_cell
1259 */
1256 */
1260 Notebook.prototype.split_cell = function () {
1257 Notebook.prototype.split_cell = function () {
1261 var cell = this.get_selected_cell();
1258 var cell = this.get_selected_cell();
1262 if (cell.is_splittable()) {
1259 if (cell.is_splittable()) {
1263 var texta = cell.get_pre_cursor();
1260 var texta = cell.get_pre_cursor();
1264 var textb = cell.get_post_cursor();
1261 var textb = cell.get_post_cursor();
1265 cell.set_text(textb);
1262 cell.set_text(textb);
1266 var new_cell = this.insert_cell_above(cell.cell_type);
1263 var new_cell = this.insert_cell_above(cell.cell_type);
1267 // Unrender the new cell so we can call set_text.
1264 // Unrender the new cell so we can call set_text.
1268 new_cell.unrender();
1265 new_cell.unrender();
1269 new_cell.set_text(texta);
1266 new_cell.set_text(texta);
1270 }
1267 }
1271 };
1268 };
1272
1269
1273 /**
1270 /**
1274 * Combine the selected cell into the cell above it.
1271 * Combine the selected cell into the cell above it.
1275 *
1272 *
1276 * @method merge_cell_above
1273 * @method merge_cell_above
1277 */
1274 */
1278 Notebook.prototype.merge_cell_above = function () {
1275 Notebook.prototype.merge_cell_above = function () {
1279 var index = this.get_selected_index();
1276 var index = this.get_selected_index();
1280 var cell = this.get_cell(index);
1277 var cell = this.get_cell(index);
1281 var render = cell.rendered;
1278 var render = cell.rendered;
1282 if (!cell.is_mergeable()) {
1279 if (!cell.is_mergeable()) {
1283 return;
1280 return;
1284 }
1281 }
1285 if (index > 0) {
1282 if (index > 0) {
1286 var upper_cell = this.get_cell(index-1);
1283 var upper_cell = this.get_cell(index-1);
1287 if (!upper_cell.is_mergeable()) {
1284 if (!upper_cell.is_mergeable()) {
1288 return;
1285 return;
1289 }
1286 }
1290 var upper_text = upper_cell.get_text();
1287 var upper_text = upper_cell.get_text();
1291 var text = cell.get_text();
1288 var text = cell.get_text();
1292 if (cell instanceof codecell.CodeCell) {
1289 if (cell instanceof codecell.CodeCell) {
1293 cell.set_text(upper_text+'\n'+text);
1290 cell.set_text(upper_text+'\n'+text);
1294 } else {
1291 } else {
1295 cell.unrender(); // Must unrender before we set_text.
1292 cell.unrender(); // Must unrender before we set_text.
1296 cell.set_text(upper_text+'\n\n'+text);
1293 cell.set_text(upper_text+'\n\n'+text);
1297 if (render) {
1294 if (render) {
1298 // The rendered state of the final cell should match
1295 // The rendered state of the final cell should match
1299 // that of the original selected cell;
1296 // that of the original selected cell;
1300 cell.render();
1297 cell.render();
1301 }
1298 }
1302 }
1299 }
1303 this.delete_cell(index-1);
1300 this.delete_cell(index-1);
1304 this.select(this.find_cell_index(cell));
1301 this.select(this.find_cell_index(cell));
1305 }
1302 }
1306 };
1303 };
1307
1304
1308 /**
1305 /**
1309 * Combine the selected cell into the cell below it.
1306 * Combine the selected cell into the cell below it.
1310 *
1307 *
1311 * @method merge_cell_below
1308 * @method merge_cell_below
1312 */
1309 */
1313 Notebook.prototype.merge_cell_below = function () {
1310 Notebook.prototype.merge_cell_below = function () {
1314 var index = this.get_selected_index();
1311 var index = this.get_selected_index();
1315 var cell = this.get_cell(index);
1312 var cell = this.get_cell(index);
1316 var render = cell.rendered;
1313 var render = cell.rendered;
1317 if (!cell.is_mergeable()) {
1314 if (!cell.is_mergeable()) {
1318 return;
1315 return;
1319 }
1316 }
1320 if (index < this.ncells()-1) {
1317 if (index < this.ncells()-1) {
1321 var lower_cell = this.get_cell(index+1);
1318 var lower_cell = this.get_cell(index+1);
1322 if (!lower_cell.is_mergeable()) {
1319 if (!lower_cell.is_mergeable()) {
1323 return;
1320 return;
1324 }
1321 }
1325 var lower_text = lower_cell.get_text();
1322 var lower_text = lower_cell.get_text();
1326 var text = cell.get_text();
1323 var text = cell.get_text();
1327 if (cell instanceof codecell.CodeCell) {
1324 if (cell instanceof codecell.CodeCell) {
1328 cell.set_text(text+'\n'+lower_text);
1325 cell.set_text(text+'\n'+lower_text);
1329 } else {
1326 } else {
1330 cell.unrender(); // Must unrender before we set_text.
1327 cell.unrender(); // Must unrender before we set_text.
1331 cell.set_text(text+'\n\n'+lower_text);
1328 cell.set_text(text+'\n\n'+lower_text);
1332 if (render) {
1329 if (render) {
1333 // The rendered state of the final cell should match
1330 // The rendered state of the final cell should match
1334 // that of the original selected cell;
1331 // that of the original selected cell;
1335 cell.render();
1332 cell.render();
1336 }
1333 }
1337 }
1334 }
1338 this.delete_cell(index+1);
1335 this.delete_cell(index+1);
1339 this.select(this.find_cell_index(cell));
1336 this.select(this.find_cell_index(cell));
1340 }
1337 }
1341 };
1338 };
1342
1339
1343
1340
1344 // Cell collapsing and output clearing
1341 // Cell collapsing and output clearing
1345
1342
1346 /**
1343 /**
1347 * Hide a cell's output.
1344 * Hide a cell's output.
1348 *
1345 *
1349 * @method collapse_output
1346 * @method collapse_output
1350 * @param {Number} index A cell's numeric index
1347 * @param {Number} index A cell's numeric index
1351 */
1348 */
1352 Notebook.prototype.collapse_output = function (index) {
1349 Notebook.prototype.collapse_output = function (index) {
1353 var i = this.index_or_selected(index);
1350 var i = this.index_or_selected(index);
1354 var cell = this.get_cell(i);
1351 var cell = this.get_cell(i);
1355 if (cell !== null && (cell instanceof codecell.CodeCell)) {
1352 if (cell !== null && (cell instanceof codecell.CodeCell)) {
1356 cell.collapse_output();
1353 cell.collapse_output();
1357 this.set_dirty(true);
1354 this.set_dirty(true);
1358 }
1355 }
1359 };
1356 };
1360
1357
1361 /**
1358 /**
1362 * Hide each code cell's output area.
1359 * Hide each code cell's output area.
1363 *
1360 *
1364 * @method collapse_all_output
1361 * @method collapse_all_output
1365 */
1362 */
1366 Notebook.prototype.collapse_all_output = function () {
1363 Notebook.prototype.collapse_all_output = function () {
1367 this.get_cells().map(function (cell, i) {
1364 this.get_cells().map(function (cell, i) {
1368 if (cell instanceof codecell.CodeCell) {
1365 if (cell instanceof codecell.CodeCell) {
1369 cell.collapse_output();
1366 cell.collapse_output();
1370 }
1367 }
1371 });
1368 });
1372 // this should not be set if the `collapse` key is removed from nbformat
1369 // this should not be set if the `collapse` key is removed from nbformat
1373 this.set_dirty(true);
1370 this.set_dirty(true);
1374 };
1371 };
1375
1372
1376 /**
1373 /**
1377 * Show a cell's output.
1374 * Show a cell's output.
1378 *
1375 *
1379 * @method expand_output
1376 * @method expand_output
1380 * @param {Number} index A cell's numeric index
1377 * @param {Number} index A cell's numeric index
1381 */
1378 */
1382 Notebook.prototype.expand_output = function (index) {
1379 Notebook.prototype.expand_output = function (index) {
1383 var i = this.index_or_selected(index);
1380 var i = this.index_or_selected(index);
1384 var cell = this.get_cell(i);
1381 var cell = this.get_cell(i);
1385 if (cell !== null && (cell instanceof codecell.CodeCell)) {
1382 if (cell !== null && (cell instanceof codecell.CodeCell)) {
1386 cell.expand_output();
1383 cell.expand_output();
1387 this.set_dirty(true);
1384 this.set_dirty(true);
1388 }
1385 }
1389 };
1386 };
1390
1387
1391 /**
1388 /**
1392 * Expand each code cell's output area, and remove scrollbars.
1389 * Expand each code cell's output area, and remove scrollbars.
1393 *
1390 *
1394 * @method expand_all_output
1391 * @method expand_all_output
1395 */
1392 */
1396 Notebook.prototype.expand_all_output = function () {
1393 Notebook.prototype.expand_all_output = function () {
1397 this.get_cells().map(function (cell, i) {
1394 this.get_cells().map(function (cell, i) {
1398 if (cell instanceof codecell.CodeCell) {
1395 if (cell instanceof codecell.CodeCell) {
1399 cell.expand_output();
1396 cell.expand_output();
1400 }
1397 }
1401 });
1398 });
1402 // this should not be set if the `collapse` key is removed from nbformat
1399 // this should not be set if the `collapse` key is removed from nbformat
1403 this.set_dirty(true);
1400 this.set_dirty(true);
1404 };
1401 };
1405
1402
1406 /**
1403 /**
1407 * Clear the selected CodeCell's output area.
1404 * Clear the selected CodeCell's output area.
1408 *
1405 *
1409 * @method clear_output
1406 * @method clear_output
1410 * @param {Number} index A cell's numeric index
1407 * @param {Number} index A cell's numeric index
1411 */
1408 */
1412 Notebook.prototype.clear_output = function (index) {
1409 Notebook.prototype.clear_output = function (index) {
1413 var i = this.index_or_selected(index);
1410 var i = this.index_or_selected(index);
1414 var cell = this.get_cell(i);
1411 var cell = this.get_cell(i);
1415 if (cell !== null && (cell instanceof codecell.CodeCell)) {
1412 if (cell !== null && (cell instanceof codecell.CodeCell)) {
1416 cell.clear_output();
1413 cell.clear_output();
1417 this.set_dirty(true);
1414 this.set_dirty(true);
1418 }
1415 }
1419 };
1416 };
1420
1417
1421 /**
1418 /**
1422 * Clear each code cell's output area.
1419 * Clear each code cell's output area.
1423 *
1420 *
1424 * @method clear_all_output
1421 * @method clear_all_output
1425 */
1422 */
1426 Notebook.prototype.clear_all_output = function () {
1423 Notebook.prototype.clear_all_output = function () {
1427 this.get_cells().map(function (cell, i) {
1424 this.get_cells().map(function (cell, i) {
1428 if (cell instanceof codecell.CodeCell) {
1425 if (cell instanceof codecell.CodeCell) {
1429 cell.clear_output();
1426 cell.clear_output();
1430 }
1427 }
1431 });
1428 });
1432 this.set_dirty(true);
1429 this.set_dirty(true);
1433 };
1430 };
1434
1431
1435 /**
1432 /**
1436 * Scroll the selected CodeCell's output area.
1433 * Scroll the selected CodeCell's output area.
1437 *
1434 *
1438 * @method scroll_output
1435 * @method scroll_output
1439 * @param {Number} index A cell's numeric index
1436 * @param {Number} index A cell's numeric index
1440 */
1437 */
1441 Notebook.prototype.scroll_output = function (index) {
1438 Notebook.prototype.scroll_output = function (index) {
1442 var i = this.index_or_selected(index);
1439 var i = this.index_or_selected(index);
1443 var cell = this.get_cell(i);
1440 var cell = this.get_cell(i);
1444 if (cell !== null && (cell instanceof codecell.CodeCell)) {
1441 if (cell !== null && (cell instanceof codecell.CodeCell)) {
1445 cell.scroll_output();
1442 cell.scroll_output();
1446 this.set_dirty(true);
1443 this.set_dirty(true);
1447 }
1444 }
1448 };
1445 };
1449
1446
1450 /**
1447 /**
1451 * Expand each code cell's output area, and add a scrollbar for long output.
1448 * Expand each code cell's output area, and add a scrollbar for long output.
1452 *
1449 *
1453 * @method scroll_all_output
1450 * @method scroll_all_output
1454 */
1451 */
1455 Notebook.prototype.scroll_all_output = function () {
1452 Notebook.prototype.scroll_all_output = function () {
1456 this.get_cells().map(function (cell, i) {
1453 this.get_cells().map(function (cell, i) {
1457 if (cell instanceof codecell.CodeCell) {
1454 if (cell instanceof codecell.CodeCell) {
1458 cell.scroll_output();
1455 cell.scroll_output();
1459 }
1456 }
1460 });
1457 });
1461 // this should not be set if the `collapse` key is removed from nbformat
1458 // this should not be set if the `collapse` key is removed from nbformat
1462 this.set_dirty(true);
1459 this.set_dirty(true);
1463 };
1460 };
1464
1461
1465 /** Toggle whether a cell's output is collapsed or expanded.
1462 /** Toggle whether a cell's output is collapsed or expanded.
1466 *
1463 *
1467 * @method toggle_output
1464 * @method toggle_output
1468 * @param {Number} index A cell's numeric index
1465 * @param {Number} index A cell's numeric index
1469 */
1466 */
1470 Notebook.prototype.toggle_output = function (index) {
1467 Notebook.prototype.toggle_output = function (index) {
1471 var i = this.index_or_selected(index);
1468 var i = this.index_or_selected(index);
1472 var cell = this.get_cell(i);
1469 var cell = this.get_cell(i);
1473 if (cell !== null && (cell instanceof codecell.CodeCell)) {
1470 if (cell !== null && (cell instanceof codecell.CodeCell)) {
1474 cell.toggle_output();
1471 cell.toggle_output();
1475 this.set_dirty(true);
1472 this.set_dirty(true);
1476 }
1473 }
1477 };
1474 };
1478
1475
1479 /**
1476 /**
1480 * Hide/show the output of all cells.
1477 * Hide/show the output of all cells.
1481 *
1478 *
1482 * @method toggle_all_output
1479 * @method toggle_all_output
1483 */
1480 */
1484 Notebook.prototype.toggle_all_output = function () {
1481 Notebook.prototype.toggle_all_output = function () {
1485 this.get_cells().map(function (cell, i) {
1482 this.get_cells().map(function (cell, i) {
1486 if (cell instanceof codecell.CodeCell) {
1483 if (cell instanceof codecell.CodeCell) {
1487 cell.toggle_output();
1484 cell.toggle_output();
1488 }
1485 }
1489 });
1486 });
1490 // this should not be set if the `collapse` key is removed from nbformat
1487 // this should not be set if the `collapse` key is removed from nbformat
1491 this.set_dirty(true);
1488 this.set_dirty(true);
1492 };
1489 };
1493
1490
1494 /**
1491 /**
1495 * Toggle a scrollbar for long cell outputs.
1492 * Toggle a scrollbar for long cell outputs.
1496 *
1493 *
1497 * @method toggle_output_scroll
1494 * @method toggle_output_scroll
1498 * @param {Number} index A cell's numeric index
1495 * @param {Number} index A cell's numeric index
1499 */
1496 */
1500 Notebook.prototype.toggle_output_scroll = function (index) {
1497 Notebook.prototype.toggle_output_scroll = function (index) {
1501 var i = this.index_or_selected(index);
1498 var i = this.index_or_selected(index);
1502 var cell = this.get_cell(i);
1499 var cell = this.get_cell(i);
1503 if (cell !== null && (cell instanceof codecell.CodeCell)) {
1500 if (cell !== null && (cell instanceof codecell.CodeCell)) {
1504 cell.toggle_output_scroll();
1501 cell.toggle_output_scroll();
1505 this.set_dirty(true);
1502 this.set_dirty(true);
1506 }
1503 }
1507 };
1504 };
1508
1505
1509 /**
1506 /**
1510 * Toggle the scrolling of long output on all cells.
1507 * Toggle the scrolling of long output on all cells.
1511 *
1508 *
1512 * @method toggle_all_output_scrolling
1509 * @method toggle_all_output_scrolling
1513 */
1510 */
1514 Notebook.prototype.toggle_all_output_scroll = function () {
1511 Notebook.prototype.toggle_all_output_scroll = function () {
1515 this.get_cells().map(function (cell, i) {
1512 this.get_cells().map(function (cell, i) {
1516 if (cell instanceof codecell.CodeCell) {
1513 if (cell instanceof codecell.CodeCell) {
1517 cell.toggle_output_scroll();
1514 cell.toggle_output_scroll();
1518 }
1515 }
1519 });
1516 });
1520 // this should not be set if the `collapse` key is removed from nbformat
1517 // this should not be set if the `collapse` key is removed from nbformat
1521 this.set_dirty(true);
1518 this.set_dirty(true);
1522 };
1519 };
1523
1520
1524 // Other cell functions: line numbers, ...
1521 // Other cell functions: line numbers, ...
1525
1522
1526 /**
1523 /**
1527 * Toggle line numbers in the selected cell's input area.
1524 * Toggle line numbers in the selected cell's input area.
1528 *
1525 *
1529 * @method cell_toggle_line_numbers
1526 * @method cell_toggle_line_numbers
1530 */
1527 */
1531 Notebook.prototype.cell_toggle_line_numbers = function() {
1528 Notebook.prototype.cell_toggle_line_numbers = function() {
1532 this.get_selected_cell().toggle_line_numbers();
1529 this.get_selected_cell().toggle_line_numbers();
1533 };
1530 };
1534
1531
1535 /**
1532 /**
1536 * Set the codemirror mode for all code cells, including the default for
1533 * Set the codemirror mode for all code cells, including the default for
1537 * new code cells.
1534 * new code cells.
1538 *
1535 *
1539 * @method set_codemirror_mode
1536 * @method set_codemirror_mode
1540 */
1537 */
1541 Notebook.prototype.set_codemirror_mode = function(newmode){
1538 Notebook.prototype.set_codemirror_mode = function(newmode){
1542 if (newmode === this.codemirror_mode) {
1539 if (newmode === this.codemirror_mode) {
1543 return;
1540 return;
1544 }
1541 }
1545 this.codemirror_mode = newmode;
1542 this.codemirror_mode = newmode;
1546 codecell.CodeCell.options_default.cm_config.mode = newmode;
1543 codecell.CodeCell.options_default.cm_config.mode = newmode;
1547 var modename = newmode.mode || newmode.name || newmode;
1544 var modename = newmode.mode || newmode.name || newmode;
1548
1545
1549 var that = this;
1546 var that = this;
1550 utils.requireCodeMirrorMode(modename, function () {
1547 utils.requireCodeMirrorMode(modename, function () {
1551 that.get_cells().map(function(cell, i) {
1548 that.get_cells().map(function(cell, i) {
1552 if (cell.cell_type === 'code'){
1549 if (cell.cell_type === 'code'){
1553 cell.code_mirror.setOption('mode', newmode);
1550 cell.code_mirror.setOption('mode', newmode);
1554 // This is currently redundant, because cm_config ends up as
1551 // This is currently redundant, because cm_config ends up as
1555 // codemirror's own .options object, but I don't want to
1552 // codemirror's own .options object, but I don't want to
1556 // rely on that.
1553 // rely on that.
1557 cell.cm_config.mode = newmode;
1554 cell.cm_config.mode = newmode;
1558 }
1555 }
1559 });
1556 });
1560 });
1557 });
1561 };
1558 };
1562
1559
1563 // Session related things
1560 // Session related things
1564
1561
1565 /**
1562 /**
1566 * Start a new session and set it on each code cell.
1563 * Start a new session and set it on each code cell.
1567 *
1564 *
1568 * @method start_session
1565 * @method start_session
1569 */
1566 */
1570 Notebook.prototype.start_session = function (kernel_name) {
1567 Notebook.prototype.start_session = function (kernel_name) {
1571 if (this._session_starting) {
1568 if (this._session_starting) {
1572 throw new session.SessionAlreadyStarting();
1569 throw new session.SessionAlreadyStarting();
1573 }
1570 }
1574 this._session_starting = true;
1571 this._session_starting = true;
1575
1572
1576 var options = {
1573 var options = {
1577 base_url: this.base_url,
1574 base_url: this.base_url,
1578 ws_url: this.ws_url,
1575 ws_url: this.ws_url,
1579 notebook_path: this.notebook_path,
1576 notebook_path: this.notebook_path,
1580 notebook_name: this.notebook_name,
1577 notebook_name: this.notebook_name,
1581 kernel_name: kernel_name,
1578 kernel_name: kernel_name,
1582 notebook: this
1579 notebook: this
1583 };
1580 };
1584
1581
1585 var success = $.proxy(this._session_started, this);
1582 var success = $.proxy(this._session_started, this);
1586 var failure = $.proxy(this._session_start_failed, this);
1583 var failure = $.proxy(this._session_start_failed, this);
1587
1584
1588 if (this.session !== null) {
1585 if (this.session !== null) {
1589 this.session.restart(options, success, failure);
1586 this.session.restart(options, success, failure);
1590 } else {
1587 } else {
1591 this.session = new session.Session(options);
1588 this.session = new session.Session(options);
1592 this.session.start(success, failure);
1589 this.session.start(success, failure);
1593 }
1590 }
1594 };
1591 };
1595
1592
1596
1593
1597 /**
1594 /**
1598 * Once a session is started, link the code cells to the kernel and pass the
1595 * Once a session is started, link the code cells to the kernel and pass the
1599 * comm manager to the widget manager
1596 * comm manager to the widget manager
1600 *
1597 *
1601 */
1598 */
1602 Notebook.prototype._session_started = function (){
1599 Notebook.prototype._session_started = function (){
1603 this._session_starting = false;
1600 this._session_starting = false;
1604 this.kernel = this.session.kernel;
1601 this.kernel = this.session.kernel;
1605 var ncells = this.ncells();
1602 var ncells = this.ncells();
1606 for (var i=0; i<ncells; i++) {
1603 for (var i=0; i<ncells; i++) {
1607 var cell = this.get_cell(i);
1604 var cell = this.get_cell(i);
1608 if (cell instanceof codecell.CodeCell) {
1605 if (cell instanceof codecell.CodeCell) {
1609 cell.set_kernel(this.session.kernel);
1606 cell.set_kernel(this.session.kernel);
1610 }
1607 }
1611 }
1608 }
1612 };
1609 };
1613 Notebook.prototype._session_start_failed = function (jqxhr, status, error){
1610 Notebook.prototype._session_start_failed = function (jqxhr, status, error){
1614 this._session_starting = false;
1611 this._session_starting = false;
1615 utils.log_ajax_error(jqxhr, status, error);
1612 utils.log_ajax_error(jqxhr, status, error);
1616 };
1613 };
1617
1614
1618 /**
1615 /**
1619 * Prompt the user to restart the IPython kernel.
1616 * Prompt the user to restart the IPython kernel.
1620 *
1617 *
1621 * @method restart_kernel
1618 * @method restart_kernel
1622 */
1619 */
1623 Notebook.prototype.restart_kernel = function () {
1620 Notebook.prototype.restart_kernel = function () {
1624 var that = this;
1621 var that = this;
1625 dialog.modal({
1622 dialog.modal({
1626 notebook: this,
1623 notebook: this,
1627 keyboard_manager: this.keyboard_manager,
1624 keyboard_manager: this.keyboard_manager,
1628 title : "Restart kernel or continue running?",
1625 title : "Restart kernel or continue running?",
1629 body : $("<p/>").text(
1626 body : $("<p/>").text(
1630 'Do you want to restart the current kernel? You will lose all variables defined in it.'
1627 'Do you want to restart the current kernel? You will lose all variables defined in it.'
1631 ),
1628 ),
1632 buttons : {
1629 buttons : {
1633 "Continue running" : {},
1630 "Continue running" : {},
1634 "Restart" : {
1631 "Restart" : {
1635 "class" : "btn-danger",
1632 "class" : "btn-danger",
1636 "click" : function() {
1633 "click" : function() {
1637 that.kernel.restart();
1634 that.kernel.restart();
1638 }
1635 }
1639 }
1636 }
1640 }
1637 }
1641 });
1638 });
1642 };
1639 };
1643
1640
1644 /**
1641 /**
1645 * Execute or render cell outputs and go into command mode.
1642 * Execute or render cell outputs and go into command mode.
1646 *
1643 *
1647 * @method execute_cell
1644 * @method execute_cell
1648 */
1645 */
1649 Notebook.prototype.execute_cell = function () {
1646 Notebook.prototype.execute_cell = function () {
1650 // mode = shift, ctrl, alt
1647 // mode = shift, ctrl, alt
1651 var cell = this.get_selected_cell();
1648 var cell = this.get_selected_cell();
1652
1649
1653 cell.execute();
1650 cell.execute();
1654 this.command_mode();
1651 this.command_mode();
1655 this.set_dirty(true);
1652 this.set_dirty(true);
1656 };
1653 };
1657
1654
1658 /**
1655 /**
1659 * Execute or render cell outputs and insert a new cell below.
1656 * Execute or render cell outputs and insert a new cell below.
1660 *
1657 *
1661 * @method execute_cell_and_insert_below
1658 * @method execute_cell_and_insert_below
1662 */
1659 */
1663 Notebook.prototype.execute_cell_and_insert_below = function () {
1660 Notebook.prototype.execute_cell_and_insert_below = function () {
1664 var cell = this.get_selected_cell();
1661 var cell = this.get_selected_cell();
1665 var cell_index = this.find_cell_index(cell);
1662 var cell_index = this.find_cell_index(cell);
1666
1663
1667 cell.execute();
1664 cell.execute();
1668
1665
1669 // If we are at the end always insert a new cell and return
1666 // If we are at the end always insert a new cell and return
1670 if (cell_index === (this.ncells()-1)) {
1667 if (cell_index === (this.ncells()-1)) {
1671 this.command_mode();
1668 this.command_mode();
1672 this.insert_cell_below();
1669 this.insert_cell_below();
1673 this.select(cell_index+1);
1670 this.select(cell_index+1);
1674 this.edit_mode();
1671 this.edit_mode();
1675 this.scroll_to_bottom();
1672 this.scroll_to_bottom();
1676 this.set_dirty(true);
1673 this.set_dirty(true);
1677 return;
1674 return;
1678 }
1675 }
1679
1676
1680 this.command_mode();
1677 this.command_mode();
1681 this.insert_cell_below();
1678 this.insert_cell_below();
1682 this.select(cell_index+1);
1679 this.select(cell_index+1);
1683 this.edit_mode();
1680 this.edit_mode();
1684 this.set_dirty(true);
1681 this.set_dirty(true);
1685 };
1682 };
1686
1683
1687 /**
1684 /**
1688 * Execute or render cell outputs and select the next cell.
1685 * Execute or render cell outputs and select the next cell.
1689 *
1686 *
1690 * @method execute_cell_and_select_below
1687 * @method execute_cell_and_select_below
1691 */
1688 */
1692 Notebook.prototype.execute_cell_and_select_below = function () {
1689 Notebook.prototype.execute_cell_and_select_below = function () {
1693
1690
1694 var cell = this.get_selected_cell();
1691 var cell = this.get_selected_cell();
1695 var cell_index = this.find_cell_index(cell);
1692 var cell_index = this.find_cell_index(cell);
1696
1693
1697 cell.execute();
1694 cell.execute();
1698
1695
1699 // If we are at the end always insert a new cell and return
1696 // If we are at the end always insert a new cell and return
1700 if (cell_index === (this.ncells()-1)) {
1697 if (cell_index === (this.ncells()-1)) {
1701 this.command_mode();
1698 this.command_mode();
1702 this.insert_cell_below();
1699 this.insert_cell_below();
1703 this.select(cell_index+1);
1700 this.select(cell_index+1);
1704 this.edit_mode();
1701 this.edit_mode();
1705 this.scroll_to_bottom();
1702 this.scroll_to_bottom();
1706 this.set_dirty(true);
1703 this.set_dirty(true);
1707 return;
1704 return;
1708 }
1705 }
1709
1706
1710 this.command_mode();
1707 this.command_mode();
1711 this.select(cell_index+1);
1708 this.select(cell_index+1);
1712 this.focus_cell();
1709 this.focus_cell();
1713 this.set_dirty(true);
1710 this.set_dirty(true);
1714 };
1711 };
1715
1712
1716 /**
1713 /**
1717 * Execute all cells below the selected cell.
1714 * Execute all cells below the selected cell.
1718 *
1715 *
1719 * @method execute_cells_below
1716 * @method execute_cells_below
1720 */
1717 */
1721 Notebook.prototype.execute_cells_below = function () {
1718 Notebook.prototype.execute_cells_below = function () {
1722 this.execute_cell_range(this.get_selected_index(), this.ncells());
1719 this.execute_cell_range(this.get_selected_index(), this.ncells());
1723 this.scroll_to_bottom();
1720 this.scroll_to_bottom();
1724 };
1721 };
1725
1722
1726 /**
1723 /**
1727 * Execute all cells above the selected cell.
1724 * Execute all cells above the selected cell.
1728 *
1725 *
1729 * @method execute_cells_above
1726 * @method execute_cells_above
1730 */
1727 */
1731 Notebook.prototype.execute_cells_above = function () {
1728 Notebook.prototype.execute_cells_above = function () {
1732 this.execute_cell_range(0, this.get_selected_index());
1729 this.execute_cell_range(0, this.get_selected_index());
1733 };
1730 };
1734
1731
1735 /**
1732 /**
1736 * Execute all cells.
1733 * Execute all cells.
1737 *
1734 *
1738 * @method execute_all_cells
1735 * @method execute_all_cells
1739 */
1736 */
1740 Notebook.prototype.execute_all_cells = function () {
1737 Notebook.prototype.execute_all_cells = function () {
1741 this.execute_cell_range(0, this.ncells());
1738 this.execute_cell_range(0, this.ncells());
1742 this.scroll_to_bottom();
1739 this.scroll_to_bottom();
1743 };
1740 };
1744
1741
1745 /**
1742 /**
1746 * Execute a contiguous range of cells.
1743 * Execute a contiguous range of cells.
1747 *
1744 *
1748 * @method execute_cell_range
1745 * @method execute_cell_range
1749 * @param {Number} start Index of the first cell to execute (inclusive)
1746 * @param {Number} start Index of the first cell to execute (inclusive)
1750 * @param {Number} end Index of the last cell to execute (exclusive)
1747 * @param {Number} end Index of the last cell to execute (exclusive)
1751 */
1748 */
1752 Notebook.prototype.execute_cell_range = function (start, end) {
1749 Notebook.prototype.execute_cell_range = function (start, end) {
1753 this.command_mode();
1750 this.command_mode();
1754 for (var i=start; i<end; i++) {
1751 for (var i=start; i<end; i++) {
1755 this.select(i);
1752 this.select(i);
1756 this.execute_cell();
1753 this.execute_cell();
1757 }
1754 }
1758 };
1755 };
1759
1756
1760 // Persistance and loading
1757 // Persistance and loading
1761
1758
1762 /**
1759 /**
1763 * Getter method for this notebook's name.
1760 * Getter method for this notebook's name.
1764 *
1761 *
1765 * @method get_notebook_name
1762 * @method get_notebook_name
1766 * @return {String} This notebook's name (excluding file extension)
1763 * @return {String} This notebook's name (excluding file extension)
1767 */
1764 */
1768 Notebook.prototype.get_notebook_name = function () {
1765 Notebook.prototype.get_notebook_name = function () {
1769 var nbname = this.notebook_name.substring(0,this.notebook_name.length-6);
1766 var nbname = this.notebook_name.substring(0,this.notebook_name.length-6);
1770 return nbname;
1767 return nbname;
1771 };
1768 };
1772
1769
1773 /**
1770 /**
1774 * Setter method for this notebook's name.
1771 * Setter method for this notebook's name.
1775 *
1772 *
1776 * @method set_notebook_name
1773 * @method set_notebook_name
1777 * @param {String} name A new name for this notebook
1774 * @param {String} name A new name for this notebook
1778 */
1775 */
1779 Notebook.prototype.set_notebook_name = function (name) {
1776 Notebook.prototype.set_notebook_name = function (name) {
1780 var parent = utils.url_path_split(this.notebook_path)[0];
1777 var parent = utils.url_path_split(this.notebook_path)[0];
1781 this.notebook_name = name;
1778 this.notebook_name = name;
1782 this.notebook_path = utils.url_path_join(parent, name);
1779 this.notebook_path = utils.url_path_join(parent, name);
1783 };
1780 };
1784
1781
1785 /**
1782 /**
1786 * Check that a notebook's name is valid.
1783 * Check that a notebook's name is valid.
1787 *
1784 *
1788 * @method test_notebook_name
1785 * @method test_notebook_name
1789 * @param {String} nbname A name for this notebook
1786 * @param {String} nbname A name for this notebook
1790 * @return {Boolean} True if the name is valid, false if invalid
1787 * @return {Boolean} True if the name is valid, false if invalid
1791 */
1788 */
1792 Notebook.prototype.test_notebook_name = function (nbname) {
1789 Notebook.prototype.test_notebook_name = function (nbname) {
1793 nbname = nbname || '';
1790 nbname = nbname || '';
1794 if (nbname.length>0 && !this.notebook_name_blacklist_re.test(nbname)) {
1791 if (nbname.length>0 && !this.notebook_name_blacklist_re.test(nbname)) {
1795 return true;
1792 return true;
1796 } else {
1793 } else {
1797 return false;
1794 return false;
1798 }
1795 }
1799 };
1796 };
1800
1797
1801 /**
1798 /**
1802 * Load a notebook from JSON (.ipynb).
1799 * Load a notebook from JSON (.ipynb).
1803 *
1800 *
1804 * @method fromJSON
1801 * @method fromJSON
1805 * @param {Object} data JSON representation of a notebook
1802 * @param {Object} data JSON representation of a notebook
1806 */
1803 */
1807 Notebook.prototype.fromJSON = function (data) {
1804 Notebook.prototype.fromJSON = function (data) {
1808
1805
1809 var content = data.content;
1806 var content = data.content;
1810 var ncells = this.ncells();
1807 var ncells = this.ncells();
1811 var i;
1808 var i;
1812 for (i=0; i<ncells; i++) {
1809 for (i=0; i<ncells; i++) {
1813 // Always delete cell 0 as they get renumbered as they are deleted.
1810 // Always delete cell 0 as they get renumbered as they are deleted.
1814 this.delete_cell(0);
1811 this.delete_cell(0);
1815 }
1812 }
1816 // Save the metadata and name.
1813 // Save the metadata and name.
1817 this.metadata = content.metadata;
1814 this.metadata = content.metadata;
1818 this.notebook_name = data.name;
1815 this.notebook_name = data.name;
1819 this.notebook_path = data.path;
1816 this.notebook_path = data.path;
1820 var trusted = true;
1817 var trusted = true;
1821
1818
1822 // Trigger an event changing the kernel spec - this will set the default
1819 // Trigger an event changing the kernel spec - this will set the default
1823 // codemirror mode
1820 // codemirror mode
1824 if (this.metadata.kernelspec !== undefined) {
1821 if (this.metadata.kernelspec !== undefined) {
1825 this.events.trigger('spec_changed.Kernel', this.metadata.kernelspec);
1822 this.events.trigger('spec_changed.Kernel', this.metadata.kernelspec);
1826 }
1823 }
1827
1824
1828 // Set the codemirror mode from language_info metadata
1825 // Set the codemirror mode from language_info metadata
1829 if (this.metadata.language_info !== undefined) {
1826 if (this.metadata.language_info !== undefined) {
1830 var langinfo = this.metadata.language_info;
1827 var langinfo = this.metadata.language_info;
1831 // Mode 'null' should be plain, unhighlighted text.
1828 // Mode 'null' should be plain, unhighlighted text.
1832 var cm_mode = langinfo.codemirror_mode || langinfo.name || 'null';
1829 var cm_mode = langinfo.codemirror_mode || langinfo.name || 'null';
1833 this.set_codemirror_mode(cm_mode);
1830 this.set_codemirror_mode(cm_mode);
1834 }
1831 }
1835
1832
1836 var new_cells = content.cells;
1833 var new_cells = content.cells;
1837 ncells = new_cells.length;
1834 ncells = new_cells.length;
1838 var cell_data = null;
1835 var cell_data = null;
1839 var new_cell = null;
1836 var new_cell = null;
1840 for (i=0; i<ncells; i++) {
1837 for (i=0; i<ncells; i++) {
1841 cell_data = new_cells[i];
1838 cell_data = new_cells[i];
1842 new_cell = this.insert_cell_at_index(cell_data.cell_type, i);
1839 new_cell = this.insert_cell_at_index(cell_data.cell_type, i);
1843 new_cell.fromJSON(cell_data);
1840 new_cell.fromJSON(cell_data);
1844 if (new_cell.cell_type == 'code' && !new_cell.output_area.trusted) {
1841 if (new_cell.cell_type == 'code' && !new_cell.output_area.trusted) {
1845 trusted = false;
1842 trusted = false;
1846 }
1843 }
1847 }
1844 }
1848 if (trusted !== this.trusted) {
1845 if (trusted !== this.trusted) {
1849 this.trusted = trusted;
1846 this.trusted = trusted;
1850 this.events.trigger("trust_changed.Notebook", trusted);
1847 this.events.trigger("trust_changed.Notebook", trusted);
1851 }
1848 }
1852 };
1849 };
1853
1850
1854 /**
1851 /**
1855 * Dump this notebook into a JSON-friendly object.
1852 * Dump this notebook into a JSON-friendly object.
1856 *
1853 *
1857 * @method toJSON
1854 * @method toJSON
1858 * @return {Object} A JSON-friendly representation of this notebook.
1855 * @return {Object} A JSON-friendly representation of this notebook.
1859 */
1856 */
1860 Notebook.prototype.toJSON = function () {
1857 Notebook.prototype.toJSON = function () {
1861 // remove the conversion indicator, which only belongs in-memory
1858 // remove the conversion indicator, which only belongs in-memory
1862 delete this.metadata.orig_nbformat;
1859 delete this.metadata.orig_nbformat;
1863 delete this.metadata.orig_nbformat_minor;
1860 delete this.metadata.orig_nbformat_minor;
1864
1861
1865 var cells = this.get_cells();
1862 var cells = this.get_cells();
1866 var ncells = cells.length;
1863 var ncells = cells.length;
1867 var cell_array = new Array(ncells);
1864 var cell_array = new Array(ncells);
1868 var trusted = true;
1865 var trusted = true;
1869 for (var i=0; i<ncells; i++) {
1866 for (var i=0; i<ncells; i++) {
1870 var cell = cells[i];
1867 var cell = cells[i];
1871 if (cell.cell_type == 'code' && !cell.output_area.trusted) {
1868 if (cell.cell_type == 'code' && !cell.output_area.trusted) {
1872 trusted = false;
1869 trusted = false;
1873 }
1870 }
1874 cell_array[i] = cell.toJSON();
1871 cell_array[i] = cell.toJSON();
1875 }
1872 }
1876 var data = {
1873 var data = {
1877 cells: cell_array,
1874 cells: cell_array,
1878 metadata: this.metadata,
1875 metadata: this.metadata,
1879 nbformat: this.nbformat,
1876 nbformat: this.nbformat,
1880 nbformat_minor: this.nbformat_minor
1877 nbformat_minor: this.nbformat_minor
1881 };
1878 };
1882 if (trusted != this.trusted) {
1879 if (trusted != this.trusted) {
1883 this.trusted = trusted;
1880 this.trusted = trusted;
1884 this.events.trigger("trust_changed.Notebook", trusted);
1881 this.events.trigger("trust_changed.Notebook", trusted);
1885 }
1882 }
1886 return data;
1883 return data;
1887 };
1884 };
1888
1885
1889 /**
1886 /**
1890 * Start an autosave timer, for periodically saving the notebook.
1887 * Start an autosave timer, for periodically saving the notebook.
1891 *
1888 *
1892 * @method set_autosave_interval
1889 * @method set_autosave_interval
1893 * @param {Integer} interval the autosave interval in milliseconds
1890 * @param {Integer} interval the autosave interval in milliseconds
1894 */
1891 */
1895 Notebook.prototype.set_autosave_interval = function (interval) {
1892 Notebook.prototype.set_autosave_interval = function (interval) {
1896 var that = this;
1893 var that = this;
1897 // clear previous interval, so we don't get simultaneous timers
1894 // clear previous interval, so we don't get simultaneous timers
1898 if (this.autosave_timer) {
1895 if (this.autosave_timer) {
1899 clearInterval(this.autosave_timer);
1896 clearInterval(this.autosave_timer);
1900 }
1897 }
1901 if (!this.writable) {
1898 if (!this.writable) {
1902 // disable autosave if not writable
1899 // disable autosave if not writable
1903 interval = 0;
1900 interval = 0;
1904 }
1901 }
1905
1902
1906 this.autosave_interval = this.minimum_autosave_interval = interval;
1903 this.autosave_interval = this.minimum_autosave_interval = interval;
1907 if (interval) {
1904 if (interval) {
1908 this.autosave_timer = setInterval(function() {
1905 this.autosave_timer = setInterval(function() {
1909 if (that.dirty) {
1906 if (that.dirty) {
1910 that.save_notebook();
1907 that.save_notebook();
1911 }
1908 }
1912 }, interval);
1909 }, interval);
1913 this.events.trigger("autosave_enabled.Notebook", interval);
1910 this.events.trigger("autosave_enabled.Notebook", interval);
1914 } else {
1911 } else {
1915 this.autosave_timer = null;
1912 this.autosave_timer = null;
1916 this.events.trigger("autosave_disabled.Notebook");
1913 this.events.trigger("autosave_disabled.Notebook");
1917 }
1914 }
1918 };
1915 };
1919
1916
1920 /**
1917 /**
1921 * Save this notebook on the server. This becomes a notebook instance's
1918 * Save this notebook on the server. This becomes a notebook instance's
1922 * .save_notebook method *after* the entire notebook has been loaded.
1919 * .save_notebook method *after* the entire notebook has been loaded.
1923 *
1920 *
1924 * @method save_notebook
1921 * @method save_notebook
1925 */
1922 */
1926 Notebook.prototype.save_notebook = function () {
1923 Notebook.prototype.save_notebook = function () {
1927 if (!this._fully_loaded) {
1924 if (!this._fully_loaded) {
1928 this.events.trigger('notebook_save_failed.Notebook',
1925 this.events.trigger('notebook_save_failed.Notebook',
1929 new Error("Load failed, save is disabled")
1926 new Error("Load failed, save is disabled")
1930 );
1927 );
1931 return;
1928 return;
1932 } else if (!this.writable) {
1929 } else if (!this.writable) {
1933 this.events.trigger('notebook_save_failed.Notebook',
1930 this.events.trigger('notebook_save_failed.Notebook',
1934 new Error("Notebook is read-only")
1931 new Error("Notebook is read-only")
1935 );
1932 );
1936 return;
1933 return;
1937 }
1934 }
1938
1935
1939 // Create a JSON model to be sent to the server.
1936 // Create a JSON model to be sent to the server.
1940 var model = {
1937 var model = {
1941 type : "notebook",
1938 type : "notebook",
1942 content : this.toJSON()
1939 content : this.toJSON()
1943 };
1940 };
1944 // time the ajax call for autosave tuning purposes.
1941 // time the ajax call for autosave tuning purposes.
1945 var start = new Date().getTime();
1942 var start = new Date().getTime();
1946
1943
1947 var that = this;
1944 var that = this;
1948 this.contents.save(this.notebook_path, model).then(
1945 this.contents.save(this.notebook_path, model).then(
1949 $.proxy(this.save_notebook_success, this, start),
1946 $.proxy(this.save_notebook_success, this, start),
1950 function (error) {
1947 function (error) {
1951 that.events.trigger('notebook_save_failed.Notebook', error);
1948 that.events.trigger('notebook_save_failed.Notebook', error);
1952 }
1949 }
1953 );
1950 );
1954 };
1951 };
1955
1952
1956 /**
1953 /**
1957 * Success callback for saving a notebook.
1954 * Success callback for saving a notebook.
1958 *
1955 *
1959 * @method save_notebook_success
1956 * @method save_notebook_success
1960 * @param {Integer} start Time when the save request start
1957 * @param {Integer} start Time when the save request start
1961 * @param {Object} data JSON representation of a notebook
1958 * @param {Object} data JSON representation of a notebook
1962 */
1959 */
1963 Notebook.prototype.save_notebook_success = function (start, data) {
1960 Notebook.prototype.save_notebook_success = function (start, data) {
1964 this.set_dirty(false);
1961 this.set_dirty(false);
1965 if (data.message) {
1962 if (data.message) {
1966 // save succeeded, but validation failed.
1963 // save succeeded, but validation failed.
1967 var body = $("<div>");
1964 var body = $("<div>");
1968 var title = "Notebook validation failed";
1965 var title = "Notebook validation failed";
1969
1966
1970 body.append($("<p>").text(
1967 body.append($("<p>").text(
1971 "The save operation succeeded," +
1968 "The save operation succeeded," +
1972 " but the notebook does not appear to be valid." +
1969 " but the notebook does not appear to be valid." +
1973 " The validation error was:"
1970 " The validation error was:"
1974 )).append($("<div>").addClass("validation-error").append(
1971 )).append($("<div>").addClass("validation-error").append(
1975 $("<pre>").text(data.message)
1972 $("<pre>").text(data.message)
1976 ));
1973 ));
1977 dialog.modal({
1974 dialog.modal({
1978 notebook: this,
1975 notebook: this,
1979 keyboard_manager: this.keyboard_manager,
1976 keyboard_manager: this.keyboard_manager,
1980 title: title,
1977 title: title,
1981 body: body,
1978 body: body,
1982 buttons : {
1979 buttons : {
1983 OK : {
1980 OK : {
1984 "class" : "btn-primary"
1981 "class" : "btn-primary"
1985 }
1982 }
1986 }
1983 }
1987 });
1984 });
1988 }
1985 }
1989 this.events.trigger('notebook_saved.Notebook');
1986 this.events.trigger('notebook_saved.Notebook');
1990 this._update_autosave_interval(start);
1987 this._update_autosave_interval(start);
1991 if (this._checkpoint_after_save) {
1988 if (this._checkpoint_after_save) {
1992 this.create_checkpoint();
1989 this.create_checkpoint();
1993 this._checkpoint_after_save = false;
1990 this._checkpoint_after_save = false;
1994 }
1991 }
1995 };
1992 };
1996
1993
1997 /**
1994 /**
1998 * update the autosave interval based on how long the last save took
1995 * update the autosave interval based on how long the last save took
1999 *
1996 *
2000 * @method _update_autosave_interval
1997 * @method _update_autosave_interval
2001 * @param {Integer} timestamp when the save request started
1998 * @param {Integer} timestamp when the save request started
2002 */
1999 */
2003 Notebook.prototype._update_autosave_interval = function (start) {
2000 Notebook.prototype._update_autosave_interval = function (start) {
2004 var duration = (new Date().getTime() - start);
2001 var duration = (new Date().getTime() - start);
2005 if (this.autosave_interval) {
2002 if (this.autosave_interval) {
2006 // new save interval: higher of 10x save duration or parameter (default 30 seconds)
2003 // new save interval: higher of 10x save duration or parameter (default 30 seconds)
2007 var interval = Math.max(10 * duration, this.minimum_autosave_interval);
2004 var interval = Math.max(10 * duration, this.minimum_autosave_interval);
2008 // round to 10 seconds, otherwise we will be setting a new interval too often
2005 // round to 10 seconds, otherwise we will be setting a new interval too often
2009 interval = 10000 * Math.round(interval / 10000);
2006 interval = 10000 * Math.round(interval / 10000);
2010 // set new interval, if it's changed
2007 // set new interval, if it's changed
2011 if (interval != this.autosave_interval) {
2008 if (interval != this.autosave_interval) {
2012 this.set_autosave_interval(interval);
2009 this.set_autosave_interval(interval);
2013 }
2010 }
2014 }
2011 }
2015 };
2012 };
2016
2013
2017 /**
2014 /**
2018 * Explicitly trust the output of this notebook.
2015 * Explicitly trust the output of this notebook.
2019 *
2016 *
2020 * @method trust_notebook
2017 * @method trust_notebook
2021 */
2018 */
2022 Notebook.prototype.trust_notebook = function () {
2019 Notebook.prototype.trust_notebook = function () {
2023 var body = $("<div>").append($("<p>")
2020 var body = $("<div>").append($("<p>")
2024 .text("A trusted IPython notebook may execute hidden malicious code ")
2021 .text("A trusted IPython notebook may execute hidden malicious code ")
2025 .append($("<strong>")
2022 .append($("<strong>")
2026 .append(
2023 .append(
2027 $("<em>").text("when you open it")
2024 $("<em>").text("when you open it")
2028 )
2025 )
2029 ).append(".").append(
2026 ).append(".").append(
2030 " Selecting trust will immediately reload this notebook in a trusted state."
2027 " Selecting trust will immediately reload this notebook in a trusted state."
2031 ).append(
2028 ).append(
2032 " For more information, see the "
2029 " For more information, see the "
2033 ).append($("<a>").attr("href", "http://ipython.org/ipython-doc/2/notebook/security.html")
2030 ).append($("<a>").attr("href", "http://ipython.org/ipython-doc/2/notebook/security.html")
2034 .text("IPython security documentation")
2031 .text("IPython security documentation")
2035 ).append(".")
2032 ).append(".")
2036 );
2033 );
2037
2034
2038 var nb = this;
2035 var nb = this;
2039 dialog.modal({
2036 dialog.modal({
2040 notebook: this,
2037 notebook: this,
2041 keyboard_manager: this.keyboard_manager,
2038 keyboard_manager: this.keyboard_manager,
2042 title: "Trust this notebook?",
2039 title: "Trust this notebook?",
2043 body: body,
2040 body: body,
2044
2041
2045 buttons: {
2042 buttons: {
2046 Cancel : {},
2043 Cancel : {},
2047 Trust : {
2044 Trust : {
2048 class : "btn-danger",
2045 class : "btn-danger",
2049 click : function () {
2046 click : function () {
2050 var cells = nb.get_cells();
2047 var cells = nb.get_cells();
2051 for (var i = 0; i < cells.length; i++) {
2048 for (var i = 0; i < cells.length; i++) {
2052 var cell = cells[i];
2049 var cell = cells[i];
2053 if (cell.cell_type == 'code') {
2050 if (cell.cell_type == 'code') {
2054 cell.output_area.trusted = true;
2051 cell.output_area.trusted = true;
2055 }
2052 }
2056 }
2053 }
2057 nb.events.on('notebook_saved.Notebook', function () {
2054 nb.events.on('notebook_saved.Notebook', function () {
2058 window.location.reload();
2055 window.location.reload();
2059 });
2056 });
2060 nb.save_notebook();
2057 nb.save_notebook();
2061 }
2058 }
2062 }
2059 }
2063 }
2060 }
2064 });
2061 });
2065 };
2062 };
2066
2063
2067 Notebook.prototype.copy_notebook = function () {
2064 Notebook.prototype.copy_notebook = function () {
2068 var that = this;
2065 var that = this;
2069 var base_url = this.base_url;
2066 var base_url = this.base_url;
2070 var w = window.open();
2067 var w = window.open();
2071 var parent = utils.url_path_split(this.notebook_path)[0];
2068 var parent = utils.url_path_split(this.notebook_path)[0];
2072 this.contents.copy(this.notebook_path, parent).then(
2069 this.contents.copy(this.notebook_path, parent).then(
2073 function (data) {
2070 function (data) {
2074 w.location = utils.url_join_encode(
2071 w.location = utils.url_join_encode(
2075 base_url, 'notebooks', data.path
2072 base_url, 'notebooks', data.path
2076 );
2073 );
2077 },
2074 },
2078 function(error) {
2075 function(error) {
2079 w.close();
2076 w.close();
2080 that.events.trigger('notebook_copy_failed', error);
2077 that.events.trigger('notebook_copy_failed', error);
2081 }
2078 }
2082 );
2079 );
2083 };
2080 };
2084
2081
2085 Notebook.prototype.rename = function (new_name) {
2082 Notebook.prototype.rename = function (new_name) {
2086 if (!new_name.match(/\.ipynb$/)) {
2083 if (!new_name.match(/\.ipynb$/)) {
2087 new_name = new_name + ".ipynb";
2084 new_name = new_name + ".ipynb";
2088 }
2085 }
2089
2086
2090 var that = this;
2087 var that = this;
2091 var parent = utils.url_path_split(this.notebook_path)[0];
2088 var parent = utils.url_path_split(this.notebook_path)[0];
2092 var new_path = utils.url_path_join(parent, new_name);
2089 var new_path = utils.url_path_join(parent, new_name);
2093 return this.contents.rename(this.notebook_path, new_path).then(
2090 return this.contents.rename(this.notebook_path, new_path).then(
2094 function (json) {
2091 function (json) {
2095 that.notebook_name = json.name;
2092 that.notebook_name = json.name;
2096 that.notebook_path = json.path;
2093 that.notebook_path = json.path;
2097 that.session.rename_notebook(json.path);
2094 that.session.rename_notebook(json.path);
2098 that.events.trigger('notebook_renamed.Notebook', json);
2095 that.events.trigger('notebook_renamed.Notebook', json);
2099 }
2096 }
2100 );
2097 );
2101 };
2098 };
2102
2099
2103 Notebook.prototype.delete = function () {
2100 Notebook.prototype.delete = function () {
2104 this.contents.delete(this.notebook_path);
2101 this.contents.delete(this.notebook_path);
2105 };
2102 };
2106
2103
2107 /**
2104 /**
2108 * Request a notebook's data from the server.
2105 * Request a notebook's data from the server.
2109 *
2106 *
2110 * @method load_notebook
2107 * @method load_notebook
2111 * @param {String} notebook_path A notebook to load
2108 * @param {String} notebook_path A notebook to load
2112 */
2109 */
2113 Notebook.prototype.load_notebook = function (notebook_path) {
2110 Notebook.prototype.load_notebook = function (notebook_path) {
2114 this.notebook_path = notebook_path;
2111 this.notebook_path = notebook_path;
2115 this.notebook_name = utils.url_path_split(this.notebook_path)[1];
2112 this.notebook_name = utils.url_path_split(this.notebook_path)[1];
2116 this.events.trigger('notebook_loading.Notebook');
2113 this.events.trigger('notebook_loading.Notebook');
2117 this.contents.get(notebook_path, {type: 'notebook'}).then(
2114 this.contents.get(notebook_path, {type: 'notebook'}).then(
2118 $.proxy(this.load_notebook_success, this),
2115 $.proxy(this.load_notebook_success, this),
2119 $.proxy(this.load_notebook_error, this)
2116 $.proxy(this.load_notebook_error, this)
2120 );
2117 );
2121 };
2118 };
2122
2119
2123 /**
2120 /**
2124 * Success callback for loading a notebook from the server.
2121 * Success callback for loading a notebook from the server.
2125 *
2122 *
2126 * Load notebook data from the JSON response.
2123 * Load notebook data from the JSON response.
2127 *
2124 *
2128 * @method load_notebook_success
2125 * @method load_notebook_success
2129 * @param {Object} data JSON representation of a notebook
2126 * @param {Object} data JSON representation of a notebook
2130 */
2127 */
2131 Notebook.prototype.load_notebook_success = function (data) {
2128 Notebook.prototype.load_notebook_success = function (data) {
2132 var failed, msg;
2129 var failed, msg;
2133 try {
2130 try {
2134 this.fromJSON(data);
2131 this.fromJSON(data);
2135 } catch (e) {
2132 } catch (e) {
2136 failed = e;
2133 failed = e;
2137 console.log("Notebook failed to load from JSON:", e);
2134 console.log("Notebook failed to load from JSON:", e);
2138 }
2135 }
2139 if (failed || data.message) {
2136 if (failed || data.message) {
2140 // *either* fromJSON failed or validation failed
2137 // *either* fromJSON failed or validation failed
2141 var body = $("<div>");
2138 var body = $("<div>");
2142 var title;
2139 var title;
2143 if (failed) {
2140 if (failed) {
2144 title = "Notebook failed to load";
2141 title = "Notebook failed to load";
2145 body.append($("<p>").text(
2142 body.append($("<p>").text(
2146 "The error was: "
2143 "The error was: "
2147 )).append($("<div>").addClass("js-error").text(
2144 )).append($("<div>").addClass("js-error").text(
2148 failed.toString()
2145 failed.toString()
2149 )).append($("<p>").text(
2146 )).append($("<p>").text(
2150 "See the error console for details."
2147 "See the error console for details."
2151 ));
2148 ));
2152 } else {
2149 } else {
2153 title = "Notebook validation failed";
2150 title = "Notebook validation failed";
2154 }
2151 }
2155
2152
2156 if (data.message) {
2153 if (data.message) {
2157 if (failed) {
2154 if (failed) {
2158 msg = "The notebook also failed validation:";
2155 msg = "The notebook also failed validation:";
2159 } else {
2156 } else {
2160 msg = "An invalid notebook may not function properly." +
2157 msg = "An invalid notebook may not function properly." +
2161 " The validation error was:";
2158 " The validation error was:";
2162 }
2159 }
2163 body.append($("<p>").text(
2160 body.append($("<p>").text(
2164 msg
2161 msg
2165 )).append($("<div>").addClass("validation-error").append(
2162 )).append($("<div>").addClass("validation-error").append(
2166 $("<pre>").text(data.message)
2163 $("<pre>").text(data.message)
2167 ));
2164 ));
2168 }
2165 }
2169
2166
2170 dialog.modal({
2167 dialog.modal({
2171 notebook: this,
2168 notebook: this,
2172 keyboard_manager: this.keyboard_manager,
2169 keyboard_manager: this.keyboard_manager,
2173 title: title,
2170 title: title,
2174 body: body,
2171 body: body,
2175 buttons : {
2172 buttons : {
2176 OK : {
2173 OK : {
2177 "class" : "btn-primary"
2174 "class" : "btn-primary"
2178 }
2175 }
2179 }
2176 }
2180 });
2177 });
2181 }
2178 }
2182 if (this.ncells() === 0) {
2179 if (this.ncells() === 0) {
2183 this.insert_cell_below('code');
2180 this.insert_cell_below('code');
2184 this.edit_mode(0);
2181 this.edit_mode(0);
2185 } else {
2182 } else {
2186 this.select(0);
2183 this.select(0);
2187 this.handle_command_mode(this.get_cell(0));
2184 this.handle_command_mode(this.get_cell(0));
2188 }
2185 }
2189 this.set_dirty(false);
2186 this.set_dirty(false);
2190 this.scroll_to_top();
2187 this.scroll_to_top();
2191 this.writable = data.writable || false;
2188 this.writable = data.writable || false;
2192 var nbmodel = data.content;
2189 var nbmodel = data.content;
2193 var orig_nbformat = nbmodel.metadata.orig_nbformat;
2190 var orig_nbformat = nbmodel.metadata.orig_nbformat;
2194 var orig_nbformat_minor = nbmodel.metadata.orig_nbformat_minor;
2191 var orig_nbformat_minor = nbmodel.metadata.orig_nbformat_minor;
2195 if (orig_nbformat !== undefined && nbmodel.nbformat !== orig_nbformat) {
2192 if (orig_nbformat !== undefined && nbmodel.nbformat !== orig_nbformat) {
2196 var src;
2193 var src;
2197 if (nbmodel.nbformat > orig_nbformat) {
2194 if (nbmodel.nbformat > orig_nbformat) {
2198 src = " an older notebook format ";
2195 src = " an older notebook format ";
2199 } else {
2196 } else {
2200 src = " a newer notebook format ";
2197 src = " a newer notebook format ";
2201 }
2198 }
2202
2199
2203 msg = "This notebook has been converted from" + src +
2200 msg = "This notebook has been converted from" + src +
2204 "(v"+orig_nbformat+") to the current notebook " +
2201 "(v"+orig_nbformat+") to the current notebook " +
2205 "format (v"+nbmodel.nbformat+"). The next time you save this notebook, the " +
2202 "format (v"+nbmodel.nbformat+"). The next time you save this notebook, the " +
2206 "current notebook format will be used.";
2203 "current notebook format will be used.";
2207
2204
2208 if (nbmodel.nbformat > orig_nbformat) {
2205 if (nbmodel.nbformat > orig_nbformat) {
2209 msg += " Older versions of IPython may not be able to read the new format.";
2206 msg += " Older versions of IPython may not be able to read the new format.";
2210 } else {
2207 } else {
2211 msg += " Some features of the original notebook may not be available.";
2208 msg += " Some features of the original notebook may not be available.";
2212 }
2209 }
2213 msg += " To preserve the original version, close the " +
2210 msg += " To preserve the original version, close the " +
2214 "notebook without saving it.";
2211 "notebook without saving it.";
2215 dialog.modal({
2212 dialog.modal({
2216 notebook: this,
2213 notebook: this,
2217 keyboard_manager: this.keyboard_manager,
2214 keyboard_manager: this.keyboard_manager,
2218 title : "Notebook converted",
2215 title : "Notebook converted",
2219 body : msg,
2216 body : msg,
2220 buttons : {
2217 buttons : {
2221 OK : {
2218 OK : {
2222 class : "btn-primary"
2219 class : "btn-primary"
2223 }
2220 }
2224 }
2221 }
2225 });
2222 });
2226 } else if (orig_nbformat_minor !== undefined && nbmodel.nbformat_minor < orig_nbformat_minor) {
2223 } else if (orig_nbformat_minor !== undefined && nbmodel.nbformat_minor < orig_nbformat_minor) {
2227 var that = this;
2224 var that = this;
2228 var orig_vs = 'v' + nbmodel.nbformat + '.' + orig_nbformat_minor;
2225 var orig_vs = 'v' + nbmodel.nbformat + '.' + orig_nbformat_minor;
2229 var this_vs = 'v' + nbmodel.nbformat + '.' + this.nbformat_minor;
2226 var this_vs = 'v' + nbmodel.nbformat + '.' + this.nbformat_minor;
2230 msg = "This notebook is version " + orig_vs + ", but we only fully support up to " +
2227 msg = "This notebook is version " + orig_vs + ", but we only fully support up to " +
2231 this_vs + ". You can still work with this notebook, but some features " +
2228 this_vs + ". You can still work with this notebook, but some features " +
2232 "introduced in later notebook versions may not be available.";
2229 "introduced in later notebook versions may not be available.";
2233
2230
2234 dialog.modal({
2231 dialog.modal({
2235 notebook: this,
2232 notebook: this,
2236 keyboard_manager: this.keyboard_manager,
2233 keyboard_manager: this.keyboard_manager,
2237 title : "Newer Notebook",
2234 title : "Newer Notebook",
2238 body : msg,
2235 body : msg,
2239 buttons : {
2236 buttons : {
2240 OK : {
2237 OK : {
2241 class : "btn-danger"
2238 class : "btn-danger"
2242 }
2239 }
2243 }
2240 }
2244 });
2241 });
2245
2242
2246 }
2243 }
2247
2244
2248 // Create the session after the notebook is completely loaded to prevent
2245 // Create the session after the notebook is completely loaded to prevent
2249 // code execution upon loading, which is a security risk.
2246 // code execution upon loading, which is a security risk.
2250 if (this.session === null) {
2247 if (this.session === null) {
2251 var kernelspec = this.metadata.kernelspec || {};
2248 var kernelspec = this.metadata.kernelspec || {};
2252 var kernel_name = kernelspec.name;
2249 var kernel_name = kernelspec.name;
2253
2250
2254 this.start_session(kernel_name);
2251 this.start_session(kernel_name);
2255 }
2252 }
2256 // load our checkpoint list
2253 // load our checkpoint list
2257 this.list_checkpoints();
2254 this.list_checkpoints();
2258
2255
2259 // load toolbar state
2256 // load toolbar state
2260 if (this.metadata.celltoolbar) {
2257 if (this.metadata.celltoolbar) {
2261 celltoolbar.CellToolbar.global_show();
2258 celltoolbar.CellToolbar.global_show();
2262 celltoolbar.CellToolbar.activate_preset(this.metadata.celltoolbar);
2259 celltoolbar.CellToolbar.activate_preset(this.metadata.celltoolbar);
2263 } else {
2260 } else {
2264 celltoolbar.CellToolbar.global_hide();
2261 celltoolbar.CellToolbar.global_hide();
2265 }
2262 }
2266
2263
2267 if (!this.writable) {
2264 if (!this.writable) {
2268 this.set_autosave_interval(0);
2265 this.set_autosave_interval(0);
2269 this.events.trigger('notebook_read_only.Notebook');
2266 this.events.trigger('notebook_read_only.Notebook');
2270 }
2267 }
2271
2268
2272 // now that we're fully loaded, it is safe to restore save functionality
2269 // now that we're fully loaded, it is safe to restore save functionality
2273 this._fully_loaded = true;
2270 this._fully_loaded = true;
2274 this.events.trigger('notebook_loaded.Notebook');
2271 this.events.trigger('notebook_loaded.Notebook');
2275 };
2272 };
2276
2273
2277 /**
2274 /**
2278 * Failure callback for loading a notebook from the server.
2275 * Failure callback for loading a notebook from the server.
2279 *
2276 *
2280 * @method load_notebook_error
2277 * @method load_notebook_error
2281 * @param {Error} error
2278 * @param {Error} error
2282 */
2279 */
2283 Notebook.prototype.load_notebook_error = function (error) {
2280 Notebook.prototype.load_notebook_error = function (error) {
2284 this.events.trigger('notebook_load_failed.Notebook', error);
2281 this.events.trigger('notebook_load_failed.Notebook', error);
2285 var msg;
2282 var msg;
2286 if (error.name === utils.XHR_ERROR && error.xhr.status === 500) {
2283 if (error.name === utils.XHR_ERROR && error.xhr.status === 500) {
2287 utils.log_ajax_error(error.xhr, error.xhr_status, error.xhr_error);
2284 utils.log_ajax_error(error.xhr, error.xhr_status, error.xhr_error);
2288 msg = "An unknown error occurred while loading this notebook. " +
2285 msg = "An unknown error occurred while loading this notebook. " +
2289 "This version can load notebook formats " +
2286 "This version can load notebook formats " +
2290 "v" + this.nbformat + " or earlier. See the server log for details.";
2287 "v" + this.nbformat + " or earlier. See the server log for details.";
2291 } else {
2288 } else {
2292 msg = error.message;
2289 msg = error.message;
2293 }
2290 }
2294 dialog.modal({
2291 dialog.modal({
2295 notebook: this,
2292 notebook: this,
2296 keyboard_manager: this.keyboard_manager,
2293 keyboard_manager: this.keyboard_manager,
2297 title: "Error loading notebook",
2294 title: "Error loading notebook",
2298 body : msg,
2295 body : msg,
2299 buttons : {
2296 buttons : {
2300 "OK": {}
2297 "OK": {}
2301 }
2298 }
2302 });
2299 });
2303 };
2300 };
2304
2301
2305 /********************* checkpoint-related *********************/
2302 /********************* checkpoint-related *********************/
2306
2303
2307 /**
2304 /**
2308 * Save the notebook then immediately create a checkpoint.
2305 * Save the notebook then immediately create a checkpoint.
2309 *
2306 *
2310 * @method save_checkpoint
2307 * @method save_checkpoint
2311 */
2308 */
2312 Notebook.prototype.save_checkpoint = function () {
2309 Notebook.prototype.save_checkpoint = function () {
2313 this._checkpoint_after_save = true;
2310 this._checkpoint_after_save = true;
2314 this.save_notebook();
2311 this.save_notebook();
2315 };
2312 };
2316
2313
2317 /**
2314 /**
2318 * Add a checkpoint for this notebook.
2315 * Add a checkpoint for this notebook.
2319 * for use as a callback from checkpoint creation.
2316 * for use as a callback from checkpoint creation.
2320 *
2317 *
2321 * @method add_checkpoint
2318 * @method add_checkpoint
2322 */
2319 */
2323 Notebook.prototype.add_checkpoint = function (checkpoint) {
2320 Notebook.prototype.add_checkpoint = function (checkpoint) {
2324 var found = false;
2321 var found = false;
2325 for (var i = 0; i < this.checkpoints.length; i++) {
2322 for (var i = 0; i < this.checkpoints.length; i++) {
2326 var existing = this.checkpoints[i];
2323 var existing = this.checkpoints[i];
2327 if (existing.id == checkpoint.id) {
2324 if (existing.id == checkpoint.id) {
2328 found = true;
2325 found = true;
2329 this.checkpoints[i] = checkpoint;
2326 this.checkpoints[i] = checkpoint;
2330 break;
2327 break;
2331 }
2328 }
2332 }
2329 }
2333 if (!found) {
2330 if (!found) {
2334 this.checkpoints.push(checkpoint);
2331 this.checkpoints.push(checkpoint);
2335 }
2332 }
2336 this.last_checkpoint = this.checkpoints[this.checkpoints.length - 1];
2333 this.last_checkpoint = this.checkpoints[this.checkpoints.length - 1];
2337 };
2334 };
2338
2335
2339 /**
2336 /**
2340 * List checkpoints for this notebook.
2337 * List checkpoints for this notebook.
2341 *
2338 *
2342 * @method list_checkpoints
2339 * @method list_checkpoints
2343 */
2340 */
2344 Notebook.prototype.list_checkpoints = function () {
2341 Notebook.prototype.list_checkpoints = function () {
2345 var that = this;
2342 var that = this;
2346 this.contents.list_checkpoints(this.notebook_path).then(
2343 this.contents.list_checkpoints(this.notebook_path).then(
2347 $.proxy(this.list_checkpoints_success, this),
2344 $.proxy(this.list_checkpoints_success, this),
2348 function(error) {
2345 function(error) {
2349 that.events.trigger('list_checkpoints_failed.Notebook', error);
2346 that.events.trigger('list_checkpoints_failed.Notebook', error);
2350 }
2347 }
2351 );
2348 );
2352 };
2349 };
2353
2350
2354 /**
2351 /**
2355 * Success callback for listing checkpoints.
2352 * Success callback for listing checkpoints.
2356 *
2353 *
2357 * @method list_checkpoint_success
2354 * @method list_checkpoint_success
2358 * @param {Object} data JSON representation of a checkpoint
2355 * @param {Object} data JSON representation of a checkpoint
2359 */
2356 */
2360 Notebook.prototype.list_checkpoints_success = function (data) {
2357 Notebook.prototype.list_checkpoints_success = function (data) {
2361 this.checkpoints = data;
2358 this.checkpoints = data;
2362 if (data.length) {
2359 if (data.length) {
2363 this.last_checkpoint = data[data.length - 1];
2360 this.last_checkpoint = data[data.length - 1];
2364 } else {
2361 } else {
2365 this.last_checkpoint = null;
2362 this.last_checkpoint = null;
2366 }
2363 }
2367 this.events.trigger('checkpoints_listed.Notebook', [data]);
2364 this.events.trigger('checkpoints_listed.Notebook', [data]);
2368 };
2365 };
2369
2366
2370 /**
2367 /**
2371 * Create a checkpoint of this notebook on the server from the most recent save.
2368 * Create a checkpoint of this notebook on the server from the most recent save.
2372 *
2369 *
2373 * @method create_checkpoint
2370 * @method create_checkpoint
2374 */
2371 */
2375 Notebook.prototype.create_checkpoint = function () {
2372 Notebook.prototype.create_checkpoint = function () {
2376 var that = this;
2373 var that = this;
2377 this.contents.create_checkpoint(this.notebook_path).then(
2374 this.contents.create_checkpoint(this.notebook_path).then(
2378 $.proxy(this.create_checkpoint_success, this),
2375 $.proxy(this.create_checkpoint_success, this),
2379 function (error) {
2376 function (error) {
2380 that.events.trigger('checkpoint_failed.Notebook', error);
2377 that.events.trigger('checkpoint_failed.Notebook', error);
2381 }
2378 }
2382 );
2379 );
2383 };
2380 };
2384
2381
2385 /**
2382 /**
2386 * Success callback for creating a checkpoint.
2383 * Success callback for creating a checkpoint.
2387 *
2384 *
2388 * @method create_checkpoint_success
2385 * @method create_checkpoint_success
2389 * @param {Object} data JSON representation of a checkpoint
2386 * @param {Object} data JSON representation of a checkpoint
2390 */
2387 */
2391 Notebook.prototype.create_checkpoint_success = function (data) {
2388 Notebook.prototype.create_checkpoint_success = function (data) {
2392 this.add_checkpoint(data);
2389 this.add_checkpoint(data);
2393 this.events.trigger('checkpoint_created.Notebook', data);
2390 this.events.trigger('checkpoint_created.Notebook', data);
2394 };
2391 };
2395
2392
2396 Notebook.prototype.restore_checkpoint_dialog = function (checkpoint) {
2393 Notebook.prototype.restore_checkpoint_dialog = function (checkpoint) {
2397 var that = this;
2394 var that = this;
2398 checkpoint = checkpoint || this.last_checkpoint;
2395 checkpoint = checkpoint || this.last_checkpoint;
2399 if ( ! checkpoint ) {
2396 if ( ! checkpoint ) {
2400 console.log("restore dialog, but no checkpoint to restore to!");
2397 console.log("restore dialog, but no checkpoint to restore to!");
2401 return;
2398 return;
2402 }
2399 }
2403 var body = $('<div/>').append(
2400 var body = $('<div/>').append(
2404 $('<p/>').addClass("p-space").text(
2401 $('<p/>').addClass("p-space").text(
2405 "Are you sure you want to revert the notebook to " +
2402 "Are you sure you want to revert the notebook to " +
2406 "the latest checkpoint?"
2403 "the latest checkpoint?"
2407 ).append(
2404 ).append(
2408 $("<strong/>").text(
2405 $("<strong/>").text(
2409 " This cannot be undone."
2406 " This cannot be undone."
2410 )
2407 )
2411 )
2408 )
2412 ).append(
2409 ).append(
2413 $('<p/>').addClass("p-space").text("The checkpoint was last updated at:")
2410 $('<p/>').addClass("p-space").text("The checkpoint was last updated at:")
2414 ).append(
2411 ).append(
2415 $('<p/>').addClass("p-space").text(
2412 $('<p/>').addClass("p-space").text(
2416 Date(checkpoint.last_modified)
2413 Date(checkpoint.last_modified)
2417 ).css("text-align", "center")
2414 ).css("text-align", "center")
2418 );
2415 );
2419
2416
2420 dialog.modal({
2417 dialog.modal({
2421 notebook: this,
2418 notebook: this,
2422 keyboard_manager: this.keyboard_manager,
2419 keyboard_manager: this.keyboard_manager,
2423 title : "Revert notebook to checkpoint",
2420 title : "Revert notebook to checkpoint",
2424 body : body,
2421 body : body,
2425 buttons : {
2422 buttons : {
2426 Revert : {
2423 Revert : {
2427 class : "btn-danger",
2424 class : "btn-danger",
2428 click : function () {
2425 click : function () {
2429 that.restore_checkpoint(checkpoint.id);
2426 that.restore_checkpoint(checkpoint.id);
2430 }
2427 }
2431 },
2428 },
2432 Cancel : {}
2429 Cancel : {}
2433 }
2430 }
2434 });
2431 });
2435 };
2432 };
2436
2433
2437 /**
2434 /**
2438 * Restore the notebook to a checkpoint state.
2435 * Restore the notebook to a checkpoint state.
2439 *
2436 *
2440 * @method restore_checkpoint
2437 * @method restore_checkpoint
2441 * @param {String} checkpoint ID
2438 * @param {String} checkpoint ID
2442 */
2439 */
2443 Notebook.prototype.restore_checkpoint = function (checkpoint) {
2440 Notebook.prototype.restore_checkpoint = function (checkpoint) {
2444 this.events.trigger('notebook_restoring.Notebook', checkpoint);
2441 this.events.trigger('notebook_restoring.Notebook', checkpoint);
2445 var that = this;
2442 var that = this;
2446 this.contents.restore_checkpoint(this.notebook_path, checkpoint).then(
2443 this.contents.restore_checkpoint(this.notebook_path, checkpoint).then(
2447 $.proxy(this.restore_checkpoint_success, this),
2444 $.proxy(this.restore_checkpoint_success, this),
2448 function (error) {
2445 function (error) {
2449 that.events.trigger('checkpoint_restore_failed.Notebook', error);
2446 that.events.trigger('checkpoint_restore_failed.Notebook', error);
2450 }
2447 }
2451 );
2448 );
2452 };
2449 };
2453
2450
2454 /**
2451 /**
2455 * Success callback for restoring a notebook to a checkpoint.
2452 * Success callback for restoring a notebook to a checkpoint.
2456 *
2453 *
2457 * @method restore_checkpoint_success
2454 * @method restore_checkpoint_success
2458 */
2455 */
2459 Notebook.prototype.restore_checkpoint_success = function () {
2456 Notebook.prototype.restore_checkpoint_success = function () {
2460 this.events.trigger('checkpoint_restored.Notebook');
2457 this.events.trigger('checkpoint_restored.Notebook');
2461 this.load_notebook(this.notebook_path);
2458 this.load_notebook(this.notebook_path);
2462 };
2459 };
2463
2460
2464 /**
2461 /**
2465 * Delete a notebook checkpoint.
2462 * Delete a notebook checkpoint.
2466 *
2463 *
2467 * @method delete_checkpoint
2464 * @method delete_checkpoint
2468 * @param {String} checkpoint ID
2465 * @param {String} checkpoint ID
2469 */
2466 */
2470 Notebook.prototype.delete_checkpoint = function (checkpoint) {
2467 Notebook.prototype.delete_checkpoint = function (checkpoint) {
2471 this.events.trigger('notebook_restoring.Notebook', checkpoint);
2468 this.events.trigger('notebook_restoring.Notebook', checkpoint);
2472 var that = this;
2469 var that = this;
2473 this.contents.delete_checkpoint(this.notebook_path, checkpoint).then(
2470 this.contents.delete_checkpoint(this.notebook_path, checkpoint).then(
2474 $.proxy(this.delete_checkpoint_success, this),
2471 $.proxy(this.delete_checkpoint_success, this),
2475 function (error) {
2472 function (error) {
2476 that.events.trigger('checkpoint_delete_failed.Notebook', error);
2473 that.events.trigger('checkpoint_delete_failed.Notebook', error);
2477 }
2474 }
2478 );
2475 );
2479 };
2476 };
2480
2477
2481 /**
2478 /**
2482 * Success callback for deleting a notebook checkpoint
2479 * Success callback for deleting a notebook checkpoint
2483 *
2480 *
2484 * @method delete_checkpoint_success
2481 * @method delete_checkpoint_success
2485 */
2482 */
2486 Notebook.prototype.delete_checkpoint_success = function () {
2483 Notebook.prototype.delete_checkpoint_success = function () {
2487 this.events.trigger('checkpoint_deleted.Notebook');
2484 this.events.trigger('checkpoint_deleted.Notebook');
2488 this.load_notebook(this.notebook_path);
2485 this.load_notebook(this.notebook_path);
2489 };
2486 };
2490
2487
2491
2488
2492 // For backwards compatability.
2489 // For backwards compatability.
2493 IPython.Notebook = Notebook;
2490 IPython.Notebook = Notebook;
2494
2491
2495 return {'Notebook': Notebook};
2492 return {'Notebook': Notebook};
2496 });
2493 });
@@ -1,356 +1,369 b''
1 """Adapters for IPython msg spec versions."""
1 """Adapters for IPython msg spec versions."""
2
2
3 # Copyright (c) IPython Development Team.
3 # Copyright (c) IPython Development Team.
4 # Distributed under the terms of the Modified BSD License.
4 # Distributed under the terms of the Modified BSD License.
5
5
6 import json
6 import json
7
7
8 from IPython.core.release import kernel_protocol_version_info
8 from IPython.core.release import kernel_protocol_version_info
9 from IPython.utils.tokenutil import token_at_cursor
9 from IPython.utils.tokenutil import token_at_cursor
10
10
11
11
12 def code_to_line(code, cursor_pos):
12 def code_to_line(code, cursor_pos):
13 """Turn a multiline code block and cursor position into a single line
13 """Turn a multiline code block and cursor position into a single line
14 and new cursor position.
14 and new cursor position.
15
15
16 For adapting ``complete_`` and ``object_info_request``.
16 For adapting ``complete_`` and ``object_info_request``.
17 """
17 """
18 if not code:
18 if not code:
19 return "", 0
19 return "", 0
20 for line in code.splitlines(True):
20 for line in code.splitlines(True):
21 n = len(line)
21 n = len(line)
22 if cursor_pos > n:
22 if cursor_pos > n:
23 cursor_pos -= n
23 cursor_pos -= n
24 else:
24 else:
25 break
25 break
26 return line, cursor_pos
26 return line, cursor_pos
27
27
28
28
29 class Adapter(object):
29 class Adapter(object):
30 """Base class for adapting messages
30 """Base class for adapting messages
31
31
32 Override message_type(msg) methods to create adapters.
32 Override message_type(msg) methods to create adapters.
33 """
33 """
34
34
35 msg_type_map = {}
35 msg_type_map = {}
36
36
37 def update_header(self, msg):
37 def update_header(self, msg):
38 return msg
38 return msg
39
39
40 def update_metadata(self, msg):
40 def update_metadata(self, msg):
41 return msg
41 return msg
42
42
43 def update_msg_type(self, msg):
43 def update_msg_type(self, msg):
44 header = msg['header']
44 header = msg['header']
45 msg_type = header['msg_type']
45 msg_type = header['msg_type']
46 if msg_type in self.msg_type_map:
46 if msg_type in self.msg_type_map:
47 msg['msg_type'] = header['msg_type'] = self.msg_type_map[msg_type]
47 msg['msg_type'] = header['msg_type'] = self.msg_type_map[msg_type]
48 return msg
48 return msg
49
49
50 def handle_reply_status_error(self, msg):
50 def handle_reply_status_error(self, msg):
51 """This will be called *instead of* the regular handler
51 """This will be called *instead of* the regular handler
52
52
53 on any reply with status != ok
53 on any reply with status != ok
54 """
54 """
55 return msg
55 return msg
56
56
57 def __call__(self, msg):
57 def __call__(self, msg):
58 msg = self.update_header(msg)
58 msg = self.update_header(msg)
59 msg = self.update_metadata(msg)
59 msg = self.update_metadata(msg)
60 msg = self.update_msg_type(msg)
60 msg = self.update_msg_type(msg)
61 header = msg['header']
61 header = msg['header']
62
62
63 handler = getattr(self, header['msg_type'], None)
63 handler = getattr(self, header['msg_type'], None)
64 if handler is None:
64 if handler is None:
65 return msg
65 return msg
66
66
67 # handle status=error replies separately (no change, at present)
67 # handle status=error replies separately (no change, at present)
68 if msg['content'].get('status', None) in {'error', 'aborted'}:
68 if msg['content'].get('status', None) in {'error', 'aborted'}:
69 return self.handle_reply_status_error(msg)
69 return self.handle_reply_status_error(msg)
70 return handler(msg)
70 return handler(msg)
71
71
72 def _version_str_to_list(version):
72 def _version_str_to_list(version):
73 """convert a version string to a list of ints
73 """convert a version string to a list of ints
74
74
75 non-int segments are excluded
75 non-int segments are excluded
76 """
76 """
77 v = []
77 v = []
78 for part in version.split('.'):
78 for part in version.split('.'):
79 try:
79 try:
80 v.append(int(part))
80 v.append(int(part))
81 except ValueError:
81 except ValueError:
82 pass
82 pass
83 return v
83 return v
84
84
85 class V5toV4(Adapter):
85 class V5toV4(Adapter):
86 """Adapt msg protocol v5 to v4"""
86 """Adapt msg protocol v5 to v4"""
87
87
88 version = '4.1'
88 version = '4.1'
89
89
90 msg_type_map = {
90 msg_type_map = {
91 'execute_result' : 'pyout',
91 'execute_result' : 'pyout',
92 'execute_input' : 'pyin',
92 'execute_input' : 'pyin',
93 'error' : 'pyerr',
93 'error' : 'pyerr',
94 'inspect_request' : 'object_info_request',
94 'inspect_request' : 'object_info_request',
95 'inspect_reply' : 'object_info_reply',
95 'inspect_reply' : 'object_info_reply',
96 }
96 }
97
97
98 def update_header(self, msg):
98 def update_header(self, msg):
99 msg['header'].pop('version', None)
99 msg['header'].pop('version', None)
100 return msg
100 return msg
101
101
102 # shell channel
102 # shell channel
103
103
104 def kernel_info_reply(self, msg):
104 def kernel_info_reply(self, msg):
105 v4c = {}
105 content = msg['content']
106 content = msg['content']
106 content.pop('banner', None)
107 for key in ('language_version', 'protocol_version'):
107 for key in ('language_version', 'protocol_version'):
108 if key in content:
108 if key in content:
109 content[key] = _version_str_to_list(content[key])
109 v4c[key] = _version_str_to_list(content[key])
110 if content.pop('implementation', '') == 'ipython' \
110 if content.get('implementation', '') == 'ipython' \
111 and 'implementation_version' in content:
111 and 'implementation_version' in content:
112 content['ipython_version'] = content.pop('implmentation_version')
112 v4c['ipython_version'] = _version_str_to_list(content['implementation_version'])
113 content.pop('implementation_version', None)
113 language_info = content.get('language_info', {})
114 content.setdefault("implmentation", content['language'])
114 language = language_info.get('name', '')
115 v4c.setdefault('language', language)
116 if 'version' in language_info:
117 v4c.setdefault('language_version', _version_str_to_list(language_info['version']))
118 msg['content'] = v4c
115 return msg
119 return msg
116
120
117 def execute_request(self, msg):
121 def execute_request(self, msg):
118 content = msg['content']
122 content = msg['content']
119 content.setdefault('user_variables', [])
123 content.setdefault('user_variables', [])
120 return msg
124 return msg
121
125
122 def execute_reply(self, msg):
126 def execute_reply(self, msg):
123 content = msg['content']
127 content = msg['content']
124 content.setdefault('user_variables', {})
128 content.setdefault('user_variables', {})
125 # TODO: handle payloads
129 # TODO: handle payloads
126 return msg
130 return msg
127
131
128 def complete_request(self, msg):
132 def complete_request(self, msg):
129 content = msg['content']
133 content = msg['content']
130 code = content['code']
134 code = content['code']
131 cursor_pos = content['cursor_pos']
135 cursor_pos = content['cursor_pos']
132 line, cursor_pos = code_to_line(code, cursor_pos)
136 line, cursor_pos = code_to_line(code, cursor_pos)
133
137
134 new_content = msg['content'] = {}
138 new_content = msg['content'] = {}
135 new_content['text'] = ''
139 new_content['text'] = ''
136 new_content['line'] = line
140 new_content['line'] = line
137 new_content['block'] = None
141 new_content['block'] = None
138 new_content['cursor_pos'] = cursor_pos
142 new_content['cursor_pos'] = cursor_pos
139 return msg
143 return msg
140
144
141 def complete_reply(self, msg):
145 def complete_reply(self, msg):
142 content = msg['content']
146 content = msg['content']
143 cursor_start = content.pop('cursor_start')
147 cursor_start = content.pop('cursor_start')
144 cursor_end = content.pop('cursor_end')
148 cursor_end = content.pop('cursor_end')
145 match_len = cursor_end - cursor_start
149 match_len = cursor_end - cursor_start
146 content['matched_text'] = content['matches'][0][:match_len]
150 content['matched_text'] = content['matches'][0][:match_len]
147 content.pop('metadata', None)
151 content.pop('metadata', None)
148 return msg
152 return msg
149
153
150 def object_info_request(self, msg):
154 def object_info_request(self, msg):
151 content = msg['content']
155 content = msg['content']
152 code = content['code']
156 code = content['code']
153 cursor_pos = content['cursor_pos']
157 cursor_pos = content['cursor_pos']
154 line, _ = code_to_line(code, cursor_pos)
158 line, _ = code_to_line(code, cursor_pos)
155
159
156 new_content = msg['content'] = {}
160 new_content = msg['content'] = {}
157 new_content['oname'] = token_at_cursor(code, cursor_pos)
161 new_content['oname'] = token_at_cursor(code, cursor_pos)
158 new_content['detail_level'] = content['detail_level']
162 new_content['detail_level'] = content['detail_level']
159 return msg
163 return msg
160
164
161 def object_info_reply(self, msg):
165 def object_info_reply(self, msg):
162 """inspect_reply can't be easily backward compatible"""
166 """inspect_reply can't be easily backward compatible"""
163 msg['content'] = {'found' : False, 'oname' : 'unknown'}
167 msg['content'] = {'found' : False, 'oname' : 'unknown'}
164 return msg
168 return msg
165
169
166 # iopub channel
170 # iopub channel
167
171
168 def stream(self, msg):
172 def stream(self, msg):
169 content = msg['content']
173 content = msg['content']
170 content['data'] = content.pop('text')
174 content['data'] = content.pop('text')
171 return msg
175 return msg
172
176
173 def display_data(self, msg):
177 def display_data(self, msg):
174 content = msg['content']
178 content = msg['content']
175 content.setdefault("source", "display")
179 content.setdefault("source", "display")
176 data = content['data']
180 data = content['data']
177 if 'application/json' in data:
181 if 'application/json' in data:
178 try:
182 try:
179 data['application/json'] = json.dumps(data['application/json'])
183 data['application/json'] = json.dumps(data['application/json'])
180 except Exception:
184 except Exception:
181 # warn?
185 # warn?
182 pass
186 pass
183 return msg
187 return msg
184
188
185 # stdin channel
189 # stdin channel
186
190
187 def input_request(self, msg):
191 def input_request(self, msg):
188 msg['content'].pop('password', None)
192 msg['content'].pop('password', None)
189 return msg
193 return msg
190
194
191
195
192 class V4toV5(Adapter):
196 class V4toV5(Adapter):
193 """Convert msg spec V4 to V5"""
197 """Convert msg spec V4 to V5"""
194 version = '5.0'
198 version = '5.0'
195
199
196 # invert message renames above
200 # invert message renames above
197 msg_type_map = {v:k for k,v in V5toV4.msg_type_map.items()}
201 msg_type_map = {v:k for k,v in V5toV4.msg_type_map.items()}
198
202
199 def update_header(self, msg):
203 def update_header(self, msg):
200 msg['header']['version'] = self.version
204 msg['header']['version'] = self.version
201 return msg
205 return msg
202
206
203 # shell channel
207 # shell channel
204
208
205 def kernel_info_reply(self, msg):
209 def kernel_info_reply(self, msg):
206 content = msg['content']
210 content = msg['content']
207 for key in ('language_version', 'protocol_version', 'ipython_version'):
211 for key in ('protocol_version', 'ipython_version'):
208 if key in content:
212 if key in content:
209 content[key] = ".".join(map(str, content[key]))
213 content[key] = '.'.join(map(str, content[key]))
214
215 content.setdefault('protocol_version', '4.1')
210
216
211 if content['language'].startswith('python') and 'ipython_version' in content:
217 if content['language'].startswith('python') and 'ipython_version' in content:
212 content['implementation'] = 'ipython'
218 content['implementation'] = 'ipython'
213 content['implementation_version'] = content.pop('ipython_version')
219 content['implementation_version'] = content.pop('ipython_version')
214
220
221 language = content.pop('language')
222 language_info = content.setdefault('language_info', {})
223 language_info.setdefault('name', language)
224 if 'language_version' in content:
225 language_version = '.'.join(map(str, content.pop('language_version')))
226 language_info.setdefault('version', language_version)
227
215 content['banner'] = ''
228 content['banner'] = ''
216 return msg
229 return msg
217
230
218 def execute_request(self, msg):
231 def execute_request(self, msg):
219 content = msg['content']
232 content = msg['content']
220 user_variables = content.pop('user_variables', [])
233 user_variables = content.pop('user_variables', [])
221 user_expressions = content.setdefault('user_expressions', {})
234 user_expressions = content.setdefault('user_expressions', {})
222 for v in user_variables:
235 for v in user_variables:
223 user_expressions[v] = v
236 user_expressions[v] = v
224 return msg
237 return msg
225
238
226 def execute_reply(self, msg):
239 def execute_reply(self, msg):
227 content = msg['content']
240 content = msg['content']
228 user_expressions = content.setdefault('user_expressions', {})
241 user_expressions = content.setdefault('user_expressions', {})
229 user_variables = content.pop('user_variables', {})
242 user_variables = content.pop('user_variables', {})
230 if user_variables:
243 if user_variables:
231 user_expressions.update(user_variables)
244 user_expressions.update(user_variables)
232
245
233 # Pager payloads became a mime bundle
246 # Pager payloads became a mime bundle
234 for payload in content.get('payload', []):
247 for payload in content.get('payload', []):
235 if payload.get('source', None) == 'page' and ('text' in payload):
248 if payload.get('source', None) == 'page' and ('text' in payload):
236 if 'data' not in payload:
249 if 'data' not in payload:
237 payload['data'] = {}
250 payload['data'] = {}
238 payload['data']['text/plain'] = payload.pop('text')
251 payload['data']['text/plain'] = payload.pop('text')
239
252
240 return msg
253 return msg
241
254
242 def complete_request(self, msg):
255 def complete_request(self, msg):
243 old_content = msg['content']
256 old_content = msg['content']
244
257
245 new_content = msg['content'] = {}
258 new_content = msg['content'] = {}
246 new_content['code'] = old_content['line']
259 new_content['code'] = old_content['line']
247 new_content['cursor_pos'] = old_content['cursor_pos']
260 new_content['cursor_pos'] = old_content['cursor_pos']
248 return msg
261 return msg
249
262
250 def complete_reply(self, msg):
263 def complete_reply(self, msg):
251 # complete_reply needs more context than we have to get cursor_start and end.
264 # complete_reply needs more context than we have to get cursor_start and end.
252 # use special value of `-1` to indicate to frontend that it should be at
265 # use special value of `-1` to indicate to frontend that it should be at
253 # the current cursor position.
266 # the current cursor position.
254 content = msg['content']
267 content = msg['content']
255 new_content = msg['content'] = {'status' : 'ok'}
268 new_content = msg['content'] = {'status' : 'ok'}
256 new_content['matches'] = content['matches']
269 new_content['matches'] = content['matches']
257 new_content['cursor_start'] = -len(content['matched_text'])
270 new_content['cursor_start'] = -len(content['matched_text'])
258 new_content['cursor_end'] = None
271 new_content['cursor_end'] = None
259 new_content['metadata'] = {}
272 new_content['metadata'] = {}
260 return msg
273 return msg
261
274
262 def inspect_request(self, msg):
275 def inspect_request(self, msg):
263 content = msg['content']
276 content = msg['content']
264 name = content['oname']
277 name = content['oname']
265
278
266 new_content = msg['content'] = {}
279 new_content = msg['content'] = {}
267 new_content['code'] = name
280 new_content['code'] = name
268 new_content['cursor_pos'] = len(name)
281 new_content['cursor_pos'] = len(name)
269 new_content['detail_level'] = content['detail_level']
282 new_content['detail_level'] = content['detail_level']
270 return msg
283 return msg
271
284
272 def inspect_reply(self, msg):
285 def inspect_reply(self, msg):
273 """inspect_reply can't be easily backward compatible"""
286 """inspect_reply can't be easily backward compatible"""
274 content = msg['content']
287 content = msg['content']
275 new_content = msg['content'] = {'status' : 'ok'}
288 new_content = msg['content'] = {'status' : 'ok'}
276 found = new_content['found'] = content['found']
289 found = new_content['found'] = content['found']
277 new_content['name'] = content['oname']
290 new_content['name'] = content['oname']
278 new_content['data'] = data = {}
291 new_content['data'] = data = {}
279 new_content['metadata'] = {}
292 new_content['metadata'] = {}
280 if found:
293 if found:
281 lines = []
294 lines = []
282 for key in ('call_def', 'init_definition', 'definition'):
295 for key in ('call_def', 'init_definition', 'definition'):
283 if content.get(key, False):
296 if content.get(key, False):
284 lines.append(content[key])
297 lines.append(content[key])
285 break
298 break
286 for key in ('call_docstring', 'init_docstring', 'docstring'):
299 for key in ('call_docstring', 'init_docstring', 'docstring'):
287 if content.get(key, False):
300 if content.get(key, False):
288 lines.append(content[key])
301 lines.append(content[key])
289 break
302 break
290 if not lines:
303 if not lines:
291 lines.append("<empty docstring>")
304 lines.append("<empty docstring>")
292 data['text/plain'] = '\n'.join(lines)
305 data['text/plain'] = '\n'.join(lines)
293 return msg
306 return msg
294
307
295 # iopub channel
308 # iopub channel
296
309
297 def stream(self, msg):
310 def stream(self, msg):
298 content = msg['content']
311 content = msg['content']
299 content['text'] = content.pop('data')
312 content['text'] = content.pop('data')
300 return msg
313 return msg
301
314
302 def display_data(self, msg):
315 def display_data(self, msg):
303 content = msg['content']
316 content = msg['content']
304 content.pop("source", None)
317 content.pop("source", None)
305 data = content['data']
318 data = content['data']
306 if 'application/json' in data:
319 if 'application/json' in data:
307 try:
320 try:
308 data['application/json'] = json.loads(data['application/json'])
321 data['application/json'] = json.loads(data['application/json'])
309 except Exception:
322 except Exception:
310 # warn?
323 # warn?
311 pass
324 pass
312 return msg
325 return msg
313
326
314 # stdin channel
327 # stdin channel
315
328
316 def input_request(self, msg):
329 def input_request(self, msg):
317 msg['content'].setdefault('password', False)
330 msg['content'].setdefault('password', False)
318 return msg
331 return msg
319
332
320
333
321
334
322 def adapt(msg, to_version=kernel_protocol_version_info[0]):
335 def adapt(msg, to_version=kernel_protocol_version_info[0]):
323 """Adapt a single message to a target version
336 """Adapt a single message to a target version
324
337
325 Parameters
338 Parameters
326 ----------
339 ----------
327
340
328 msg : dict
341 msg : dict
329 An IPython message.
342 An IPython message.
330 to_version : int, optional
343 to_version : int, optional
331 The target major version.
344 The target major version.
332 If unspecified, adapt to the current version for IPython.
345 If unspecified, adapt to the current version for IPython.
333
346
334 Returns
347 Returns
335 -------
348 -------
336
349
337 msg : dict
350 msg : dict
338 An IPython message appropriate in the new version.
351 An IPython message appropriate in the new version.
339 """
352 """
340 header = msg['header']
353 header = msg['header']
341 if 'version' in header:
354 if 'version' in header:
342 from_version = int(header['version'].split('.')[0])
355 from_version = int(header['version'].split('.')[0])
343 else:
356 else:
344 # assume last version before adding the key to the header
357 # assume last version before adding the key to the header
345 from_version = 4
358 from_version = 4
346 adapter = adapters.get((from_version, to_version), None)
359 adapter = adapters.get((from_version, to_version), None)
347 if adapter is None:
360 if adapter is None:
348 return msg
361 return msg
349 return adapter(msg)
362 return adapter(msg)
350
363
351
364
352 # one adapter per major version from,to
365 # one adapter per major version from,to
353 adapters = {
366 adapters = {
354 (5,4) : V5toV4(),
367 (5,4) : V5toV4(),
355 (4,5) : V4toV5(),
368 (4,5) : V4toV5(),
356 }
369 }
@@ -1,334 +1,377 b''
1 """Tests for adapting IPython msg spec versions"""
1 """Tests for adapting IPython msg spec versions"""
2
2
3 # Copyright (c) IPython Development Team.
3 # Copyright (c) IPython Development Team.
4 # Distributed under the terms of the Modified BSD License.
4 # Distributed under the terms of the Modified BSD License.
5
5
6 import copy
6 import copy
7 import json
7 import json
8 from unittest import TestCase
8 from unittest import TestCase
9 import nose.tools as nt
9 import nose.tools as nt
10
10
11 from IPython.kernel.adapter import adapt, V4toV5, V5toV4, code_to_line
11 from IPython.kernel.adapter import adapt, V4toV5, V5toV4, code_to_line
12 from IPython.kernel.zmq.session import Session
12 from IPython.kernel.zmq.session import Session
13
13
14
14
15 def test_default_version():
15 def test_default_version():
16 s = Session()
16 s = Session()
17 msg = s.msg("msg_type")
17 msg = s.msg("msg_type")
18 msg['header'].pop('version')
18 msg['header'].pop('version')
19 original = copy.deepcopy(msg)
19 original = copy.deepcopy(msg)
20 adapted = adapt(original)
20 adapted = adapt(original)
21 nt.assert_equal(adapted['header']['version'], V4toV5.version)
21 nt.assert_equal(adapted['header']['version'], V4toV5.version)
22
22
23 def test_code_to_line_no_code():
23 def test_code_to_line_no_code():
24 line, pos = code_to_line("", 0)
24 line, pos = code_to_line("", 0)
25 nt.assert_equal(line, "")
25 nt.assert_equal(line, "")
26 nt.assert_equal(pos, 0)
26 nt.assert_equal(pos, 0)
27
27
28 class AdapterTest(TestCase):
28 class AdapterTest(TestCase):
29
29
30 def setUp(self):
30 def setUp(self):
31 self.session = Session()
31 self.session = Session()
32
32
33 def adapt(self, msg, version=None):
33 def adapt(self, msg, version=None):
34 original = copy.deepcopy(msg)
34 original = copy.deepcopy(msg)
35 adapted = adapt(msg, version or self.to_version)
35 adapted = adapt(msg, version or self.to_version)
36 return original, adapted
36 return original, adapted
37
37
38 def check_header(self, msg):
38 def check_header(self, msg):
39 pass
39 pass
40
40
41
41
42 class V4toV5TestCase(AdapterTest):
42 class V4toV5TestCase(AdapterTest):
43 from_version = 4
43 from_version = 4
44 to_version = 5
44 to_version = 5
45
45
46 def msg(self, msg_type, content):
46 def msg(self, msg_type, content):
47 """Create a v4 msg (same as v5, minus version header)"""
47 """Create a v4 msg (same as v5, minus version header)"""
48 msg = self.session.msg(msg_type, content)
48 msg = self.session.msg(msg_type, content)
49 msg['header'].pop('version')
49 msg['header'].pop('version')
50 return msg
50 return msg
51
51
52 def test_same_version(self):
52 def test_same_version(self):
53 msg = self.msg("execute_result",
53 msg = self.msg("execute_result",
54 content={'status' : 'ok'}
54 content={'status' : 'ok'}
55 )
55 )
56 original, adapted = self.adapt(msg, self.from_version)
56 original, adapted = self.adapt(msg, self.from_version)
57
57
58 self.assertEqual(original, adapted)
58 self.assertEqual(original, adapted)
59
59
60 def test_no_adapt(self):
60 def test_no_adapt(self):
61 msg = self.msg("input_reply", {'value' : 'some text'})
61 msg = self.msg("input_reply", {'value' : 'some text'})
62 v4, v5 = self.adapt(msg)
62 v4, v5 = self.adapt(msg)
63 self.assertEqual(v5['header']['version'], V4toV5.version)
63 self.assertEqual(v5['header']['version'], V4toV5.version)
64 v5['header'].pop('version')
64 v5['header'].pop('version')
65 self.assertEqual(v4, v5)
65 self.assertEqual(v4, v5)
66
66
67 def test_rename_type(self):
67 def test_rename_type(self):
68 for v5_type, v4_type in [
68 for v5_type, v4_type in [
69 ('execute_result', 'pyout'),
69 ('execute_result', 'pyout'),
70 ('execute_input', 'pyin'),
70 ('execute_input', 'pyin'),
71 ('error', 'pyerr'),
71 ('error', 'pyerr'),
72 ]:
72 ]:
73 msg = self.msg(v4_type, {'key' : 'value'})
73 msg = self.msg(v4_type, {'key' : 'value'})
74 v4, v5 = self.adapt(msg)
74 v4, v5 = self.adapt(msg)
75 self.assertEqual(v5['header']['version'], V4toV5.version)
75 self.assertEqual(v5['header']['version'], V4toV5.version)
76 self.assertEqual(v5['header']['msg_type'], v5_type)
76 self.assertEqual(v5['header']['msg_type'], v5_type)
77 self.assertEqual(v4['content'], v5['content'])
77 self.assertEqual(v4['content'], v5['content'])
78
78
79 def test_execute_request(self):
79 def test_execute_request(self):
80 msg = self.msg("execute_request", {
80 msg = self.msg("execute_request", {
81 'code' : 'a=5',
81 'code' : 'a=5',
82 'silent' : False,
82 'silent' : False,
83 'user_expressions' : {'a' : 'apple'},
83 'user_expressions' : {'a' : 'apple'},
84 'user_variables' : ['b'],
84 'user_variables' : ['b'],
85 })
85 })
86 v4, v5 = self.adapt(msg)
86 v4, v5 = self.adapt(msg)
87 self.assertEqual(v4['header']['msg_type'], v5['header']['msg_type'])
87 self.assertEqual(v4['header']['msg_type'], v5['header']['msg_type'])
88 v4c = v4['content']
88 v4c = v4['content']
89 v5c = v5['content']
89 v5c = v5['content']
90 self.assertEqual(v5c['user_expressions'], {'a' : 'apple', 'b': 'b'})
90 self.assertEqual(v5c['user_expressions'], {'a' : 'apple', 'b': 'b'})
91 self.assertNotIn('user_variables', v5c)
91 self.assertNotIn('user_variables', v5c)
92 self.assertEqual(v5c['code'], v4c['code'])
92 self.assertEqual(v5c['code'], v4c['code'])
93
93
94 def test_execute_reply(self):
94 def test_execute_reply(self):
95 msg = self.msg("execute_reply", {
95 msg = self.msg("execute_reply", {
96 'status': 'ok',
96 'status': 'ok',
97 'execution_count': 7,
97 'execution_count': 7,
98 'user_variables': {'a': 1},
98 'user_variables': {'a': 1},
99 'user_expressions': {'a+a': 2},
99 'user_expressions': {'a+a': 2},
100 'payload': [{'source':'page', 'text':'blah'}]
100 'payload': [{'source':'page', 'text':'blah'}]
101 })
101 })
102 v4, v5 = self.adapt(msg)
102 v4, v5 = self.adapt(msg)
103 v5c = v5['content']
103 v5c = v5['content']
104 self.assertNotIn('user_variables', v5c)
104 self.assertNotIn('user_variables', v5c)
105 self.assertEqual(v5c['user_expressions'], {'a': 1, 'a+a': 2})
105 self.assertEqual(v5c['user_expressions'], {'a': 1, 'a+a': 2})
106 self.assertEqual(v5c['payload'], [{'source': 'page',
106 self.assertEqual(v5c['payload'], [{'source': 'page',
107 'data': {'text/plain': 'blah'}}
107 'data': {'text/plain': 'blah'}}
108 ])
108 ])
109
109
110 def test_complete_request(self):
110 def test_complete_request(self):
111 msg = self.msg("complete_request", {
111 msg = self.msg("complete_request", {
112 'text' : 'a.is',
112 'text' : 'a.is',
113 'line' : 'foo = a.is',
113 'line' : 'foo = a.is',
114 'block' : None,
114 'block' : None,
115 'cursor_pos' : 10,
115 'cursor_pos' : 10,
116 })
116 })
117 v4, v5 = self.adapt(msg)
117 v4, v5 = self.adapt(msg)
118 v4c = v4['content']
118 v4c = v4['content']
119 v5c = v5['content']
119 v5c = v5['content']
120 for key in ('text', 'line', 'block'):
120 for key in ('text', 'line', 'block'):
121 self.assertNotIn(key, v5c)
121 self.assertNotIn(key, v5c)
122 self.assertEqual(v5c['cursor_pos'], v4c['cursor_pos'])
122 self.assertEqual(v5c['cursor_pos'], v4c['cursor_pos'])
123 self.assertEqual(v5c['code'], v4c['line'])
123 self.assertEqual(v5c['code'], v4c['line'])
124
124
125 def test_complete_reply(self):
125 def test_complete_reply(self):
126 msg = self.msg("complete_reply", {
126 msg = self.msg("complete_reply", {
127 'matched_text' : 'a.is',
127 'matched_text' : 'a.is',
128 'matches' : ['a.isalnum',
128 'matches' : ['a.isalnum',
129 'a.isalpha',
129 'a.isalpha',
130 'a.isdigit',
130 'a.isdigit',
131 'a.islower',
131 'a.islower',
132 ],
132 ],
133 })
133 })
134 v4, v5 = self.adapt(msg)
134 v4, v5 = self.adapt(msg)
135 v4c = v4['content']
135 v4c = v4['content']
136 v5c = v5['content']
136 v5c = v5['content']
137
137
138 self.assertEqual(v5c['matches'], v4c['matches'])
138 self.assertEqual(v5c['matches'], v4c['matches'])
139 self.assertEqual(v5c['metadata'], {})
139 self.assertEqual(v5c['metadata'], {})
140 self.assertEqual(v5c['cursor_start'], -4)
140 self.assertEqual(v5c['cursor_start'], -4)
141 self.assertEqual(v5c['cursor_end'], None)
141 self.assertEqual(v5c['cursor_end'], None)
142
142
143 def test_object_info_request(self):
143 def test_object_info_request(self):
144 msg = self.msg("object_info_request", {
144 msg = self.msg("object_info_request", {
145 'oname' : 'foo',
145 'oname' : 'foo',
146 'detail_level' : 1,
146 'detail_level' : 1,
147 })
147 })
148 v4, v5 = self.adapt(msg)
148 v4, v5 = self.adapt(msg)
149 self.assertEqual(v5['header']['msg_type'], 'inspect_request')
149 self.assertEqual(v5['header']['msg_type'], 'inspect_request')
150 v4c = v4['content']
150 v4c = v4['content']
151 v5c = v5['content']
151 v5c = v5['content']
152 self.assertEqual(v5c['code'], v4c['oname'])
152 self.assertEqual(v5c['code'], v4c['oname'])
153 self.assertEqual(v5c['cursor_pos'], len(v4c['oname']))
153 self.assertEqual(v5c['cursor_pos'], len(v4c['oname']))
154 self.assertEqual(v5c['detail_level'], v4c['detail_level'])
154 self.assertEqual(v5c['detail_level'], v4c['detail_level'])
155
155
156 def test_object_info_reply(self):
156 def test_object_info_reply(self):
157 msg = self.msg("object_info_reply", {
157 msg = self.msg("object_info_reply", {
158 'oname' : 'foo',
158 'oname' : 'foo',
159 'found' : True,
159 'found' : True,
160 'status' : 'ok',
160 'status' : 'ok',
161 'definition' : 'foo(a=5)',
161 'definition' : 'foo(a=5)',
162 'docstring' : "the docstring",
162 'docstring' : "the docstring",
163 })
163 })
164 v4, v5 = self.adapt(msg)
164 v4, v5 = self.adapt(msg)
165 self.assertEqual(v5['header']['msg_type'], 'inspect_reply')
165 self.assertEqual(v5['header']['msg_type'], 'inspect_reply')
166 v4c = v4['content']
166 v4c = v4['content']
167 v5c = v5['content']
167 v5c = v5['content']
168 self.assertEqual(sorted(v5c), [ 'data', 'found', 'metadata', 'name', 'status'])
168 self.assertEqual(sorted(v5c), [ 'data', 'found', 'metadata', 'name', 'status'])
169 text = v5c['data']['text/plain']
169 text = v5c['data']['text/plain']
170 self.assertEqual(text, '\n'.join([v4c['definition'], v4c['docstring']]))
170 self.assertEqual(text, '\n'.join([v4c['definition'], v4c['docstring']]))
171
171
172 def test_kernel_info_reply(self):
173 msg = self.msg("kernel_info_reply", {
174 'language': 'python',
175 'language_version': [2,8,0],
176 'ipython_version': [1,2,3],
177 })
178 v4, v5 = self.adapt(msg)
179 v4c = v4['content']
180 v5c = v5['content']
181 self.assertEqual(v5c, {
182 'protocol_version': '4.1',
183 'implementation': 'ipython',
184 'implementation_version': '1.2.3',
185 'language_info': {
186 'name': 'python',
187 'version': '2.8.0',
188 },
189 'banner' : '',
190 })
191
172 # iopub channel
192 # iopub channel
173
193
174 def test_display_data(self):
194 def test_display_data(self):
175 jsondata = dict(a=5)
195 jsondata = dict(a=5)
176 msg = self.msg("display_data", {
196 msg = self.msg("display_data", {
177 'data' : {
197 'data' : {
178 'text/plain' : 'some text',
198 'text/plain' : 'some text',
179 'application/json' : json.dumps(jsondata)
199 'application/json' : json.dumps(jsondata)
180 },
200 },
181 'metadata' : {'text/plain' : { 'key' : 'value' }},
201 'metadata' : {'text/plain' : { 'key' : 'value' }},
182 })
202 })
183 v4, v5 = self.adapt(msg)
203 v4, v5 = self.adapt(msg)
184 v4c = v4['content']
204 v4c = v4['content']
185 v5c = v5['content']
205 v5c = v5['content']
186 self.assertEqual(v5c['metadata'], v4c['metadata'])
206 self.assertEqual(v5c['metadata'], v4c['metadata'])
187 self.assertEqual(v5c['data']['text/plain'], v4c['data']['text/plain'])
207 self.assertEqual(v5c['data']['text/plain'], v4c['data']['text/plain'])
188 self.assertEqual(v5c['data']['application/json'], jsondata)
208 self.assertEqual(v5c['data']['application/json'], jsondata)
189
209
190 # stdin channel
210 # stdin channel
191
211
192 def test_input_request(self):
212 def test_input_request(self):
193 msg = self.msg('input_request', {'prompt': "$>"})
213 msg = self.msg('input_request', {'prompt': "$>"})
194 v4, v5 = self.adapt(msg)
214 v4, v5 = self.adapt(msg)
195 self.assertEqual(v5['content']['prompt'], v4['content']['prompt'])
215 self.assertEqual(v5['content']['prompt'], v4['content']['prompt'])
196 self.assertFalse(v5['content']['password'])
216 self.assertFalse(v5['content']['password'])
197
217
198
218
199 class V5toV4TestCase(AdapterTest):
219 class V5toV4TestCase(AdapterTest):
200 from_version = 5
220 from_version = 5
201 to_version = 4
221 to_version = 4
202
222
203 def msg(self, msg_type, content):
223 def msg(self, msg_type, content):
204 return self.session.msg(msg_type, content)
224 return self.session.msg(msg_type, content)
205
225
206 def test_same_version(self):
226 def test_same_version(self):
207 msg = self.msg("execute_result",
227 msg = self.msg("execute_result",
208 content={'status' : 'ok'}
228 content={'status' : 'ok'}
209 )
229 )
210 original, adapted = self.adapt(msg, self.from_version)
230 original, adapted = self.adapt(msg, self.from_version)
211
231
212 self.assertEqual(original, adapted)
232 self.assertEqual(original, adapted)
213
233
214 def test_no_adapt(self):
234 def test_no_adapt(self):
215 msg = self.msg("input_reply", {'value' : 'some text'})
235 msg = self.msg("input_reply", {'value' : 'some text'})
216 v5, v4 = self.adapt(msg)
236 v5, v4 = self.adapt(msg)
217 self.assertNotIn('version', v4['header'])
237 self.assertNotIn('version', v4['header'])
218 v5['header'].pop('version')
238 v5['header'].pop('version')
219 self.assertEqual(v4, v5)
239 self.assertEqual(v4, v5)
220
240
221 def test_rename_type(self):
241 def test_rename_type(self):
222 for v5_type, v4_type in [
242 for v5_type, v4_type in [
223 ('execute_result', 'pyout'),
243 ('execute_result', 'pyout'),
224 ('execute_input', 'pyin'),
244 ('execute_input', 'pyin'),
225 ('error', 'pyerr'),
245 ('error', 'pyerr'),
226 ]:
246 ]:
227 msg = self.msg(v5_type, {'key' : 'value'})
247 msg = self.msg(v5_type, {'key' : 'value'})
228 v5, v4 = self.adapt(msg)
248 v5, v4 = self.adapt(msg)
229 self.assertEqual(v4['header']['msg_type'], v4_type)
249 self.assertEqual(v4['header']['msg_type'], v4_type)
230 nt.assert_not_in('version', v4['header'])
250 nt.assert_not_in('version', v4['header'])
231 self.assertEqual(v4['content'], v5['content'])
251 self.assertEqual(v4['content'], v5['content'])
232
252
233 def test_execute_request(self):
253 def test_execute_request(self):
234 msg = self.msg("execute_request", {
254 msg = self.msg("execute_request", {
235 'code' : 'a=5',
255 'code' : 'a=5',
236 'silent' : False,
256 'silent' : False,
237 'user_expressions' : {'a' : 'apple'},
257 'user_expressions' : {'a' : 'apple'},
238 })
258 })
239 v5, v4 = self.adapt(msg)
259 v5, v4 = self.adapt(msg)
240 self.assertEqual(v4['header']['msg_type'], v5['header']['msg_type'])
260 self.assertEqual(v4['header']['msg_type'], v5['header']['msg_type'])
241 v4c = v4['content']
261 v4c = v4['content']
242 v5c = v5['content']
262 v5c = v5['content']
243 self.assertEqual(v4c['user_variables'], [])
263 self.assertEqual(v4c['user_variables'], [])
244 self.assertEqual(v5c['code'], v4c['code'])
264 self.assertEqual(v5c['code'], v4c['code'])
245
265
246 def test_complete_request(self):
266 def test_complete_request(self):
247 msg = self.msg("complete_request", {
267 msg = self.msg("complete_request", {
248 'code' : 'def foo():\n'
268 'code' : 'def foo():\n'
249 ' a.is\n'
269 ' a.is\n'
250 'foo()',
270 'foo()',
251 'cursor_pos': 19,
271 'cursor_pos': 19,
252 })
272 })
253 v5, v4 = self.adapt(msg)
273 v5, v4 = self.adapt(msg)
254 v4c = v4['content']
274 v4c = v4['content']
255 v5c = v5['content']
275 v5c = v5['content']
256 self.assertNotIn('code', v4c)
276 self.assertNotIn('code', v4c)
257 self.assertEqual(v4c['line'], v5c['code'].splitlines(True)[1])
277 self.assertEqual(v4c['line'], v5c['code'].splitlines(True)[1])
258 self.assertEqual(v4c['cursor_pos'], 8)
278 self.assertEqual(v4c['cursor_pos'], 8)
259 self.assertEqual(v4c['text'], '')
279 self.assertEqual(v4c['text'], '')
260 self.assertEqual(v4c['block'], None)
280 self.assertEqual(v4c['block'], None)
261
281
262 def test_complete_reply(self):
282 def test_complete_reply(self):
263 msg = self.msg("complete_reply", {
283 msg = self.msg("complete_reply", {
264 'cursor_start' : 10,
284 'cursor_start' : 10,
265 'cursor_end' : 14,
285 'cursor_end' : 14,
266 'matches' : ['a.isalnum',
286 'matches' : ['a.isalnum',
267 'a.isalpha',
287 'a.isalpha',
268 'a.isdigit',
288 'a.isdigit',
269 'a.islower',
289 'a.islower',
270 ],
290 ],
271 'metadata' : {},
291 'metadata' : {},
272 })
292 })
273 v5, v4 = self.adapt(msg)
293 v5, v4 = self.adapt(msg)
274 v4c = v4['content']
294 v4c = v4['content']
275 v5c = v5['content']
295 v5c = v5['content']
276 self.assertEqual(v4c['matched_text'], 'a.is')
296 self.assertEqual(v4c['matched_text'], 'a.is')
277 self.assertEqual(v4c['matches'], v5c['matches'])
297 self.assertEqual(v4c['matches'], v5c['matches'])
278
298
279 def test_inspect_request(self):
299 def test_inspect_request(self):
280 msg = self.msg("inspect_request", {
300 msg = self.msg("inspect_request", {
281 'code' : 'def foo():\n'
301 'code' : 'def foo():\n'
282 ' apple\n'
302 ' apple\n'
283 'bar()',
303 'bar()',
284 'cursor_pos': 18,
304 'cursor_pos': 18,
285 'detail_level' : 1,
305 'detail_level' : 1,
286 })
306 })
287 v5, v4 = self.adapt(msg)
307 v5, v4 = self.adapt(msg)
288 self.assertEqual(v4['header']['msg_type'], 'object_info_request')
308 self.assertEqual(v4['header']['msg_type'], 'object_info_request')
289 v4c = v4['content']
309 v4c = v4['content']
290 v5c = v5['content']
310 v5c = v5['content']
291 self.assertEqual(v4c['oname'], 'apple')
311 self.assertEqual(v4c['oname'], 'apple')
292 self.assertEqual(v5c['detail_level'], v4c['detail_level'])
312 self.assertEqual(v5c['detail_level'], v4c['detail_level'])
293
313
294 def test_inspect_reply(self):
314 def test_inspect_reply(self):
295 msg = self.msg("inspect_reply", {
315 msg = self.msg("inspect_reply", {
296 'name' : 'foo',
316 'name' : 'foo',
297 'found' : True,
317 'found' : True,
298 'data' : {'text/plain' : 'some text'},
318 'data' : {'text/plain' : 'some text'},
299 'metadata' : {},
319 'metadata' : {},
300 })
320 })
301 v5, v4 = self.adapt(msg)
321 v5, v4 = self.adapt(msg)
302 self.assertEqual(v4['header']['msg_type'], 'object_info_reply')
322 self.assertEqual(v4['header']['msg_type'], 'object_info_reply')
303 v4c = v4['content']
323 v4c = v4['content']
304 v5c = v5['content']
324 v5c = v5['content']
305 self.assertEqual(sorted(v4c), ['found', 'oname'])
325 self.assertEqual(sorted(v4c), ['found', 'oname'])
306 self.assertEqual(v4c['found'], False)
326 self.assertEqual(v4c['found'], False)
307
327
328 def test_kernel_info_reply(self):
329 msg = self.msg("kernel_info_reply", {
330 'protocol_version': '5.0',
331 'implementation': 'ipython',
332 'implementation_version': '1.2.3',
333 'language_info': {
334 'name': 'python',
335 'version': '2.8.0',
336 'mimetype': 'text/x-python',
337 },
338 'banner' : 'the banner',
339 })
340 v5, v4 = self.adapt(msg)
341 v4c = v4['content']
342 v5c = v5['content']
343 info = v5c['language_info']
344 self.assertEqual(v4c, {
345 'protocol_version': [5,0],
346 'language': 'python',
347 'language_version': [2,8,0],
348 'ipython_version': [1,2,3],
349 })
350
308 # iopub channel
351 # iopub channel
309
352
310 def test_display_data(self):
353 def test_display_data(self):
311 jsondata = dict(a=5)
354 jsondata = dict(a=5)
312 msg = self.msg("display_data", {
355 msg = self.msg("display_data", {
313 'data' : {
356 'data' : {
314 'text/plain' : 'some text',
357 'text/plain' : 'some text',
315 'application/json' : jsondata,
358 'application/json' : jsondata,
316 },
359 },
317 'metadata' : {'text/plain' : { 'key' : 'value' }},
360 'metadata' : {'text/plain' : { 'key' : 'value' }},
318 })
361 })
319 v5, v4 = self.adapt(msg)
362 v5, v4 = self.adapt(msg)
320 v4c = v4['content']
363 v4c = v4['content']
321 v5c = v5['content']
364 v5c = v5['content']
322 self.assertEqual(v5c['metadata'], v4c['metadata'])
365 self.assertEqual(v5c['metadata'], v4c['metadata'])
323 self.assertEqual(v5c['data']['text/plain'], v4c['data']['text/plain'])
366 self.assertEqual(v5c['data']['text/plain'], v4c['data']['text/plain'])
324 self.assertEqual(v4c['data']['application/json'], json.dumps(jsondata))
367 self.assertEqual(v4c['data']['application/json'], json.dumps(jsondata))
325
368
326 # stdin channel
369 # stdin channel
327
370
328 def test_input_request(self):
371 def test_input_request(self):
329 msg = self.msg('input_request', {'prompt': "$>", 'password' : True})
372 msg = self.msg('input_request', {'prompt': "$>", 'password' : True})
330 v5, v4 = self.adapt(msg)
373 v5, v4 = self.adapt(msg)
331 self.assertEqual(v5['content']['prompt'], v4['content']['prompt'])
374 self.assertEqual(v5['content']['prompt'], v4['content']['prompt'])
332 self.assertNotIn('password', v4['content'])
375 self.assertNotIn('password', v4['content'])
333
376
334
377
@@ -1,328 +1,329 b''
1 """The IPython kernel implementation"""
1 """The IPython kernel implementation"""
2
2
3 import getpass
3 import getpass
4 import sys
4 import sys
5 import traceback
5 import traceback
6
6
7 from IPython.core import release
7 from IPython.core import release
8 from IPython.html.widgets import Widget
8 from IPython.html.widgets import Widget
9 from IPython.utils.py3compat import builtin_mod, PY3
9 from IPython.utils.py3compat import builtin_mod, PY3
10 from IPython.utils.tokenutil import token_at_cursor, line_at_cursor
10 from IPython.utils.tokenutil import token_at_cursor, line_at_cursor
11 from IPython.utils.traitlets import Instance, Type, Any
11 from IPython.utils.traitlets import Instance, Type, Any
12 from IPython.utils.decorators import undoc
12 from IPython.utils.decorators import undoc
13
13
14 from ..comm import CommManager
14 from ..comm import CommManager
15 from .kernelbase import Kernel as KernelBase
15 from .kernelbase import Kernel as KernelBase
16 from .serialize import serialize_object, unpack_apply_message
16 from .serialize import serialize_object, unpack_apply_message
17 from .zmqshell import ZMQInteractiveShell
17 from .zmqshell import ZMQInteractiveShell
18
18
19 class IPythonKernel(KernelBase):
19 class IPythonKernel(KernelBase):
20 shell = Instance('IPython.core.interactiveshell.InteractiveShellABC')
20 shell = Instance('IPython.core.interactiveshell.InteractiveShellABC')
21 shell_class = Type(ZMQInteractiveShell)
21 shell_class = Type(ZMQInteractiveShell)
22
22
23 user_module = Any()
23 user_module = Any()
24 def _user_module_changed(self, name, old, new):
24 def _user_module_changed(self, name, old, new):
25 if self.shell is not None:
25 if self.shell is not None:
26 self.shell.user_module = new
26 self.shell.user_module = new
27
27
28 user_ns = Instance(dict, args=None, allow_none=True)
28 user_ns = Instance(dict, args=None, allow_none=True)
29 def _user_ns_changed(self, name, old, new):
29 def _user_ns_changed(self, name, old, new):
30 if self.shell is not None:
30 if self.shell is not None:
31 self.shell.user_ns = new
31 self.shell.user_ns = new
32 self.shell.init_user_ns()
32 self.shell.init_user_ns()
33
33
34 # A reference to the Python builtin 'raw_input' function.
34 # A reference to the Python builtin 'raw_input' function.
35 # (i.e., __builtin__.raw_input for Python 2.7, builtins.input for Python 3)
35 # (i.e., __builtin__.raw_input for Python 2.7, builtins.input for Python 3)
36 _sys_raw_input = Any()
36 _sys_raw_input = Any()
37 _sys_eval_input = Any()
37 _sys_eval_input = Any()
38
38
39 def __init__(self, **kwargs):
39 def __init__(self, **kwargs):
40 super(IPythonKernel, self).__init__(**kwargs)
40 super(IPythonKernel, self).__init__(**kwargs)
41
41
42 # Initialize the InteractiveShell subclass
42 # Initialize the InteractiveShell subclass
43 self.shell = self.shell_class.instance(parent=self,
43 self.shell = self.shell_class.instance(parent=self,
44 profile_dir = self.profile_dir,
44 profile_dir = self.profile_dir,
45 user_module = self.user_module,
45 user_module = self.user_module,
46 user_ns = self.user_ns,
46 user_ns = self.user_ns,
47 kernel = self,
47 kernel = self,
48 )
48 )
49 self.shell.displayhook.session = self.session
49 self.shell.displayhook.session = self.session
50 self.shell.displayhook.pub_socket = self.iopub_socket
50 self.shell.displayhook.pub_socket = self.iopub_socket
51 self.shell.displayhook.topic = self._topic('execute_result')
51 self.shell.displayhook.topic = self._topic('execute_result')
52 self.shell.display_pub.session = self.session
52 self.shell.display_pub.session = self.session
53 self.shell.display_pub.pub_socket = self.iopub_socket
53 self.shell.display_pub.pub_socket = self.iopub_socket
54 self.shell.data_pub.session = self.session
54 self.shell.data_pub.session = self.session
55 self.shell.data_pub.pub_socket = self.iopub_socket
55 self.shell.data_pub.pub_socket = self.iopub_socket
56
56
57 # TMP - hack while developing
57 # TMP - hack while developing
58 self.shell._reply_content = None
58 self.shell._reply_content = None
59
59
60 self.comm_manager = CommManager(shell=self.shell, parent=self,
60 self.comm_manager = CommManager(shell=self.shell, parent=self,
61 kernel=self)
61 kernel=self)
62 self.comm_manager.register_target('ipython.widget', Widget.handle_comm_opened)
62 self.comm_manager.register_target('ipython.widget', Widget.handle_comm_opened)
63
63
64 self.shell.configurables.append(self.comm_manager)
64 self.shell.configurables.append(self.comm_manager)
65 comm_msg_types = [ 'comm_open', 'comm_msg', 'comm_close' ]
65 comm_msg_types = [ 'comm_open', 'comm_msg', 'comm_close' ]
66 for msg_type in comm_msg_types:
66 for msg_type in comm_msg_types:
67 self.shell_handlers[msg_type] = getattr(self.comm_manager, msg_type)
67 self.shell_handlers[msg_type] = getattr(self.comm_manager, msg_type)
68
68
69 # Kernel info fields
69 # Kernel info fields
70 implementation = 'ipython'
70 implementation = 'ipython'
71 implementation_version = release.version
71 implementation_version = release.version
72 language = 'python'
72 language_info = {
73 language_version = sys.version.split()[0]
73 'name': 'python',
74 language_info = {'mimetype': 'text/x-python',
74 'version': sys.version.split()[0],
75 'mimetype': 'text/x-python',
75 'codemirror_mode': {'name': 'ipython',
76 'codemirror_mode': {'name': 'ipython',
76 'version': sys.version_info[0]},
77 'version': sys.version_info[0]},
77 'pygments_lexer': 'ipython%d' % (3 if PY3 else 2),
78 'pygments_lexer': 'ipython%d' % (3 if PY3 else 2),
78 }
79 }
79 @property
80 @property
80 def banner(self):
81 def banner(self):
81 return self.shell.banner
82 return self.shell.banner
82
83
83 def start(self):
84 def start(self):
84 self.shell.exit_now = False
85 self.shell.exit_now = False
85 super(IPythonKernel, self).start()
86 super(IPythonKernel, self).start()
86
87
87 def set_parent(self, ident, parent):
88 def set_parent(self, ident, parent):
88 """Overridden from parent to tell the display hook and output streams
89 """Overridden from parent to tell the display hook and output streams
89 about the parent message.
90 about the parent message.
90 """
91 """
91 super(IPythonKernel, self).set_parent(ident, parent)
92 super(IPythonKernel, self).set_parent(ident, parent)
92 self.shell.set_parent(parent)
93 self.shell.set_parent(parent)
93
94
94 def _forward_input(self, allow_stdin=False):
95 def _forward_input(self, allow_stdin=False):
95 """Forward raw_input and getpass to the current frontend.
96 """Forward raw_input and getpass to the current frontend.
96
97
97 via input_request
98 via input_request
98 """
99 """
99 self._allow_stdin = allow_stdin
100 self._allow_stdin = allow_stdin
100
101
101 if PY3:
102 if PY3:
102 self._sys_raw_input = builtin_mod.input
103 self._sys_raw_input = builtin_mod.input
103 builtin_mod.input = self.raw_input
104 builtin_mod.input = self.raw_input
104 else:
105 else:
105 self._sys_raw_input = builtin_mod.raw_input
106 self._sys_raw_input = builtin_mod.raw_input
106 self._sys_eval_input = builtin_mod.input
107 self._sys_eval_input = builtin_mod.input
107 builtin_mod.raw_input = self.raw_input
108 builtin_mod.raw_input = self.raw_input
108 builtin_mod.input = lambda prompt='': eval(self.raw_input(prompt))
109 builtin_mod.input = lambda prompt='': eval(self.raw_input(prompt))
109 self._save_getpass = getpass.getpass
110 self._save_getpass = getpass.getpass
110 getpass.getpass = self.getpass
111 getpass.getpass = self.getpass
111
112
112 def _restore_input(self):
113 def _restore_input(self):
113 """Restore raw_input, getpass"""
114 """Restore raw_input, getpass"""
114 if PY3:
115 if PY3:
115 builtin_mod.input = self._sys_raw_input
116 builtin_mod.input = self._sys_raw_input
116 else:
117 else:
117 builtin_mod.raw_input = self._sys_raw_input
118 builtin_mod.raw_input = self._sys_raw_input
118 builtin_mod.input = self._sys_eval_input
119 builtin_mod.input = self._sys_eval_input
119
120
120 getpass.getpass = self._save_getpass
121 getpass.getpass = self._save_getpass
121
122
122 @property
123 @property
123 def execution_count(self):
124 def execution_count(self):
124 return self.shell.execution_count
125 return self.shell.execution_count
125
126
126 @execution_count.setter
127 @execution_count.setter
127 def execution_count(self, value):
128 def execution_count(self, value):
128 # Ignore the incrememnting done by KernelBase, in favour of our shell's
129 # Ignore the incrememnting done by KernelBase, in favour of our shell's
129 # execution counter.
130 # execution counter.
130 pass
131 pass
131
132
132 def do_execute(self, code, silent, store_history=True,
133 def do_execute(self, code, silent, store_history=True,
133 user_expressions=None, allow_stdin=False):
134 user_expressions=None, allow_stdin=False):
134 shell = self.shell # we'll need this a lot here
135 shell = self.shell # we'll need this a lot here
135
136
136 self._forward_input(allow_stdin)
137 self._forward_input(allow_stdin)
137
138
138 reply_content = {}
139 reply_content = {}
139 # FIXME: the shell calls the exception handler itself.
140 # FIXME: the shell calls the exception handler itself.
140 shell._reply_content = None
141 shell._reply_content = None
141 try:
142 try:
142 shell.run_cell(code, store_history=store_history, silent=silent)
143 shell.run_cell(code, store_history=store_history, silent=silent)
143 except:
144 except:
144 status = u'error'
145 status = u'error'
145 # FIXME: this code right now isn't being used yet by default,
146 # FIXME: this code right now isn't being used yet by default,
146 # because the run_cell() call above directly fires off exception
147 # because the run_cell() call above directly fires off exception
147 # reporting. This code, therefore, is only active in the scenario
148 # reporting. This code, therefore, is only active in the scenario
148 # where runlines itself has an unhandled exception. We need to
149 # where runlines itself has an unhandled exception. We need to
149 # uniformize this, for all exception construction to come from a
150 # uniformize this, for all exception construction to come from a
150 # single location in the codbase.
151 # single location in the codbase.
151 etype, evalue, tb = sys.exc_info()
152 etype, evalue, tb = sys.exc_info()
152 tb_list = traceback.format_exception(etype, evalue, tb)
153 tb_list = traceback.format_exception(etype, evalue, tb)
153 reply_content.update(shell._showtraceback(etype, evalue, tb_list))
154 reply_content.update(shell._showtraceback(etype, evalue, tb_list))
154 else:
155 else:
155 status = u'ok'
156 status = u'ok'
156 finally:
157 finally:
157 self._restore_input()
158 self._restore_input()
158
159
159 reply_content[u'status'] = status
160 reply_content[u'status'] = status
160
161
161 # Return the execution counter so clients can display prompts
162 # Return the execution counter so clients can display prompts
162 reply_content['execution_count'] = shell.execution_count - 1
163 reply_content['execution_count'] = shell.execution_count - 1
163
164
164 # FIXME - fish exception info out of shell, possibly left there by
165 # FIXME - fish exception info out of shell, possibly left there by
165 # runlines. We'll need to clean up this logic later.
166 # runlines. We'll need to clean up this logic later.
166 if shell._reply_content is not None:
167 if shell._reply_content is not None:
167 reply_content.update(shell._reply_content)
168 reply_content.update(shell._reply_content)
168 e_info = dict(engine_uuid=self.ident, engine_id=self.int_id, method='execute')
169 e_info = dict(engine_uuid=self.ident, engine_id=self.int_id, method='execute')
169 reply_content['engine_info'] = e_info
170 reply_content['engine_info'] = e_info
170 # reset after use
171 # reset after use
171 shell._reply_content = None
172 shell._reply_content = None
172
173
173 if 'traceback' in reply_content:
174 if 'traceback' in reply_content:
174 self.log.info("Exception in execute request:\n%s", '\n'.join(reply_content['traceback']))
175 self.log.info("Exception in execute request:\n%s", '\n'.join(reply_content['traceback']))
175
176
176
177
177 # At this point, we can tell whether the main code execution succeeded
178 # At this point, we can tell whether the main code execution succeeded
178 # or not. If it did, we proceed to evaluate user_expressions
179 # or not. If it did, we proceed to evaluate user_expressions
179 if reply_content['status'] == 'ok':
180 if reply_content['status'] == 'ok':
180 reply_content[u'user_expressions'] = \
181 reply_content[u'user_expressions'] = \
181 shell.user_expressions(user_expressions or {})
182 shell.user_expressions(user_expressions or {})
182 else:
183 else:
183 # If there was an error, don't even try to compute expressions
184 # If there was an error, don't even try to compute expressions
184 reply_content[u'user_expressions'] = {}
185 reply_content[u'user_expressions'] = {}
185
186
186 # Payloads should be retrieved regardless of outcome, so we can both
187 # Payloads should be retrieved regardless of outcome, so we can both
187 # recover partial output (that could have been generated early in a
188 # recover partial output (that could have been generated early in a
188 # block, before an error) and clear the payload system always.
189 # block, before an error) and clear the payload system always.
189 reply_content[u'payload'] = shell.payload_manager.read_payload()
190 reply_content[u'payload'] = shell.payload_manager.read_payload()
190 # Be agressive about clearing the payload because we don't want
191 # Be agressive about clearing the payload because we don't want
191 # it to sit in memory until the next execute_request comes in.
192 # it to sit in memory until the next execute_request comes in.
192 shell.payload_manager.clear_payload()
193 shell.payload_manager.clear_payload()
193
194
194 return reply_content
195 return reply_content
195
196
196 def do_complete(self, code, cursor_pos):
197 def do_complete(self, code, cursor_pos):
197 # FIXME: IPython completers currently assume single line,
198 # FIXME: IPython completers currently assume single line,
198 # but completion messages give multi-line context
199 # but completion messages give multi-line context
199 # For now, extract line from cell, based on cursor_pos:
200 # For now, extract line from cell, based on cursor_pos:
200 if cursor_pos is None:
201 if cursor_pos is None:
201 cursor_pos = len(code)
202 cursor_pos = len(code)
202 line, offset = line_at_cursor(code, cursor_pos)
203 line, offset = line_at_cursor(code, cursor_pos)
203 line_cursor = cursor_pos - offset
204 line_cursor = cursor_pos - offset
204
205
205 txt, matches = self.shell.complete('', line, line_cursor)
206 txt, matches = self.shell.complete('', line, line_cursor)
206 return {'matches' : matches,
207 return {'matches' : matches,
207 'cursor_end' : cursor_pos,
208 'cursor_end' : cursor_pos,
208 'cursor_start' : cursor_pos - len(txt),
209 'cursor_start' : cursor_pos - len(txt),
209 'metadata' : {},
210 'metadata' : {},
210 'status' : 'ok'}
211 'status' : 'ok'}
211
212
212 def do_inspect(self, code, cursor_pos, detail_level=0):
213 def do_inspect(self, code, cursor_pos, detail_level=0):
213 name = token_at_cursor(code, cursor_pos)
214 name = token_at_cursor(code, cursor_pos)
214 info = self.shell.object_inspect(name)
215 info = self.shell.object_inspect(name)
215
216
216 reply_content = {'status' : 'ok'}
217 reply_content = {'status' : 'ok'}
217 reply_content['data'] = data = {}
218 reply_content['data'] = data = {}
218 reply_content['metadata'] = {}
219 reply_content['metadata'] = {}
219 reply_content['found'] = info['found']
220 reply_content['found'] = info['found']
220 if info['found']:
221 if info['found']:
221 info_text = self.shell.object_inspect_text(
222 info_text = self.shell.object_inspect_text(
222 name,
223 name,
223 detail_level=detail_level,
224 detail_level=detail_level,
224 )
225 )
225 data['text/plain'] = info_text
226 data['text/plain'] = info_text
226
227
227 return reply_content
228 return reply_content
228
229
229 def do_history(self, hist_access_type, output, raw, session=None, start=None,
230 def do_history(self, hist_access_type, output, raw, session=None, start=None,
230 stop=None, n=None, pattern=None, unique=False):
231 stop=None, n=None, pattern=None, unique=False):
231 if hist_access_type == 'tail':
232 if hist_access_type == 'tail':
232 hist = self.shell.history_manager.get_tail(n, raw=raw, output=output,
233 hist = self.shell.history_manager.get_tail(n, raw=raw, output=output,
233 include_latest=True)
234 include_latest=True)
234
235
235 elif hist_access_type == 'range':
236 elif hist_access_type == 'range':
236 hist = self.shell.history_manager.get_range(session, start, stop,
237 hist = self.shell.history_manager.get_range(session, start, stop,
237 raw=raw, output=output)
238 raw=raw, output=output)
238
239
239 elif hist_access_type == 'search':
240 elif hist_access_type == 'search':
240 hist = self.shell.history_manager.search(
241 hist = self.shell.history_manager.search(
241 pattern, raw=raw, output=output, n=n, unique=unique)
242 pattern, raw=raw, output=output, n=n, unique=unique)
242 else:
243 else:
243 hist = []
244 hist = []
244
245
245 return {'history' : list(hist)}
246 return {'history' : list(hist)}
246
247
247 def do_shutdown(self, restart):
248 def do_shutdown(self, restart):
248 self.shell.exit_now = True
249 self.shell.exit_now = True
249 return dict(status='ok', restart=restart)
250 return dict(status='ok', restart=restart)
250
251
251 def do_is_complete(self, code):
252 def do_is_complete(self, code):
252 status, indent_spaces = self.shell.input_transformer_manager.check_complete(code)
253 status, indent_spaces = self.shell.input_transformer_manager.check_complete(code)
253 r = {'status': status}
254 r = {'status': status}
254 if status == 'incomplete':
255 if status == 'incomplete':
255 r['indent'] = ' ' * indent_spaces
256 r['indent'] = ' ' * indent_spaces
256 return r
257 return r
257
258
258 def do_apply(self, content, bufs, msg_id, reply_metadata):
259 def do_apply(self, content, bufs, msg_id, reply_metadata):
259 shell = self.shell
260 shell = self.shell
260 try:
261 try:
261 working = shell.user_ns
262 working = shell.user_ns
262
263
263 prefix = "_"+str(msg_id).replace("-","")+"_"
264 prefix = "_"+str(msg_id).replace("-","")+"_"
264
265
265 f,args,kwargs = unpack_apply_message(bufs, working, copy=False)
266 f,args,kwargs = unpack_apply_message(bufs, working, copy=False)
266
267
267 fname = getattr(f, '__name__', 'f')
268 fname = getattr(f, '__name__', 'f')
268
269
269 fname = prefix+"f"
270 fname = prefix+"f"
270 argname = prefix+"args"
271 argname = prefix+"args"
271 kwargname = prefix+"kwargs"
272 kwargname = prefix+"kwargs"
272 resultname = prefix+"result"
273 resultname = prefix+"result"
273
274
274 ns = { fname : f, argname : args, kwargname : kwargs , resultname : None }
275 ns = { fname : f, argname : args, kwargname : kwargs , resultname : None }
275 # print ns
276 # print ns
276 working.update(ns)
277 working.update(ns)
277 code = "%s = %s(*%s,**%s)" % (resultname, fname, argname, kwargname)
278 code = "%s = %s(*%s,**%s)" % (resultname, fname, argname, kwargname)
278 try:
279 try:
279 exec(code, shell.user_global_ns, shell.user_ns)
280 exec(code, shell.user_global_ns, shell.user_ns)
280 result = working.get(resultname)
281 result = working.get(resultname)
281 finally:
282 finally:
282 for key in ns:
283 for key in ns:
283 working.pop(key)
284 working.pop(key)
284
285
285 result_buf = serialize_object(result,
286 result_buf = serialize_object(result,
286 buffer_threshold=self.session.buffer_threshold,
287 buffer_threshold=self.session.buffer_threshold,
287 item_threshold=self.session.item_threshold,
288 item_threshold=self.session.item_threshold,
288 )
289 )
289
290
290 except:
291 except:
291 # invoke IPython traceback formatting
292 # invoke IPython traceback formatting
292 shell.showtraceback()
293 shell.showtraceback()
293 # FIXME - fish exception info out of shell, possibly left there by
294 # FIXME - fish exception info out of shell, possibly left there by
294 # run_code. We'll need to clean up this logic later.
295 # run_code. We'll need to clean up this logic later.
295 reply_content = {}
296 reply_content = {}
296 if shell._reply_content is not None:
297 if shell._reply_content is not None:
297 reply_content.update(shell._reply_content)
298 reply_content.update(shell._reply_content)
298 e_info = dict(engine_uuid=self.ident, engine_id=self.int_id, method='apply')
299 e_info = dict(engine_uuid=self.ident, engine_id=self.int_id, method='apply')
299 reply_content['engine_info'] = e_info
300 reply_content['engine_info'] = e_info
300 # reset after use
301 # reset after use
301 shell._reply_content = None
302 shell._reply_content = None
302
303
303 self.send_response(self.iopub_socket, u'error', reply_content,
304 self.send_response(self.iopub_socket, u'error', reply_content,
304 ident=self._topic('error'))
305 ident=self._topic('error'))
305 self.log.info("Exception in apply request:\n%s", '\n'.join(reply_content['traceback']))
306 self.log.info("Exception in apply request:\n%s", '\n'.join(reply_content['traceback']))
306 result_buf = []
307 result_buf = []
307
308
308 if reply_content['ename'] == 'UnmetDependency':
309 if reply_content['ename'] == 'UnmetDependency':
309 reply_metadata['dependencies_met'] = False
310 reply_metadata['dependencies_met'] = False
310 else:
311 else:
311 reply_content = {'status' : 'ok'}
312 reply_content = {'status' : 'ok'}
312
313
313 return reply_content, result_buf
314 return reply_content, result_buf
314
315
315 def do_clear(self):
316 def do_clear(self):
316 self.shell.reset(False)
317 self.shell.reset(False)
317 return dict(status='ok')
318 return dict(status='ok')
318
319
319
320
320 # This exists only for backwards compatibility - use IPythonKernel instead
321 # This exists only for backwards compatibility - use IPythonKernel instead
321
322
322 @undoc
323 @undoc
323 class Kernel(IPythonKernel):
324 class Kernel(IPythonKernel):
324 def __init__(self, *args, **kwargs):
325 def __init__(self, *args, **kwargs):
325 import warnings
326 import warnings
326 warnings.warn('Kernel is a deprecated alias of IPython.kernel.zmq.ipkernel.IPythonKernel',
327 warnings.warn('Kernel is a deprecated alias of IPython.kernel.zmq.ipkernel.IPythonKernel',
327 DeprecationWarning)
328 DeprecationWarning)
328 super(Kernel, self).__init__(*args, **kwargs) No newline at end of file
329 super(Kernel, self).__init__(*args, **kwargs)
@@ -1,697 +1,695 b''
1 """Base class for a kernel that talks to frontends over 0MQ."""
1 """Base class for a kernel that talks to frontends over 0MQ."""
2
2
3 # Copyright (c) IPython Development Team.
3 # Copyright (c) IPython Development Team.
4 # Distributed under the terms of the Modified BSD License.
4 # Distributed under the terms of the Modified BSD License.
5
5
6 from __future__ import print_function
6 from __future__ import print_function
7
7
8 import sys
8 import sys
9 import time
9 import time
10 import logging
10 import logging
11 import uuid
11 import uuid
12
12
13 from datetime import datetime
13 from datetime import datetime
14 from signal import (
14 from signal import (
15 signal, default_int_handler, SIGINT
15 signal, default_int_handler, SIGINT
16 )
16 )
17
17
18 import zmq
18 import zmq
19 from zmq.eventloop import ioloop
19 from zmq.eventloop import ioloop
20 from zmq.eventloop.zmqstream import ZMQStream
20 from zmq.eventloop.zmqstream import ZMQStream
21
21
22 from IPython.config.configurable import SingletonConfigurable
22 from IPython.config.configurable import SingletonConfigurable
23 from IPython.core.error import StdinNotImplementedError
23 from IPython.core.error import StdinNotImplementedError
24 from IPython.core import release
24 from IPython.core import release
25 from IPython.utils import py3compat
25 from IPython.utils import py3compat
26 from IPython.utils.py3compat import unicode_type, string_types
26 from IPython.utils.py3compat import unicode_type, string_types
27 from IPython.utils.jsonutil import json_clean
27 from IPython.utils.jsonutil import json_clean
28 from IPython.utils.traitlets import (
28 from IPython.utils.traitlets import (
29 Any, Instance, Float, Dict, List, Set, Integer, Unicode, Bool,
29 Any, Instance, Float, Dict, List, Set, Integer, Unicode, Bool,
30 )
30 )
31
31
32 from .session import Session
32 from .session import Session
33
33
34
34
35 class Kernel(SingletonConfigurable):
35 class Kernel(SingletonConfigurable):
36
36
37 #---------------------------------------------------------------------------
37 #---------------------------------------------------------------------------
38 # Kernel interface
38 # Kernel interface
39 #---------------------------------------------------------------------------
39 #---------------------------------------------------------------------------
40
40
41 # attribute to override with a GUI
41 # attribute to override with a GUI
42 eventloop = Any(None)
42 eventloop = Any(None)
43 def _eventloop_changed(self, name, old, new):
43 def _eventloop_changed(self, name, old, new):
44 """schedule call to eventloop from IOLoop"""
44 """schedule call to eventloop from IOLoop"""
45 loop = ioloop.IOLoop.instance()
45 loop = ioloop.IOLoop.instance()
46 loop.add_callback(self.enter_eventloop)
46 loop.add_callback(self.enter_eventloop)
47
47
48 session = Instance(Session)
48 session = Instance(Session)
49 profile_dir = Instance('IPython.core.profiledir.ProfileDir')
49 profile_dir = Instance('IPython.core.profiledir.ProfileDir')
50 shell_streams = List()
50 shell_streams = List()
51 control_stream = Instance(ZMQStream)
51 control_stream = Instance(ZMQStream)
52 iopub_socket = Instance(zmq.Socket)
52 iopub_socket = Instance(zmq.Socket)
53 stdin_socket = Instance(zmq.Socket)
53 stdin_socket = Instance(zmq.Socket)
54 log = Instance(logging.Logger)
54 log = Instance(logging.Logger)
55
55
56 # identities:
56 # identities:
57 int_id = Integer(-1)
57 int_id = Integer(-1)
58 ident = Unicode()
58 ident = Unicode()
59
59
60 def _ident_default(self):
60 def _ident_default(self):
61 return unicode_type(uuid.uuid4())
61 return unicode_type(uuid.uuid4())
62
62
63 # This should be overridden by wrapper kernels that implement any real
63 # This should be overridden by wrapper kernels that implement any real
64 # language.
64 # language.
65 language_info = {}
65 language_info = {}
66
66
67 # Private interface
67 # Private interface
68
68
69 _darwin_app_nap = Bool(True, config=True,
69 _darwin_app_nap = Bool(True, config=True,
70 help="""Whether to use appnope for compatiblity with OS X App Nap.
70 help="""Whether to use appnope for compatiblity with OS X App Nap.
71
71
72 Only affects OS X >= 10.9.
72 Only affects OS X >= 10.9.
73 """
73 """
74 )
74 )
75
75
76 # track associations with current request
76 # track associations with current request
77 _allow_stdin = Bool(False)
77 _allow_stdin = Bool(False)
78 _parent_header = Dict()
78 _parent_header = Dict()
79 _parent_ident = Any(b'')
79 _parent_ident = Any(b'')
80 # Time to sleep after flushing the stdout/err buffers in each execute
80 # Time to sleep after flushing the stdout/err buffers in each execute
81 # cycle. While this introduces a hard limit on the minimal latency of the
81 # cycle. While this introduces a hard limit on the minimal latency of the
82 # execute cycle, it helps prevent output synchronization problems for
82 # execute cycle, it helps prevent output synchronization problems for
83 # clients.
83 # clients.
84 # Units are in seconds. The minimum zmq latency on local host is probably
84 # Units are in seconds. The minimum zmq latency on local host is probably
85 # ~150 microseconds, set this to 500us for now. We may need to increase it
85 # ~150 microseconds, set this to 500us for now. We may need to increase it
86 # a little if it's not enough after more interactive testing.
86 # a little if it's not enough after more interactive testing.
87 _execute_sleep = Float(0.0005, config=True)
87 _execute_sleep = Float(0.0005, config=True)
88
88
89 # Frequency of the kernel's event loop.
89 # Frequency of the kernel's event loop.
90 # Units are in seconds, kernel subclasses for GUI toolkits may need to
90 # Units are in seconds, kernel subclasses for GUI toolkits may need to
91 # adapt to milliseconds.
91 # adapt to milliseconds.
92 _poll_interval = Float(0.05, config=True)
92 _poll_interval = Float(0.05, config=True)
93
93
94 # If the shutdown was requested over the network, we leave here the
94 # If the shutdown was requested over the network, we leave here the
95 # necessary reply message so it can be sent by our registered atexit
95 # necessary reply message so it can be sent by our registered atexit
96 # handler. This ensures that the reply is only sent to clients truly at
96 # handler. This ensures that the reply is only sent to clients truly at
97 # the end of our shutdown process (which happens after the underlying
97 # the end of our shutdown process (which happens after the underlying
98 # IPython shell's own shutdown).
98 # IPython shell's own shutdown).
99 _shutdown_message = None
99 _shutdown_message = None
100
100
101 # This is a dict of port number that the kernel is listening on. It is set
101 # This is a dict of port number that the kernel is listening on. It is set
102 # by record_ports and used by connect_request.
102 # by record_ports and used by connect_request.
103 _recorded_ports = Dict()
103 _recorded_ports = Dict()
104
104
105 # set of aborted msg_ids
105 # set of aborted msg_ids
106 aborted = Set()
106 aborted = Set()
107
107
108 # Track execution count here. For IPython, we override this to use the
108 # Track execution count here. For IPython, we override this to use the
109 # execution count we store in the shell.
109 # execution count we store in the shell.
110 execution_count = 0
110 execution_count = 0
111
111
112
112
113 def __init__(self, **kwargs):
113 def __init__(self, **kwargs):
114 super(Kernel, self).__init__(**kwargs)
114 super(Kernel, self).__init__(**kwargs)
115
115
116 # Build dict of handlers for message types
116 # Build dict of handlers for message types
117 msg_types = [ 'execute_request', 'complete_request',
117 msg_types = [ 'execute_request', 'complete_request',
118 'inspect_request', 'history_request',
118 'inspect_request', 'history_request',
119 'kernel_info_request',
119 'kernel_info_request',
120 'connect_request', 'shutdown_request',
120 'connect_request', 'shutdown_request',
121 'apply_request', 'is_complete_request',
121 'apply_request', 'is_complete_request',
122 ]
122 ]
123 self.shell_handlers = {}
123 self.shell_handlers = {}
124 for msg_type in msg_types:
124 for msg_type in msg_types:
125 self.shell_handlers[msg_type] = getattr(self, msg_type)
125 self.shell_handlers[msg_type] = getattr(self, msg_type)
126
126
127 control_msg_types = msg_types + [ 'clear_request', 'abort_request' ]
127 control_msg_types = msg_types + [ 'clear_request', 'abort_request' ]
128 self.control_handlers = {}
128 self.control_handlers = {}
129 for msg_type in control_msg_types:
129 for msg_type in control_msg_types:
130 self.control_handlers[msg_type] = getattr(self, msg_type)
130 self.control_handlers[msg_type] = getattr(self, msg_type)
131
131
132
132
133 def dispatch_control(self, msg):
133 def dispatch_control(self, msg):
134 """dispatch control requests"""
134 """dispatch control requests"""
135 idents,msg = self.session.feed_identities(msg, copy=False)
135 idents,msg = self.session.feed_identities(msg, copy=False)
136 try:
136 try:
137 msg = self.session.deserialize(msg, content=True, copy=False)
137 msg = self.session.deserialize(msg, content=True, copy=False)
138 except:
138 except:
139 self.log.error("Invalid Control Message", exc_info=True)
139 self.log.error("Invalid Control Message", exc_info=True)
140 return
140 return
141
141
142 self.log.debug("Control received: %s", msg)
142 self.log.debug("Control received: %s", msg)
143
143
144 # Set the parent message for side effects.
144 # Set the parent message for side effects.
145 self.set_parent(idents, msg)
145 self.set_parent(idents, msg)
146 self._publish_status(u'busy')
146 self._publish_status(u'busy')
147
147
148 header = msg['header']
148 header = msg['header']
149 msg_type = header['msg_type']
149 msg_type = header['msg_type']
150
150
151 handler = self.control_handlers.get(msg_type, None)
151 handler = self.control_handlers.get(msg_type, None)
152 if handler is None:
152 if handler is None:
153 self.log.error("UNKNOWN CONTROL MESSAGE TYPE: %r", msg_type)
153 self.log.error("UNKNOWN CONTROL MESSAGE TYPE: %r", msg_type)
154 else:
154 else:
155 try:
155 try:
156 handler(self.control_stream, idents, msg)
156 handler(self.control_stream, idents, msg)
157 except Exception:
157 except Exception:
158 self.log.error("Exception in control handler:", exc_info=True)
158 self.log.error("Exception in control handler:", exc_info=True)
159
159
160 sys.stdout.flush()
160 sys.stdout.flush()
161 sys.stderr.flush()
161 sys.stderr.flush()
162 self._publish_status(u'idle')
162 self._publish_status(u'idle')
163
163
164 def dispatch_shell(self, stream, msg):
164 def dispatch_shell(self, stream, msg):
165 """dispatch shell requests"""
165 """dispatch shell requests"""
166 # flush control requests first
166 # flush control requests first
167 if self.control_stream:
167 if self.control_stream:
168 self.control_stream.flush()
168 self.control_stream.flush()
169
169
170 idents,msg = self.session.feed_identities(msg, copy=False)
170 idents,msg = self.session.feed_identities(msg, copy=False)
171 try:
171 try:
172 msg = self.session.deserialize(msg, content=True, copy=False)
172 msg = self.session.deserialize(msg, content=True, copy=False)
173 except:
173 except:
174 self.log.error("Invalid Message", exc_info=True)
174 self.log.error("Invalid Message", exc_info=True)
175 return
175 return
176
176
177 # Set the parent message for side effects.
177 # Set the parent message for side effects.
178 self.set_parent(idents, msg)
178 self.set_parent(idents, msg)
179 self._publish_status(u'busy')
179 self._publish_status(u'busy')
180
180
181 header = msg['header']
181 header = msg['header']
182 msg_id = header['msg_id']
182 msg_id = header['msg_id']
183 msg_type = msg['header']['msg_type']
183 msg_type = msg['header']['msg_type']
184
184
185 # Print some info about this message and leave a '--->' marker, so it's
185 # Print some info about this message and leave a '--->' marker, so it's
186 # easier to trace visually the message chain when debugging. Each
186 # easier to trace visually the message chain when debugging. Each
187 # handler prints its message at the end.
187 # handler prints its message at the end.
188 self.log.debug('\n*** MESSAGE TYPE:%s***', msg_type)
188 self.log.debug('\n*** MESSAGE TYPE:%s***', msg_type)
189 self.log.debug(' Content: %s\n --->\n ', msg['content'])
189 self.log.debug(' Content: %s\n --->\n ', msg['content'])
190
190
191 if msg_id in self.aborted:
191 if msg_id in self.aborted:
192 self.aborted.remove(msg_id)
192 self.aborted.remove(msg_id)
193 # is it safe to assume a msg_id will not be resubmitted?
193 # is it safe to assume a msg_id will not be resubmitted?
194 reply_type = msg_type.split('_')[0] + '_reply'
194 reply_type = msg_type.split('_')[0] + '_reply'
195 status = {'status' : 'aborted'}
195 status = {'status' : 'aborted'}
196 md = {'engine' : self.ident}
196 md = {'engine' : self.ident}
197 md.update(status)
197 md.update(status)
198 self.session.send(stream, reply_type, metadata=md,
198 self.session.send(stream, reply_type, metadata=md,
199 content=status, parent=msg, ident=idents)
199 content=status, parent=msg, ident=idents)
200 return
200 return
201
201
202 handler = self.shell_handlers.get(msg_type, None)
202 handler = self.shell_handlers.get(msg_type, None)
203 if handler is None:
203 if handler is None:
204 self.log.error("UNKNOWN MESSAGE TYPE: %r", msg_type)
204 self.log.error("UNKNOWN MESSAGE TYPE: %r", msg_type)
205 else:
205 else:
206 # ensure default_int_handler during handler call
206 # ensure default_int_handler during handler call
207 sig = signal(SIGINT, default_int_handler)
207 sig = signal(SIGINT, default_int_handler)
208 self.log.debug("%s: %s", msg_type, msg)
208 self.log.debug("%s: %s", msg_type, msg)
209 try:
209 try:
210 handler(stream, idents, msg)
210 handler(stream, idents, msg)
211 except Exception:
211 except Exception:
212 self.log.error("Exception in message handler:", exc_info=True)
212 self.log.error("Exception in message handler:", exc_info=True)
213 finally:
213 finally:
214 signal(SIGINT, sig)
214 signal(SIGINT, sig)
215
215
216 sys.stdout.flush()
216 sys.stdout.flush()
217 sys.stderr.flush()
217 sys.stderr.flush()
218 self._publish_status(u'idle')
218 self._publish_status(u'idle')
219
219
220 def enter_eventloop(self):
220 def enter_eventloop(self):
221 """enter eventloop"""
221 """enter eventloop"""
222 self.log.info("entering eventloop %s", self.eventloop)
222 self.log.info("entering eventloop %s", self.eventloop)
223 for stream in self.shell_streams:
223 for stream in self.shell_streams:
224 # flush any pending replies,
224 # flush any pending replies,
225 # which may be skipped by entering the eventloop
225 # which may be skipped by entering the eventloop
226 stream.flush(zmq.POLLOUT)
226 stream.flush(zmq.POLLOUT)
227 # restore default_int_handler
227 # restore default_int_handler
228 signal(SIGINT, default_int_handler)
228 signal(SIGINT, default_int_handler)
229 while self.eventloop is not None:
229 while self.eventloop is not None:
230 try:
230 try:
231 self.eventloop(self)
231 self.eventloop(self)
232 except KeyboardInterrupt:
232 except KeyboardInterrupt:
233 # Ctrl-C shouldn't crash the kernel
233 # Ctrl-C shouldn't crash the kernel
234 self.log.error("KeyboardInterrupt caught in kernel")
234 self.log.error("KeyboardInterrupt caught in kernel")
235 continue
235 continue
236 else:
236 else:
237 # eventloop exited cleanly, this means we should stop (right?)
237 # eventloop exited cleanly, this means we should stop (right?)
238 self.eventloop = None
238 self.eventloop = None
239 break
239 break
240 self.log.info("exiting eventloop")
240 self.log.info("exiting eventloop")
241
241
242 def start(self):
242 def start(self):
243 """register dispatchers for streams"""
243 """register dispatchers for streams"""
244 if self.control_stream:
244 if self.control_stream:
245 self.control_stream.on_recv(self.dispatch_control, copy=False)
245 self.control_stream.on_recv(self.dispatch_control, copy=False)
246
246
247 def make_dispatcher(stream):
247 def make_dispatcher(stream):
248 def dispatcher(msg):
248 def dispatcher(msg):
249 return self.dispatch_shell(stream, msg)
249 return self.dispatch_shell(stream, msg)
250 return dispatcher
250 return dispatcher
251
251
252 for s in self.shell_streams:
252 for s in self.shell_streams:
253 s.on_recv(make_dispatcher(s), copy=False)
253 s.on_recv(make_dispatcher(s), copy=False)
254
254
255 # publish idle status
255 # publish idle status
256 self._publish_status('starting')
256 self._publish_status('starting')
257
257
258 def do_one_iteration(self):
258 def do_one_iteration(self):
259 """step eventloop just once"""
259 """step eventloop just once"""
260 if self.control_stream:
260 if self.control_stream:
261 self.control_stream.flush()
261 self.control_stream.flush()
262 for stream in self.shell_streams:
262 for stream in self.shell_streams:
263 # handle at most one request per iteration
263 # handle at most one request per iteration
264 stream.flush(zmq.POLLIN, 1)
264 stream.flush(zmq.POLLIN, 1)
265 stream.flush(zmq.POLLOUT)
265 stream.flush(zmq.POLLOUT)
266
266
267
267
268 def record_ports(self, ports):
268 def record_ports(self, ports):
269 """Record the ports that this kernel is using.
269 """Record the ports that this kernel is using.
270
270
271 The creator of the Kernel instance must call this methods if they
271 The creator of the Kernel instance must call this methods if they
272 want the :meth:`connect_request` method to return the port numbers.
272 want the :meth:`connect_request` method to return the port numbers.
273 """
273 """
274 self._recorded_ports = ports
274 self._recorded_ports = ports
275
275
276 #---------------------------------------------------------------------------
276 #---------------------------------------------------------------------------
277 # Kernel request handlers
277 # Kernel request handlers
278 #---------------------------------------------------------------------------
278 #---------------------------------------------------------------------------
279
279
280 def _make_metadata(self, other=None):
280 def _make_metadata(self, other=None):
281 """init metadata dict, for execute/apply_reply"""
281 """init metadata dict, for execute/apply_reply"""
282 new_md = {
282 new_md = {
283 'dependencies_met' : True,
283 'dependencies_met' : True,
284 'engine' : self.ident,
284 'engine' : self.ident,
285 'started': datetime.now(),
285 'started': datetime.now(),
286 }
286 }
287 if other:
287 if other:
288 new_md.update(other)
288 new_md.update(other)
289 return new_md
289 return new_md
290
290
291 def _publish_execute_input(self, code, parent, execution_count):
291 def _publish_execute_input(self, code, parent, execution_count):
292 """Publish the code request on the iopub stream."""
292 """Publish the code request on the iopub stream."""
293
293
294 self.session.send(self.iopub_socket, u'execute_input',
294 self.session.send(self.iopub_socket, u'execute_input',
295 {u'code':code, u'execution_count': execution_count},
295 {u'code':code, u'execution_count': execution_count},
296 parent=parent, ident=self._topic('execute_input')
296 parent=parent, ident=self._topic('execute_input')
297 )
297 )
298
298
299 def _publish_status(self, status, parent=None):
299 def _publish_status(self, status, parent=None):
300 """send status (busy/idle) on IOPub"""
300 """send status (busy/idle) on IOPub"""
301 self.session.send(self.iopub_socket,
301 self.session.send(self.iopub_socket,
302 u'status',
302 u'status',
303 {u'execution_state': status},
303 {u'execution_state': status},
304 parent=parent or self._parent_header,
304 parent=parent or self._parent_header,
305 ident=self._topic('status'),
305 ident=self._topic('status'),
306 )
306 )
307
307
308 def set_parent(self, ident, parent):
308 def set_parent(self, ident, parent):
309 """Set the current parent_header
309 """Set the current parent_header
310
310
311 Side effects (IOPub messages) and replies are associated with
311 Side effects (IOPub messages) and replies are associated with
312 the request that caused them via the parent_header.
312 the request that caused them via the parent_header.
313
313
314 The parent identity is used to route input_request messages
314 The parent identity is used to route input_request messages
315 on the stdin channel.
315 on the stdin channel.
316 """
316 """
317 self._parent_ident = ident
317 self._parent_ident = ident
318 self._parent_header = parent
318 self._parent_header = parent
319
319
320 def send_response(self, stream, msg_or_type, content=None, ident=None,
320 def send_response(self, stream, msg_or_type, content=None, ident=None,
321 buffers=None, track=False, header=None, metadata=None):
321 buffers=None, track=False, header=None, metadata=None):
322 """Send a response to the message we're currently processing.
322 """Send a response to the message we're currently processing.
323
323
324 This accepts all the parameters of :meth:`IPython.kernel.zmq.session.Session.send`
324 This accepts all the parameters of :meth:`IPython.kernel.zmq.session.Session.send`
325 except ``parent``.
325 except ``parent``.
326
326
327 This relies on :meth:`set_parent` having been called for the current
327 This relies on :meth:`set_parent` having been called for the current
328 message.
328 message.
329 """
329 """
330 return self.session.send(stream, msg_or_type, content, self._parent_header,
330 return self.session.send(stream, msg_or_type, content, self._parent_header,
331 ident, buffers, track, header, metadata)
331 ident, buffers, track, header, metadata)
332
332
333 def execute_request(self, stream, ident, parent):
333 def execute_request(self, stream, ident, parent):
334 """handle an execute_request"""
334 """handle an execute_request"""
335
335
336 try:
336 try:
337 content = parent[u'content']
337 content = parent[u'content']
338 code = py3compat.cast_unicode_py2(content[u'code'])
338 code = py3compat.cast_unicode_py2(content[u'code'])
339 silent = content[u'silent']
339 silent = content[u'silent']
340 store_history = content.get(u'store_history', not silent)
340 store_history = content.get(u'store_history', not silent)
341 user_expressions = content.get('user_expressions', {})
341 user_expressions = content.get('user_expressions', {})
342 allow_stdin = content.get('allow_stdin', False)
342 allow_stdin = content.get('allow_stdin', False)
343 except:
343 except:
344 self.log.error("Got bad msg: ")
344 self.log.error("Got bad msg: ")
345 self.log.error("%s", parent)
345 self.log.error("%s", parent)
346 return
346 return
347
347
348 md = self._make_metadata(parent['metadata'])
348 md = self._make_metadata(parent['metadata'])
349
349
350 # Re-broadcast our input for the benefit of listening clients, and
350 # Re-broadcast our input for the benefit of listening clients, and
351 # start computing output
351 # start computing output
352 if not silent:
352 if not silent:
353 self.execution_count += 1
353 self.execution_count += 1
354 self._publish_execute_input(code, parent, self.execution_count)
354 self._publish_execute_input(code, parent, self.execution_count)
355
355
356 reply_content = self.do_execute(code, silent, store_history,
356 reply_content = self.do_execute(code, silent, store_history,
357 user_expressions, allow_stdin)
357 user_expressions, allow_stdin)
358
358
359 # Flush output before sending the reply.
359 # Flush output before sending the reply.
360 sys.stdout.flush()
360 sys.stdout.flush()
361 sys.stderr.flush()
361 sys.stderr.flush()
362 # FIXME: on rare occasions, the flush doesn't seem to make it to the
362 # FIXME: on rare occasions, the flush doesn't seem to make it to the
363 # clients... This seems to mitigate the problem, but we definitely need
363 # clients... This seems to mitigate the problem, but we definitely need
364 # to better understand what's going on.
364 # to better understand what's going on.
365 if self._execute_sleep:
365 if self._execute_sleep:
366 time.sleep(self._execute_sleep)
366 time.sleep(self._execute_sleep)
367
367
368 # Send the reply.
368 # Send the reply.
369 reply_content = json_clean(reply_content)
369 reply_content = json_clean(reply_content)
370
370
371 md['status'] = reply_content['status']
371 md['status'] = reply_content['status']
372 if reply_content['status'] == 'error' and \
372 if reply_content['status'] == 'error' and \
373 reply_content['ename'] == 'UnmetDependency':
373 reply_content['ename'] == 'UnmetDependency':
374 md['dependencies_met'] = False
374 md['dependencies_met'] = False
375
375
376 reply_msg = self.session.send(stream, u'execute_reply',
376 reply_msg = self.session.send(stream, u'execute_reply',
377 reply_content, parent, metadata=md,
377 reply_content, parent, metadata=md,
378 ident=ident)
378 ident=ident)
379
379
380 self.log.debug("%s", reply_msg)
380 self.log.debug("%s", reply_msg)
381
381
382 if not silent and reply_msg['content']['status'] == u'error':
382 if not silent and reply_msg['content']['status'] == u'error':
383 self._abort_queues()
383 self._abort_queues()
384
384
385 def do_execute(self, code, silent, store_history=True,
385 def do_execute(self, code, silent, store_history=True,
386 user_experssions=None, allow_stdin=False):
386 user_experssions=None, allow_stdin=False):
387 """Execute user code. Must be overridden by subclasses.
387 """Execute user code. Must be overridden by subclasses.
388 """
388 """
389 raise NotImplementedError
389 raise NotImplementedError
390
390
391 def complete_request(self, stream, ident, parent):
391 def complete_request(self, stream, ident, parent):
392 content = parent['content']
392 content = parent['content']
393 code = content['code']
393 code = content['code']
394 cursor_pos = content['cursor_pos']
394 cursor_pos = content['cursor_pos']
395
395
396 matches = self.do_complete(code, cursor_pos)
396 matches = self.do_complete(code, cursor_pos)
397 matches = json_clean(matches)
397 matches = json_clean(matches)
398 completion_msg = self.session.send(stream, 'complete_reply',
398 completion_msg = self.session.send(stream, 'complete_reply',
399 matches, parent, ident)
399 matches, parent, ident)
400 self.log.debug("%s", completion_msg)
400 self.log.debug("%s", completion_msg)
401
401
402 def do_complete(self, code, cursor_pos):
402 def do_complete(self, code, cursor_pos):
403 """Override in subclasses to find completions.
403 """Override in subclasses to find completions.
404 """
404 """
405 return {'matches' : [],
405 return {'matches' : [],
406 'cursor_end' : cursor_pos,
406 'cursor_end' : cursor_pos,
407 'cursor_start' : cursor_pos,
407 'cursor_start' : cursor_pos,
408 'metadata' : {},
408 'metadata' : {},
409 'status' : 'ok'}
409 'status' : 'ok'}
410
410
411 def inspect_request(self, stream, ident, parent):
411 def inspect_request(self, stream, ident, parent):
412 content = parent['content']
412 content = parent['content']
413
413
414 reply_content = self.do_inspect(content['code'], content['cursor_pos'],
414 reply_content = self.do_inspect(content['code'], content['cursor_pos'],
415 content.get('detail_level', 0))
415 content.get('detail_level', 0))
416 # Before we send this object over, we scrub it for JSON usage
416 # Before we send this object over, we scrub it for JSON usage
417 reply_content = json_clean(reply_content)
417 reply_content = json_clean(reply_content)
418 msg = self.session.send(stream, 'inspect_reply',
418 msg = self.session.send(stream, 'inspect_reply',
419 reply_content, parent, ident)
419 reply_content, parent, ident)
420 self.log.debug("%s", msg)
420 self.log.debug("%s", msg)
421
421
422 def do_inspect(self, code, cursor_pos, detail_level=0):
422 def do_inspect(self, code, cursor_pos, detail_level=0):
423 """Override in subclasses to allow introspection.
423 """Override in subclasses to allow introspection.
424 """
424 """
425 return {'status': 'ok', 'data':{}, 'metadata':{}, 'found':False}
425 return {'status': 'ok', 'data':{}, 'metadata':{}, 'found':False}
426
426
427 def history_request(self, stream, ident, parent):
427 def history_request(self, stream, ident, parent):
428 content = parent['content']
428 content = parent['content']
429
429
430 reply_content = self.do_history(**content)
430 reply_content = self.do_history(**content)
431
431
432 reply_content = json_clean(reply_content)
432 reply_content = json_clean(reply_content)
433 msg = self.session.send(stream, 'history_reply',
433 msg = self.session.send(stream, 'history_reply',
434 reply_content, parent, ident)
434 reply_content, parent, ident)
435 self.log.debug("%s", msg)
435 self.log.debug("%s", msg)
436
436
437 def do_history(self, hist_access_type, output, raw, session=None, start=None,
437 def do_history(self, hist_access_type, output, raw, session=None, start=None,
438 stop=None, n=None, pattern=None, unique=False):
438 stop=None, n=None, pattern=None, unique=False):
439 """Override in subclasses to access history.
439 """Override in subclasses to access history.
440 """
440 """
441 return {'history': []}
441 return {'history': []}
442
442
443 def connect_request(self, stream, ident, parent):
443 def connect_request(self, stream, ident, parent):
444 if self._recorded_ports is not None:
444 if self._recorded_ports is not None:
445 content = self._recorded_ports.copy()
445 content = self._recorded_ports.copy()
446 else:
446 else:
447 content = {}
447 content = {}
448 msg = self.session.send(stream, 'connect_reply',
448 msg = self.session.send(stream, 'connect_reply',
449 content, parent, ident)
449 content, parent, ident)
450 self.log.debug("%s", msg)
450 self.log.debug("%s", msg)
451
451
452 @property
452 @property
453 def kernel_info(self):
453 def kernel_info(self):
454 return {
454 return {
455 'protocol_version': release.kernel_protocol_version,
455 'protocol_version': release.kernel_protocol_version,
456 'implementation': self.implementation,
456 'implementation': self.implementation,
457 'implementation_version': self.implementation_version,
457 'implementation_version': self.implementation_version,
458 'language': self.language,
459 'language_version': self.language_version,
460 'language_info': self.language_info,
458 'language_info': self.language_info,
461 'banner': self.banner,
459 'banner': self.banner,
462 }
460 }
463
461
464 def kernel_info_request(self, stream, ident, parent):
462 def kernel_info_request(self, stream, ident, parent):
465 msg = self.session.send(stream, 'kernel_info_reply',
463 msg = self.session.send(stream, 'kernel_info_reply',
466 self.kernel_info, parent, ident)
464 self.kernel_info, parent, ident)
467 self.log.debug("%s", msg)
465 self.log.debug("%s", msg)
468
466
469 def shutdown_request(self, stream, ident, parent):
467 def shutdown_request(self, stream, ident, parent):
470 content = self.do_shutdown(parent['content']['restart'])
468 content = self.do_shutdown(parent['content']['restart'])
471 self.session.send(stream, u'shutdown_reply', content, parent, ident=ident)
469 self.session.send(stream, u'shutdown_reply', content, parent, ident=ident)
472 # same content, but different msg_id for broadcasting on IOPub
470 # same content, but different msg_id for broadcasting on IOPub
473 self._shutdown_message = self.session.msg(u'shutdown_reply',
471 self._shutdown_message = self.session.msg(u'shutdown_reply',
474 content, parent
472 content, parent
475 )
473 )
476
474
477 self._at_shutdown()
475 self._at_shutdown()
478 # call sys.exit after a short delay
476 # call sys.exit after a short delay
479 loop = ioloop.IOLoop.instance()
477 loop = ioloop.IOLoop.instance()
480 loop.add_timeout(time.time()+0.1, loop.stop)
478 loop.add_timeout(time.time()+0.1, loop.stop)
481
479
482 def do_shutdown(self, restart):
480 def do_shutdown(self, restart):
483 """Override in subclasses to do things when the frontend shuts down the
481 """Override in subclasses to do things when the frontend shuts down the
484 kernel.
482 kernel.
485 """
483 """
486 return {'status': 'ok', 'restart': restart}
484 return {'status': 'ok', 'restart': restart}
487
485
488 def is_complete_request(self, stream, ident, parent):
486 def is_complete_request(self, stream, ident, parent):
489 content = parent['content']
487 content = parent['content']
490 code = content['code']
488 code = content['code']
491
489
492 reply_content = self.do_is_complete(code)
490 reply_content = self.do_is_complete(code)
493 reply_content = json_clean(reply_content)
491 reply_content = json_clean(reply_content)
494 reply_msg = self.session.send(stream, 'is_complete_reply',
492 reply_msg = self.session.send(stream, 'is_complete_reply',
495 reply_content, parent, ident)
493 reply_content, parent, ident)
496 self.log.debug("%s", reply_msg)
494 self.log.debug("%s", reply_msg)
497
495
498 def do_is_complete(self, code):
496 def do_is_complete(self, code):
499 """Override in subclasses to find completions.
497 """Override in subclasses to find completions.
500 """
498 """
501 return {'status' : 'unknown',
499 return {'status' : 'unknown',
502 }
500 }
503
501
504 #---------------------------------------------------------------------------
502 #---------------------------------------------------------------------------
505 # Engine methods
503 # Engine methods
506 #---------------------------------------------------------------------------
504 #---------------------------------------------------------------------------
507
505
508 def apply_request(self, stream, ident, parent):
506 def apply_request(self, stream, ident, parent):
509 try:
507 try:
510 content = parent[u'content']
508 content = parent[u'content']
511 bufs = parent[u'buffers']
509 bufs = parent[u'buffers']
512 msg_id = parent['header']['msg_id']
510 msg_id = parent['header']['msg_id']
513 except:
511 except:
514 self.log.error("Got bad msg: %s", parent, exc_info=True)
512 self.log.error("Got bad msg: %s", parent, exc_info=True)
515 return
513 return
516
514
517 md = self._make_metadata(parent['metadata'])
515 md = self._make_metadata(parent['metadata'])
518
516
519 reply_content, result_buf = self.do_apply(content, bufs, msg_id, md)
517 reply_content, result_buf = self.do_apply(content, bufs, msg_id, md)
520
518
521 # put 'ok'/'error' status in header, for scheduler introspection:
519 # put 'ok'/'error' status in header, for scheduler introspection:
522 md['status'] = reply_content['status']
520 md['status'] = reply_content['status']
523
521
524 # flush i/o
522 # flush i/o
525 sys.stdout.flush()
523 sys.stdout.flush()
526 sys.stderr.flush()
524 sys.stderr.flush()
527
525
528 self.session.send(stream, u'apply_reply', reply_content,
526 self.session.send(stream, u'apply_reply', reply_content,
529 parent=parent, ident=ident,buffers=result_buf, metadata=md)
527 parent=parent, ident=ident,buffers=result_buf, metadata=md)
530
528
531 def do_apply(self, content, bufs, msg_id, reply_metadata):
529 def do_apply(self, content, bufs, msg_id, reply_metadata):
532 """Override in subclasses to support the IPython parallel framework.
530 """Override in subclasses to support the IPython parallel framework.
533 """
531 """
534 raise NotImplementedError
532 raise NotImplementedError
535
533
536 #---------------------------------------------------------------------------
534 #---------------------------------------------------------------------------
537 # Control messages
535 # Control messages
538 #---------------------------------------------------------------------------
536 #---------------------------------------------------------------------------
539
537
540 def abort_request(self, stream, ident, parent):
538 def abort_request(self, stream, ident, parent):
541 """abort a specific msg by id"""
539 """abort a specific msg by id"""
542 msg_ids = parent['content'].get('msg_ids', None)
540 msg_ids = parent['content'].get('msg_ids', None)
543 if isinstance(msg_ids, string_types):
541 if isinstance(msg_ids, string_types):
544 msg_ids = [msg_ids]
542 msg_ids = [msg_ids]
545 if not msg_ids:
543 if not msg_ids:
546 self._abort_queues()
544 self._abort_queues()
547 for mid in msg_ids:
545 for mid in msg_ids:
548 self.aborted.add(str(mid))
546 self.aborted.add(str(mid))
549
547
550 content = dict(status='ok')
548 content = dict(status='ok')
551 reply_msg = self.session.send(stream, 'abort_reply', content=content,
549 reply_msg = self.session.send(stream, 'abort_reply', content=content,
552 parent=parent, ident=ident)
550 parent=parent, ident=ident)
553 self.log.debug("%s", reply_msg)
551 self.log.debug("%s", reply_msg)
554
552
555 def clear_request(self, stream, idents, parent):
553 def clear_request(self, stream, idents, parent):
556 """Clear our namespace."""
554 """Clear our namespace."""
557 content = self.do_clear()
555 content = self.do_clear()
558 self.session.send(stream, 'clear_reply', ident=idents, parent=parent,
556 self.session.send(stream, 'clear_reply', ident=idents, parent=parent,
559 content = content)
557 content = content)
560
558
561 def do_clear(self):
559 def do_clear(self):
562 """Override in subclasses to clear the namespace
560 """Override in subclasses to clear the namespace
563
561
564 This is only required for IPython.parallel.
562 This is only required for IPython.parallel.
565 """
563 """
566 raise NotImplementedError
564 raise NotImplementedError
567
565
568 #---------------------------------------------------------------------------
566 #---------------------------------------------------------------------------
569 # Protected interface
567 # Protected interface
570 #---------------------------------------------------------------------------
568 #---------------------------------------------------------------------------
571
569
572 def _topic(self, topic):
570 def _topic(self, topic):
573 """prefixed topic for IOPub messages"""
571 """prefixed topic for IOPub messages"""
574 if self.int_id >= 0:
572 if self.int_id >= 0:
575 base = "engine.%i" % self.int_id
573 base = "engine.%i" % self.int_id
576 else:
574 else:
577 base = "kernel.%s" % self.ident
575 base = "kernel.%s" % self.ident
578
576
579 return py3compat.cast_bytes("%s.%s" % (base, topic))
577 return py3compat.cast_bytes("%s.%s" % (base, topic))
580
578
581 def _abort_queues(self):
579 def _abort_queues(self):
582 for stream in self.shell_streams:
580 for stream in self.shell_streams:
583 if stream:
581 if stream:
584 self._abort_queue(stream)
582 self._abort_queue(stream)
585
583
586 def _abort_queue(self, stream):
584 def _abort_queue(self, stream):
587 poller = zmq.Poller()
585 poller = zmq.Poller()
588 poller.register(stream.socket, zmq.POLLIN)
586 poller.register(stream.socket, zmq.POLLIN)
589 while True:
587 while True:
590 idents,msg = self.session.recv(stream, zmq.NOBLOCK, content=True)
588 idents,msg = self.session.recv(stream, zmq.NOBLOCK, content=True)
591 if msg is None:
589 if msg is None:
592 return
590 return
593
591
594 self.log.info("Aborting:")
592 self.log.info("Aborting:")
595 self.log.info("%s", msg)
593 self.log.info("%s", msg)
596 msg_type = msg['header']['msg_type']
594 msg_type = msg['header']['msg_type']
597 reply_type = msg_type.split('_')[0] + '_reply'
595 reply_type = msg_type.split('_')[0] + '_reply'
598
596
599 status = {'status' : 'aborted'}
597 status = {'status' : 'aborted'}
600 md = {'engine' : self.ident}
598 md = {'engine' : self.ident}
601 md.update(status)
599 md.update(status)
602 reply_msg = self.session.send(stream, reply_type, metadata=md,
600 reply_msg = self.session.send(stream, reply_type, metadata=md,
603 content=status, parent=msg, ident=idents)
601 content=status, parent=msg, ident=idents)
604 self.log.debug("%s", reply_msg)
602 self.log.debug("%s", reply_msg)
605 # We need to wait a bit for requests to come in. This can probably
603 # We need to wait a bit for requests to come in. This can probably
606 # be set shorter for true asynchronous clients.
604 # be set shorter for true asynchronous clients.
607 poller.poll(50)
605 poller.poll(50)
608
606
609
607
610 def _no_raw_input(self):
608 def _no_raw_input(self):
611 """Raise StdinNotImplentedError if active frontend doesn't support
609 """Raise StdinNotImplentedError if active frontend doesn't support
612 stdin."""
610 stdin."""
613 raise StdinNotImplementedError("raw_input was called, but this "
611 raise StdinNotImplementedError("raw_input was called, but this "
614 "frontend does not support stdin.")
612 "frontend does not support stdin.")
615
613
616 def getpass(self, prompt=''):
614 def getpass(self, prompt=''):
617 """Forward getpass to frontends
615 """Forward getpass to frontends
618
616
619 Raises
617 Raises
620 ------
618 ------
621 StdinNotImplentedError if active frontend doesn't support stdin.
619 StdinNotImplentedError if active frontend doesn't support stdin.
622 """
620 """
623 if not self._allow_stdin:
621 if not self._allow_stdin:
624 raise StdinNotImplementedError(
622 raise StdinNotImplementedError(
625 "getpass was called, but this frontend does not support input requests."
623 "getpass was called, but this frontend does not support input requests."
626 )
624 )
627 return self._input_request(prompt,
625 return self._input_request(prompt,
628 self._parent_ident,
626 self._parent_ident,
629 self._parent_header,
627 self._parent_header,
630 password=True,
628 password=True,
631 )
629 )
632
630
633 def raw_input(self, prompt=''):
631 def raw_input(self, prompt=''):
634 """Forward raw_input to frontends
632 """Forward raw_input to frontends
635
633
636 Raises
634 Raises
637 ------
635 ------
638 StdinNotImplentedError if active frontend doesn't support stdin.
636 StdinNotImplentedError if active frontend doesn't support stdin.
639 """
637 """
640 if not self._allow_stdin:
638 if not self._allow_stdin:
641 raise StdinNotImplementedError(
639 raise StdinNotImplementedError(
642 "raw_input was called, but this frontend does not support input requests."
640 "raw_input was called, but this frontend does not support input requests."
643 )
641 )
644 return self._input_request(prompt,
642 return self._input_request(prompt,
645 self._parent_ident,
643 self._parent_ident,
646 self._parent_header,
644 self._parent_header,
647 password=False,
645 password=False,
648 )
646 )
649
647
650 def _input_request(self, prompt, ident, parent, password=False):
648 def _input_request(self, prompt, ident, parent, password=False):
651 # Flush output before making the request.
649 # Flush output before making the request.
652 sys.stderr.flush()
650 sys.stderr.flush()
653 sys.stdout.flush()
651 sys.stdout.flush()
654 # flush the stdin socket, to purge stale replies
652 # flush the stdin socket, to purge stale replies
655 while True:
653 while True:
656 try:
654 try:
657 self.stdin_socket.recv_multipart(zmq.NOBLOCK)
655 self.stdin_socket.recv_multipart(zmq.NOBLOCK)
658 except zmq.ZMQError as e:
656 except zmq.ZMQError as e:
659 if e.errno == zmq.EAGAIN:
657 if e.errno == zmq.EAGAIN:
660 break
658 break
661 else:
659 else:
662 raise
660 raise
663
661
664 # Send the input request.
662 # Send the input request.
665 content = json_clean(dict(prompt=prompt, password=password))
663 content = json_clean(dict(prompt=prompt, password=password))
666 self.session.send(self.stdin_socket, u'input_request', content, parent,
664 self.session.send(self.stdin_socket, u'input_request', content, parent,
667 ident=ident)
665 ident=ident)
668
666
669 # Await a response.
667 # Await a response.
670 while True:
668 while True:
671 try:
669 try:
672 ident, reply = self.session.recv(self.stdin_socket, 0)
670 ident, reply = self.session.recv(self.stdin_socket, 0)
673 except Exception:
671 except Exception:
674 self.log.warn("Invalid Message:", exc_info=True)
672 self.log.warn("Invalid Message:", exc_info=True)
675 except KeyboardInterrupt:
673 except KeyboardInterrupt:
676 # re-raise KeyboardInterrupt, to truncate traceback
674 # re-raise KeyboardInterrupt, to truncate traceback
677 raise KeyboardInterrupt
675 raise KeyboardInterrupt
678 else:
676 else:
679 break
677 break
680 try:
678 try:
681 value = py3compat.unicode_to_str(reply['content']['value'])
679 value = py3compat.unicode_to_str(reply['content']['value'])
682 except:
680 except:
683 self.log.error("Bad input_reply: %s", parent)
681 self.log.error("Bad input_reply: %s", parent)
684 value = ''
682 value = ''
685 if value == '\x04':
683 if value == '\x04':
686 # EOF
684 # EOF
687 raise EOFError
685 raise EOFError
688 return value
686 return value
689
687
690 def _at_shutdown(self):
688 def _at_shutdown(self):
691 """Actions taken at shutdown by the kernel, called by python's atexit.
689 """Actions taken at shutdown by the kernel, called by python's atexit.
692 """
690 """
693 # io.rprint("Kernel at_shutdown") # dbg
691 # io.rprint("Kernel at_shutdown") # dbg
694 if self._shutdown_message is not None:
692 if self._shutdown_message is not None:
695 self.session.send(self.iopub_socket, self._shutdown_message, ident=self._topic('shutdown'))
693 self.session.send(self.iopub_socket, self._shutdown_message, ident=self._topic('shutdown'))
696 self.log.debug("%s", self._shutdown_message)
694 self.log.debug("%s", self._shutdown_message)
697 [ s.flush(zmq.POLLOUT) for s in self.shell_streams ]
695 [ s.flush(zmq.POLLOUT) for s in self.shell_streams ]
@@ -1,1138 +1,1147 b''
1 .. _messaging:
1 .. _messaging:
2
2
3 ======================
3 ======================
4 Messaging in IPython
4 Messaging in IPython
5 ======================
5 ======================
6
6
7
7
8 Versioning
8 Versioning
9 ==========
9 ==========
10
10
11 The IPython message specification is versioned independently of IPython.
11 The IPython message specification is versioned independently of IPython.
12 The current version of the specification is 5.0.
12 The current version of the specification is 5.0.
13
13
14
14
15 Introduction
15 Introduction
16 ============
16 ============
17
17
18 This document explains the basic communications design and messaging
18 This document explains the basic communications design and messaging
19 specification for how the various IPython objects interact over a network
19 specification for how the various IPython objects interact over a network
20 transport. The current implementation uses the ZeroMQ_ library for messaging
20 transport. The current implementation uses the ZeroMQ_ library for messaging
21 within and between hosts.
21 within and between hosts.
22
22
23 .. Note::
23 .. Note::
24
24
25 This document should be considered the authoritative description of the
25 This document should be considered the authoritative description of the
26 IPython messaging protocol, and all developers are strongly encouraged to
26 IPython messaging protocol, and all developers are strongly encouraged to
27 keep it updated as the implementation evolves, so that we have a single
27 keep it updated as the implementation evolves, so that we have a single
28 common reference for all protocol details.
28 common reference for all protocol details.
29
29
30 The basic design is explained in the following diagram:
30 The basic design is explained in the following diagram:
31
31
32 .. image:: figs/frontend-kernel.png
32 .. image:: figs/frontend-kernel.png
33 :width: 450px
33 :width: 450px
34 :alt: IPython kernel/frontend messaging architecture.
34 :alt: IPython kernel/frontend messaging architecture.
35 :align: center
35 :align: center
36 :target: ../_images/frontend-kernel.png
36 :target: ../_images/frontend-kernel.png
37
37
38 A single kernel can be simultaneously connected to one or more frontends. The
38 A single kernel can be simultaneously connected to one or more frontends. The
39 kernel has three sockets that serve the following functions:
39 kernel has three sockets that serve the following functions:
40
40
41 1. Shell: this single ROUTER socket allows multiple incoming connections from
41 1. Shell: this single ROUTER socket allows multiple incoming connections from
42 frontends, and this is the socket where requests for code execution, object
42 frontends, and this is the socket where requests for code execution, object
43 information, prompts, etc. are made to the kernel by any frontend. The
43 information, prompts, etc. are made to the kernel by any frontend. The
44 communication on this socket is a sequence of request/reply actions from
44 communication on this socket is a sequence of request/reply actions from
45 each frontend and the kernel.
45 each frontend and the kernel.
46
46
47 2. IOPub: this socket is the 'broadcast channel' where the kernel publishes all
47 2. IOPub: this socket is the 'broadcast channel' where the kernel publishes all
48 side effects (stdout, stderr, etc.) as well as the requests coming from any
48 side effects (stdout, stderr, etc.) as well as the requests coming from any
49 client over the shell socket and its own requests on the stdin socket. There
49 client over the shell socket and its own requests on the stdin socket. There
50 are a number of actions in Python which generate side effects: :func:`print`
50 are a number of actions in Python which generate side effects: :func:`print`
51 writes to ``sys.stdout``, errors generate tracebacks, etc. Additionally, in
51 writes to ``sys.stdout``, errors generate tracebacks, etc. Additionally, in
52 a multi-client scenario, we want all frontends to be able to know what each
52 a multi-client scenario, we want all frontends to be able to know what each
53 other has sent to the kernel (this can be useful in collaborative scenarios,
53 other has sent to the kernel (this can be useful in collaborative scenarios,
54 for example). This socket allows both side effects and the information
54 for example). This socket allows both side effects and the information
55 about communications taking place with one client over the shell channel
55 about communications taking place with one client over the shell channel
56 to be made available to all clients in a uniform manner.
56 to be made available to all clients in a uniform manner.
57
57
58 3. stdin: this ROUTER socket is connected to all frontends, and it allows
58 3. stdin: this ROUTER socket is connected to all frontends, and it allows
59 the kernel to request input from the active frontend when :func:`raw_input` is called.
59 the kernel to request input from the active frontend when :func:`raw_input` is called.
60 The frontend that executed the code has a DEALER socket that acts as a 'virtual keyboard'
60 The frontend that executed the code has a DEALER socket that acts as a 'virtual keyboard'
61 for the kernel while this communication is happening (illustrated in the
61 for the kernel while this communication is happening (illustrated in the
62 figure by the black outline around the central keyboard). In practice,
62 figure by the black outline around the central keyboard). In practice,
63 frontends may display such kernel requests using a special input widget or
63 frontends may display such kernel requests using a special input widget or
64 otherwise indicating that the user is to type input for the kernel instead
64 otherwise indicating that the user is to type input for the kernel instead
65 of normal commands in the frontend.
65 of normal commands in the frontend.
66
66
67 All messages are tagged with enough information (details below) for clients
67 All messages are tagged with enough information (details below) for clients
68 to know which messages come from their own interaction with the kernel and
68 to know which messages come from their own interaction with the kernel and
69 which ones are from other clients, so they can display each type
69 which ones are from other clients, so they can display each type
70 appropriately.
70 appropriately.
71
71
72 4. Control: This channel is identical to Shell, but operates on a separate socket,
72 4. Control: This channel is identical to Shell, but operates on a separate socket,
73 to allow important messages to avoid queueing behind execution requests (e.g. shutdown or abort).
73 to allow important messages to avoid queueing behind execution requests (e.g. shutdown or abort).
74
74
75 The actual format of the messages allowed on each of these channels is
75 The actual format of the messages allowed on each of these channels is
76 specified below. Messages are dicts of dicts with string keys and values that
76 specified below. Messages are dicts of dicts with string keys and values that
77 are reasonably representable in JSON. Our current implementation uses JSON
77 are reasonably representable in JSON. Our current implementation uses JSON
78 explicitly as its message format, but this shouldn't be considered a permanent
78 explicitly as its message format, but this shouldn't be considered a permanent
79 feature. As we've discovered that JSON has non-trivial performance issues due
79 feature. As we've discovered that JSON has non-trivial performance issues due
80 to excessive copying, we may in the future move to a pure pickle-based raw
80 to excessive copying, we may in the future move to a pure pickle-based raw
81 message format. However, it should be possible to easily convert from the raw
81 message format. However, it should be possible to easily convert from the raw
82 objects to JSON, since we may have non-python clients (e.g. a web frontend).
82 objects to JSON, since we may have non-python clients (e.g. a web frontend).
83 As long as it's easy to make a JSON version of the objects that is a faithful
83 As long as it's easy to make a JSON version of the objects that is a faithful
84 representation of all the data, we can communicate with such clients.
84 representation of all the data, we can communicate with such clients.
85
85
86 .. Note::
86 .. Note::
87
87
88 Not all of these have yet been fully fleshed out, but the key ones are, see
88 Not all of these have yet been fully fleshed out, but the key ones are, see
89 kernel and frontend files for actual implementation details.
89 kernel and frontend files for actual implementation details.
90
90
91 General Message Format
91 General Message Format
92 ======================
92 ======================
93
93
94 A message is defined by the following four-dictionary structure::
94 A message is defined by the following four-dictionary structure::
95
95
96 {
96 {
97 # The message header contains a pair of unique identifiers for the
97 # The message header contains a pair of unique identifiers for the
98 # originating session and the actual message id, in addition to the
98 # originating session and the actual message id, in addition to the
99 # username for the process that generated the message. This is useful in
99 # username for the process that generated the message. This is useful in
100 # collaborative settings where multiple users may be interacting with the
100 # collaborative settings where multiple users may be interacting with the
101 # same kernel simultaneously, so that frontends can label the various
101 # same kernel simultaneously, so that frontends can label the various
102 # messages in a meaningful way.
102 # messages in a meaningful way.
103 'header' : {
103 'header' : {
104 'msg_id' : uuid,
104 'msg_id' : uuid,
105 'username' : str,
105 'username' : str,
106 'session' : uuid,
106 'session' : uuid,
107 # All recognized message type strings are listed below.
107 # All recognized message type strings are listed below.
108 'msg_type' : str,
108 'msg_type' : str,
109 # the message protocol version
109 # the message protocol version
110 'version' : '5.0',
110 'version' : '5.0',
111 },
111 },
112
112
113 # In a chain of messages, the header from the parent is copied so that
113 # In a chain of messages, the header from the parent is copied so that
114 # clients can track where messages come from.
114 # clients can track where messages come from.
115 'parent_header' : dict,
115 'parent_header' : dict,
116
116
117 # Any metadata associated with the message.
117 # Any metadata associated with the message.
118 'metadata' : dict,
118 'metadata' : dict,
119
119
120 # The actual content of the message must be a dict, whose structure
120 # The actual content of the message must be a dict, whose structure
121 # depends on the message type.
121 # depends on the message type.
122 'content' : dict,
122 'content' : dict,
123 }
123 }
124
124
125 .. versionchanged:: 5.0
125 .. versionchanged:: 5.0
126
126
127 ``version`` key added to the header.
127 ``version`` key added to the header.
128
128
129 .. _wire_protocol:
129 .. _wire_protocol:
130
130
131 The Wire Protocol
131 The Wire Protocol
132 =================
132 =================
133
133
134
134
135 This message format exists at a high level,
135 This message format exists at a high level,
136 but does not describe the actual *implementation* at the wire level in zeromq.
136 but does not describe the actual *implementation* at the wire level in zeromq.
137 The canonical implementation of the message spec is our :class:`~IPython.kernel.zmq.session.Session` class.
137 The canonical implementation of the message spec is our :class:`~IPython.kernel.zmq.session.Session` class.
138
138
139 .. note::
139 .. note::
140
140
141 This section should only be relevant to non-Python consumers of the protocol.
141 This section should only be relevant to non-Python consumers of the protocol.
142 Python consumers should simply import and use IPython's own implementation of the wire protocol
142 Python consumers should simply import and use IPython's own implementation of the wire protocol
143 in the :class:`IPython.kernel.zmq.session.Session` object.
143 in the :class:`IPython.kernel.zmq.session.Session` object.
144
144
145 Every message is serialized to a sequence of at least six blobs of bytes:
145 Every message is serialized to a sequence of at least six blobs of bytes:
146
146
147 .. sourcecode:: python
147 .. sourcecode:: python
148
148
149 [
149 [
150 b'u-u-i-d', # zmq identity(ies)
150 b'u-u-i-d', # zmq identity(ies)
151 b'<IDS|MSG>', # delimiter
151 b'<IDS|MSG>', # delimiter
152 b'baddad42', # HMAC signature
152 b'baddad42', # HMAC signature
153 b'{header}', # serialized header dict
153 b'{header}', # serialized header dict
154 b'{parent_header}', # serialized parent header dict
154 b'{parent_header}', # serialized parent header dict
155 b'{metadata}', # serialized metadata dict
155 b'{metadata}', # serialized metadata dict
156 b'{content}, # serialized content dict
156 b'{content}, # serialized content dict
157 b'blob', # extra raw data buffer(s)
157 b'blob', # extra raw data buffer(s)
158 ...
158 ...
159 ]
159 ]
160
160
161 The front of the message is the ZeroMQ routing prefix,
161 The front of the message is the ZeroMQ routing prefix,
162 which can be zero or more socket identities.
162 which can be zero or more socket identities.
163 This is every piece of the message prior to the delimiter key ``<IDS|MSG>``.
163 This is every piece of the message prior to the delimiter key ``<IDS|MSG>``.
164 In the case of IOPub, there should be just one prefix component,
164 In the case of IOPub, there should be just one prefix component,
165 which is the topic for IOPub subscribers, e.g. ``execute_result``, ``display_data``.
165 which is the topic for IOPub subscribers, e.g. ``execute_result``, ``display_data``.
166
166
167 .. note::
167 .. note::
168
168
169 In most cases, the IOPub topics are irrelevant and completely ignored,
169 In most cases, the IOPub topics are irrelevant and completely ignored,
170 because frontends just subscribe to all topics.
170 because frontends just subscribe to all topics.
171 The convention used in the IPython kernel is to use the msg_type as the topic,
171 The convention used in the IPython kernel is to use the msg_type as the topic,
172 and possibly extra information about the message, e.g. ``execute_result`` or ``stream.stdout``
172 and possibly extra information about the message, e.g. ``execute_result`` or ``stream.stdout``
173
173
174 After the delimiter is the `HMAC`_ signature of the message, used for authentication.
174 After the delimiter is the `HMAC`_ signature of the message, used for authentication.
175 If authentication is disabled, this should be an empty string.
175 If authentication is disabled, this should be an empty string.
176 By default, the hashing function used for computing these signatures is sha256.
176 By default, the hashing function used for computing these signatures is sha256.
177
177
178 .. _HMAC: http://en.wikipedia.org/wiki/HMAC
178 .. _HMAC: http://en.wikipedia.org/wiki/HMAC
179
179
180 .. note::
180 .. note::
181
181
182 To disable authentication and signature checking,
182 To disable authentication and signature checking,
183 set the `key` field of a connection file to an empty string.
183 set the `key` field of a connection file to an empty string.
184
184
185 The signature is the HMAC hex digest of the concatenation of:
185 The signature is the HMAC hex digest of the concatenation of:
186
186
187 - A shared key (typically the ``key`` field of a connection file)
187 - A shared key (typically the ``key`` field of a connection file)
188 - The serialized header dict
188 - The serialized header dict
189 - The serialized parent header dict
189 - The serialized parent header dict
190 - The serialized metadata dict
190 - The serialized metadata dict
191 - The serialized content dict
191 - The serialized content dict
192
192
193 In Python, this is implemented via:
193 In Python, this is implemented via:
194
194
195 .. sourcecode:: python
195 .. sourcecode:: python
196
196
197 # once:
197 # once:
198 digester = HMAC(key, digestmod=hashlib.sha256)
198 digester = HMAC(key, digestmod=hashlib.sha256)
199
199
200 # for each message
200 # for each message
201 d = digester.copy()
201 d = digester.copy()
202 for serialized_dict in (header, parent, metadata, content):
202 for serialized_dict in (header, parent, metadata, content):
203 d.update(serialized_dict)
203 d.update(serialized_dict)
204 signature = d.hexdigest()
204 signature = d.hexdigest()
205
205
206 After the signature is the actual message, always in four frames of bytes.
206 After the signature is the actual message, always in four frames of bytes.
207 The four dictionaries that compose a message are serialized separately,
207 The four dictionaries that compose a message are serialized separately,
208 in the order of header, parent header, metadata, and content.
208 in the order of header, parent header, metadata, and content.
209 These can be serialized by any function that turns a dict into bytes.
209 These can be serialized by any function that turns a dict into bytes.
210 The default and most common serialization is JSON, but msgpack and pickle
210 The default and most common serialization is JSON, but msgpack and pickle
211 are common alternatives.
211 are common alternatives.
212
212
213 After the serialized dicts are zero to many raw data buffers,
213 After the serialized dicts are zero to many raw data buffers,
214 which can be used by message types that support binary data (mainly apply and data_pub).
214 which can be used by message types that support binary data (mainly apply and data_pub).
215
215
216
216
217 Python functional API
217 Python functional API
218 =====================
218 =====================
219
219
220 As messages are dicts, they map naturally to a ``func(**kw)`` call form. We
220 As messages are dicts, they map naturally to a ``func(**kw)`` call form. We
221 should develop, at a few key points, functional forms of all the requests that
221 should develop, at a few key points, functional forms of all the requests that
222 take arguments in this manner and automatically construct the necessary dict
222 take arguments in this manner and automatically construct the necessary dict
223 for sending.
223 for sending.
224
224
225 In addition, the Python implementation of the message specification extends
225 In addition, the Python implementation of the message specification extends
226 messages upon deserialization to the following form for convenience::
226 messages upon deserialization to the following form for convenience::
227
227
228 {
228 {
229 'header' : dict,
229 'header' : dict,
230 # The msg's unique identifier and type are always stored in the header,
230 # The msg's unique identifier and type are always stored in the header,
231 # but the Python implementation copies them to the top level.
231 # but the Python implementation copies them to the top level.
232 'msg_id' : uuid,
232 'msg_id' : uuid,
233 'msg_type' : str,
233 'msg_type' : str,
234 'parent_header' : dict,
234 'parent_header' : dict,
235 'content' : dict,
235 'content' : dict,
236 'metadata' : dict,
236 'metadata' : dict,
237 }
237 }
238
238
239 All messages sent to or received by any IPython process should have this
239 All messages sent to or received by any IPython process should have this
240 extended structure.
240 extended structure.
241
241
242
242
243 Messages on the shell ROUTER/DEALER sockets
243 Messages on the shell ROUTER/DEALER sockets
244 ===========================================
244 ===========================================
245
245
246 .. _execute:
246 .. _execute:
247
247
248 Execute
248 Execute
249 -------
249 -------
250
250
251 This message type is used by frontends to ask the kernel to execute code on
251 This message type is used by frontends to ask the kernel to execute code on
252 behalf of the user, in a namespace reserved to the user's variables (and thus
252 behalf of the user, in a namespace reserved to the user's variables (and thus
253 separate from the kernel's own internal code and variables).
253 separate from the kernel's own internal code and variables).
254
254
255 Message type: ``execute_request``::
255 Message type: ``execute_request``::
256
256
257 content = {
257 content = {
258 # Source code to be executed by the kernel, one or more lines.
258 # Source code to be executed by the kernel, one or more lines.
259 'code' : str,
259 'code' : str,
260
260
261 # A boolean flag which, if True, signals the kernel to execute
261 # A boolean flag which, if True, signals the kernel to execute
262 # this code as quietly as possible.
262 # this code as quietly as possible.
263 # silent=True forces store_history to be False,
263 # silent=True forces store_history to be False,
264 # and will *not*:
264 # and will *not*:
265 # - broadcast output on the IOPUB channel
265 # - broadcast output on the IOPUB channel
266 # - have an execute_result
266 # - have an execute_result
267 # The default is False.
267 # The default is False.
268 'silent' : bool,
268 'silent' : bool,
269
269
270 # A boolean flag which, if True, signals the kernel to populate history
270 # A boolean flag which, if True, signals the kernel to populate history
271 # The default is True if silent is False. If silent is True, store_history
271 # The default is True if silent is False. If silent is True, store_history
272 # is forced to be False.
272 # is forced to be False.
273 'store_history' : bool,
273 'store_history' : bool,
274
274
275 # A dict mapping names to expressions to be evaluated in the
275 # A dict mapping names to expressions to be evaluated in the
276 # user's dict. The rich display-data representation of each will be evaluated after execution.
276 # user's dict. The rich display-data representation of each will be evaluated after execution.
277 # See the display_data content for the structure of the representation data.
277 # See the display_data content for the structure of the representation data.
278 'user_expressions' : dict,
278 'user_expressions' : dict,
279
279
280 # Some frontends do not support stdin requests.
280 # Some frontends do not support stdin requests.
281 # If raw_input is called from code executed from such a frontend,
281 # If raw_input is called from code executed from such a frontend,
282 # a StdinNotImplementedError will be raised.
282 # a StdinNotImplementedError will be raised.
283 'allow_stdin' : True,
283 'allow_stdin' : True,
284 }
284 }
285
285
286 .. versionchanged:: 5.0
286 .. versionchanged:: 5.0
287
287
288 ``user_variables`` removed, because it is redundant with user_expressions.
288 ``user_variables`` removed, because it is redundant with user_expressions.
289
289
290 The ``code`` field contains a single string (possibly multiline) to be executed.
290 The ``code`` field contains a single string (possibly multiline) to be executed.
291
291
292 The ``user_expressions`` field deserves a detailed explanation. In the past, IPython had
292 The ``user_expressions`` field deserves a detailed explanation. In the past, IPython had
293 the notion of a prompt string that allowed arbitrary code to be evaluated, and
293 the notion of a prompt string that allowed arbitrary code to be evaluated, and
294 this was put to good use by many in creating prompts that displayed system
294 this was put to good use by many in creating prompts that displayed system
295 status, path information, and even more esoteric uses like remote instrument
295 status, path information, and even more esoteric uses like remote instrument
296 status acquired over the network. But now that IPython has a clean separation
296 status acquired over the network. But now that IPython has a clean separation
297 between the kernel and the clients, the kernel has no prompt knowledge; prompts
297 between the kernel and the clients, the kernel has no prompt knowledge; prompts
298 are a frontend feature, and it should be even possible for different
298 are a frontend feature, and it should be even possible for different
299 frontends to display different prompts while interacting with the same kernel.
299 frontends to display different prompts while interacting with the same kernel.
300 ``user_expressions`` can be used to retrieve this information.
300 ``user_expressions`` can be used to retrieve this information.
301
301
302 Any error in evaluating any expression in ``user_expressions`` will result in
302 Any error in evaluating any expression in ``user_expressions`` will result in
303 only that key containing a standard error message, of the form::
303 only that key containing a standard error message, of the form::
304
304
305 {
305 {
306 'status' : 'error',
306 'status' : 'error',
307 'ename' : 'NameError',
307 'ename' : 'NameError',
308 'evalue' : 'foo',
308 'evalue' : 'foo',
309 'traceback' : ...
309 'traceback' : ...
310 }
310 }
311
311
312 .. Note::
312 .. Note::
313
313
314 In order to obtain the current execution counter for the purposes of
314 In order to obtain the current execution counter for the purposes of
315 displaying input prompts, frontends may make an execution request with an
315 displaying input prompts, frontends may make an execution request with an
316 empty code string and ``silent=True``.
316 empty code string and ``silent=True``.
317
317
318 Upon completion of the execution request, the kernel *always* sends a reply,
318 Upon completion of the execution request, the kernel *always* sends a reply,
319 with a status code indicating what happened and additional data depending on
319 with a status code indicating what happened and additional data depending on
320 the outcome. See :ref:`below <execution_results>` for the possible return
320 the outcome. See :ref:`below <execution_results>` for the possible return
321 codes and associated data.
321 codes and associated data.
322
322
323 .. seealso::
323 .. seealso::
324
324
325 :ref:`execution_semantics`
325 :ref:`execution_semantics`
326
326
327 .. _execution_counter:
327 .. _execution_counter:
328
328
329 Execution counter (prompt number)
329 Execution counter (prompt number)
330 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
330 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
331
331
332 The kernel should have a single, monotonically increasing counter of all execution
332 The kernel should have a single, monotonically increasing counter of all execution
333 requests that are made with ``store_history=True``. This counter is used to populate
333 requests that are made with ``store_history=True``. This counter is used to populate
334 the ``In[n]`` and ``Out[n]`` prompts. The value of this counter will be returned as the
334 the ``In[n]`` and ``Out[n]`` prompts. The value of this counter will be returned as the
335 ``execution_count`` field of all ``execute_reply`` and ``execute_input`` messages.
335 ``execution_count`` field of all ``execute_reply`` and ``execute_input`` messages.
336
336
337 .. _execution_results:
337 .. _execution_results:
338
338
339 Execution results
339 Execution results
340 ~~~~~~~~~~~~~~~~~
340 ~~~~~~~~~~~~~~~~~
341
341
342 Message type: ``execute_reply``::
342 Message type: ``execute_reply``::
343
343
344 content = {
344 content = {
345 # One of: 'ok' OR 'error' OR 'abort'
345 # One of: 'ok' OR 'error' OR 'abort'
346 'status' : str,
346 'status' : str,
347
347
348 # The global kernel counter that increases by one with each request that
348 # The global kernel counter that increases by one with each request that
349 # stores history. This will typically be used by clients to display
349 # stores history. This will typically be used by clients to display
350 # prompt numbers to the user. If the request did not store history, this will
350 # prompt numbers to the user. If the request did not store history, this will
351 # be the current value of the counter in the kernel.
351 # be the current value of the counter in the kernel.
352 'execution_count' : int,
352 'execution_count' : int,
353 }
353 }
354
354
355 When status is 'ok', the following extra fields are present::
355 When status is 'ok', the following extra fields are present::
356
356
357 {
357 {
358 # 'payload' will be a list of payload dicts.
358 # 'payload' will be a list of payload dicts.
359 # Each execution payload is a dict with string keys that may have been
359 # Each execution payload is a dict with string keys that may have been
360 # produced by the code being executed. It is retrieved by the kernel at
360 # produced by the code being executed. It is retrieved by the kernel at
361 # the end of the execution and sent back to the front end, which can take
361 # the end of the execution and sent back to the front end, which can take
362 # action on it as needed.
362 # action on it as needed.
363 # The only requirement of each payload dict is that it have a 'source' key,
363 # The only requirement of each payload dict is that it have a 'source' key,
364 # which is a string classifying the payload (e.g. 'pager').
364 # which is a string classifying the payload (e.g. 'pager').
365 'payload' : list(dict),
365 'payload' : list(dict),
366
366
367 # Results for the user_expressions.
367 # Results for the user_expressions.
368 'user_expressions' : dict,
368 'user_expressions' : dict,
369 }
369 }
370
370
371 .. versionchanged:: 5.0
371 .. versionchanged:: 5.0
372
372
373 ``user_variables`` is removed, use user_expressions instead.
373 ``user_variables`` is removed, use user_expressions instead.
374
374
375 .. admonition:: Execution payloads
375 .. admonition:: Execution payloads
376
376
377 The notion of an 'execution payload' is different from a return value of a
377 The notion of an 'execution payload' is different from a return value of a
378 given set of code, which normally is just displayed on the execute_result stream
378 given set of code, which normally is just displayed on the execute_result stream
379 through the PUB socket. The idea of a payload is to allow special types of
379 through the PUB socket. The idea of a payload is to allow special types of
380 code, typically magics, to populate a data container in the IPython kernel
380 code, typically magics, to populate a data container in the IPython kernel
381 that will be shipped back to the caller via this channel. The kernel
381 that will be shipped back to the caller via this channel. The kernel
382 has an API for this in the PayloadManager::
382 has an API for this in the PayloadManager::
383
383
384 ip.payload_manager.write_payload(payload_dict)
384 ip.payload_manager.write_payload(payload_dict)
385
385
386 which appends a dictionary to the list of payloads.
386 which appends a dictionary to the list of payloads.
387
387
388 The payload API is not yet stabilized,
388 The payload API is not yet stabilized,
389 and should probably not be supported by non-Python kernels at this time.
389 and should probably not be supported by non-Python kernels at this time.
390 In such cases, the payload list should always be empty.
390 In such cases, the payload list should always be empty.
391
391
392
392
393 When status is 'error', the following extra fields are present::
393 When status is 'error', the following extra fields are present::
394
394
395 {
395 {
396 'ename' : str, # Exception name, as a string
396 'ename' : str, # Exception name, as a string
397 'evalue' : str, # Exception value, as a string
397 'evalue' : str, # Exception value, as a string
398
398
399 # The traceback will contain a list of frames, represented each as a
399 # The traceback will contain a list of frames, represented each as a
400 # string. For now we'll stick to the existing design of ultraTB, which
400 # string. For now we'll stick to the existing design of ultraTB, which
401 # controls exception level of detail statefully. But eventually we'll
401 # controls exception level of detail statefully. But eventually we'll
402 # want to grow into a model where more information is collected and
402 # want to grow into a model where more information is collected and
403 # packed into the traceback object, with clients deciding how little or
403 # packed into the traceback object, with clients deciding how little or
404 # how much of it to unpack. But for now, let's start with a simple list
404 # how much of it to unpack. But for now, let's start with a simple list
405 # of strings, since that requires only minimal changes to ultratb as
405 # of strings, since that requires only minimal changes to ultratb as
406 # written.
406 # written.
407 'traceback' : list,
407 'traceback' : list,
408 }
408 }
409
409
410
410
411 When status is 'abort', there are for now no additional data fields. This
411 When status is 'abort', there are for now no additional data fields. This
412 happens when the kernel was interrupted by a signal.
412 happens when the kernel was interrupted by a signal.
413
413
414 .. _msging_inspection:
414 .. _msging_inspection:
415
415
416 Introspection
416 Introspection
417 -------------
417 -------------
418
418
419 Code can be inspected to show useful information to the user.
419 Code can be inspected to show useful information to the user.
420 It is up to the Kernel to decide what information should be displayed, and its formatting.
420 It is up to the Kernel to decide what information should be displayed, and its formatting.
421
421
422 Message type: ``inspect_request``::
422 Message type: ``inspect_request``::
423
423
424 content = {
424 content = {
425 # The code context in which introspection is requested
425 # The code context in which introspection is requested
426 # this may be up to an entire multiline cell.
426 # this may be up to an entire multiline cell.
427 'code' : str,
427 'code' : str,
428
428
429 # The cursor position within 'code' (in unicode characters) where inspection is requested
429 # The cursor position within 'code' (in unicode characters) where inspection is requested
430 'cursor_pos' : int,
430 'cursor_pos' : int,
431
431
432 # The level of detail desired. In IPython, the default (0) is equivalent to typing
432 # The level of detail desired. In IPython, the default (0) is equivalent to typing
433 # 'x?' at the prompt, 1 is equivalent to 'x??'.
433 # 'x?' at the prompt, 1 is equivalent to 'x??'.
434 # The difference is up to kernels, but in IPython level 1 includes the source code
434 # The difference is up to kernels, but in IPython level 1 includes the source code
435 # if available.
435 # if available.
436 'detail_level' : 0 or 1,
436 'detail_level' : 0 or 1,
437 }
437 }
438
438
439 .. versionchanged:: 5.0
439 .. versionchanged:: 5.0
440
440
441 ``object_info_request`` renamed to ``inspect_request``.
441 ``object_info_request`` renamed to ``inspect_request``.
442
442
443 .. versionchanged:: 5.0
443 .. versionchanged:: 5.0
444
444
445 ``name`` key replaced with ``code`` and ``cursor_pos``,
445 ``name`` key replaced with ``code`` and ``cursor_pos``,
446 moving the lexing responsibility to the kernel.
446 moving the lexing responsibility to the kernel.
447
447
448 The reply is a mime-bundle, like a `display_data`_ message,
448 The reply is a mime-bundle, like a `display_data`_ message,
449 which should be a formatted representation of information about the context.
449 which should be a formatted representation of information about the context.
450 In the notebook, this is used to show tooltips over function calls, etc.
450 In the notebook, this is used to show tooltips over function calls, etc.
451
451
452 Message type: ``inspect_reply``::
452 Message type: ``inspect_reply``::
453
453
454 content = {
454 content = {
455 # 'ok' if the request succeeded or 'error', with error information as in all other replies.
455 # 'ok' if the request succeeded or 'error', with error information as in all other replies.
456 'status' : 'ok',
456 'status' : 'ok',
457
457
458 # data can be empty if nothing is found
458 # data can be empty if nothing is found
459 'data' : dict,
459 'data' : dict,
460 'metadata' : dict,
460 'metadata' : dict,
461 }
461 }
462
462
463 .. versionchanged:: 5.0
463 .. versionchanged:: 5.0
464
464
465 ``object_info_reply`` renamed to ``inspect_reply``.
465 ``object_info_reply`` renamed to ``inspect_reply``.
466
466
467 .. versionchanged:: 5.0
467 .. versionchanged:: 5.0
468
468
469 Reply is changed from structured data to a mime bundle, allowing formatting decisions to be made by the kernel.
469 Reply is changed from structured data to a mime bundle, allowing formatting decisions to be made by the kernel.
470
470
471 .. _msging_completion:
471 .. _msging_completion:
472
472
473 Completion
473 Completion
474 ----------
474 ----------
475
475
476 Message type: ``complete_request``::
476 Message type: ``complete_request``::
477
477
478 content = {
478 content = {
479 # The code context in which completion is requested
479 # The code context in which completion is requested
480 # this may be up to an entire multiline cell, such as
480 # this may be up to an entire multiline cell, such as
481 # 'foo = a.isal'
481 # 'foo = a.isal'
482 'code' : str,
482 'code' : str,
483
483
484 # The cursor position within 'code' (in unicode characters) where completion is requested
484 # The cursor position within 'code' (in unicode characters) where completion is requested
485 'cursor_pos' : int,
485 'cursor_pos' : int,
486 }
486 }
487
487
488 .. versionchanged:: 5.0
488 .. versionchanged:: 5.0
489
489
490 ``line``, ``block``, and ``text`` keys are removed in favor of a single ``code`` for context.
490 ``line``, ``block``, and ``text`` keys are removed in favor of a single ``code`` for context.
491 Lexing is up to the kernel.
491 Lexing is up to the kernel.
492
492
493
493
494 Message type: ``complete_reply``::
494 Message type: ``complete_reply``::
495
495
496 content = {
496 content = {
497 # The list of all matches to the completion request, such as
497 # The list of all matches to the completion request, such as
498 # ['a.isalnum', 'a.isalpha'] for the above example.
498 # ['a.isalnum', 'a.isalpha'] for the above example.
499 'matches' : list,
499 'matches' : list,
500
500
501 # The range of text that should be replaced by the above matches when a completion is accepted.
501 # The range of text that should be replaced by the above matches when a completion is accepted.
502 # typically cursor_end is the same as cursor_pos in the request.
502 # typically cursor_end is the same as cursor_pos in the request.
503 'cursor_start' : int,
503 'cursor_start' : int,
504 'cursor_end' : int,
504 'cursor_end' : int,
505
505
506 # Information that frontend plugins might use for extra display information about completions.
506 # Information that frontend plugins might use for extra display information about completions.
507 'metadata' : dict,
507 'metadata' : dict,
508
508
509 # status should be 'ok' unless an exception was raised during the request,
509 # status should be 'ok' unless an exception was raised during the request,
510 # in which case it should be 'error', along with the usual error message content
510 # in which case it should be 'error', along with the usual error message content
511 # in other messages.
511 # in other messages.
512 'status' : 'ok'
512 'status' : 'ok'
513 }
513 }
514
514
515 .. versionchanged:: 5.0
515 .. versionchanged:: 5.0
516
516
517 - ``matched_text`` is removed in favor of ``cursor_start`` and ``cursor_end``.
517 - ``matched_text`` is removed in favor of ``cursor_start`` and ``cursor_end``.
518 - ``metadata`` is added for extended information.
518 - ``metadata`` is added for extended information.
519
519
520 .. _msging_history:
520 .. _msging_history:
521
521
522 History
522 History
523 -------
523 -------
524
524
525 For clients to explicitly request history from a kernel. The kernel has all
525 For clients to explicitly request history from a kernel. The kernel has all
526 the actual execution history stored in a single location, so clients can
526 the actual execution history stored in a single location, so clients can
527 request it from the kernel when needed.
527 request it from the kernel when needed.
528
528
529 Message type: ``history_request``::
529 Message type: ``history_request``::
530
530
531 content = {
531 content = {
532
532
533 # If True, also return output history in the resulting dict.
533 # If True, also return output history in the resulting dict.
534 'output' : bool,
534 'output' : bool,
535
535
536 # If True, return the raw input history, else the transformed input.
536 # If True, return the raw input history, else the transformed input.
537 'raw' : bool,
537 'raw' : bool,
538
538
539 # So far, this can be 'range', 'tail' or 'search'.
539 # So far, this can be 'range', 'tail' or 'search'.
540 'hist_access_type' : str,
540 'hist_access_type' : str,
541
541
542 # If hist_access_type is 'range', get a range of input cells. session can
542 # If hist_access_type is 'range', get a range of input cells. session can
543 # be a positive session number, or a negative number to count back from
543 # be a positive session number, or a negative number to count back from
544 # the current session.
544 # the current session.
545 'session' : int,
545 'session' : int,
546 # start and stop are line numbers within that session.
546 # start and stop are line numbers within that session.
547 'start' : int,
547 'start' : int,
548 'stop' : int,
548 'stop' : int,
549
549
550 # If hist_access_type is 'tail' or 'search', get the last n cells.
550 # If hist_access_type is 'tail' or 'search', get the last n cells.
551 'n' : int,
551 'n' : int,
552
552
553 # If hist_access_type is 'search', get cells matching the specified glob
553 # If hist_access_type is 'search', get cells matching the specified glob
554 # pattern (with * and ? as wildcards).
554 # pattern (with * and ? as wildcards).
555 'pattern' : str,
555 'pattern' : str,
556
556
557 # If hist_access_type is 'search' and unique is true, do not
557 # If hist_access_type is 'search' and unique is true, do not
558 # include duplicated history. Default is false.
558 # include duplicated history. Default is false.
559 'unique' : bool,
559 'unique' : bool,
560
560
561 }
561 }
562
562
563 .. versionadded:: 4.0
563 .. versionadded:: 4.0
564 The key ``unique`` for ``history_request``.
564 The key ``unique`` for ``history_request``.
565
565
566 Message type: ``history_reply``::
566 Message type: ``history_reply``::
567
567
568 content = {
568 content = {
569 # A list of 3 tuples, either:
569 # A list of 3 tuples, either:
570 # (session, line_number, input) or
570 # (session, line_number, input) or
571 # (session, line_number, (input, output)),
571 # (session, line_number, (input, output)),
572 # depending on whether output was False or True, respectively.
572 # depending on whether output was False or True, respectively.
573 'history' : list,
573 'history' : list,
574 }
574 }
575
575
576 .. _msging_is_complete:
576 .. _msging_is_complete:
577
577
578 Code completeness
578 Code completeness
579 -----------------
579 -----------------
580
580
581 .. versionadded:: 5.0
581 .. versionadded:: 5.0
582
582
583 When the user enters a line in a console style interface, the console must
583 When the user enters a line in a console style interface, the console must
584 decide whether to immediately execute the current code, or whether to show a
584 decide whether to immediately execute the current code, or whether to show a
585 continuation prompt for further input. For instance, in Python ``a = 5`` would
585 continuation prompt for further input. For instance, in Python ``a = 5`` would
586 be executed immediately, while ``for i in range(5):`` would expect further input.
586 be executed immediately, while ``for i in range(5):`` would expect further input.
587
587
588 There are four possible replies:
588 There are four possible replies:
589
589
590 - *complete* code is ready to be executed
590 - *complete* code is ready to be executed
591 - *incomplete* code should prompt for another line
591 - *incomplete* code should prompt for another line
592 - *invalid* code will typically be sent for execution, so that the user sees the
592 - *invalid* code will typically be sent for execution, so that the user sees the
593 error soonest.
593 error soonest.
594 - *unknown* - if the kernel is not able to determine this. The frontend should
594 - *unknown* - if the kernel is not able to determine this. The frontend should
595 also handle the kernel not replying promptly. It may default to sending the
595 also handle the kernel not replying promptly. It may default to sending the
596 code for execution, or it may implement simple fallback heuristics for whether
596 code for execution, or it may implement simple fallback heuristics for whether
597 to execute the code (e.g. execute after a blank line).
597 to execute the code (e.g. execute after a blank line).
598
598
599 Frontends may have ways to override this, forcing the code to be sent for
599 Frontends may have ways to override this, forcing the code to be sent for
600 execution or forcing a continuation prompt.
600 execution or forcing a continuation prompt.
601
601
602 Message type: ``is_complete_request``::
602 Message type: ``is_complete_request``::
603
603
604 content = {
604 content = {
605 # The code entered so far as a multiline string
605 # The code entered so far as a multiline string
606 'code' : str,
606 'code' : str,
607 }
607 }
608
608
609 Message type: ``is_complete_reply``::
609 Message type: ``is_complete_reply``::
610
610
611 content = {
611 content = {
612 # One of 'complete', 'incomplete', 'invalid', 'unknown'
612 # One of 'complete', 'incomplete', 'invalid', 'unknown'
613 'status' : str,
613 'status' : str,
614
614
615 # If status is 'incomplete', indent should contain the characters to use
615 # If status is 'incomplete', indent should contain the characters to use
616 # to indent the next line. This is only a hint: frontends may ignore it
616 # to indent the next line. This is only a hint: frontends may ignore it
617 # and use their own autoindentation rules. For other statuses, this
617 # and use their own autoindentation rules. For other statuses, this
618 # field does not exist.
618 # field does not exist.
619 'indent': str,
619 'indent': str,
620 }
620 }
621
621
622 Connect
622 Connect
623 -------
623 -------
624
624
625 When a client connects to the request/reply socket of the kernel, it can issue
625 When a client connects to the request/reply socket of the kernel, it can issue
626 a connect request to get basic information about the kernel, such as the ports
626 a connect request to get basic information about the kernel, such as the ports
627 the other ZeroMQ sockets are listening on. This allows clients to only have
627 the other ZeroMQ sockets are listening on. This allows clients to only have
628 to know about a single port (the shell channel) to connect to a kernel.
628 to know about a single port (the shell channel) to connect to a kernel.
629
629
630 Message type: ``connect_request``::
630 Message type: ``connect_request``::
631
631
632 content = {
632 content = {
633 }
633 }
634
634
635 Message type: ``connect_reply``::
635 Message type: ``connect_reply``::
636
636
637 content = {
637 content = {
638 'shell_port' : int, # The port the shell ROUTER socket is listening on.
638 'shell_port' : int, # The port the shell ROUTER socket is listening on.
639 'iopub_port' : int, # The port the PUB socket is listening on.
639 'iopub_port' : int, # The port the PUB socket is listening on.
640 'stdin_port' : int, # The port the stdin ROUTER socket is listening on.
640 'stdin_port' : int, # The port the stdin ROUTER socket is listening on.
641 'hb_port' : int, # The port the heartbeat socket is listening on.
641 'hb_port' : int, # The port the heartbeat socket is listening on.
642 }
642 }
643
643
644 .. _msging_kernel_info:
644 .. _msging_kernel_info:
645
645
646 Kernel info
646 Kernel info
647 -----------
647 -----------
648
648
649 If a client needs to know information about the kernel, it can
649 If a client needs to know information about the kernel, it can
650 make a request of the kernel's information.
650 make a request of the kernel's information.
651 This message can be used to fetch core information of the
651 This message can be used to fetch core information of the
652 kernel, including language (e.g., Python), language version number and
652 kernel, including language (e.g., Python), language version number and
653 IPython version number, and the IPython message spec version number.
653 IPython version number, and the IPython message spec version number.
654
654
655 Message type: ``kernel_info_request``::
655 Message type: ``kernel_info_request``::
656
656
657 content = {
657 content = {
658 }
658 }
659
659
660 Message type: ``kernel_info_reply``::
660 Message type: ``kernel_info_reply``::
661
661
662 content = {
662 content = {
663 # Version of messaging protocol.
663 # Version of messaging protocol.
664 # The first integer indicates major version. It is incremented when
664 # The first integer indicates major version. It is incremented when
665 # there is any backward incompatible change.
665 # there is any backward incompatible change.
666 # The second integer indicates minor version. It is incremented when
666 # The second integer indicates minor version. It is incremented when
667 # there is any backward compatible change.
667 # there is any backward compatible change.
668 'protocol_version': 'X.Y.Z',
668 'protocol_version': 'X.Y.Z',
669
669
670 # The kernel implementation name
670 # The kernel implementation name
671 # (e.g. 'ipython' for the IPython kernel)
671 # (e.g. 'ipython' for the IPython kernel)
672 'implementation': str,
672 'implementation': str,
673
673
674 # Implementation version number.
674 # Implementation version number.
675 # The version number of the kernel's implementation
675 # The version number of the kernel's implementation
676 # (e.g. IPython.__version__ for the IPython kernel)
676 # (e.g. IPython.__version__ for the IPython kernel)
677 'implementation_version': 'X.Y.Z',
677 'implementation_version': 'X.Y.Z',
678
679 # Programming language in which kernel is implemented.
680 # Kernel included in IPython returns 'python'.
681 'language': str,
682
683 # Language version number.
684 # It is Python version number (e.g., '2.7.3') for the kernel
685 # included in IPython.
686 'language_version': 'X.Y.Z',
687
678
688 # Information about the language of code for the kernel
679 # Information about the language of code for the kernel
689 'language_info': {
680 'language_info': {
681 # Name of the programming language in which kernel is implemented.
682 # Kernel included in IPython returns 'python'.
683 'name': str,
684
685 # Language version number.
686 # It is Python version number (e.g., '2.7.3') for the kernel
687 # included in IPython.
688 'version': 'X.Y.Z',
689
690 # mimetype for script files in this language
690 'mimetype': str,
691 'mimetype': str,
691
692
692 # Pygments lexer, for highlighting
693 # Pygments lexer, for highlighting
693 # Only needed if it differs from the top level 'language' field.
694 # Only needed if it differs from the top level 'language' field.
694 'pygments_lexer': str,
695 'pygments_lexer': str,
695
696
696 # Codemirror mode, for for highlighting in the notebook.
697 # Codemirror mode, for for highlighting in the notebook.
697 # Only needed if it differs from the top level 'language' field.
698 # Only needed if it differs from the top level 'language' field.
698 'codemirror_mode': str or dict,
699 'codemirror_mode': str or dict,
699 },
700 },
700
701
701 # A banner of information about the kernel,
702 # A banner of information about the kernel,
702 # which may be desplayed in console environments.
703 # which may be desplayed in console environments.
703 'banner' : str,
704 'banner' : str,
704
705
705 # Optional: A list of dictionaries, each with keys 'text' and 'url'.
706 # Optional: A list of dictionaries, each with keys 'text' and 'url'.
706 # These will be displayed in the help menu in the notebook UI.
707 # These will be displayed in the help menu in the notebook UI.
707 'help_links': [
708 'help_links': [
708 {'text': str, 'url': str}
709 {'text': str, 'url': str}
709 ],
710 ],
710 }
711 }
711
712
712 Refer to the lists of available `Pygments lexers <http://pygments.org/docs/lexers/>`_
713 Refer to the lists of available `Pygments lexers <http://pygments.org/docs/lexers/>`_
713 and `codemirror modes <http://codemirror.net/mode/index.html>`_ for those fields.
714 and `codemirror modes <http://codemirror.net/mode/index.html>`_ for those fields.
714
715
715 .. versionchanged:: 5.0
716 .. versionchanged:: 5.0
716
717
717 Versions changed from lists of integers to strings.
718 Versions changed from lists of integers to strings.
718
719
719 .. versionchanged:: 5.0
720 .. versionchanged:: 5.0
720
721
721 ``ipython_version`` is removed.
722 ``ipython_version`` is removed.
722
723
723 .. versionchanged:: 5.0
724 .. versionchanged:: 5.0
724
725
725 ``language_info``, ``implementation``, ``implementation_version``, ``banner``
726 ``language_info``, ``implementation``, ``implementation_version``, ``banner``
726 and ``help_links`` keys are added.
727 and ``help_links`` keys are added.
727
728
729 .. versionchanged:: 5.0
730
731 ``language_version`` moved to ``language_info.version``
732
733 .. versionchanged:: 5.0
734
735 ``language`` moved to ``language_info.name``
736
728 .. _msging_shutdown:
737 .. _msging_shutdown:
729
738
730 Kernel shutdown
739 Kernel shutdown
731 ---------------
740 ---------------
732
741
733 The clients can request the kernel to shut itself down; this is used in
742 The clients can request the kernel to shut itself down; this is used in
734 multiple cases:
743 multiple cases:
735
744
736 - when the user chooses to close the client application via a menu or window
745 - when the user chooses to close the client application via a menu or window
737 control.
746 control.
738 - when the user types 'exit' or 'quit' (or their uppercase magic equivalents).
747 - when the user types 'exit' or 'quit' (or their uppercase magic equivalents).
739 - when the user chooses a GUI method (like the 'Ctrl-C' shortcut in the
748 - when the user chooses a GUI method (like the 'Ctrl-C' shortcut in the
740 IPythonQt client) to force a kernel restart to get a clean kernel without
749 IPythonQt client) to force a kernel restart to get a clean kernel without
741 losing client-side state like history or inlined figures.
750 losing client-side state like history or inlined figures.
742
751
743 The client sends a shutdown request to the kernel, and once it receives the
752 The client sends a shutdown request to the kernel, and once it receives the
744 reply message (which is otherwise empty), it can assume that the kernel has
753 reply message (which is otherwise empty), it can assume that the kernel has
745 completed shutdown safely.
754 completed shutdown safely.
746
755
747 Upon their own shutdown, client applications will typically execute a last
756 Upon their own shutdown, client applications will typically execute a last
748 minute sanity check and forcefully terminate any kernel that is still alive, to
757 minute sanity check and forcefully terminate any kernel that is still alive, to
749 avoid leaving stray processes in the user's machine.
758 avoid leaving stray processes in the user's machine.
750
759
751 Message type: ``shutdown_request``::
760 Message type: ``shutdown_request``::
752
761
753 content = {
762 content = {
754 'restart' : bool # whether the shutdown is final, or precedes a restart
763 'restart' : bool # whether the shutdown is final, or precedes a restart
755 }
764 }
756
765
757 Message type: ``shutdown_reply``::
766 Message type: ``shutdown_reply``::
758
767
759 content = {
768 content = {
760 'restart' : bool # whether the shutdown is final, or precedes a restart
769 'restart' : bool # whether the shutdown is final, or precedes a restart
761 }
770 }
762
771
763 .. Note::
772 .. Note::
764
773
765 When the clients detect a dead kernel thanks to inactivity on the heartbeat
774 When the clients detect a dead kernel thanks to inactivity on the heartbeat
766 socket, they simply send a forceful process termination signal, since a dead
775 socket, they simply send a forceful process termination signal, since a dead
767 process is unlikely to respond in any useful way to messages.
776 process is unlikely to respond in any useful way to messages.
768
777
769
778
770 Messages on the PUB/SUB socket
779 Messages on the PUB/SUB socket
771 ==============================
780 ==============================
772
781
773 Streams (stdout, stderr, etc)
782 Streams (stdout, stderr, etc)
774 ------------------------------
783 ------------------------------
775
784
776 Message type: ``stream``::
785 Message type: ``stream``::
777
786
778 content = {
787 content = {
779 # The name of the stream is one of 'stdout', 'stderr'
788 # The name of the stream is one of 'stdout', 'stderr'
780 'name' : str,
789 'name' : str,
781
790
782 # The text is an arbitrary string to be written to that stream
791 # The text is an arbitrary string to be written to that stream
783 'text' : str,
792 'text' : str,
784 }
793 }
785
794
786 .. versionchanged:: 5.0
795 .. versionchanged:: 5.0
787
796
788 'data' key renamed to 'text' for conistency with the notebook format.
797 'data' key renamed to 'text' for conistency with the notebook format.
789
798
790 Display Data
799 Display Data
791 ------------
800 ------------
792
801
793 This type of message is used to bring back data that should be displayed (text,
802 This type of message is used to bring back data that should be displayed (text,
794 html, svg, etc.) in the frontends. This data is published to all frontends.
803 html, svg, etc.) in the frontends. This data is published to all frontends.
795 Each message can have multiple representations of the data; it is up to the
804 Each message can have multiple representations of the data; it is up to the
796 frontend to decide which to use and how. A single message should contain all
805 frontend to decide which to use and how. A single message should contain all
797 possible representations of the same information. Each representation should
806 possible representations of the same information. Each representation should
798 be a JSON'able data structure, and should be a valid MIME type.
807 be a JSON'able data structure, and should be a valid MIME type.
799
808
800 Some questions remain about this design:
809 Some questions remain about this design:
801
810
802 * Do we use this message type for execute_result/displayhook? Probably not, because
811 * Do we use this message type for execute_result/displayhook? Probably not, because
803 the displayhook also has to handle the Out prompt display. On the other hand
812 the displayhook also has to handle the Out prompt display. On the other hand
804 we could put that information into the metadata section.
813 we could put that information into the metadata section.
805
814
806 .. _display_data:
815 .. _display_data:
807
816
808 Message type: ``display_data``::
817 Message type: ``display_data``::
809
818
810 content = {
819 content = {
811
820
812 # Who create the data
821 # Who create the data
813 'source' : str,
822 'source' : str,
814
823
815 # The data dict contains key/value pairs, where the keys are MIME
824 # The data dict contains key/value pairs, where the keys are MIME
816 # types and the values are the raw data of the representation in that
825 # types and the values are the raw data of the representation in that
817 # format.
826 # format.
818 'data' : dict,
827 'data' : dict,
819
828
820 # Any metadata that describes the data
829 # Any metadata that describes the data
821 'metadata' : dict
830 'metadata' : dict
822 }
831 }
823
832
824
833
825 The ``metadata`` contains any metadata that describes the output.
834 The ``metadata`` contains any metadata that describes the output.
826 Global keys are assumed to apply to the output as a whole.
835 Global keys are assumed to apply to the output as a whole.
827 The ``metadata`` dict can also contain mime-type keys, which will be sub-dictionaries,
836 The ``metadata`` dict can also contain mime-type keys, which will be sub-dictionaries,
828 which are interpreted as applying only to output of that type.
837 which are interpreted as applying only to output of that type.
829 Third parties should put any data they write into a single dict
838 Third parties should put any data they write into a single dict
830 with a reasonably unique name to avoid conflicts.
839 with a reasonably unique name to avoid conflicts.
831
840
832 The only metadata keys currently defined in IPython are the width and height
841 The only metadata keys currently defined in IPython are the width and height
833 of images::
842 of images::
834
843
835 metadata = {
844 metadata = {
836 'image/png' : {
845 'image/png' : {
837 'width': 640,
846 'width': 640,
838 'height': 480
847 'height': 480
839 }
848 }
840 }
849 }
841
850
842
851
843 .. versionchanged:: 5.0
852 .. versionchanged:: 5.0
844
853
845 `application/json` data should be unpacked JSON data,
854 `application/json` data should be unpacked JSON data,
846 not double-serialized as a JSON string.
855 not double-serialized as a JSON string.
847
856
848
857
849 Raw Data Publication
858 Raw Data Publication
850 --------------------
859 --------------------
851
860
852 ``display_data`` lets you publish *representations* of data, such as images and html.
861 ``display_data`` lets you publish *representations* of data, such as images and html.
853 This ``data_pub`` message lets you publish *actual raw data*, sent via message buffers.
862 This ``data_pub`` message lets you publish *actual raw data*, sent via message buffers.
854
863
855 data_pub messages are constructed via the :func:`IPython.lib.datapub.publish_data` function:
864 data_pub messages are constructed via the :func:`IPython.lib.datapub.publish_data` function:
856
865
857 .. sourcecode:: python
866 .. sourcecode:: python
858
867
859 from IPython.kernel.zmq.datapub import publish_data
868 from IPython.kernel.zmq.datapub import publish_data
860 ns = dict(x=my_array)
869 ns = dict(x=my_array)
861 publish_data(ns)
870 publish_data(ns)
862
871
863
872
864 Message type: ``data_pub``::
873 Message type: ``data_pub``::
865
874
866 content = {
875 content = {
867 # the keys of the data dict, after it has been unserialized
876 # the keys of the data dict, after it has been unserialized
868 'keys' : ['a', 'b']
877 'keys' : ['a', 'b']
869 }
878 }
870 # the namespace dict will be serialized in the message buffers,
879 # the namespace dict will be serialized in the message buffers,
871 # which will have a length of at least one
880 # which will have a length of at least one
872 buffers = [b'pdict', ...]
881 buffers = [b'pdict', ...]
873
882
874
883
875 The interpretation of a sequence of data_pub messages for a given parent request should be
884 The interpretation of a sequence of data_pub messages for a given parent request should be
876 to update a single namespace with subsequent results.
885 to update a single namespace with subsequent results.
877
886
878 .. note::
887 .. note::
879
888
880 No frontends directly handle data_pub messages at this time.
889 No frontends directly handle data_pub messages at this time.
881 It is currently only used by the client/engines in :mod:`IPython.parallel`,
890 It is currently only used by the client/engines in :mod:`IPython.parallel`,
882 where engines may publish *data* to the Client,
891 where engines may publish *data* to the Client,
883 of which the Client can then publish *representations* via ``display_data``
892 of which the Client can then publish *representations* via ``display_data``
884 to various frontends.
893 to various frontends.
885
894
886 Code inputs
895 Code inputs
887 -----------
896 -----------
888
897
889 To let all frontends know what code is being executed at any given time, these
898 To let all frontends know what code is being executed at any given time, these
890 messages contain a re-broadcast of the ``code`` portion of an
899 messages contain a re-broadcast of the ``code`` portion of an
891 :ref:`execute_request <execute>`, along with the :ref:`execution_count
900 :ref:`execute_request <execute>`, along with the :ref:`execution_count
892 <execution_counter>`.
901 <execution_counter>`.
893
902
894 Message type: ``execute_input``::
903 Message type: ``execute_input``::
895
904
896 content = {
905 content = {
897 'code' : str, # Source code to be executed, one or more lines
906 'code' : str, # Source code to be executed, one or more lines
898
907
899 # The counter for this execution is also provided so that clients can
908 # The counter for this execution is also provided so that clients can
900 # display it, since IPython automatically creates variables called _iN
909 # display it, since IPython automatically creates variables called _iN
901 # (for input prompt In[N]).
910 # (for input prompt In[N]).
902 'execution_count' : int
911 'execution_count' : int
903 }
912 }
904
913
905 .. versionchanged:: 5.0
914 .. versionchanged:: 5.0
906
915
907 ``pyin`` is renamed to ``execute_input``.
916 ``pyin`` is renamed to ``execute_input``.
908
917
909
918
910 Execution results
919 Execution results
911 -----------------
920 -----------------
912
921
913 Results of an execution are published as an ``execute_result``.
922 Results of an execution are published as an ``execute_result``.
914 These are identical to `display_data`_ messages, with the addition of an ``execution_count`` key.
923 These are identical to `display_data`_ messages, with the addition of an ``execution_count`` key.
915
924
916 Results can have multiple simultaneous formats depending on its
925 Results can have multiple simultaneous formats depending on its
917 configuration. A plain text representation should always be provided
926 configuration. A plain text representation should always be provided
918 in the ``text/plain`` mime-type. Frontends are free to display any or all of these
927 in the ``text/plain`` mime-type. Frontends are free to display any or all of these
919 according to its capabilities.
928 according to its capabilities.
920 Frontends should ignore mime-types they do not understand. The data itself is
929 Frontends should ignore mime-types they do not understand. The data itself is
921 any JSON object and depends on the format. It is often, but not always a string.
930 any JSON object and depends on the format. It is often, but not always a string.
922
931
923 Message type: ``execute_result``::
932 Message type: ``execute_result``::
924
933
925 content = {
934 content = {
926
935
927 # The counter for this execution is also provided so that clients can
936 # The counter for this execution is also provided so that clients can
928 # display it, since IPython automatically creates variables called _N
937 # display it, since IPython automatically creates variables called _N
929 # (for prompt N).
938 # (for prompt N).
930 'execution_count' : int,
939 'execution_count' : int,
931
940
932 # data and metadata are identical to a display_data message.
941 # data and metadata are identical to a display_data message.
933 # the object being displayed is that passed to the display hook,
942 # the object being displayed is that passed to the display hook,
934 # i.e. the *result* of the execution.
943 # i.e. the *result* of the execution.
935 'data' : dict,
944 'data' : dict,
936 'metadata' : dict,
945 'metadata' : dict,
937 }
946 }
938
947
939 Execution errors
948 Execution errors
940 ----------------
949 ----------------
941
950
942 When an error occurs during code execution
951 When an error occurs during code execution
943
952
944 Message type: ``error``::
953 Message type: ``error``::
945
954
946 content = {
955 content = {
947 # Similar content to the execute_reply messages for the 'error' case,
956 # Similar content to the execute_reply messages for the 'error' case,
948 # except the 'status' field is omitted.
957 # except the 'status' field is omitted.
949 }
958 }
950
959
951 .. versionchanged:: 5.0
960 .. versionchanged:: 5.0
952
961
953 ``pyerr`` renamed to ``error``
962 ``pyerr`` renamed to ``error``
954
963
955 Kernel status
964 Kernel status
956 -------------
965 -------------
957
966
958 This message type is used by frontends to monitor the status of the kernel.
967 This message type is used by frontends to monitor the status of the kernel.
959
968
960 Message type: ``status``::
969 Message type: ``status``::
961
970
962 content = {
971 content = {
963 # When the kernel starts to handle a message, it will enter the 'busy'
972 # When the kernel starts to handle a message, it will enter the 'busy'
964 # state and when it finishes, it will enter the 'idle' state.
973 # state and when it finishes, it will enter the 'idle' state.
965 # The kernel will publish state 'starting' exactly once at process startup.
974 # The kernel will publish state 'starting' exactly once at process startup.
966 execution_state : ('busy', 'idle', 'starting')
975 execution_state : ('busy', 'idle', 'starting')
967 }
976 }
968
977
969 .. versionchanged:: 5.0
978 .. versionchanged:: 5.0
970
979
971 Busy and idle messages should be sent before/after handling every message,
980 Busy and idle messages should be sent before/after handling every message,
972 not just execution.
981 not just execution.
973
982
974 Clear output
983 Clear output
975 ------------
984 ------------
976
985
977 This message type is used to clear the output that is visible on the frontend.
986 This message type is used to clear the output that is visible on the frontend.
978
987
979 Message type: ``clear_output``::
988 Message type: ``clear_output``::
980
989
981 content = {
990 content = {
982
991
983 # Wait to clear the output until new output is available. Clears the
992 # Wait to clear the output until new output is available. Clears the
984 # existing output immediately before the new output is displayed.
993 # existing output immediately before the new output is displayed.
985 # Useful for creating simple animations with minimal flickering.
994 # Useful for creating simple animations with minimal flickering.
986 'wait' : bool,
995 'wait' : bool,
987 }
996 }
988
997
989 .. versionchanged:: 4.1
998 .. versionchanged:: 4.1
990
999
991 ``stdout``, ``stderr``, and ``display`` boolean keys for selective clearing are removed,
1000 ``stdout``, ``stderr``, and ``display`` boolean keys for selective clearing are removed,
992 and ``wait`` is added.
1001 and ``wait`` is added.
993 The selective clearing keys are ignored in v4 and the default behavior remains the same,
1002 The selective clearing keys are ignored in v4 and the default behavior remains the same,
994 so v4 clear_output messages will be safely handled by a v4.1 frontend.
1003 so v4 clear_output messages will be safely handled by a v4.1 frontend.
995
1004
996
1005
997 Messages on the stdin ROUTER/DEALER sockets
1006 Messages on the stdin ROUTER/DEALER sockets
998 ===========================================
1007 ===========================================
999
1008
1000 This is a socket where the request/reply pattern goes in the opposite direction:
1009 This is a socket where the request/reply pattern goes in the opposite direction:
1001 from the kernel to a *single* frontend, and its purpose is to allow
1010 from the kernel to a *single* frontend, and its purpose is to allow
1002 ``raw_input`` and similar operations that read from ``sys.stdin`` on the kernel
1011 ``raw_input`` and similar operations that read from ``sys.stdin`` on the kernel
1003 to be fulfilled by the client. The request should be made to the frontend that
1012 to be fulfilled by the client. The request should be made to the frontend that
1004 made the execution request that prompted ``raw_input`` to be called. For now we
1013 made the execution request that prompted ``raw_input`` to be called. For now we
1005 will keep these messages as simple as possible, since they only mean to convey
1014 will keep these messages as simple as possible, since they only mean to convey
1006 the ``raw_input(prompt)`` call.
1015 the ``raw_input(prompt)`` call.
1007
1016
1008 Message type: ``input_request``::
1017 Message type: ``input_request``::
1009
1018
1010 content = {
1019 content = {
1011 # the text to show at the prompt
1020 # the text to show at the prompt
1012 'prompt' : str,
1021 'prompt' : str,
1013 # Is the request for a password?
1022 # Is the request for a password?
1014 # If so, the frontend shouldn't echo input.
1023 # If so, the frontend shouldn't echo input.
1015 'password' : bool
1024 'password' : bool
1016 }
1025 }
1017
1026
1018 Message type: ``input_reply``::
1027 Message type: ``input_reply``::
1019
1028
1020 content = { 'value' : str }
1029 content = { 'value' : str }
1021
1030
1022
1031
1023 When ``password`` is True, the frontend should not echo the input as it is entered.
1032 When ``password`` is True, the frontend should not echo the input as it is entered.
1024
1033
1025 .. versionchanged:: 5.0
1034 .. versionchanged:: 5.0
1026
1035
1027 ``password`` key added.
1036 ``password`` key added.
1028
1037
1029 .. note::
1038 .. note::
1030
1039
1031 The stdin socket of the client is required to have the same zmq IDENTITY
1040 The stdin socket of the client is required to have the same zmq IDENTITY
1032 as the client's shell socket.
1041 as the client's shell socket.
1033 Because of this, the ``input_request`` must be sent with the same IDENTITY
1042 Because of this, the ``input_request`` must be sent with the same IDENTITY
1034 routing prefix as the ``execute_reply`` in order for the frontend to receive
1043 routing prefix as the ``execute_reply`` in order for the frontend to receive
1035 the message.
1044 the message.
1036
1045
1037 .. note::
1046 .. note::
1038
1047
1039 We do not explicitly try to forward the raw ``sys.stdin`` object, because in
1048 We do not explicitly try to forward the raw ``sys.stdin`` object, because in
1040 practice the kernel should behave like an interactive program. When a
1049 practice the kernel should behave like an interactive program. When a
1041 program is opened on the console, the keyboard effectively takes over the
1050 program is opened on the console, the keyboard effectively takes over the
1042 ``stdin`` file descriptor, and it can't be used for raw reading anymore.
1051 ``stdin`` file descriptor, and it can't be used for raw reading anymore.
1043 Since the IPython kernel effectively behaves like a console program (albeit
1052 Since the IPython kernel effectively behaves like a console program (albeit
1044 one whose "keyboard" is actually living in a separate process and
1053 one whose "keyboard" is actually living in a separate process and
1045 transported over the zmq connection), raw ``stdin`` isn't expected to be
1054 transported over the zmq connection), raw ``stdin`` isn't expected to be
1046 available.
1055 available.
1047
1056
1048 .. _kernel_heartbeat:
1057 .. _kernel_heartbeat:
1049
1058
1050 Heartbeat for kernels
1059 Heartbeat for kernels
1051 =====================
1060 =====================
1052
1061
1053 Clients send ping messages on a REQ socket, which are echoed right back
1062 Clients send ping messages on a REQ socket, which are echoed right back
1054 from the Kernel's REP socket. These are simple bytestrings, not full JSON messages described above.
1063 from the Kernel's REP socket. These are simple bytestrings, not full JSON messages described above.
1055
1064
1056
1065
1057 Custom Messages
1066 Custom Messages
1058 ===============
1067 ===============
1059
1068
1060 .. versionadded:: 4.1
1069 .. versionadded:: 4.1
1061
1070
1062 IPython 2.0 (msgspec v4.1) adds a messaging system for developers to add their own objects with Frontend
1071 IPython 2.0 (msgspec v4.1) adds a messaging system for developers to add their own objects with Frontend
1063 and Kernel-side components, and allow them to communicate with each other.
1072 and Kernel-side components, and allow them to communicate with each other.
1064 To do this, IPython adds a notion of a ``Comm``, which exists on both sides,
1073 To do this, IPython adds a notion of a ``Comm``, which exists on both sides,
1065 and can communicate in either direction.
1074 and can communicate in either direction.
1066
1075
1067 These messages are fully symmetrical - both the Kernel and the Frontend can send each message,
1076 These messages are fully symmetrical - both the Kernel and the Frontend can send each message,
1068 and no messages expect a reply.
1077 and no messages expect a reply.
1069 The Kernel listens for these messages on the Shell channel,
1078 The Kernel listens for these messages on the Shell channel,
1070 and the Frontend listens for them on the IOPub channel.
1079 and the Frontend listens for them on the IOPub channel.
1071
1080
1072 Opening a Comm
1081 Opening a Comm
1073 --------------
1082 --------------
1074
1083
1075 Opening a Comm produces a ``comm_open`` message, to be sent to the other side::
1084 Opening a Comm produces a ``comm_open`` message, to be sent to the other side::
1076
1085
1077 {
1086 {
1078 'comm_id' : 'u-u-i-d',
1087 'comm_id' : 'u-u-i-d',
1079 'target_name' : 'my_comm',
1088 'target_name' : 'my_comm',
1080 'data' : {}
1089 'data' : {}
1081 }
1090 }
1082
1091
1083 Every Comm has an ID and a target name.
1092 Every Comm has an ID and a target name.
1084 The code handling the message on the receiving side is responsible for maintaining a mapping
1093 The code handling the message on the receiving side is responsible for maintaining a mapping
1085 of target_name keys to constructors.
1094 of target_name keys to constructors.
1086 After a ``comm_open`` message has been sent,
1095 After a ``comm_open`` message has been sent,
1087 there should be a corresponding Comm instance on both sides.
1096 there should be a corresponding Comm instance on both sides.
1088 The ``data`` key is always a dict and can be any extra JSON information used in initialization of the comm.
1097 The ``data`` key is always a dict and can be any extra JSON information used in initialization of the comm.
1089
1098
1090 If the ``target_name`` key is not found on the receiving side,
1099 If the ``target_name`` key is not found on the receiving side,
1091 then it should immediately reply with a ``comm_close`` message to avoid an inconsistent state.
1100 then it should immediately reply with a ``comm_close`` message to avoid an inconsistent state.
1092
1101
1093 Comm Messages
1102 Comm Messages
1094 -------------
1103 -------------
1095
1104
1096 Comm messages are one-way communications to update comm state,
1105 Comm messages are one-way communications to update comm state,
1097 used for synchronizing widget state, or simply requesting actions of a comm's counterpart.
1106 used for synchronizing widget state, or simply requesting actions of a comm's counterpart.
1098
1107
1099 Essentially, each comm pair defines their own message specification implemented inside the ``data`` dict.
1108 Essentially, each comm pair defines their own message specification implemented inside the ``data`` dict.
1100
1109
1101 There are no expected replies (of course, one side can send another ``comm_msg`` in reply).
1110 There are no expected replies (of course, one side can send another ``comm_msg`` in reply).
1102
1111
1103 Message type: ``comm_msg``::
1112 Message type: ``comm_msg``::
1104
1113
1105 {
1114 {
1106 'comm_id' : 'u-u-i-d',
1115 'comm_id' : 'u-u-i-d',
1107 'data' : {}
1116 'data' : {}
1108 }
1117 }
1109
1118
1110 Tearing Down Comms
1119 Tearing Down Comms
1111 ------------------
1120 ------------------
1112
1121
1113 Since comms live on both sides, when a comm is destroyed the other side must be notified.
1122 Since comms live on both sides, when a comm is destroyed the other side must be notified.
1114 This is done with a ``comm_close`` message.
1123 This is done with a ``comm_close`` message.
1115
1124
1116 Message type: ``comm_close``::
1125 Message type: ``comm_close``::
1117
1126
1118 {
1127 {
1119 'comm_id' : 'u-u-i-d',
1128 'comm_id' : 'u-u-i-d',
1120 'data' : {}
1129 'data' : {}
1121 }
1130 }
1122
1131
1123 Output Side Effects
1132 Output Side Effects
1124 -------------------
1133 -------------------
1125
1134
1126 Since comm messages can execute arbitrary user code,
1135 Since comm messages can execute arbitrary user code,
1127 handlers should set the parent header and publish status busy / idle,
1136 handlers should set the parent header and publish status busy / idle,
1128 just like an execute request.
1137 just like an execute request.
1129
1138
1130
1139
1131 To Do
1140 To Do
1132 =====
1141 =====
1133
1142
1134 Missing things include:
1143 Missing things include:
1135
1144
1136 * Important: finish thinking through the payload concept and API.
1145 * Important: finish thinking through the payload concept and API.
1137
1146
1138 .. include:: ../links.txt
1147 .. include:: ../links.txt
General Comments 0
You need to be logged in to leave comments. Login now