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