##// END OF EJS Templates
Merge pull request #6793 from takluyver/langinfo...
Thomas Kluyver -
r18545:dbb3d415 merge
parent child Browse files
Show More
@@ -1,2700 +1,2707 b''
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/textcell',
10 10 'notebook/js/codecell',
11 11 'services/sessions/session',
12 12 'notebook/js/celltoolbar',
13 13 'components/marked/lib/marked',
14 14 'highlight',
15 15 'notebook/js/mathjaxutils',
16 16 'base/js/keyboard',
17 17 'notebook/js/tooltip',
18 18 'notebook/js/celltoolbarpresets/default',
19 19 'notebook/js/celltoolbarpresets/rawcell',
20 20 'notebook/js/celltoolbarpresets/slideshow',
21 21 'notebook/js/scrollmanager'
22 22 ], function (
23 23 IPython,
24 24 $,
25 25 utils,
26 26 dialog,
27 27 textcell,
28 28 codecell,
29 29 session,
30 30 celltoolbar,
31 31 marked,
32 32 hljs,
33 33 mathjaxutils,
34 34 keyboard,
35 35 tooltip,
36 36 default_celltoolbar,
37 37 rawcell_celltoolbar,
38 38 slideshow_celltoolbar,
39 39 scrollmanager
40 40 ) {
41 41
42 42 var Notebook = function (selector, options) {
43 43 // Constructor
44 44 //
45 45 // A notebook contains and manages cells.
46 46 //
47 47 // Parameters:
48 48 // selector: string
49 49 // options: dictionary
50 50 // Dictionary of keyword arguments.
51 51 // events: $(Events) instance
52 52 // keyboard_manager: KeyboardManager instance
53 53 // save_widget: SaveWidget instance
54 54 // config: dictionary
55 55 // base_url : string
56 56 // notebook_path : string
57 57 // notebook_name : string
58 58 this.config = utils.mergeopt(Notebook, options.config);
59 59 this.base_url = options.base_url;
60 60 this.notebook_path = options.notebook_path;
61 61 this.notebook_name = options.notebook_name;
62 62 this.events = options.events;
63 63 this.keyboard_manager = options.keyboard_manager;
64 64 this.save_widget = options.save_widget;
65 65 this.tooltip = new tooltip.Tooltip(this.events);
66 66 this.ws_url = options.ws_url;
67 67 this._session_starting = false;
68 68 this.default_cell_type = this.config.default_cell_type || 'code';
69 69
70 70 // Create default scroll manager.
71 71 this.scroll_manager = new scrollmanager.ScrollManager(this);
72 72
73 73 // TODO: This code smells (and the other `= this` line a couple lines down)
74 74 // We need a better way to deal with circular instance references.
75 75 this.keyboard_manager.notebook = this;
76 76 this.save_widget.notebook = this;
77 77
78 78 mathjaxutils.init();
79 79
80 80 if (marked) {
81 81 marked.setOptions({
82 82 gfm : true,
83 83 tables: true,
84 84 langPrefix: "language-",
85 85 highlight: function(code, lang) {
86 86 if (!lang) {
87 87 // no language, no highlight
88 88 return code;
89 89 }
90 90 var highlighted;
91 91 try {
92 92 highlighted = hljs.highlight(lang, code, false);
93 93 } catch(err) {
94 94 highlighted = hljs.highlightAuto(code);
95 95 }
96 96 return highlighted.value;
97 97 }
98 98 });
99 99 }
100 100
101 101 this.element = $(selector);
102 102 this.element.scroll();
103 103 this.element.data("notebook", this);
104 104 this.next_prompt_number = 1;
105 105 this.session = null;
106 106 this.kernel = null;
107 107 this.clipboard = null;
108 108 this.undelete_backup = null;
109 109 this.undelete_index = null;
110 110 this.undelete_below = false;
111 111 this.paste_enabled = false;
112 112 // It is important to start out in command mode to match the intial mode
113 113 // of the KeyboardManager.
114 114 this.mode = 'command';
115 115 this.set_dirty(false);
116 116 this.metadata = {};
117 117 this._checkpoint_after_save = false;
118 118 this.last_checkpoint = null;
119 119 this.checkpoints = [];
120 120 this.autosave_interval = 0;
121 121 this.autosave_timer = null;
122 122 // autosave *at most* every two minutes
123 123 this.minimum_autosave_interval = 120000;
124 124 // single worksheet for now
125 125 this.worksheet_metadata = {};
126 126 this.notebook_name_blacklist_re = /[\/\\:]/;
127 127 this.nbformat = 3; // Increment this when changing the nbformat
128 128 this.nbformat_minor = 0; // Increment this when changing the nbformat
129 129 this.codemirror_mode = 'ipython';
130 130 this.create_elements();
131 131 this.bind_events();
132 132 this.save_notebook = function() { // don't allow save until notebook_loaded
133 133 this.save_notebook_error(null, null, "Load failed, save is disabled");
134 134 };
135 135
136 136 // Trigger cell toolbar registration.
137 137 default_celltoolbar.register(this);
138 138 rawcell_celltoolbar.register(this);
139 139 slideshow_celltoolbar.register(this);
140 140 };
141 141
142 142 Notebook.options_default = {
143 143 // can be any cell type, or the special values of
144 144 // 'above', 'below', or 'selected' to get the value from another cell.
145 145 Notebook: {
146 146 default_cell_type: 'code',
147 147 }
148 148 };
149 149
150 150
151 151 /**
152 152 * Create an HTML and CSS representation of the notebook.
153 153 *
154 154 * @method create_elements
155 155 */
156 156 Notebook.prototype.create_elements = function () {
157 157 var that = this;
158 158 this.element.attr('tabindex','-1');
159 159 this.container = $("<div/>").addClass("container").attr("id", "notebook-container");
160 160 // We add this end_space div to the end of the notebook div to:
161 161 // i) provide a margin between the last cell and the end of the notebook
162 162 // ii) to prevent the div from scrolling up when the last cell is being
163 163 // edited, but is too low on the page, which browsers will do automatically.
164 164 var end_space = $('<div/>').addClass('end_space');
165 165 end_space.dblclick(function (e) {
166 166 var ncells = that.ncells();
167 167 that.insert_cell_below('code',ncells-1);
168 168 });
169 169 this.element.append(this.container);
170 170 this.container.append(end_space);
171 171 };
172 172
173 173 /**
174 174 * Bind JavaScript events: key presses and custom IPython events.
175 175 *
176 176 * @method bind_events
177 177 */
178 178 Notebook.prototype.bind_events = function () {
179 179 var that = this;
180 180
181 181 this.events.on('set_next_input.Notebook', function (event, data) {
182 182 var index = that.find_cell_index(data.cell);
183 183 var new_cell = that.insert_cell_below('code',index);
184 184 new_cell.set_text(data.text);
185 185 that.dirty = true;
186 186 });
187 187
188 188 this.events.on('set_dirty.Notebook', function (event, data) {
189 189 that.dirty = data.value;
190 190 });
191 191
192 192 this.events.on('trust_changed.Notebook', function (event, trusted) {
193 193 that.trusted = trusted;
194 194 });
195 195
196 196 this.events.on('select.Cell', function (event, data) {
197 197 var index = that.find_cell_index(data.cell);
198 198 that.select(index);
199 199 });
200 200
201 201 this.events.on('edit_mode.Cell', function (event, data) {
202 202 that.handle_edit_mode(data.cell);
203 203 });
204 204
205 205 this.events.on('command_mode.Cell', function (event, data) {
206 206 that.handle_command_mode(data.cell);
207 207 });
208 208
209 209 this.events.on('spec_changed.Kernel', function(event, data) {
210 that.set_kernelspec_metadata(data);
210 that.metadata.kernelspec =
211 {name: data.name, display_name: data.display_name};
212 });
213
214 this.events.on('kernel_ready.Kernel', function(event, data) {
215 var kinfo = data.kernel.info_reply
216 var langinfo = kinfo.language_info || {};
217 if (!langinfo.name) langinfo.name = kinfo.language;
218
219 that.metadata.language_info = langinfo;
211 220 // Mode 'null' should be plain, unhighlighted text.
212 cm_mode = data.codemirror_mode || data.language || 'null'
221 var cm_mode = langinfo.codemirror_mode || langinfo.language || 'null'
213 222 that.set_codemirror_mode(cm_mode);
214 223 });
215 224
216 225 var collapse_time = function (time) {
217 226 var app_height = $('#ipython-main-app').height(); // content height
218 227 var splitter_height = $('div#pager_splitter').outerHeight(true);
219 228 var new_height = app_height - splitter_height;
220 229 that.element.animate({height : new_height + 'px'}, time);
221 230 };
222 231
223 232 this.element.bind('collapse_pager', function (event, extrap) {
224 233 var time = (extrap !== undefined) ? ((extrap.duration !== undefined ) ? extrap.duration : 'fast') : 'fast';
225 234 collapse_time(time);
226 235 });
227 236
228 237 var expand_time = function (time) {
229 238 var app_height = $('#ipython-main-app').height(); // content height
230 239 var splitter_height = $('div#pager_splitter').outerHeight(true);
231 240 var pager_height = $('div#pager').outerHeight(true);
232 241 var new_height = app_height - pager_height - splitter_height;
233 242 that.element.animate({height : new_height + 'px'}, time);
234 243 };
235 244
236 245 this.element.bind('expand_pager', function (event, extrap) {
237 246 var time = (extrap !== undefined) ? ((extrap.duration !== undefined ) ? extrap.duration : 'fast') : 'fast';
238 247 expand_time(time);
239 248 });
240 249
241 250 // Firefox 22 broke $(window).on("beforeunload")
242 251 // I'm not sure why or how.
243 252 window.onbeforeunload = function (e) {
244 253 // TODO: Make killing the kernel configurable.
245 254 var kill_kernel = false;
246 255 if (kill_kernel) {
247 256 that.session.delete();
248 257 }
249 258 // if we are autosaving, trigger an autosave on nav-away.
250 259 // still warn, because if we don't the autosave may fail.
251 260 if (that.dirty) {
252 261 if ( that.autosave_interval ) {
253 262 // schedule autosave in a timeout
254 263 // this gives you a chance to forcefully discard changes
255 264 // by reloading the page if you *really* want to.
256 265 // the timer doesn't start until you *dismiss* the dialog.
257 266 setTimeout(function () {
258 267 if (that.dirty) {
259 268 that.save_notebook();
260 269 }
261 270 }, 1000);
262 271 return "Autosave in progress, latest changes may be lost.";
263 272 } else {
264 273 return "Unsaved changes will be lost.";
265 274 }
266 275 }
267 276 // Null is the *only* return value that will make the browser not
268 277 // pop up the "don't leave" dialog.
269 278 return null;
270 279 };
271 280 };
272 281
273 282 /**
274 283 * Set the dirty flag, and trigger the set_dirty.Notebook event
275 284 *
276 285 * @method set_dirty
277 286 */
278 287 Notebook.prototype.set_dirty = function (value) {
279 288 if (value === undefined) {
280 289 value = true;
281 290 }
282 291 if (this.dirty == value) {
283 292 return;
284 293 }
285 294 this.events.trigger('set_dirty.Notebook', {value: value});
286 295 };
287 296
288 297 /**
289 298 * Scroll the top of the page to a given cell.
290 299 *
291 300 * @method scroll_to_cell
292 301 * @param {Number} cell_number An index of the cell to view
293 302 * @param {Number} time Animation time in milliseconds
294 303 * @return {Number} Pixel offset from the top of the container
295 304 */
296 305 Notebook.prototype.scroll_to_cell = function (cell_number, time) {
297 306 var cells = this.get_cells();
298 307 time = time || 0;
299 308 cell_number = Math.min(cells.length-1,cell_number);
300 309 cell_number = Math.max(0 ,cell_number);
301 310 var scroll_value = cells[cell_number].element.position().top-cells[0].element.position().top ;
302 311 this.element.animate({scrollTop:scroll_value}, time);
303 312 return scroll_value;
304 313 };
305 314
306 315 /**
307 316 * Scroll to the bottom of the page.
308 317 *
309 318 * @method scroll_to_bottom
310 319 */
311 320 Notebook.prototype.scroll_to_bottom = function () {
312 321 this.element.animate({scrollTop:this.element.get(0).scrollHeight}, 0);
313 322 };
314 323
315 324 /**
316 325 * Scroll to the top of the page.
317 326 *
318 327 * @method scroll_to_top
319 328 */
320 329 Notebook.prototype.scroll_to_top = function () {
321 330 this.element.animate({scrollTop:0}, 0);
322 331 };
323 332
324 333 // Edit Notebook metadata
325 334
326 335 Notebook.prototype.edit_metadata = function () {
327 336 var that = this;
328 337 dialog.edit_metadata({
329 338 md: this.metadata,
330 339 callback: function (md) {
331 340 that.metadata = md;
332 341 },
333 342 name: 'Notebook',
334 343 notebook: this,
335 344 keyboard_manager: this.keyboard_manager});
336 345 };
337 346
338 Notebook.prototype.set_kernelspec_metadata = function(ks) {
339 var tostore = {};
340 $.map(ks, function(value, field) {
341 if (field !== 'argv' && field !== 'env') {
342 tostore[field] = value;
343 }
344 });
345 this.metadata.kernelspec = tostore;
346 }
347
348 347 // Cell indexing, retrieval, etc.
349 348
350 349 /**
351 350 * Get all cell elements in the notebook.
352 351 *
353 352 * @method get_cell_elements
354 353 * @return {jQuery} A selector of all cell elements
355 354 */
356 355 Notebook.prototype.get_cell_elements = function () {
357 356 return this.container.children("div.cell");
358 357 };
359 358
360 359 /**
361 360 * Get a particular cell element.
362 361 *
363 362 * @method get_cell_element
364 363 * @param {Number} index An index of a cell to select
365 364 * @return {jQuery} A selector of the given cell.
366 365 */
367 366 Notebook.prototype.get_cell_element = function (index) {
368 367 var result = null;
369 368 var e = this.get_cell_elements().eq(index);
370 369 if (e.length !== 0) {
371 370 result = e;
372 371 }
373 372 return result;
374 373 };
375 374
376 375 /**
377 376 * Try to get a particular cell by msg_id.
378 377 *
379 378 * @method get_msg_cell
380 379 * @param {String} msg_id A message UUID
381 380 * @return {Cell} Cell or null if no cell was found.
382 381 */
383 382 Notebook.prototype.get_msg_cell = function (msg_id) {
384 383 return codecell.CodeCell.msg_cells[msg_id] || null;
385 384 };
386 385
387 386 /**
388 387 * Count the cells in this notebook.
389 388 *
390 389 * @method ncells
391 390 * @return {Number} The number of cells in this notebook
392 391 */
393 392 Notebook.prototype.ncells = function () {
394 393 return this.get_cell_elements().length;
395 394 };
396 395
397 396 /**
398 397 * Get all Cell objects in this notebook.
399 398 *
400 399 * @method get_cells
401 400 * @return {Array} This notebook's Cell objects
402 401 */
403 402 // TODO: we are often calling cells as cells()[i], which we should optimize
404 403 // to cells(i) or a new method.
405 404 Notebook.prototype.get_cells = function () {
406 405 return this.get_cell_elements().toArray().map(function (e) {
407 406 return $(e).data("cell");
408 407 });
409 408 };
410 409
411 410 /**
412 411 * Get a Cell object from this notebook.
413 412 *
414 413 * @method get_cell
415 414 * @param {Number} index An index of a cell to retrieve
416 415 * @return {Cell} Cell or null if no cell was found.
417 416 */
418 417 Notebook.prototype.get_cell = function (index) {
419 418 var result = null;
420 419 var ce = this.get_cell_element(index);
421 420 if (ce !== null) {
422 421 result = ce.data('cell');
423 422 }
424 423 return result;
425 424 };
426 425
427 426 /**
428 427 * Get the cell below a given cell.
429 428 *
430 429 * @method get_next_cell
431 430 * @param {Cell} cell The provided cell
432 431 * @return {Cell} the next cell or null if no cell was found.
433 432 */
434 433 Notebook.prototype.get_next_cell = function (cell) {
435 434 var result = null;
436 435 var index = this.find_cell_index(cell);
437 436 if (this.is_valid_cell_index(index+1)) {
438 437 result = this.get_cell(index+1);
439 438 }
440 439 return result;
441 440 };
442 441
443 442 /**
444 443 * Get the cell above a given cell.
445 444 *
446 445 * @method get_prev_cell
447 446 * @param {Cell} cell The provided cell
448 447 * @return {Cell} The previous cell or null if no cell was found.
449 448 */
450 449 Notebook.prototype.get_prev_cell = function (cell) {
451 450 var result = null;
452 451 var index = this.find_cell_index(cell);
453 452 if (index !== null && index > 0) {
454 453 result = this.get_cell(index-1);
455 454 }
456 455 return result;
457 456 };
458 457
459 458 /**
460 459 * Get the numeric index of a given cell.
461 460 *
462 461 * @method find_cell_index
463 462 * @param {Cell} cell The provided cell
464 463 * @return {Number} The cell's numeric index or null if no cell was found.
465 464 */
466 465 Notebook.prototype.find_cell_index = function (cell) {
467 466 var result = null;
468 467 this.get_cell_elements().filter(function (index) {
469 468 if ($(this).data("cell") === cell) {
470 469 result = index;
471 470 }
472 471 });
473 472 return result;
474 473 };
475 474
476 475 /**
477 476 * Get a given index , or the selected index if none is provided.
478 477 *
479 478 * @method index_or_selected
480 479 * @param {Number} index A cell's index
481 480 * @return {Number} The given index, or selected index if none is provided.
482 481 */
483 482 Notebook.prototype.index_or_selected = function (index) {
484 483 var i;
485 484 if (index === undefined || index === null) {
486 485 i = this.get_selected_index();
487 486 if (i === null) {
488 487 i = 0;
489 488 }
490 489 } else {
491 490 i = index;
492 491 }
493 492 return i;
494 493 };
495 494
496 495 /**
497 496 * Get the currently selected cell.
498 497 * @method get_selected_cell
499 498 * @return {Cell} The selected cell
500 499 */
501 500 Notebook.prototype.get_selected_cell = function () {
502 501 var index = this.get_selected_index();
503 502 return this.get_cell(index);
504 503 };
505 504
506 505 /**
507 506 * Check whether a cell index is valid.
508 507 *
509 508 * @method is_valid_cell_index
510 509 * @param {Number} index A cell index
511 510 * @return True if the index is valid, false otherwise
512 511 */
513 512 Notebook.prototype.is_valid_cell_index = function (index) {
514 513 if (index !== null && index >= 0 && index < this.ncells()) {
515 514 return true;
516 515 } else {
517 516 return false;
518 517 }
519 518 };
520 519
521 520 /**
522 521 * Get the index of the currently selected cell.
523 522
524 523 * @method get_selected_index
525 524 * @return {Number} The selected cell's numeric index
526 525 */
527 526 Notebook.prototype.get_selected_index = function () {
528 527 var result = null;
529 528 this.get_cell_elements().filter(function (index) {
530 529 if ($(this).data("cell").selected === true) {
531 530 result = index;
532 531 }
533 532 });
534 533 return result;
535 534 };
536 535
537 536
538 537 // Cell selection.
539 538
540 539 /**
541 540 * Programmatically select a cell.
542 541 *
543 542 * @method select
544 543 * @param {Number} index A cell's index
545 544 * @return {Notebook} This notebook
546 545 */
547 546 Notebook.prototype.select = function (index) {
548 547 if (this.is_valid_cell_index(index)) {
549 548 var sindex = this.get_selected_index();
550 549 if (sindex !== null && index !== sindex) {
551 550 // If we are about to select a different cell, make sure we are
552 551 // first in command mode.
553 552 if (this.mode !== 'command') {
554 553 this.command_mode();
555 554 }
556 555 this.get_cell(sindex).unselect();
557 556 }
558 557 var cell = this.get_cell(index);
559 558 cell.select();
560 559 if (cell.cell_type === 'heading') {
561 560 this.events.trigger('selected_cell_type_changed.Notebook',
562 561 {'cell_type':cell.cell_type,level:cell.level}
563 562 );
564 563 } else {
565 564 this.events.trigger('selected_cell_type_changed.Notebook',
566 565 {'cell_type':cell.cell_type}
567 566 );
568 567 }
569 568 }
570 569 return this;
571 570 };
572 571
573 572 /**
574 573 * Programmatically select the next cell.
575 574 *
576 575 * @method select_next
577 576 * @return {Notebook} This notebook
578 577 */
579 578 Notebook.prototype.select_next = function () {
580 579 var index = this.get_selected_index();
581 580 this.select(index+1);
582 581 return this;
583 582 };
584 583
585 584 /**
586 585 * Programmatically select the previous cell.
587 586 *
588 587 * @method select_prev
589 588 * @return {Notebook} This notebook
590 589 */
591 590 Notebook.prototype.select_prev = function () {
592 591 var index = this.get_selected_index();
593 592 this.select(index-1);
594 593 return this;
595 594 };
596 595
597 596
598 597 // Edit/Command mode
599 598
600 599 /**
601 600 * Gets the index of the cell that is in edit mode.
602 601 *
603 602 * @method get_edit_index
604 603 *
605 604 * @return index {int}
606 605 **/
607 606 Notebook.prototype.get_edit_index = function () {
608 607 var result = null;
609 608 this.get_cell_elements().filter(function (index) {
610 609 if ($(this).data("cell").mode === 'edit') {
611 610 result = index;
612 611 }
613 612 });
614 613 return result;
615 614 };
616 615
617 616 /**
618 617 * Handle when a a cell blurs and the notebook should enter command mode.
619 618 *
620 619 * @method handle_command_mode
621 620 * @param [cell] {Cell} Cell to enter command mode on.
622 621 **/
623 622 Notebook.prototype.handle_command_mode = function (cell) {
624 623 if (this.mode !== 'command') {
625 624 cell.command_mode();
626 625 this.mode = 'command';
627 626 this.events.trigger('command_mode.Notebook');
628 627 this.keyboard_manager.command_mode();
629 628 }
630 629 };
631 630
632 631 /**
633 632 * Make the notebook enter command mode.
634 633 *
635 634 * @method command_mode
636 635 **/
637 636 Notebook.prototype.command_mode = function () {
638 637 var cell = this.get_cell(this.get_edit_index());
639 638 if (cell && this.mode !== 'command') {
640 639 // We don't call cell.command_mode, but rather call cell.focus_cell()
641 640 // which will blur and CM editor and trigger the call to
642 641 // handle_command_mode.
643 642 cell.focus_cell();
644 643 }
645 644 };
646 645
647 646 /**
648 647 * Handle when a cell fires it's edit_mode event.
649 648 *
650 649 * @method handle_edit_mode
651 650 * @param [cell] {Cell} Cell to enter edit mode on.
652 651 **/
653 652 Notebook.prototype.handle_edit_mode = function (cell) {
654 653 if (cell && this.mode !== 'edit') {
655 654 cell.edit_mode();
656 655 this.mode = 'edit';
657 656 this.events.trigger('edit_mode.Notebook');
658 657 this.keyboard_manager.edit_mode();
659 658 }
660 659 };
661 660
662 661 /**
663 662 * Make a cell enter edit mode.
664 663 *
665 664 * @method edit_mode
666 665 **/
667 666 Notebook.prototype.edit_mode = function () {
668 667 var cell = this.get_selected_cell();
669 668 if (cell && this.mode !== 'edit') {
670 669 cell.unrender();
671 670 cell.focus_editor();
672 671 }
673 672 };
674 673
675 674 /**
676 675 * Focus the currently selected cell.
677 676 *
678 677 * @method focus_cell
679 678 **/
680 679 Notebook.prototype.focus_cell = function () {
681 680 var cell = this.get_selected_cell();
682 681 if (cell === null) {return;} // No cell is selected
683 682 cell.focus_cell();
684 683 };
685 684
686 685 // Cell movement
687 686
688 687 /**
689 688 * Move given (or selected) cell up and select it.
690 689 *
691 690 * @method move_cell_up
692 691 * @param [index] {integer} cell index
693 692 * @return {Notebook} This notebook
694 693 **/
695 694 Notebook.prototype.move_cell_up = function (index) {
696 695 var i = this.index_or_selected(index);
697 696 if (this.is_valid_cell_index(i) && i > 0) {
698 697 var pivot = this.get_cell_element(i-1);
699 698 var tomove = this.get_cell_element(i);
700 699 if (pivot !== null && tomove !== null) {
701 700 tomove.detach();
702 701 pivot.before(tomove);
703 702 this.select(i-1);
704 703 var cell = this.get_selected_cell();
705 704 cell.focus_cell();
706 705 }
707 706 this.set_dirty(true);
708 707 }
709 708 return this;
710 709 };
711 710
712 711
713 712 /**
714 713 * Move given (or selected) cell down and select it
715 714 *
716 715 * @method move_cell_down
717 716 * @param [index] {integer} cell index
718 717 * @return {Notebook} This notebook
719 718 **/
720 719 Notebook.prototype.move_cell_down = function (index) {
721 720 var i = this.index_or_selected(index);
722 721 if (this.is_valid_cell_index(i) && this.is_valid_cell_index(i+1)) {
723 722 var pivot = this.get_cell_element(i+1);
724 723 var tomove = this.get_cell_element(i);
725 724 if (pivot !== null && tomove !== null) {
726 725 tomove.detach();
727 726 pivot.after(tomove);
728 727 this.select(i+1);
729 728 var cell = this.get_selected_cell();
730 729 cell.focus_cell();
731 730 }
732 731 }
733 732 this.set_dirty();
734 733 return this;
735 734 };
736 735
737 736
738 737 // Insertion, deletion.
739 738
740 739 /**
741 740 * Delete a cell from the notebook.
742 741 *
743 742 * @method delete_cell
744 743 * @param [index] A cell's numeric index
745 744 * @return {Notebook} This notebook
746 745 */
747 746 Notebook.prototype.delete_cell = function (index) {
748 747 var i = this.index_or_selected(index);
749 748 var cell = this.get_cell(i);
750 749 if (!cell.is_deletable()) {
751 750 return this;
752 751 }
753 752
754 753 this.undelete_backup = cell.toJSON();
755 754 $('#undelete_cell').removeClass('disabled');
756 755 if (this.is_valid_cell_index(i)) {
757 756 var old_ncells = this.ncells();
758 757 var ce = this.get_cell_element(i);
759 758 ce.remove();
760 759 if (i === 0) {
761 760 // Always make sure we have at least one cell.
762 761 if (old_ncells === 1) {
763 762 this.insert_cell_below('code');
764 763 }
765 764 this.select(0);
766 765 this.undelete_index = 0;
767 766 this.undelete_below = false;
768 767 } else if (i === old_ncells-1 && i !== 0) {
769 768 this.select(i-1);
770 769 this.undelete_index = i - 1;
771 770 this.undelete_below = true;
772 771 } else {
773 772 this.select(i);
774 773 this.undelete_index = i;
775 774 this.undelete_below = false;
776 775 }
777 776 this.events.trigger('delete.Cell', {'cell': cell, 'index': i});
778 777 this.set_dirty(true);
779 778 }
780 779 return this;
781 780 };
782 781
783 782 /**
784 783 * Restore the most recently deleted cell.
785 784 *
786 785 * @method undelete
787 786 */
788 787 Notebook.prototype.undelete_cell = function() {
789 788 if (this.undelete_backup !== null && this.undelete_index !== null) {
790 789 var current_index = this.get_selected_index();
791 790 if (this.undelete_index < current_index) {
792 791 current_index = current_index + 1;
793 792 }
794 793 if (this.undelete_index >= this.ncells()) {
795 794 this.select(this.ncells() - 1);
796 795 }
797 796 else {
798 797 this.select(this.undelete_index);
799 798 }
800 799 var cell_data = this.undelete_backup;
801 800 var new_cell = null;
802 801 if (this.undelete_below) {
803 802 new_cell = this.insert_cell_below(cell_data.cell_type);
804 803 } else {
805 804 new_cell = this.insert_cell_above(cell_data.cell_type);
806 805 }
807 806 new_cell.fromJSON(cell_data);
808 807 if (this.undelete_below) {
809 808 this.select(current_index+1);
810 809 } else {
811 810 this.select(current_index);
812 811 }
813 812 this.undelete_backup = null;
814 813 this.undelete_index = null;
815 814 }
816 815 $('#undelete_cell').addClass('disabled');
817 816 };
818 817
819 818 /**
820 819 * Insert a cell so that after insertion the cell is at given index.
821 820 *
822 821 * If cell type is not provided, it will default to the type of the
823 822 * currently active cell.
824 823 *
825 824 * Similar to insert_above, but index parameter is mandatory
826 825 *
827 826 * Index will be brought back into the accessible range [0,n]
828 827 *
829 828 * @method insert_cell_at_index
830 829 * @param [type] {string} in ['code','markdown','heading'], defaults to 'code'
831 830 * @param [index] {int} a valid index where to insert cell
832 831 *
833 832 * @return cell {cell|null} created cell or null
834 833 **/
835 834 Notebook.prototype.insert_cell_at_index = function(type, index){
836 835
837 836 var ncells = this.ncells();
838 837 index = Math.min(index, ncells);
839 838 index = Math.max(index, 0);
840 839 var cell = null;
841 840 type = type || this.default_cell_type;
842 841 if (type === 'above') {
843 842 if (index > 0) {
844 843 type = this.get_cell(index-1).cell_type;
845 844 } else {
846 845 type = 'code';
847 846 }
848 847 } else if (type === 'below') {
849 848 if (index < ncells) {
850 849 type = this.get_cell(index).cell_type;
851 850 } else {
852 851 type = 'code';
853 852 }
854 853 } else if (type === 'selected') {
855 854 type = this.get_selected_cell().cell_type;
856 855 }
857 856
858 857 if (ncells === 0 || this.is_valid_cell_index(index) || index === ncells) {
859 858 var cell_options = {
860 859 events: this.events,
861 860 config: this.config,
862 861 keyboard_manager: this.keyboard_manager,
863 862 notebook: this,
864 863 tooltip: this.tooltip,
865 864 };
866 865 if (type === 'code') {
867 866 cell = new codecell.CodeCell(this.kernel, cell_options);
868 867 cell.set_input_prompt();
869 868 } else if (type === 'markdown') {
870 869 cell = new textcell.MarkdownCell(cell_options);
871 870 } else if (type === 'raw') {
872 871 cell = new textcell.RawCell(cell_options);
873 872 } else if (type === 'heading') {
874 873 cell = new textcell.HeadingCell(cell_options);
875 874 }
876 875
877 876 if(this._insert_element_at_index(cell.element,index)) {
878 877 cell.render();
879 878 this.events.trigger('create.Cell', {'cell': cell, 'index': index});
880 879 cell.refresh();
881 880 // We used to select the cell after we refresh it, but there
882 881 // are now cases were this method is called where select is
883 882 // not appropriate. The selection logic should be handled by the
884 883 // caller of the the top level insert_cell methods.
885 884 this.set_dirty(true);
886 885 }
887 886 }
888 887 return cell;
889 888
890 889 };
891 890
892 891 /**
893 892 * Insert an element at given cell index.
894 893 *
895 894 * @method _insert_element_at_index
896 895 * @param element {dom_element} a cell element
897 896 * @param [index] {int} a valid index where to inser cell
898 897 * @private
899 898 *
900 899 * return true if everything whent fine.
901 900 **/
902 901 Notebook.prototype._insert_element_at_index = function(element, index){
903 902 if (element === undefined){
904 903 return false;
905 904 }
906 905
907 906 var ncells = this.ncells();
908 907
909 908 if (ncells === 0) {
910 909 // special case append if empty
911 910 this.element.find('div.end_space').before(element);
912 911 } else if ( ncells === index ) {
913 912 // special case append it the end, but not empty
914 913 this.get_cell_element(index-1).after(element);
915 914 } else if (this.is_valid_cell_index(index)) {
916 915 // otherwise always somewhere to append to
917 916 this.get_cell_element(index).before(element);
918 917 } else {
919 918 return false;
920 919 }
921 920
922 921 if (this.undelete_index !== null && index <= this.undelete_index) {
923 922 this.undelete_index = this.undelete_index + 1;
924 923 this.set_dirty(true);
925 924 }
926 925 return true;
927 926 };
928 927
929 928 /**
930 929 * Insert a cell of given type above given index, or at top
931 930 * of notebook if index smaller than 0.
932 931 *
933 932 * default index value is the one of currently selected cell
934 933 *
935 934 * @method insert_cell_above
936 935 * @param [type] {string} cell type
937 936 * @param [index] {integer}
938 937 *
939 938 * @return handle to created cell or null
940 939 **/
941 940 Notebook.prototype.insert_cell_above = function (type, index) {
942 941 index = this.index_or_selected(index);
943 942 return this.insert_cell_at_index(type, index);
944 943 };
945 944
946 945 /**
947 946 * Insert a cell of given type below given index, or at bottom
948 947 * of notebook if index greater than number of cells
949 948 *
950 949 * default index value is the one of currently selected cell
951 950 *
952 951 * @method insert_cell_below
953 952 * @param [type] {string} cell type
954 953 * @param [index] {integer}
955 954 *
956 955 * @return handle to created cell or null
957 956 *
958 957 **/
959 958 Notebook.prototype.insert_cell_below = function (type, index) {
960 959 index = this.index_or_selected(index);
961 960 return this.insert_cell_at_index(type, index+1);
962 961 };
963 962
964 963
965 964 /**
966 965 * Insert cell at end of notebook
967 966 *
968 967 * @method insert_cell_at_bottom
969 968 * @param {String} type cell type
970 969 *
971 970 * @return the added cell; or null
972 971 **/
973 972 Notebook.prototype.insert_cell_at_bottom = function (type){
974 973 var len = this.ncells();
975 974 return this.insert_cell_below(type,len-1);
976 975 };
977 976
978 977 /**
979 978 * Turn a cell into a code cell.
980 979 *
981 980 * @method to_code
982 981 * @param {Number} [index] A cell's index
983 982 */
984 983 Notebook.prototype.to_code = function (index) {
985 984 var i = this.index_or_selected(index);
986 985 if (this.is_valid_cell_index(i)) {
987 986 var source_cell = this.get_cell(i);
988 987 if (!(source_cell instanceof codecell.CodeCell)) {
989 988 var target_cell = this.insert_cell_below('code',i);
990 989 var text = source_cell.get_text();
991 990 if (text === source_cell.placeholder) {
992 991 text = '';
993 992 }
994 993 //metadata
995 994 target_cell.metadata = source_cell.metadata;
996 995
997 996 target_cell.set_text(text);
998 997 // make this value the starting point, so that we can only undo
999 998 // to this state, instead of a blank cell
1000 999 target_cell.code_mirror.clearHistory();
1001 1000 source_cell.element.remove();
1002 1001 this.select(i);
1003 1002 var cursor = source_cell.code_mirror.getCursor();
1004 1003 target_cell.code_mirror.setCursor(cursor);
1005 1004 this.set_dirty(true);
1006 1005 }
1007 1006 }
1008 1007 };
1009 1008
1010 1009 /**
1011 1010 * Turn a cell into a Markdown cell.
1012 1011 *
1013 1012 * @method to_markdown
1014 1013 * @param {Number} [index] A cell's index
1015 1014 */
1016 1015 Notebook.prototype.to_markdown = function (index) {
1017 1016 var i = this.index_or_selected(index);
1018 1017 if (this.is_valid_cell_index(i)) {
1019 1018 var source_cell = this.get_cell(i);
1020 1019
1021 1020 if (!(source_cell instanceof textcell.MarkdownCell)) {
1022 1021 var target_cell = this.insert_cell_below('markdown',i);
1023 1022 var text = source_cell.get_text();
1024 1023
1025 1024 if (text === source_cell.placeholder) {
1026 1025 text = '';
1027 1026 }
1028 1027 // metadata
1029 1028 target_cell.metadata = source_cell.metadata
1030 1029 // We must show the editor before setting its contents
1031 1030 target_cell.unrender();
1032 1031 target_cell.set_text(text);
1033 1032 // make this value the starting point, so that we can only undo
1034 1033 // to this state, instead of a blank cell
1035 1034 target_cell.code_mirror.clearHistory();
1036 1035 source_cell.element.remove();
1037 1036 this.select(i);
1038 1037 if ((source_cell instanceof textcell.TextCell) && source_cell.rendered) {
1039 1038 target_cell.render();
1040 1039 }
1041 1040 var cursor = source_cell.code_mirror.getCursor();
1042 1041 target_cell.code_mirror.setCursor(cursor);
1043 1042 this.set_dirty(true);
1044 1043 }
1045 1044 }
1046 1045 };
1047 1046
1048 1047 /**
1049 1048 * Turn a cell into a raw text cell.
1050 1049 *
1051 1050 * @method to_raw
1052 1051 * @param {Number} [index] A cell's index
1053 1052 */
1054 1053 Notebook.prototype.to_raw = function (index) {
1055 1054 var i = this.index_or_selected(index);
1056 1055 if (this.is_valid_cell_index(i)) {
1057 1056 var target_cell = null;
1058 1057 var source_cell = this.get_cell(i);
1059 1058
1060 1059 if (!(source_cell instanceof textcell.RawCell)) {
1061 1060 target_cell = this.insert_cell_below('raw',i);
1062 1061 var text = source_cell.get_text();
1063 1062 if (text === source_cell.placeholder) {
1064 1063 text = '';
1065 1064 }
1066 1065 //metadata
1067 1066 target_cell.metadata = source_cell.metadata;
1068 1067 // We must show the editor before setting its contents
1069 1068 target_cell.unrender();
1070 1069 target_cell.set_text(text);
1071 1070 // make this value the starting point, so that we can only undo
1072 1071 // to this state, instead of a blank cell
1073 1072 target_cell.code_mirror.clearHistory();
1074 1073 source_cell.element.remove();
1075 1074 this.select(i);
1076 1075 var cursor = source_cell.code_mirror.getCursor();
1077 1076 target_cell.code_mirror.setCursor(cursor);
1078 1077 this.set_dirty(true);
1079 1078 }
1080 1079 }
1081 1080 };
1082 1081
1083 1082 /**
1084 1083 * Turn a cell into a heading cell.
1085 1084 *
1086 1085 * @method to_heading
1087 1086 * @param {Number} [index] A cell's index
1088 1087 * @param {Number} [level] A heading level (e.g., 1 becomes &lt;h1&gt;)
1089 1088 */
1090 1089 Notebook.prototype.to_heading = function (index, level) {
1091 1090 level = level || 1;
1092 1091 var i = this.index_or_selected(index);
1093 1092 if (this.is_valid_cell_index(i)) {
1094 1093 var source_cell = this.get_cell(i);
1095 1094 var target_cell = null;
1096 1095 if (source_cell instanceof textcell.HeadingCell) {
1097 1096 source_cell.set_level(level);
1098 1097 } else {
1099 1098 target_cell = this.insert_cell_below('heading',i);
1100 1099 var text = source_cell.get_text();
1101 1100 if (text === source_cell.placeholder) {
1102 1101 text = '';
1103 1102 }
1104 1103 //metadata
1105 1104 target_cell.metadata = source_cell.metadata;
1106 1105 // We must show the editor before setting its contents
1107 1106 target_cell.set_level(level);
1108 1107 target_cell.unrender();
1109 1108 target_cell.set_text(text);
1110 1109 // make this value the starting point, so that we can only undo
1111 1110 // to this state, instead of a blank cell
1112 1111 target_cell.code_mirror.clearHistory();
1113 1112 source_cell.element.remove();
1114 1113 this.select(i);
1115 1114 var cursor = source_cell.code_mirror.getCursor();
1116 1115 target_cell.code_mirror.setCursor(cursor);
1117 1116 if ((source_cell instanceof textcell.TextCell) && source_cell.rendered) {
1118 1117 target_cell.render();
1119 1118 }
1120 1119 }
1121 1120 this.set_dirty(true);
1122 1121 this.events.trigger('selected_cell_type_changed.Notebook',
1123 1122 {'cell_type':'heading',level:level}
1124 1123 );
1125 1124 }
1126 1125 };
1127 1126
1128 1127
1129 1128 // Cut/Copy/Paste
1130 1129
1131 1130 /**
1132 1131 * Enable UI elements for pasting cells.
1133 1132 *
1134 1133 * @method enable_paste
1135 1134 */
1136 1135 Notebook.prototype.enable_paste = function () {
1137 1136 var that = this;
1138 1137 if (!this.paste_enabled) {
1139 1138 $('#paste_cell_replace').removeClass('disabled')
1140 1139 .on('click', function () {that.paste_cell_replace();});
1141 1140 $('#paste_cell_above').removeClass('disabled')
1142 1141 .on('click', function () {that.paste_cell_above();});
1143 1142 $('#paste_cell_below').removeClass('disabled')
1144 1143 .on('click', function () {that.paste_cell_below();});
1145 1144 this.paste_enabled = true;
1146 1145 }
1147 1146 };
1148 1147
1149 1148 /**
1150 1149 * Disable UI elements for pasting cells.
1151 1150 *
1152 1151 * @method disable_paste
1153 1152 */
1154 1153 Notebook.prototype.disable_paste = function () {
1155 1154 if (this.paste_enabled) {
1156 1155 $('#paste_cell_replace').addClass('disabled').off('click');
1157 1156 $('#paste_cell_above').addClass('disabled').off('click');
1158 1157 $('#paste_cell_below').addClass('disabled').off('click');
1159 1158 this.paste_enabled = false;
1160 1159 }
1161 1160 };
1162 1161
1163 1162 /**
1164 1163 * Cut a cell.
1165 1164 *
1166 1165 * @method cut_cell
1167 1166 */
1168 1167 Notebook.prototype.cut_cell = function () {
1169 1168 this.copy_cell();
1170 1169 this.delete_cell();
1171 1170 };
1172 1171
1173 1172 /**
1174 1173 * Copy a cell.
1175 1174 *
1176 1175 * @method copy_cell
1177 1176 */
1178 1177 Notebook.prototype.copy_cell = function () {
1179 1178 var cell = this.get_selected_cell();
1180 1179 this.clipboard = cell.toJSON();
1181 1180 // remove undeletable status from the copied cell
1182 1181 if (this.clipboard.metadata.deletable !== undefined) {
1183 1182 delete this.clipboard.metadata.deletable;
1184 1183 }
1185 1184 this.enable_paste();
1186 1185 };
1187 1186
1188 1187 /**
1189 1188 * Replace the selected cell with a cell in the clipboard.
1190 1189 *
1191 1190 * @method paste_cell_replace
1192 1191 */
1193 1192 Notebook.prototype.paste_cell_replace = function () {
1194 1193 if (this.clipboard !== null && this.paste_enabled) {
1195 1194 var cell_data = this.clipboard;
1196 1195 var new_cell = this.insert_cell_above(cell_data.cell_type);
1197 1196 new_cell.fromJSON(cell_data);
1198 1197 var old_cell = this.get_next_cell(new_cell);
1199 1198 this.delete_cell(this.find_cell_index(old_cell));
1200 1199 this.select(this.find_cell_index(new_cell));
1201 1200 }
1202 1201 };
1203 1202
1204 1203 /**
1205 1204 * Paste a cell from the clipboard above the selected cell.
1206 1205 *
1207 1206 * @method paste_cell_above
1208 1207 */
1209 1208 Notebook.prototype.paste_cell_above = function () {
1210 1209 if (this.clipboard !== null && this.paste_enabled) {
1211 1210 var cell_data = this.clipboard;
1212 1211 var new_cell = this.insert_cell_above(cell_data.cell_type);
1213 1212 new_cell.fromJSON(cell_data);
1214 1213 new_cell.focus_cell();
1215 1214 }
1216 1215 };
1217 1216
1218 1217 /**
1219 1218 * Paste a cell from the clipboard below the selected cell.
1220 1219 *
1221 1220 * @method paste_cell_below
1222 1221 */
1223 1222 Notebook.prototype.paste_cell_below = function () {
1224 1223 if (this.clipboard !== null && this.paste_enabled) {
1225 1224 var cell_data = this.clipboard;
1226 1225 var new_cell = this.insert_cell_below(cell_data.cell_type);
1227 1226 new_cell.fromJSON(cell_data);
1228 1227 new_cell.focus_cell();
1229 1228 }
1230 1229 };
1231 1230
1232 1231 // Split/merge
1233 1232
1234 1233 /**
1235 1234 * Split the selected cell into two, at the cursor.
1236 1235 *
1237 1236 * @method split_cell
1238 1237 */
1239 1238 Notebook.prototype.split_cell = function () {
1240 1239 var mdc = textcell.MarkdownCell;
1241 1240 var rc = textcell.RawCell;
1242 1241 var cell = this.get_selected_cell();
1243 1242 if (cell.is_splittable()) {
1244 1243 var texta = cell.get_pre_cursor();
1245 1244 var textb = cell.get_post_cursor();
1246 1245 cell.set_text(textb);
1247 1246 var new_cell = this.insert_cell_above(cell.cell_type);
1248 1247 // Unrender the new cell so we can call set_text.
1249 1248 new_cell.unrender();
1250 1249 new_cell.set_text(texta);
1251 1250 }
1252 1251 };
1253 1252
1254 1253 /**
1255 1254 * Combine the selected cell into the cell above it.
1256 1255 *
1257 1256 * @method merge_cell_above
1258 1257 */
1259 1258 Notebook.prototype.merge_cell_above = function () {
1260 1259 var mdc = textcell.MarkdownCell;
1261 1260 var rc = textcell.RawCell;
1262 1261 var index = this.get_selected_index();
1263 1262 var cell = this.get_cell(index);
1264 1263 var render = cell.rendered;
1265 1264 if (!cell.is_mergeable()) {
1266 1265 return;
1267 1266 }
1268 1267 if (index > 0) {
1269 1268 var upper_cell = this.get_cell(index-1);
1270 1269 if (!upper_cell.is_mergeable()) {
1271 1270 return;
1272 1271 }
1273 1272 var upper_text = upper_cell.get_text();
1274 1273 var text = cell.get_text();
1275 1274 if (cell instanceof codecell.CodeCell) {
1276 1275 cell.set_text(upper_text+'\n'+text);
1277 1276 } else {
1278 1277 cell.unrender(); // Must unrender before we set_text.
1279 1278 cell.set_text(upper_text+'\n\n'+text);
1280 1279 if (render) {
1281 1280 // The rendered state of the final cell should match
1282 1281 // that of the original selected cell;
1283 1282 cell.render();
1284 1283 }
1285 1284 }
1286 1285 this.delete_cell(index-1);
1287 1286 this.select(this.find_cell_index(cell));
1288 1287 }
1289 1288 };
1290 1289
1291 1290 /**
1292 1291 * Combine the selected cell into the cell below it.
1293 1292 *
1294 1293 * @method merge_cell_below
1295 1294 */
1296 1295 Notebook.prototype.merge_cell_below = function () {
1297 1296 var mdc = textcell.MarkdownCell;
1298 1297 var rc = textcell.RawCell;
1299 1298 var index = this.get_selected_index();
1300 1299 var cell = this.get_cell(index);
1301 1300 var render = cell.rendered;
1302 1301 if (!cell.is_mergeable()) {
1303 1302 return;
1304 1303 }
1305 1304 if (index < this.ncells()-1) {
1306 1305 var lower_cell = this.get_cell(index+1);
1307 1306 if (!lower_cell.is_mergeable()) {
1308 1307 return;
1309 1308 }
1310 1309 var lower_text = lower_cell.get_text();
1311 1310 var text = cell.get_text();
1312 1311 if (cell instanceof codecell.CodeCell) {
1313 1312 cell.set_text(text+'\n'+lower_text);
1314 1313 } else {
1315 1314 cell.unrender(); // Must unrender before we set_text.
1316 1315 cell.set_text(text+'\n\n'+lower_text);
1317 1316 if (render) {
1318 1317 // The rendered state of the final cell should match
1319 1318 // that of the original selected cell;
1320 1319 cell.render();
1321 1320 }
1322 1321 }
1323 1322 this.delete_cell(index+1);
1324 1323 this.select(this.find_cell_index(cell));
1325 1324 }
1326 1325 };
1327 1326
1328 1327
1329 1328 // Cell collapsing and output clearing
1330 1329
1331 1330 /**
1332 1331 * Hide a cell's output.
1333 1332 *
1334 1333 * @method collapse_output
1335 1334 * @param {Number} index A cell's numeric index
1336 1335 */
1337 1336 Notebook.prototype.collapse_output = function (index) {
1338 1337 var i = this.index_or_selected(index);
1339 1338 var cell = this.get_cell(i);
1340 1339 if (cell !== null && (cell instanceof codecell.CodeCell)) {
1341 1340 cell.collapse_output();
1342 1341 this.set_dirty(true);
1343 1342 }
1344 1343 };
1345 1344
1346 1345 /**
1347 1346 * Hide each code cell's output area.
1348 1347 *
1349 1348 * @method collapse_all_output
1350 1349 */
1351 1350 Notebook.prototype.collapse_all_output = function () {
1352 1351 $.map(this.get_cells(), function (cell, i) {
1353 1352 if (cell instanceof codecell.CodeCell) {
1354 1353 cell.collapse_output();
1355 1354 }
1356 1355 });
1357 1356 // this should not be set if the `collapse` key is removed from nbformat
1358 1357 this.set_dirty(true);
1359 1358 };
1360 1359
1361 1360 /**
1362 1361 * Show a cell's output.
1363 1362 *
1364 1363 * @method expand_output
1365 1364 * @param {Number} index A cell's numeric index
1366 1365 */
1367 1366 Notebook.prototype.expand_output = function (index) {
1368 1367 var i = this.index_or_selected(index);
1369 1368 var cell = this.get_cell(i);
1370 1369 if (cell !== null && (cell instanceof codecell.CodeCell)) {
1371 1370 cell.expand_output();
1372 1371 this.set_dirty(true);
1373 1372 }
1374 1373 };
1375 1374
1376 1375 /**
1377 1376 * Expand each code cell's output area, and remove scrollbars.
1378 1377 *
1379 1378 * @method expand_all_output
1380 1379 */
1381 1380 Notebook.prototype.expand_all_output = function () {
1382 1381 $.map(this.get_cells(), function (cell, i) {
1383 1382 if (cell instanceof codecell.CodeCell) {
1384 1383 cell.expand_output();
1385 1384 }
1386 1385 });
1387 1386 // this should not be set if the `collapse` key is removed from nbformat
1388 1387 this.set_dirty(true);
1389 1388 };
1390 1389
1391 1390 /**
1392 1391 * Clear the selected CodeCell's output area.
1393 1392 *
1394 1393 * @method clear_output
1395 1394 * @param {Number} index A cell's numeric index
1396 1395 */
1397 1396 Notebook.prototype.clear_output = function (index) {
1398 1397 var i = this.index_or_selected(index);
1399 1398 var cell = this.get_cell(i);
1400 1399 if (cell !== null && (cell instanceof codecell.CodeCell)) {
1401 1400 cell.clear_output();
1402 1401 this.set_dirty(true);
1403 1402 }
1404 1403 };
1405 1404
1406 1405 /**
1407 1406 * Clear each code cell's output area.
1408 1407 *
1409 1408 * @method clear_all_output
1410 1409 */
1411 1410 Notebook.prototype.clear_all_output = function () {
1412 1411 $.map(this.get_cells(), function (cell, i) {
1413 1412 if (cell instanceof codecell.CodeCell) {
1414 1413 cell.clear_output();
1415 1414 }
1416 1415 });
1417 1416 this.set_dirty(true);
1418 1417 };
1419 1418
1420 1419 /**
1421 1420 * Scroll the selected CodeCell's output area.
1422 1421 *
1423 1422 * @method scroll_output
1424 1423 * @param {Number} index A cell's numeric index
1425 1424 */
1426 1425 Notebook.prototype.scroll_output = function (index) {
1427 1426 var i = this.index_or_selected(index);
1428 1427 var cell = this.get_cell(i);
1429 1428 if (cell !== null && (cell instanceof codecell.CodeCell)) {
1430 1429 cell.scroll_output();
1431 1430 this.set_dirty(true);
1432 1431 }
1433 1432 };
1434 1433
1435 1434 /**
1436 1435 * Expand each code cell's output area, and add a scrollbar for long output.
1437 1436 *
1438 1437 * @method scroll_all_output
1439 1438 */
1440 1439 Notebook.prototype.scroll_all_output = function () {
1441 1440 $.map(this.get_cells(), function (cell, i) {
1442 1441 if (cell instanceof codecell.CodeCell) {
1443 1442 cell.scroll_output();
1444 1443 }
1445 1444 });
1446 1445 // this should not be set if the `collapse` key is removed from nbformat
1447 1446 this.set_dirty(true);
1448 1447 };
1449 1448
1450 1449 /** Toggle whether a cell's output is collapsed or expanded.
1451 1450 *
1452 1451 * @method toggle_output
1453 1452 * @param {Number} index A cell's numeric index
1454 1453 */
1455 1454 Notebook.prototype.toggle_output = function (index) {
1456 1455 var i = this.index_or_selected(index);
1457 1456 var cell = this.get_cell(i);
1458 1457 if (cell !== null && (cell instanceof codecell.CodeCell)) {
1459 1458 cell.toggle_output();
1460 1459 this.set_dirty(true);
1461 1460 }
1462 1461 };
1463 1462
1464 1463 /**
1465 1464 * Hide/show the output of all cells.
1466 1465 *
1467 1466 * @method toggle_all_output
1468 1467 */
1469 1468 Notebook.prototype.toggle_all_output = function () {
1470 1469 $.map(this.get_cells(), function (cell, i) {
1471 1470 if (cell instanceof codecell.CodeCell) {
1472 1471 cell.toggle_output();
1473 1472 }
1474 1473 });
1475 1474 // this should not be set if the `collapse` key is removed from nbformat
1476 1475 this.set_dirty(true);
1477 1476 };
1478 1477
1479 1478 /**
1480 1479 * Toggle a scrollbar for long cell outputs.
1481 1480 *
1482 1481 * @method toggle_output_scroll
1483 1482 * @param {Number} index A cell's numeric index
1484 1483 */
1485 1484 Notebook.prototype.toggle_output_scroll = function (index) {
1486 1485 var i = this.index_or_selected(index);
1487 1486 var cell = this.get_cell(i);
1488 1487 if (cell !== null && (cell instanceof codecell.CodeCell)) {
1489 1488 cell.toggle_output_scroll();
1490 1489 this.set_dirty(true);
1491 1490 }
1492 1491 };
1493 1492
1494 1493 /**
1495 1494 * Toggle the scrolling of long output on all cells.
1496 1495 *
1497 1496 * @method toggle_all_output_scrolling
1498 1497 */
1499 1498 Notebook.prototype.toggle_all_output_scroll = function () {
1500 1499 $.map(this.get_cells(), function (cell, i) {
1501 1500 if (cell instanceof codecell.CodeCell) {
1502 1501 cell.toggle_output_scroll();
1503 1502 }
1504 1503 });
1505 1504 // this should not be set if the `collapse` key is removed from nbformat
1506 1505 this.set_dirty(true);
1507 1506 };
1508 1507
1509 1508 // Other cell functions: line numbers, ...
1510 1509
1511 1510 /**
1512 1511 * Toggle line numbers in the selected cell's input area.
1513 1512 *
1514 1513 * @method cell_toggle_line_numbers
1515 1514 */
1516 1515 Notebook.prototype.cell_toggle_line_numbers = function() {
1517 1516 this.get_selected_cell().toggle_line_numbers();
1518 1517 };
1519 1518
1520 1519 /**
1521 1520 * Set the codemirror mode for all code cells, including the default for
1522 1521 * new code cells.
1523 1522 *
1524 1523 * @method set_codemirror_mode
1525 1524 */
1526 1525 Notebook.prototype.set_codemirror_mode = function(newmode){
1527 1526 if (newmode === this.codemirror_mode) {
1528 1527 return;
1529 1528 }
1530 1529 this.codemirror_mode = newmode;
1531 1530 codecell.CodeCell.options_default.cm_config.mode = newmode;
1532 1531 modename = newmode.mode || newmode.name || newmode
1533 1532
1534 1533 that = this;
1535 1534 utils.requireCodeMirrorMode(modename, function () {
1536 1535 $.map(that.get_cells(), function(cell, i) {
1537 1536 if (cell.cell_type === 'code'){
1538 1537 cell.code_mirror.setOption('mode', newmode);
1539 1538 // This is currently redundant, because cm_config ends up as
1540 1539 // codemirror's own .options object, but I don't want to
1541 1540 // rely on that.
1542 1541 cell.cm_config.mode = newmode;
1543 1542 }
1544 1543 });
1545 1544 })
1546 1545 };
1547 1546
1548 1547 // Session related things
1549 1548
1550 1549 /**
1551 1550 * Start a new session and set it on each code cell.
1552 1551 *
1553 1552 * @method start_session
1554 1553 */
1555 1554 Notebook.prototype.start_session = function (kernel_name) {
1556 1555 var that = this;
1557 1556 if (this._session_starting) {
1558 1557 throw new session.SessionAlreadyStarting();
1559 1558 }
1560 1559 this._session_starting = true;
1561 1560
1562 1561 var options = {
1563 1562 base_url: this.base_url,
1564 1563 ws_url: this.ws_url,
1565 1564 notebook_path: this.notebook_path,
1566 1565 notebook_name: this.notebook_name,
1567 1566 kernel_name: kernel_name,
1568 1567 notebook: this
1569 1568 };
1570 1569
1571 1570 var success = $.proxy(this._session_started, this);
1572 1571 var failure = $.proxy(this._session_start_failed, this);
1573 1572
1574 1573 if (this.session !== null) {
1575 1574 this.session.restart(options, success, failure);
1576 1575 } else {
1577 1576 this.session = new session.Session(options);
1578 1577 this.session.start(success, failure);
1579 1578 }
1580 1579 };
1581 1580
1582 1581
1583 1582 /**
1584 1583 * Once a session is started, link the code cells to the kernel and pass the
1585 1584 * comm manager to the widget manager
1586 1585 *
1587 1586 */
1588 1587 Notebook.prototype._session_started = function (){
1589 1588 this._session_starting = false;
1590 1589 this.kernel = this.session.kernel;
1591 1590 var ncells = this.ncells();
1592 1591 for (var i=0; i<ncells; i++) {
1593 1592 var cell = this.get_cell(i);
1594 1593 if (cell instanceof codecell.CodeCell) {
1595 1594 cell.set_kernel(this.session.kernel);
1596 1595 }
1597 1596 }
1598 1597 };
1599 1598 Notebook.prototype._session_start_failed = function (jqxhr, status, error){
1600 1599 this._session_starting = false;
1601 1600 utils.log_ajax_error(jqxhr, status, error);
1602 1601 };
1603 1602
1604 1603 /**
1605 1604 * Prompt the user to restart the IPython kernel.
1606 1605 *
1607 1606 * @method restart_kernel
1608 1607 */
1609 1608 Notebook.prototype.restart_kernel = function () {
1610 1609 var that = this;
1611 1610 dialog.modal({
1612 1611 notebook: this,
1613 1612 keyboard_manager: this.keyboard_manager,
1614 1613 title : "Restart kernel or continue running?",
1615 1614 body : $("<p/>").text(
1616 1615 'Do you want to restart the current kernel? You will lose all variables defined in it.'
1617 1616 ),
1618 1617 buttons : {
1619 1618 "Continue running" : {},
1620 1619 "Restart" : {
1621 1620 "class" : "btn-danger",
1622 1621 "click" : function() {
1623 1622 that.kernel.restart();
1624 1623 }
1625 1624 }
1626 1625 }
1627 1626 });
1628 1627 };
1629 1628
1630 1629 /**
1631 1630 * Execute or render cell outputs and go into command mode.
1632 1631 *
1633 1632 * @method execute_cell
1634 1633 */
1635 1634 Notebook.prototype.execute_cell = function () {
1636 1635 // mode = shift, ctrl, alt
1637 1636 var cell = this.get_selected_cell();
1638 1637 var cell_index = this.find_cell_index(cell);
1639 1638
1640 1639 cell.execute();
1641 1640 this.command_mode();
1642 1641 this.set_dirty(true);
1643 1642 };
1644 1643
1645 1644 /**
1646 1645 * Execute or render cell outputs and insert a new cell below.
1647 1646 *
1648 1647 * @method execute_cell_and_insert_below
1649 1648 */
1650 1649 Notebook.prototype.execute_cell_and_insert_below = function () {
1651 1650 var cell = this.get_selected_cell();
1652 1651 var cell_index = this.find_cell_index(cell);
1653 1652
1654 1653 cell.execute();
1655 1654
1656 1655 // If we are at the end always insert a new cell and return
1657 1656 if (cell_index === (this.ncells()-1)) {
1658 1657 this.command_mode();
1659 1658 this.insert_cell_below();
1660 1659 this.select(cell_index+1);
1661 1660 this.edit_mode();
1662 1661 this.scroll_to_bottom();
1663 1662 this.set_dirty(true);
1664 1663 return;
1665 1664 }
1666 1665
1667 1666 this.command_mode();
1668 1667 this.insert_cell_below();
1669 1668 this.select(cell_index+1);
1670 1669 this.edit_mode();
1671 1670 this.set_dirty(true);
1672 1671 };
1673 1672
1674 1673 /**
1675 1674 * Execute or render cell outputs and select the next cell.
1676 1675 *
1677 1676 * @method execute_cell_and_select_below
1678 1677 */
1679 1678 Notebook.prototype.execute_cell_and_select_below = function () {
1680 1679
1681 1680 var cell = this.get_selected_cell();
1682 1681 var cell_index = this.find_cell_index(cell);
1683 1682
1684 1683 cell.execute();
1685 1684
1686 1685 // If we are at the end always insert a new cell and return
1687 1686 if (cell_index === (this.ncells()-1)) {
1688 1687 this.command_mode();
1689 1688 this.insert_cell_below();
1690 1689 this.select(cell_index+1);
1691 1690 this.edit_mode();
1692 1691 this.scroll_to_bottom();
1693 1692 this.set_dirty(true);
1694 1693 return;
1695 1694 }
1696 1695
1697 1696 this.command_mode();
1698 1697 this.select(cell_index+1);
1699 1698 this.focus_cell();
1700 1699 this.set_dirty(true);
1701 1700 };
1702 1701
1703 1702 /**
1704 1703 * Execute all cells below the selected cell.
1705 1704 *
1706 1705 * @method execute_cells_below
1707 1706 */
1708 1707 Notebook.prototype.execute_cells_below = function () {
1709 1708 this.execute_cell_range(this.get_selected_index(), this.ncells());
1710 1709 this.scroll_to_bottom();
1711 1710 };
1712 1711
1713 1712 /**
1714 1713 * Execute all cells above the selected cell.
1715 1714 *
1716 1715 * @method execute_cells_above
1717 1716 */
1718 1717 Notebook.prototype.execute_cells_above = function () {
1719 1718 this.execute_cell_range(0, this.get_selected_index());
1720 1719 };
1721 1720
1722 1721 /**
1723 1722 * Execute all cells.
1724 1723 *
1725 1724 * @method execute_all_cells
1726 1725 */
1727 1726 Notebook.prototype.execute_all_cells = function () {
1728 1727 this.execute_cell_range(0, this.ncells());
1729 1728 this.scroll_to_bottom();
1730 1729 };
1731 1730
1732 1731 /**
1733 1732 * Execute a contiguous range of cells.
1734 1733 *
1735 1734 * @method execute_cell_range
1736 1735 * @param {Number} start Index of the first cell to execute (inclusive)
1737 1736 * @param {Number} end Index of the last cell to execute (exclusive)
1738 1737 */
1739 1738 Notebook.prototype.execute_cell_range = function (start, end) {
1740 1739 this.command_mode();
1741 1740 for (var i=start; i<end; i++) {
1742 1741 this.select(i);
1743 1742 this.execute_cell();
1744 1743 }
1745 1744 };
1746 1745
1747 1746 // Persistance and loading
1748 1747
1749 1748 /**
1750 1749 * Getter method for this notebook's name.
1751 1750 *
1752 1751 * @method get_notebook_name
1753 1752 * @return {String} This notebook's name (excluding file extension)
1754 1753 */
1755 1754 Notebook.prototype.get_notebook_name = function () {
1756 1755 var nbname = this.notebook_name.substring(0,this.notebook_name.length-6);
1757 1756 return nbname;
1758 1757 };
1759 1758
1760 1759 /**
1761 1760 * Setter method for this notebook's name.
1762 1761 *
1763 1762 * @method set_notebook_name
1764 1763 * @param {String} name A new name for this notebook
1765 1764 */
1766 1765 Notebook.prototype.set_notebook_name = function (name) {
1767 1766 this.notebook_name = name;
1768 1767 };
1769 1768
1770 1769 /**
1771 1770 * Check that a notebook's name is valid.
1772 1771 *
1773 1772 * @method test_notebook_name
1774 1773 * @param {String} nbname A name for this notebook
1775 1774 * @return {Boolean} True if the name is valid, false if invalid
1776 1775 */
1777 1776 Notebook.prototype.test_notebook_name = function (nbname) {
1778 1777 nbname = nbname || '';
1779 1778 if (nbname.length>0 && !this.notebook_name_blacklist_re.test(nbname)) {
1780 1779 return true;
1781 1780 } else {
1782 1781 return false;
1783 1782 }
1784 1783 };
1785 1784
1786 1785 /**
1787 1786 * Load a notebook from JSON (.ipynb).
1788 1787 *
1789 1788 * This currently handles one worksheet: others are deleted.
1790 1789 *
1791 1790 * @method fromJSON
1792 1791 * @param {Object} data JSON representation of a notebook
1793 1792 */
1794 1793 Notebook.prototype.fromJSON = function (data) {
1795 1794
1796 1795 var content = data.content;
1797 1796 var ncells = this.ncells();
1798 1797 var i;
1799 1798 for (i=0; i<ncells; i++) {
1800 1799 // Always delete cell 0 as they get renumbered as they are deleted.
1801 1800 this.delete_cell(0);
1802 1801 }
1803 1802 // Save the metadata and name.
1804 1803 this.metadata = content.metadata;
1805 1804 this.notebook_name = data.name;
1806 1805 var trusted = true;
1807 1806
1808 1807 // Trigger an event changing the kernel spec - this will set the default
1809 1808 // codemirror mode
1810 1809 if (this.metadata.kernelspec !== undefined) {
1811 1810 this.events.trigger('spec_changed.Kernel', this.metadata.kernelspec);
1812 1811 }
1813 1812
1813 // Set the codemirror mode from language_info metadata
1814 if (this.metadata.language_info !== undefined) {
1815 var langinfo = this.metadata.language_info;
1816 // Mode 'null' should be plain, unhighlighted text.
1817 var cm_mode = langinfo.codemirror_mode || langinfo.language || 'null'
1818 this.set_codemirror_mode(cm_mode);
1819 }
1820
1814 1821 // Only handle 1 worksheet for now.
1815 1822 var worksheet = content.worksheets[0];
1816 1823 if (worksheet !== undefined) {
1817 1824 if (worksheet.metadata) {
1818 1825 this.worksheet_metadata = worksheet.metadata;
1819 1826 }
1820 1827 var new_cells = worksheet.cells;
1821 1828 ncells = new_cells.length;
1822 1829 var cell_data = null;
1823 1830 var new_cell = null;
1824 1831 for (i=0; i<ncells; i++) {
1825 1832 cell_data = new_cells[i];
1826 1833 // VERSIONHACK: plaintext -> raw
1827 1834 // handle never-released plaintext name for raw cells
1828 1835 if (cell_data.cell_type === 'plaintext'){
1829 1836 cell_data.cell_type = 'raw';
1830 1837 }
1831 1838
1832 1839 new_cell = this.insert_cell_at_index(cell_data.cell_type, i);
1833 1840 new_cell.fromJSON(cell_data);
1834 1841 if (new_cell.cell_type == 'code' && !new_cell.output_area.trusted) {
1835 1842 trusted = false;
1836 1843 }
1837 1844 }
1838 1845 }
1839 1846 if (trusted !== this.trusted) {
1840 1847 this.trusted = trusted;
1841 1848 this.events.trigger("trust_changed.Notebook", trusted);
1842 1849 }
1843 1850 if (content.worksheets.length > 1) {
1844 1851 dialog.modal({
1845 1852 notebook: this,
1846 1853 keyboard_manager: this.keyboard_manager,
1847 1854 title : "Multiple worksheets",
1848 1855 body : "This notebook has " + data.worksheets.length + " worksheets, " +
1849 1856 "but this version of IPython can only handle the first. " +
1850 1857 "If you save this notebook, worksheets after the first will be lost.",
1851 1858 buttons : {
1852 1859 OK : {
1853 1860 class : "btn-danger"
1854 1861 }
1855 1862 }
1856 1863 });
1857 1864 }
1858 1865 };
1859 1866
1860 1867 /**
1861 1868 * Dump this notebook into a JSON-friendly object.
1862 1869 *
1863 1870 * @method toJSON
1864 1871 * @return {Object} A JSON-friendly representation of this notebook.
1865 1872 */
1866 1873 Notebook.prototype.toJSON = function () {
1867 1874 var cells = this.get_cells();
1868 1875 var ncells = cells.length;
1869 1876 var cell_array = new Array(ncells);
1870 1877 var trusted = true;
1871 1878 for (var i=0; i<ncells; i++) {
1872 1879 var cell = cells[i];
1873 1880 if (cell.cell_type == 'code' && !cell.output_area.trusted) {
1874 1881 trusted = false;
1875 1882 }
1876 1883 cell_array[i] = cell.toJSON();
1877 1884 }
1878 1885 var data = {
1879 1886 // Only handle 1 worksheet for now.
1880 1887 worksheets : [{
1881 1888 cells: cell_array,
1882 1889 metadata: this.worksheet_metadata
1883 1890 }],
1884 1891 metadata : this.metadata
1885 1892 };
1886 1893 if (trusted != this.trusted) {
1887 1894 this.trusted = trusted;
1888 1895 this.events.trigger("trust_changed.Notebook", trusted);
1889 1896 }
1890 1897 return data;
1891 1898 };
1892 1899
1893 1900 /**
1894 1901 * Start an autosave timer, for periodically saving the notebook.
1895 1902 *
1896 1903 * @method set_autosave_interval
1897 1904 * @param {Integer} interval the autosave interval in milliseconds
1898 1905 */
1899 1906 Notebook.prototype.set_autosave_interval = function (interval) {
1900 1907 var that = this;
1901 1908 // clear previous interval, so we don't get simultaneous timers
1902 1909 if (this.autosave_timer) {
1903 1910 clearInterval(this.autosave_timer);
1904 1911 }
1905 1912
1906 1913 this.autosave_interval = this.minimum_autosave_interval = interval;
1907 1914 if (interval) {
1908 1915 this.autosave_timer = setInterval(function() {
1909 1916 if (that.dirty) {
1910 1917 that.save_notebook();
1911 1918 }
1912 1919 }, interval);
1913 1920 this.events.trigger("autosave_enabled.Notebook", interval);
1914 1921 } else {
1915 1922 this.autosave_timer = null;
1916 1923 this.events.trigger("autosave_disabled.Notebook");
1917 1924 }
1918 1925 };
1919 1926
1920 1927 /**
1921 1928 * Save this notebook on the server. This becomes a notebook instance's
1922 1929 * .save_notebook method *after* the entire notebook has been loaded.
1923 1930 *
1924 1931 * @method save_notebook
1925 1932 */
1926 1933 Notebook.prototype.save_notebook = function (extra_settings) {
1927 1934 // Create a JSON model to be sent to the server.
1928 1935 var model = {};
1929 1936 model.name = this.notebook_name;
1930 1937 model.path = this.notebook_path;
1931 1938 model.type = 'notebook';
1932 1939 model.format = 'json';
1933 1940 model.content = this.toJSON();
1934 1941 model.content.nbformat = this.nbformat;
1935 1942 model.content.nbformat_minor = this.nbformat_minor;
1936 1943 // time the ajax call for autosave tuning purposes.
1937 1944 var start = new Date().getTime();
1938 1945 // We do the call with settings so we can set cache to false.
1939 1946 var settings = {
1940 1947 processData : false,
1941 1948 cache : false,
1942 1949 type : "PUT",
1943 1950 data : JSON.stringify(model),
1944 1951 contentType: 'application/json',
1945 1952 dataType : "json",
1946 1953 success : $.proxy(this.save_notebook_success, this, start),
1947 1954 error : $.proxy(this.save_notebook_error, this)
1948 1955 };
1949 1956 if (extra_settings) {
1950 1957 for (var key in extra_settings) {
1951 1958 settings[key] = extra_settings[key];
1952 1959 }
1953 1960 }
1954 1961 this.events.trigger('notebook_saving.Notebook');
1955 1962 var url = utils.url_join_encode(
1956 1963 this.base_url,
1957 1964 'api/contents',
1958 1965 this.notebook_path,
1959 1966 this.notebook_name
1960 1967 );
1961 1968 $.ajax(url, settings);
1962 1969 };
1963 1970
1964 1971 /**
1965 1972 * Success callback for saving a notebook.
1966 1973 *
1967 1974 * @method save_notebook_success
1968 1975 * @param {Integer} start the time when the save request started
1969 1976 * @param {Object} data JSON representation of a notebook
1970 1977 * @param {String} status Description of response status
1971 1978 * @param {jqXHR} xhr jQuery Ajax object
1972 1979 */
1973 1980 Notebook.prototype.save_notebook_success = function (start, data, status, xhr) {
1974 1981 this.set_dirty(false);
1975 1982 if (data.message) {
1976 1983 // save succeeded, but validation failed.
1977 1984 var body = $("<div>");
1978 1985 var title = "Notebook validation failed";
1979 1986
1980 1987 body.append($("<p>").text(
1981 1988 "The save operation succeeded," +
1982 1989 " but the notebook does not appear to be valid." +
1983 1990 " The validation error was:"
1984 1991 )).append($("<div>").addClass("validation-error").append(
1985 1992 $("<pre>").text(data.message)
1986 1993 ));
1987 1994 dialog.modal({
1988 1995 notebook: this,
1989 1996 keyboard_manager: this.keyboard_manager,
1990 1997 title: title,
1991 1998 body: body,
1992 1999 buttons : {
1993 2000 OK : {
1994 2001 "class" : "btn-primary"
1995 2002 }
1996 2003 }
1997 2004 });
1998 2005 }
1999 2006 this.events.trigger('notebook_saved.Notebook');
2000 2007 this._update_autosave_interval(start);
2001 2008 if (this._checkpoint_after_save) {
2002 2009 this.create_checkpoint();
2003 2010 this._checkpoint_after_save = false;
2004 2011 }
2005 2012 };
2006 2013
2007 2014 /**
2008 2015 * update the autosave interval based on how long the last save took
2009 2016 *
2010 2017 * @method _update_autosave_interval
2011 2018 * @param {Integer} timestamp when the save request started
2012 2019 */
2013 2020 Notebook.prototype._update_autosave_interval = function (start) {
2014 2021 var duration = (new Date().getTime() - start);
2015 2022 if (this.autosave_interval) {
2016 2023 // new save interval: higher of 10x save duration or parameter (default 30 seconds)
2017 2024 var interval = Math.max(10 * duration, this.minimum_autosave_interval);
2018 2025 // round to 10 seconds, otherwise we will be setting a new interval too often
2019 2026 interval = 10000 * Math.round(interval / 10000);
2020 2027 // set new interval, if it's changed
2021 2028 if (interval != this.autosave_interval) {
2022 2029 this.set_autosave_interval(interval);
2023 2030 }
2024 2031 }
2025 2032 };
2026 2033
2027 2034 /**
2028 2035 * Failure callback for saving a notebook.
2029 2036 *
2030 2037 * @method save_notebook_error
2031 2038 * @param {jqXHR} xhr jQuery Ajax object
2032 2039 * @param {String} status Description of response status
2033 2040 * @param {String} error HTTP error message
2034 2041 */
2035 2042 Notebook.prototype.save_notebook_error = function (xhr, status, error) {
2036 2043 this.events.trigger('notebook_save_failed.Notebook', [xhr, status, error]);
2037 2044 };
2038 2045
2039 2046 /**
2040 2047 * Explicitly trust the output of this notebook.
2041 2048 *
2042 2049 * @method trust_notebook
2043 2050 */
2044 2051 Notebook.prototype.trust_notebook = function (extra_settings) {
2045 2052 var body = $("<div>").append($("<p>")
2046 2053 .text("A trusted IPython notebook may execute hidden malicious code ")
2047 2054 .append($("<strong>")
2048 2055 .append(
2049 2056 $("<em>").text("when you open it")
2050 2057 )
2051 2058 ).append(".").append(
2052 2059 " Selecting trust will immediately reload this notebook in a trusted state."
2053 2060 ).append(
2054 2061 " For more information, see the "
2055 2062 ).append($("<a>").attr("href", "http://ipython.org/ipython-doc/2/notebook/security.html")
2056 2063 .text("IPython security documentation")
2057 2064 ).append(".")
2058 2065 );
2059 2066
2060 2067 var nb = this;
2061 2068 dialog.modal({
2062 2069 notebook: this,
2063 2070 keyboard_manager: this.keyboard_manager,
2064 2071 title: "Trust this notebook?",
2065 2072 body: body,
2066 2073
2067 2074 buttons: {
2068 2075 Cancel : {},
2069 2076 Trust : {
2070 2077 class : "btn-danger",
2071 2078 click : function () {
2072 2079 var cells = nb.get_cells();
2073 2080 for (var i = 0; i < cells.length; i++) {
2074 2081 var cell = cells[i];
2075 2082 if (cell.cell_type == 'code') {
2076 2083 cell.output_area.trusted = true;
2077 2084 }
2078 2085 }
2079 2086 nb.events.on('notebook_saved.Notebook', function () {
2080 2087 window.location.reload();
2081 2088 });
2082 2089 nb.save_notebook();
2083 2090 }
2084 2091 }
2085 2092 }
2086 2093 });
2087 2094 };
2088 2095
2089 2096 Notebook.prototype.new_notebook = function(){
2090 2097 var path = this.notebook_path;
2091 2098 var base_url = this.base_url;
2092 2099 var settings = {
2093 2100 processData : false,
2094 2101 cache : false,
2095 2102 type : "POST",
2096 2103 dataType : "json",
2097 2104 async : false,
2098 2105 success : function (data, status, xhr){
2099 2106 var notebook_name = data.name;
2100 2107 window.open(
2101 2108 utils.url_join_encode(
2102 2109 base_url,
2103 2110 'notebooks',
2104 2111 path,
2105 2112 notebook_name
2106 2113 ),
2107 2114 '_blank'
2108 2115 );
2109 2116 },
2110 2117 error : utils.log_ajax_error,
2111 2118 };
2112 2119 var url = utils.url_join_encode(
2113 2120 base_url,
2114 2121 'api/contents',
2115 2122 path
2116 2123 );
2117 2124 $.ajax(url,settings);
2118 2125 };
2119 2126
2120 2127
2121 2128 Notebook.prototype.copy_notebook = function(){
2122 2129 var path = this.notebook_path;
2123 2130 var base_url = this.base_url;
2124 2131 var settings = {
2125 2132 processData : false,
2126 2133 cache : false,
2127 2134 type : "POST",
2128 2135 dataType : "json",
2129 2136 data : JSON.stringify({copy_from : this.notebook_name}),
2130 2137 async : false,
2131 2138 success : function (data, status, xhr) {
2132 2139 window.open(utils.url_join_encode(
2133 2140 base_url,
2134 2141 'notebooks',
2135 2142 data.path,
2136 2143 data.name
2137 2144 ), '_blank');
2138 2145 },
2139 2146 error : utils.log_ajax_error,
2140 2147 };
2141 2148 var url = utils.url_join_encode(
2142 2149 base_url,
2143 2150 'api/contents',
2144 2151 path
2145 2152 );
2146 2153 $.ajax(url,settings);
2147 2154 };
2148 2155
2149 2156 Notebook.prototype.rename = function (nbname) {
2150 2157 var that = this;
2151 2158 if (!nbname.match(/\.ipynb$/)) {
2152 2159 nbname = nbname + ".ipynb";
2153 2160 }
2154 2161 var data = {name: nbname};
2155 2162 var settings = {
2156 2163 processData : false,
2157 2164 cache : false,
2158 2165 type : "PATCH",
2159 2166 data : JSON.stringify(data),
2160 2167 dataType: "json",
2161 2168 contentType: 'application/json',
2162 2169 success : $.proxy(that.rename_success, this),
2163 2170 error : $.proxy(that.rename_error, this)
2164 2171 };
2165 2172 this.events.trigger('rename_notebook.Notebook', data);
2166 2173 var url = utils.url_join_encode(
2167 2174 this.base_url,
2168 2175 'api/contents',
2169 2176 this.notebook_path,
2170 2177 this.notebook_name
2171 2178 );
2172 2179 $.ajax(url, settings);
2173 2180 };
2174 2181
2175 2182 Notebook.prototype.delete = function () {
2176 2183 var that = this;
2177 2184 var settings = {
2178 2185 processData : false,
2179 2186 cache : false,
2180 2187 type : "DELETE",
2181 2188 dataType: "json",
2182 2189 error : utils.log_ajax_error,
2183 2190 };
2184 2191 var url = utils.url_join_encode(
2185 2192 this.base_url,
2186 2193 'api/contents',
2187 2194 this.notebook_path,
2188 2195 this.notebook_name
2189 2196 );
2190 2197 $.ajax(url, settings);
2191 2198 };
2192 2199
2193 2200
2194 2201 Notebook.prototype.rename_success = function (json, status, xhr) {
2195 2202 var name = this.notebook_name = json.name;
2196 2203 var path = json.path;
2197 2204 this.session.rename_notebook(name, path);
2198 2205 this.events.trigger('notebook_renamed.Notebook', json);
2199 2206 };
2200 2207
2201 2208 Notebook.prototype.rename_error = function (xhr, status, error) {
2202 2209 var that = this;
2203 2210 var dialog_body = $('<div/>').append(
2204 2211 $("<p/>").text('This notebook name already exists.')
2205 2212 );
2206 2213 this.events.trigger('notebook_rename_failed.Notebook', [xhr, status, error]);
2207 2214 dialog.modal({
2208 2215 notebook: this,
2209 2216 keyboard_manager: this.keyboard_manager,
2210 2217 title: "Notebook Rename Error!",
2211 2218 body: dialog_body,
2212 2219 buttons : {
2213 2220 "Cancel": {},
2214 2221 "OK": {
2215 2222 class: "btn-primary",
2216 2223 click: function () {
2217 2224 this.save_widget.rename_notebook({notebook:that});
2218 2225 }}
2219 2226 },
2220 2227 open : function (event, ui) {
2221 2228 var that = $(this);
2222 2229 // Upon ENTER, click the OK button.
2223 2230 that.find('input[type="text"]').keydown(function (event, ui) {
2224 2231 if (event.which === this.keyboard.keycodes.enter) {
2225 2232 that.find('.btn-primary').first().click();
2226 2233 }
2227 2234 });
2228 2235 that.find('input[type="text"]').focus();
2229 2236 }
2230 2237 });
2231 2238 };
2232 2239
2233 2240 /**
2234 2241 * Request a notebook's data from the server.
2235 2242 *
2236 2243 * @method load_notebook
2237 2244 * @param {String} notebook_name and path A notebook to load
2238 2245 */
2239 2246 Notebook.prototype.load_notebook = function (notebook_name, notebook_path) {
2240 2247 var that = this;
2241 2248 this.notebook_name = notebook_name;
2242 2249 this.notebook_path = notebook_path;
2243 2250 // We do the call with settings so we can set cache to false.
2244 2251 var settings = {
2245 2252 processData : false,
2246 2253 cache : false,
2247 2254 type : "GET",
2248 2255 dataType : "json",
2249 2256 success : $.proxy(this.load_notebook_success,this),
2250 2257 error : $.proxy(this.load_notebook_error,this),
2251 2258 };
2252 2259 this.events.trigger('notebook_loading.Notebook');
2253 2260 var url = utils.url_join_encode(
2254 2261 this.base_url,
2255 2262 'api/contents',
2256 2263 this.notebook_path,
2257 2264 this.notebook_name
2258 2265 );
2259 2266 $.ajax(url, settings);
2260 2267 };
2261 2268
2262 2269 /**
2263 2270 * Success callback for loading a notebook from the server.
2264 2271 *
2265 2272 * Load notebook data from the JSON response.
2266 2273 *
2267 2274 * @method load_notebook_success
2268 2275 * @param {Object} data JSON representation of a notebook
2269 2276 * @param {String} status Description of response status
2270 2277 * @param {jqXHR} xhr jQuery Ajax object
2271 2278 */
2272 2279 Notebook.prototype.load_notebook_success = function (data, status, xhr) {
2273 2280 var failed;
2274 2281 try {
2275 2282 this.fromJSON(data);
2276 2283 } catch (e) {
2277 2284 failed = e;
2278 2285 console.log("Notebook failed to load from JSON:", e);
2279 2286 }
2280 2287 if (failed || data.message) {
2281 2288 // *either* fromJSON failed or validation failed
2282 2289 var body = $("<div>");
2283 2290 var title;
2284 2291 if (failed) {
2285 2292 title = "Notebook failed to load";
2286 2293 body.append($("<p>").text(
2287 2294 "The error was: "
2288 2295 )).append($("<div>").addClass("js-error").text(
2289 2296 failed.toString()
2290 2297 )).append($("<p>").text(
2291 2298 "See the error console for details."
2292 2299 ));
2293 2300 } else {
2294 2301 title = "Notebook validation failed";
2295 2302 }
2296 2303
2297 2304 if (data.message) {
2298 2305 var msg;
2299 2306 if (failed) {
2300 2307 msg = "The notebook also failed validation:"
2301 2308 } else {
2302 2309 msg = "An invalid notebook may not function properly." +
2303 2310 " The validation error was:"
2304 2311 }
2305 2312 body.append($("<p>").text(
2306 2313 msg
2307 2314 )).append($("<div>").addClass("validation-error").append(
2308 2315 $("<pre>").text(data.message)
2309 2316 ));
2310 2317 }
2311 2318
2312 2319 dialog.modal({
2313 2320 notebook: this,
2314 2321 keyboard_manager: this.keyboard_manager,
2315 2322 title: title,
2316 2323 body: body,
2317 2324 buttons : {
2318 2325 OK : {
2319 2326 "class" : "btn-primary"
2320 2327 }
2321 2328 }
2322 2329 });
2323 2330 }
2324 2331 if (this.ncells() === 0) {
2325 2332 this.insert_cell_below('code');
2326 2333 this.edit_mode(0);
2327 2334 } else {
2328 2335 this.select(0);
2329 2336 this.handle_command_mode(this.get_cell(0));
2330 2337 }
2331 2338 this.set_dirty(false);
2332 2339 this.scroll_to_top();
2333 2340 if (data.orig_nbformat !== undefined && data.nbformat !== data.orig_nbformat) {
2334 2341 var msg = "This notebook has been converted from an older " +
2335 2342 "notebook format (v"+data.orig_nbformat+") to the current notebook " +
2336 2343 "format (v"+data.nbformat+"). The next time you save this notebook, the " +
2337 2344 "newer notebook format will be used and older versions of IPython " +
2338 2345 "may not be able to read it. To keep the older version, close the " +
2339 2346 "notebook without saving it.";
2340 2347 dialog.modal({
2341 2348 notebook: this,
2342 2349 keyboard_manager: this.keyboard_manager,
2343 2350 title : "Notebook converted",
2344 2351 body : msg,
2345 2352 buttons : {
2346 2353 OK : {
2347 2354 class : "btn-primary"
2348 2355 }
2349 2356 }
2350 2357 });
2351 2358 } else if (data.orig_nbformat_minor !== undefined && data.nbformat_minor !== data.orig_nbformat_minor) {
2352 2359 var that = this;
2353 2360 var orig_vs = 'v' + data.nbformat + '.' + data.orig_nbformat_minor;
2354 2361 var this_vs = 'v' + data.nbformat + '.' + this.nbformat_minor;
2355 2362 var msg = "This notebook is version " + orig_vs + ", but we only fully support up to " +
2356 2363 this_vs + ". You can still work with this notebook, but some features " +
2357 2364 "introduced in later notebook versions may not be available.";
2358 2365
2359 2366 dialog.modal({
2360 2367 notebook: this,
2361 2368 keyboard_manager: this.keyboard_manager,
2362 2369 title : "Newer Notebook",
2363 2370 body : msg,
2364 2371 buttons : {
2365 2372 OK : {
2366 2373 class : "btn-danger"
2367 2374 }
2368 2375 }
2369 2376 });
2370 2377
2371 2378 }
2372 2379
2373 2380 // Create the session after the notebook is completely loaded to prevent
2374 2381 // code execution upon loading, which is a security risk.
2375 2382 if (this.session === null) {
2376 2383 var kernelspec = this.metadata.kernelspec || {};
2377 2384 var kernel_name = kernelspec.name;
2378 2385
2379 2386 this.start_session(kernel_name);
2380 2387 }
2381 2388 // load our checkpoint list
2382 2389 this.list_checkpoints();
2383 2390
2384 2391 // load toolbar state
2385 2392 if (this.metadata.celltoolbar) {
2386 2393 celltoolbar.CellToolbar.global_show();
2387 2394 celltoolbar.CellToolbar.activate_preset(this.metadata.celltoolbar);
2388 2395 } else {
2389 2396 celltoolbar.CellToolbar.global_hide();
2390 2397 }
2391 2398
2392 2399 // now that we're fully loaded, it is safe to restore save functionality
2393 2400 delete(this.save_notebook);
2394 2401 this.events.trigger('notebook_loaded.Notebook');
2395 2402 };
2396 2403
2397 2404 /**
2398 2405 * Failure callback for loading a notebook from the server.
2399 2406 *
2400 2407 * @method load_notebook_error
2401 2408 * @param {jqXHR} xhr jQuery Ajax object
2402 2409 * @param {String} status Description of response status
2403 2410 * @param {String} error HTTP error message
2404 2411 */
2405 2412 Notebook.prototype.load_notebook_error = function (xhr, status, error) {
2406 2413 this.events.trigger('notebook_load_failed.Notebook', [xhr, status, error]);
2407 2414 utils.log_ajax_error(xhr, status, error);
2408 2415 var msg;
2409 2416 if (xhr.status === 400) {
2410 2417 msg = escape(utils.ajax_error_msg(xhr));
2411 2418 } else if (xhr.status === 500) {
2412 2419 msg = "An unknown error occurred while loading this notebook. " +
2413 2420 "This version can load notebook formats " +
2414 2421 "v" + this.nbformat + " or earlier. See the server log for details.";
2415 2422 }
2416 2423 dialog.modal({
2417 2424 notebook: this,
2418 2425 keyboard_manager: this.keyboard_manager,
2419 2426 title: "Error loading notebook",
2420 2427 body : msg,
2421 2428 buttons : {
2422 2429 "OK": {}
2423 2430 }
2424 2431 });
2425 2432 };
2426 2433
2427 2434 /********************* checkpoint-related *********************/
2428 2435
2429 2436 /**
2430 2437 * Save the notebook then immediately create a checkpoint.
2431 2438 *
2432 2439 * @method save_checkpoint
2433 2440 */
2434 2441 Notebook.prototype.save_checkpoint = function () {
2435 2442 this._checkpoint_after_save = true;
2436 2443 this.save_notebook();
2437 2444 };
2438 2445
2439 2446 /**
2440 2447 * Add a checkpoint for this notebook.
2441 2448 * for use as a callback from checkpoint creation.
2442 2449 *
2443 2450 * @method add_checkpoint
2444 2451 */
2445 2452 Notebook.prototype.add_checkpoint = function (checkpoint) {
2446 2453 var found = false;
2447 2454 for (var i = 0; i < this.checkpoints.length; i++) {
2448 2455 var existing = this.checkpoints[i];
2449 2456 if (existing.id == checkpoint.id) {
2450 2457 found = true;
2451 2458 this.checkpoints[i] = checkpoint;
2452 2459 break;
2453 2460 }
2454 2461 }
2455 2462 if (!found) {
2456 2463 this.checkpoints.push(checkpoint);
2457 2464 }
2458 2465 this.last_checkpoint = this.checkpoints[this.checkpoints.length - 1];
2459 2466 };
2460 2467
2461 2468 /**
2462 2469 * List checkpoints for this notebook.
2463 2470 *
2464 2471 * @method list_checkpoints
2465 2472 */
2466 2473 Notebook.prototype.list_checkpoints = function () {
2467 2474 var url = utils.url_join_encode(
2468 2475 this.base_url,
2469 2476 'api/contents',
2470 2477 this.notebook_path,
2471 2478 this.notebook_name,
2472 2479 'checkpoints'
2473 2480 );
2474 2481 $.get(url).done(
2475 2482 $.proxy(this.list_checkpoints_success, this)
2476 2483 ).fail(
2477 2484 $.proxy(this.list_checkpoints_error, this)
2478 2485 );
2479 2486 };
2480 2487
2481 2488 /**
2482 2489 * Success callback for listing checkpoints.
2483 2490 *
2484 2491 * @method list_checkpoint_success
2485 2492 * @param {Object} data JSON representation of a checkpoint
2486 2493 * @param {String} status Description of response status
2487 2494 * @param {jqXHR} xhr jQuery Ajax object
2488 2495 */
2489 2496 Notebook.prototype.list_checkpoints_success = function (data, status, xhr) {
2490 2497 data = $.parseJSON(data);
2491 2498 this.checkpoints = data;
2492 2499 if (data.length) {
2493 2500 this.last_checkpoint = data[data.length - 1];
2494 2501 } else {
2495 2502 this.last_checkpoint = null;
2496 2503 }
2497 2504 this.events.trigger('checkpoints_listed.Notebook', [data]);
2498 2505 };
2499 2506
2500 2507 /**
2501 2508 * Failure callback for listing a checkpoint.
2502 2509 *
2503 2510 * @method list_checkpoint_error
2504 2511 * @param {jqXHR} xhr jQuery Ajax object
2505 2512 * @param {String} status Description of response status
2506 2513 * @param {String} error_msg HTTP error message
2507 2514 */
2508 2515 Notebook.prototype.list_checkpoints_error = function (xhr, status, error_msg) {
2509 2516 this.events.trigger('list_checkpoints_failed.Notebook');
2510 2517 };
2511 2518
2512 2519 /**
2513 2520 * Create a checkpoint of this notebook on the server from the most recent save.
2514 2521 *
2515 2522 * @method create_checkpoint
2516 2523 */
2517 2524 Notebook.prototype.create_checkpoint = function () {
2518 2525 var url = utils.url_join_encode(
2519 2526 this.base_url,
2520 2527 'api/contents',
2521 2528 this.notebook_path,
2522 2529 this.notebook_name,
2523 2530 'checkpoints'
2524 2531 );
2525 2532 $.post(url).done(
2526 2533 $.proxy(this.create_checkpoint_success, this)
2527 2534 ).fail(
2528 2535 $.proxy(this.create_checkpoint_error, this)
2529 2536 );
2530 2537 };
2531 2538
2532 2539 /**
2533 2540 * Success callback for creating a checkpoint.
2534 2541 *
2535 2542 * @method create_checkpoint_success
2536 2543 * @param {Object} data JSON representation of a checkpoint
2537 2544 * @param {String} status Description of response status
2538 2545 * @param {jqXHR} xhr jQuery Ajax object
2539 2546 */
2540 2547 Notebook.prototype.create_checkpoint_success = function (data, status, xhr) {
2541 2548 data = $.parseJSON(data);
2542 2549 this.add_checkpoint(data);
2543 2550 this.events.trigger('checkpoint_created.Notebook', data);
2544 2551 };
2545 2552
2546 2553 /**
2547 2554 * Failure callback for creating a checkpoint.
2548 2555 *
2549 2556 * @method create_checkpoint_error
2550 2557 * @param {jqXHR} xhr jQuery Ajax object
2551 2558 * @param {String} status Description of response status
2552 2559 * @param {String} error_msg HTTP error message
2553 2560 */
2554 2561 Notebook.prototype.create_checkpoint_error = function (xhr, status, error_msg) {
2555 2562 this.events.trigger('checkpoint_failed.Notebook');
2556 2563 };
2557 2564
2558 2565 Notebook.prototype.restore_checkpoint_dialog = function (checkpoint) {
2559 2566 var that = this;
2560 2567 checkpoint = checkpoint || this.last_checkpoint;
2561 2568 if ( ! checkpoint ) {
2562 2569 console.log("restore dialog, but no checkpoint to restore to!");
2563 2570 return;
2564 2571 }
2565 2572 var body = $('<div/>').append(
2566 2573 $('<p/>').addClass("p-space").text(
2567 2574 "Are you sure you want to revert the notebook to " +
2568 2575 "the latest checkpoint?"
2569 2576 ).append(
2570 2577 $("<strong/>").text(
2571 2578 " This cannot be undone."
2572 2579 )
2573 2580 )
2574 2581 ).append(
2575 2582 $('<p/>').addClass("p-space").text("The checkpoint was last updated at:")
2576 2583 ).append(
2577 2584 $('<p/>').addClass("p-space").text(
2578 2585 Date(checkpoint.last_modified)
2579 2586 ).css("text-align", "center")
2580 2587 );
2581 2588
2582 2589 dialog.modal({
2583 2590 notebook: this,
2584 2591 keyboard_manager: this.keyboard_manager,
2585 2592 title : "Revert notebook to checkpoint",
2586 2593 body : body,
2587 2594 buttons : {
2588 2595 Revert : {
2589 2596 class : "btn-danger",
2590 2597 click : function () {
2591 2598 that.restore_checkpoint(checkpoint.id);
2592 2599 }
2593 2600 },
2594 2601 Cancel : {}
2595 2602 }
2596 2603 });
2597 2604 };
2598 2605
2599 2606 /**
2600 2607 * Restore the notebook to a checkpoint state.
2601 2608 *
2602 2609 * @method restore_checkpoint
2603 2610 * @param {String} checkpoint ID
2604 2611 */
2605 2612 Notebook.prototype.restore_checkpoint = function (checkpoint) {
2606 2613 this.events.trigger('notebook_restoring.Notebook', checkpoint);
2607 2614 var url = utils.url_join_encode(
2608 2615 this.base_url,
2609 2616 'api/contents',
2610 2617 this.notebook_path,
2611 2618 this.notebook_name,
2612 2619 'checkpoints',
2613 2620 checkpoint
2614 2621 );
2615 2622 $.post(url).done(
2616 2623 $.proxy(this.restore_checkpoint_success, this)
2617 2624 ).fail(
2618 2625 $.proxy(this.restore_checkpoint_error, this)
2619 2626 );
2620 2627 };
2621 2628
2622 2629 /**
2623 2630 * Success callback for restoring a notebook to a checkpoint.
2624 2631 *
2625 2632 * @method restore_checkpoint_success
2626 2633 * @param {Object} data (ignored, should be empty)
2627 2634 * @param {String} status Description of response status
2628 2635 * @param {jqXHR} xhr jQuery Ajax object
2629 2636 */
2630 2637 Notebook.prototype.restore_checkpoint_success = function (data, status, xhr) {
2631 2638 this.events.trigger('checkpoint_restored.Notebook');
2632 2639 this.load_notebook(this.notebook_name, this.notebook_path);
2633 2640 };
2634 2641
2635 2642 /**
2636 2643 * Failure callback for restoring a notebook to a checkpoint.
2637 2644 *
2638 2645 * @method restore_checkpoint_error
2639 2646 * @param {jqXHR} xhr jQuery Ajax object
2640 2647 * @param {String} status Description of response status
2641 2648 * @param {String} error_msg HTTP error message
2642 2649 */
2643 2650 Notebook.prototype.restore_checkpoint_error = function (xhr, status, error_msg) {
2644 2651 this.events.trigger('checkpoint_restore_failed.Notebook');
2645 2652 };
2646 2653
2647 2654 /**
2648 2655 * Delete a notebook checkpoint.
2649 2656 *
2650 2657 * @method delete_checkpoint
2651 2658 * @param {String} checkpoint ID
2652 2659 */
2653 2660 Notebook.prototype.delete_checkpoint = function (checkpoint) {
2654 2661 this.events.trigger('notebook_restoring.Notebook', checkpoint);
2655 2662 var url = utils.url_join_encode(
2656 2663 this.base_url,
2657 2664 'api/contents',
2658 2665 this.notebook_path,
2659 2666 this.notebook_name,
2660 2667 'checkpoints',
2661 2668 checkpoint
2662 2669 );
2663 2670 $.ajax(url, {
2664 2671 type: 'DELETE',
2665 2672 success: $.proxy(this.delete_checkpoint_success, this),
2666 2673 error: $.proxy(this.delete_checkpoint_error, this)
2667 2674 });
2668 2675 };
2669 2676
2670 2677 /**
2671 2678 * Success callback for deleting a notebook checkpoint
2672 2679 *
2673 2680 * @method delete_checkpoint_success
2674 2681 * @param {Object} data (ignored, should be empty)
2675 2682 * @param {String} status Description of response status
2676 2683 * @param {jqXHR} xhr jQuery Ajax object
2677 2684 */
2678 2685 Notebook.prototype.delete_checkpoint_success = function (data, status, xhr) {
2679 2686 this.events.trigger('checkpoint_deleted.Notebook', data);
2680 2687 this.load_notebook(this.notebook_name, this.notebook_path);
2681 2688 };
2682 2689
2683 2690 /**
2684 2691 * Failure callback for deleting a notebook checkpoint.
2685 2692 *
2686 2693 * @method delete_checkpoint_error
2687 2694 * @param {jqXHR} xhr jQuery Ajax object
2688 2695 * @param {String} status Description of response status
2689 2696 * @param {String} error HTTP error message
2690 2697 */
2691 2698 Notebook.prototype.delete_checkpoint_error = function (xhr, status, error) {
2692 2699 this.events.trigger('checkpoint_delete_failed.Notebook', [xhr, status, error]);
2693 2700 };
2694 2701
2695 2702
2696 2703 // For backwards compatability.
2697 2704 IPython.Notebook = Notebook;
2698 2705
2699 2706 return {'Notebook': Notebook};
2700 2707 });
@@ -1,1026 +1,1029 b''
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 './comm',
9 9 './serialize',
10 10 'widgets/js/init'
11 11 ], function(IPython, $, utils, comm, serialize, widgetmanager) {
12 12 "use strict";
13 13
14 14 /**
15 15 * A Kernel class to communicate with the Python kernel. This
16 16 * should generally not be constructed directly, but be created
17 17 * by. the `Session` object. Once created, this object should be
18 18 * used to communicate with the kernel.
19 19 *
20 20 * @class Kernel
21 21 * @param {string} kernel_service_url - the URL to access the kernel REST api
22 22 * @param {string} ws_url - the websockets URL
23 23 * @param {Notebook} notebook - notebook object
24 24 * @param {string} name - the kernel type (e.g. python3)
25 25 */
26 26 var Kernel = function (kernel_service_url, ws_url, notebook, name) {
27 27 this.events = notebook.events;
28 28
29 29 this.id = null;
30 30 this.name = name;
31 31
32 32 this.channels = {
33 33 'shell': null,
34 34 'iopub': null,
35 35 'stdin': null
36 36 };
37 37
38 38 this.kernel_service_url = kernel_service_url;
39 39 this.kernel_url = null;
40 40 this.ws_url = ws_url || IPython.utils.get_body_data("wsUrl");
41 41 if (!this.ws_url) {
42 42 // trailing 's' in https will become wss for secure web sockets
43 43 this.ws_url = location.protocol.replace('http', 'ws') + "//" + location.host;
44 44 }
45 45
46 46 this.username = "username";
47 47 this.session_id = utils.uuid();
48 48 this._msg_callbacks = {};
49 this.info_reply = {}; // kernel_info_reply stored here after starting
49 50
50 51 if (typeof(WebSocket) !== 'undefined') {
51 52 this.WebSocket = WebSocket;
52 53 } else if (typeof(MozWebSocket) !== 'undefined') {
53 54 this.WebSocket = MozWebSocket;
54 55 } else {
55 56 alert('Your browser does not have WebSocket support, please try Chrome, Safari or Firefox β‰₯ 6. Firefox 4 and 5 are also supported by you have to enable WebSockets in about:config.');
56 57 }
57 58
58 59 this.bind_events();
59 60 this.init_iopub_handlers();
60 61 this.comm_manager = new comm.CommManager(this);
61 62 this.widget_manager = new widgetmanager.WidgetManager(this.comm_manager, notebook);
62 63
63 64 this.last_msg_id = null;
64 65 this.last_msg_callbacks = {};
65 66
66 67 this._autorestart_attempt = 0;
67 68 this._reconnect_attempt = 0;
68 69 };
69 70
70 71 /**
71 72 * @function _get_msg
72 73 */
73 74 Kernel.prototype._get_msg = function (msg_type, content, metadata, buffers) {
74 75 var msg = {
75 76 header : {
76 77 msg_id : utils.uuid(),
77 78 username : this.username,
78 79 session : this.session_id,
79 80 msg_type : msg_type,
80 81 version : "5.0"
81 82 },
82 83 metadata : metadata || {},
83 84 content : content,
84 85 buffers : buffers || [],
85 86 parent_header : {}
86 87 };
87 88 return msg;
88 89 };
89 90
90 91 /**
91 92 * @function bind_events
92 93 */
93 94 Kernel.prototype.bind_events = function () {
94 95 var that = this;
95 96 this.events.on('send_input_reply.Kernel', function(evt, data) {
96 97 that.send_input_reply(data);
97 98 });
98 99
99 100 var record_status = function (evt, info) {
100 101 console.log('Kernel: ' + evt.type + ' (' + info.kernel.id + ')');
101 102 };
102 103
103 104 this.events.on('kernel_created.Kernel', record_status);
104 105 this.events.on('kernel_reconnecting.Kernel', record_status);
105 106 this.events.on('kernel_connected.Kernel', record_status);
106 107 this.events.on('kernel_starting.Kernel', record_status);
107 108 this.events.on('kernel_restarting.Kernel', record_status);
108 109 this.events.on('kernel_autorestarting.Kernel', record_status);
109 110 this.events.on('kernel_interrupting.Kernel', record_status);
110 111 this.events.on('kernel_disconnected.Kernel', record_status);
111 112 // these are commented out because they are triggered a lot, but can
112 113 // be uncommented for debugging purposes
113 114 //this.events.on('kernel_idle.Kernel', record_status);
114 115 //this.events.on('kernel_busy.Kernel', record_status);
115 116 this.events.on('kernel_ready.Kernel', record_status);
116 117 this.events.on('kernel_killed.Kernel', record_status);
117 118 this.events.on('kernel_dead.Kernel', record_status);
118 119
119 120 this.events.on('kernel_ready.Kernel', function () {
120 121 that._autorestart_attempt = 0;
121 122 });
122 123 this.events.on('kernel_connected.Kernel', function () {
123 124 that._reconnect_attempt = 0;
124 125 });
125 126 };
126 127
127 128 /**
128 129 * Initialize the iopub handlers.
129 130 *
130 131 * @function init_iopub_handlers
131 132 */
132 133 Kernel.prototype.init_iopub_handlers = function () {
133 134 var output_msg_types = ['stream', 'display_data', 'execute_result', 'error'];
134 135 this._iopub_handlers = {};
135 136 this.register_iopub_handler('status', $.proxy(this._handle_status_message, this));
136 137 this.register_iopub_handler('clear_output', $.proxy(this._handle_clear_output, this));
137 138
138 139 for (var i=0; i < output_msg_types.length; i++) {
139 140 this.register_iopub_handler(output_msg_types[i], $.proxy(this._handle_output_message, this));
140 141 }
141 142 };
142 143
143 144 /**
144 145 * GET /api/kernels
145 146 *
146 147 * Get the list of running kernels.
147 148 *
148 149 * @function list
149 150 * @param {function} [success] - function executed on ajax success
150 151 * @param {function} [error] - functon executed on ajax error
151 152 */
152 153 Kernel.prototype.list = function (success, error) {
153 154 $.ajax(this.kernel_service_url, {
154 155 processData: false,
155 156 cache: false,
156 157 type: "GET",
157 158 dataType: "json",
158 159 success: success,
159 160 error: this._on_error(error)
160 161 });
161 162 };
162 163
163 164 /**
164 165 * POST /api/kernels
165 166 *
166 167 * Start a new kernel.
167 168 *
168 169 * In general this shouldn't be used -- the kernel should be
169 170 * started through the session API. If you use this function and
170 171 * are also using the session API then your session and kernel
171 172 * WILL be out of sync!
172 173 *
173 174 * @function start
174 175 * @param {params} [Object] - parameters to include in the query string
175 176 * @param {function} [success] - function executed on ajax success
176 177 * @param {function} [error] - functon executed on ajax error
177 178 */
178 179 Kernel.prototype.start = function (params, success, error) {
179 180 var url = this.kernel_service_url;
180 181 var qs = $.param(params || {}); // query string for sage math stuff
181 182 if (qs !== "") {
182 183 url = url + "?" + qs;
183 184 }
184 185
185 186 var that = this;
186 187 var on_success = function (data, status, xhr) {
187 188 that.events.trigger('kernel_created.Kernel', {kernel: that});
188 189 that._kernel_created(data);
189 190 if (success) {
190 191 success(data, status, xhr);
191 192 }
192 193 };
193 194
194 195 $.ajax(url, {
195 196 processData: false,
196 197 cache: false,
197 198 type: "POST",
198 199 data: JSON.stringify({name: this.name}),
199 200 dataType: "json",
200 201 success: this._on_success(on_success),
201 202 error: this._on_error(error)
202 203 });
203 204
204 205 return url;
205 206 };
206 207
207 208 /**
208 209 * GET /api/kernels/[:kernel_id]
209 210 *
210 211 * Get information about the kernel.
211 212 *
212 213 * @function get_info
213 214 * @param {function} [success] - function executed on ajax success
214 215 * @param {function} [error] - functon executed on ajax error
215 216 */
216 217 Kernel.prototype.get_info = function (success, error) {
217 218 $.ajax(this.kernel_url, {
218 219 processData: false,
219 220 cache: false,
220 221 type: "GET",
221 222 dataType: "json",
222 223 success: this._on_success(success),
223 224 error: this._on_error(error)
224 225 });
225 226 };
226 227
227 228 /**
228 229 * DELETE /api/kernels/[:kernel_id]
229 230 *
230 231 * Shutdown the kernel.
231 232 *
232 233 * If you are also using sessions, then this function shoul NOT be
233 234 * used. Instead, use Session.delete. Otherwise, the session and
234 235 * kernel WILL be out of sync.
235 236 *
236 237 * @function kill
237 238 * @param {function} [success] - function executed on ajax success
238 239 * @param {function} [error] - functon executed on ajax error
239 240 */
240 241 Kernel.prototype.kill = function (success, error) {
241 242 this.events.trigger('kernel_killed.Kernel', {kernel: this});
242 243 this._kernel_dead();
243 244 $.ajax(this.kernel_url, {
244 245 processData: false,
245 246 cache: false,
246 247 type: "DELETE",
247 248 dataType: "json",
248 249 success: this._on_success(success),
249 250 error: this._on_error(error)
250 251 });
251 252 };
252 253
253 254 /**
254 255 * POST /api/kernels/[:kernel_id]/interrupt
255 256 *
256 257 * Interrupt the kernel.
257 258 *
258 259 * @function interrupt
259 260 * @param {function} [success] - function executed on ajax success
260 261 * @param {function} [error] - functon executed on ajax error
261 262 */
262 263 Kernel.prototype.interrupt = function (success, error) {
263 264 this.events.trigger('kernel_interrupting.Kernel', {kernel: this});
264 265
265 266 var that = this;
266 267 var on_success = function (data, status, xhr) {
267 268 // get kernel info so we know what state the kernel is in
268 269 that.kernel_info();
269 270 if (success) {
270 271 success(data, status, xhr);
271 272 }
272 273 };
273 274
274 275 var url = utils.url_join_encode(this.kernel_url, 'interrupt');
275 276 $.ajax(url, {
276 277 processData: false,
277 278 cache: false,
278 279 type: "POST",
279 280 dataType: "json",
280 281 success: this._on_success(on_success),
281 282 error: this._on_error(error)
282 283 });
283 284 };
284 285
285 286 /**
286 287 * POST /api/kernels/[:kernel_id]/restart
287 288 *
288 289 * Restart the kernel.
289 290 *
290 291 * @function interrupt
291 292 * @param {function} [success] - function executed on ajax success
292 293 * @param {function} [error] - functon executed on ajax error
293 294 */
294 295 Kernel.prototype.restart = function (success, error) {
295 296 this.events.trigger('kernel_restarting.Kernel', {kernel: this});
296 297 this.stop_channels();
297 298
298 299 var that = this;
299 300 var on_success = function (data, status, xhr) {
300 301 that.events.trigger('kernel_created.Kernel', {kernel: that});
301 302 that._kernel_created(data);
302 303 if (success) {
303 304 success(data, status, xhr);
304 305 }
305 306 };
306 307
307 308 var on_error = function (xhr, status, err) {
308 309 that.events.trigger('kernel_dead.Kernel', {kernel: that});
309 310 that._kernel_dead();
310 311 if (error) {
311 312 error(xhr, status, err);
312 313 }
313 314 };
314 315
315 316 var url = utils.url_join_encode(this.kernel_url, 'restart');
316 317 $.ajax(url, {
317 318 processData: false,
318 319 cache: false,
319 320 type: "POST",
320 321 dataType: "json",
321 322 success: this._on_success(on_success),
322 323 error: this._on_error(on_error)
323 324 });
324 325 };
325 326
326 327 /**
327 328 * Reconnect to a disconnected kernel. This is not actually a
328 329 * standard HTTP request, but useful function nonetheless for
329 330 * reconnecting to the kernel if the connection is somehow lost.
330 331 *
331 332 * @function reconnect
332 333 */
333 334 Kernel.prototype.reconnect = function () {
334 335 this.events.trigger('kernel_reconnecting.Kernel', {kernel: this});
335 336 setTimeout($.proxy(this.start_channels, this), 3000);
336 337 };
337 338
338 339 /**
339 340 * Handle a successful AJAX request by updating the kernel id and
340 341 * name from the response, and then optionally calling a provided
341 342 * callback.
342 343 *
343 344 * @function _on_success
344 345 * @param {function} success - callback
345 346 */
346 347 Kernel.prototype._on_success = function (success) {
347 348 var that = this;
348 349 return function (data, status, xhr) {
349 350 if (data) {
350 351 that.id = data.id;
351 352 that.name = data.name;
352 353 }
353 354 that.kernel_url = utils.url_join_encode(that.kernel_service_url, that.id);
354 355 if (success) {
355 356 success(data, status, xhr);
356 357 }
357 358 };
358 359 };
359 360
360 361 /**
361 362 * Handle a failed AJAX request by logging the error message, and
362 363 * then optionally calling a provided callback.
363 364 *
364 365 * @function _on_error
365 366 * @param {function} error - callback
366 367 */
367 368 Kernel.prototype._on_error = function (error) {
368 369 return function (xhr, status, err) {
369 370 utils.log_ajax_error(xhr, status, err);
370 371 if (error) {
371 372 error(xhr, status, err);
372 373 }
373 374 };
374 375 };
375 376
376 377 /**
377 378 * Perform necessary tasks once the kernel has been started,
378 379 * including actually connecting to the kernel.
379 380 *
380 381 * @function _kernel_created
381 382 * @param {Object} data - information about the kernel including id
382 383 */
383 384 Kernel.prototype._kernel_created = function (data) {
384 385 this.id = data.id;
385 386 this.kernel_url = utils.url_join_encode(this.kernel_service_url, this.id);
386 387 this.start_channels();
387 388 };
388 389
389 390 /**
390 391 * Perform necessary tasks once the connection to the kernel has
391 392 * been established. This includes requesting information about
392 393 * the kernel.
393 394 *
394 395 * @function _kernel_connected
395 396 */
396 397 Kernel.prototype._kernel_connected = function () {
397 398 this.events.trigger('kernel_connected.Kernel', {kernel: this});
398 399 this.events.trigger('kernel_starting.Kernel', {kernel: this});
399 400 // get kernel info so we know what state the kernel is in
400 401 var that = this;
401 this.kernel_info(function () {
402 this.kernel_info(function (reply) {
403 that.info_reply = reply.content;
402 404 that.events.trigger('kernel_ready.Kernel', {kernel: that});
403 405 });
404 406 };
405 407
406 408 /**
407 409 * Perform necessary tasks after the kernel has died. This closing
408 410 * communication channels to the kernel if they are still somehow
409 411 * open.
410 412 *
411 413 * @function _kernel_dead
412 414 */
413 415 Kernel.prototype._kernel_dead = function () {
414 416 this.stop_channels();
415 417 };
416 418
417 419 /**
418 420 * Start the `shell`and `iopub` channels.
419 421 * Will stop and restart them if they already exist.
420 422 *
421 423 * @function start_channels
422 424 */
423 425 Kernel.prototype.start_channels = function () {
424 426 var that = this;
425 427 this.stop_channels();
426 428 var ws_host_url = this.ws_url + this.kernel_url;
427 429
428 430 console.log("Starting WebSockets:", ws_host_url);
429 431
430 432 var channel_url = function(channel) {
431 433 return [
432 434 that.ws_url,
433 435 utils.url_join_encode(that.kernel_url, channel),
434 436 "?session_id=" + that.session_id
435 437 ].join('');
436 438 };
437 439 this.channels.shell = new this.WebSocket(channel_url("shell"));
438 440 this.channels.stdin = new this.WebSocket(channel_url("stdin"));
439 441 this.channels.iopub = new this.WebSocket(channel_url("iopub"));
440 442
441 443 var already_called_onclose = false; // only alert once
442 444 var ws_closed_early = function(evt){
443 445 if (already_called_onclose){
444 446 return;
445 447 }
446 448 already_called_onclose = true;
447 449 if ( ! evt.wasClean ){
448 450 // If the websocket was closed early, that could mean
449 451 // that the kernel is actually dead. Try getting
450 452 // information about the kernel from the API call --
451 453 // if that fails, then assume the kernel is dead,
452 454 // otherwise just follow the typical websocket closed
453 455 // protocol.
454 456 that.get_info(function () {
455 457 that._ws_closed(ws_host_url, false);
456 458 }, function () {
457 459 that.events.trigger('kernel_dead.Kernel', {kernel: that});
458 460 that._kernel_dead();
459 461 });
460 462 }
461 463 };
462 464 var ws_closed_late = function(evt){
463 465 if (already_called_onclose){
464 466 return;
465 467 }
466 468 already_called_onclose = true;
467 469 if ( ! evt.wasClean ){
468 470 that._ws_closed(ws_host_url, false);
469 471 }
470 472 };
471 473 var ws_error = function(evt){
472 474 if (already_called_onclose){
473 475 return;
474 476 }
475 477 already_called_onclose = true;
476 478 that._ws_closed(ws_host_url, true);
477 479 };
478 480
479 481 for (var c in this.channels) {
480 482 this.channels[c].onopen = $.proxy(this._ws_opened, this);
481 483 this.channels[c].onclose = ws_closed_early;
482 484 this.channels[c].onerror = ws_error;
483 485 }
484 486 // switch from early-close to late-close message after 1s
485 487 setTimeout(function() {
486 488 for (var c in that.channels) {
487 489 if (that.channels[c] !== null) {
488 490 that.channels[c].onclose = ws_closed_late;
489 491 }
490 492 }
491 493 }, 1000);
492 494 this.channels.shell.onmessage = $.proxy(this._handle_shell_reply, this);
493 495 this.channels.iopub.onmessage = $.proxy(this._handle_iopub_message, this);
494 496 this.channels.stdin.onmessage = $.proxy(this._handle_input_request, this);
495 497 };
496 498
497 499 /**
498 500 * Handle a websocket entering the open state,
499 501 * signaling that the kernel is connected when all channels are open.
500 502 *
501 503 * @function _ws_opened
502 504 */
503 505 Kernel.prototype._ws_opened = function (evt) {
504 506 if (this.is_connected()) {
505 507 // all events ready, trigger started event.
506 508 this._kernel_connected();
507 509 }
508 510 };
509 511
510 512 /**
511 513 * Handle a websocket entering the closed state. This closes the
512 514 * other communication channels if they are open. If the websocket
513 515 * was not closed due to an error, try to reconnect to the kernel.
514 516 *
515 517 * @function _ws_closed
516 518 * @param {string} ws_url - the websocket url
517 519 * @param {bool} error - whether the connection was closed due to an error
518 520 */
519 521 Kernel.prototype._ws_closed = function(ws_url, error) {
520 522 this.stop_channels();
521 523
522 524 this.events.trigger('kernel_disconnected.Kernel', {kernel: this});
523 525 if (error) {
524 526 console.log('WebSocket connection failed: ', ws_url);
525 527 this._reconnect_attempt = this._reconnect_attempt + 1;
526 528 this.events.trigger('kernel_connection_failed.Kernel', {kernel: this, ws_url: ws_url, attempt: this._reconnect_attempt});
527 529 }
528 530 this.reconnect();
529 531 };
530 532
531 533 /**
532 534 * Close the websocket channels. After successful close, the value
533 535 * in `this.channels[channel_name]` will be null.
534 536 *
535 537 * @function stop_channels
536 538 */
537 539 Kernel.prototype.stop_channels = function () {
538 540 var that = this;
539 541 var close = function (c) {
540 542 return function () {
541 543 if (that.channels[c] && that.channels[c].readyState === WebSocket.CLOSED) {
542 544 that.channels[c] = null;
543 545 }
544 546 };
545 547 };
546 548 for (var c in this.channels) {
547 549 if ( this.channels[c] !== null ) {
548 550 if (this.channels[c].readyState === WebSocket.OPEN) {
549 551 this.channels[c].onclose = close(c);
550 552 this.channels[c].close();
551 553 } else {
552 554 close(c)();
553 555 }
554 556 }
555 557 }
556 558 };
557 559
558 560 /**
559 561 * Check whether there is a connection to the kernel. This
560 562 * function only returns true if all channel objects have been
561 563 * created and have a state of WebSocket.OPEN.
562 564 *
563 565 * @function is_connected
564 566 * @returns {bool} - whether there is a connection
565 567 */
566 568 Kernel.prototype.is_connected = function () {
567 569 for (var c in this.channels) {
568 570 // if any channel is not ready, then we're not connected
569 571 if (this.channels[c] === null) {
570 572 return false;
571 573 }
572 574 if (this.channels[c].readyState !== WebSocket.OPEN) {
573 575 return false;
574 576 }
575 577 }
576 578 return true;
577 579 };
578 580
579 581 /**
580 582 * Check whether the connection to the kernel has been completely
581 583 * severed. This function only returns true if all channel objects
582 584 * are null.
583 585 *
584 586 * @function is_fully_disconnected
585 587 * @returns {bool} - whether the kernel is fully disconnected
586 588 */
587 589 Kernel.prototype.is_fully_disconnected = function () {
588 590 for (var c in this.channels) {
589 591 if (this.channels[c] === null) {
590 592 return true;
591 593 }
592 594 }
593 595 return false;
594 596 };
595 597
596 598 /**
597 599 * Send a message on the Kernel's shell channel
598 600 *
599 601 * @function send_shell_message
600 602 */
601 603 Kernel.prototype.send_shell_message = function (msg_type, content, callbacks, metadata, buffers) {
602 604 if (!this.is_connected()) {
603 605 throw new Error("kernel is not connected");
604 606 }
605 607 var msg = this._get_msg(msg_type, content, metadata, buffers);
606 608 this.channels.shell.send(serialize.serialize(msg));
607 609 this.set_callbacks_for_msg(msg.header.msg_id, callbacks);
608 610 return msg.header.msg_id;
609 611 };
610 612
611 613 /**
612 614 * Get kernel info
613 615 *
614 616 * @function kernel_info
615 617 * @param callback {function}
616 618 *
617 619 * When calling this method, pass a callback function that expects one argument.
618 620 * The callback will be passed the complete `kernel_info_reply` message documented
619 621 * [here](http://ipython.org/ipython-doc/dev/development/messaging.html#kernel-info)
620 622 */
621 623 Kernel.prototype.kernel_info = function (callback) {
622 624 var callbacks;
623 625 if (callback) {
624 626 callbacks = { shell : { reply : callback } };
625 627 }
626 628 return this.send_shell_message("kernel_info_request", {}, callbacks);
627 629 };
628 630
629 631 /**
630 632 * Get info on an object
631 633 *
632 634 * When calling this method, pass a callback function that expects one argument.
633 635 * The callback will be passed the complete `inspect_reply` message documented
634 636 * [here](http://ipython.org/ipython-doc/dev/development/messaging.html#object-information)
635 637 *
636 638 * @function inspect
637 639 * @param code {string}
638 640 * @param cursor_pos {integer}
639 641 * @param callback {function}
640 642 */
641 643 Kernel.prototype.inspect = function (code, cursor_pos, callback) {
642 644 var callbacks;
643 645 if (callback) {
644 646 callbacks = { shell : { reply : callback } };
645 647 }
646 648
647 649 var content = {
648 650 code : code,
649 651 cursor_pos : cursor_pos,
650 652 detail_level : 0
651 653 };
652 654 return this.send_shell_message("inspect_request", content, callbacks);
653 655 };
654 656
655 657 /**
656 658 * Execute given code into kernel, and pass result to callback.
657 659 *
658 660 * @async
659 661 * @function execute
660 662 * @param {string} code
661 663 * @param [callbacks] {Object} With the following keys (all optional)
662 664 * @param callbacks.shell.reply {function}
663 665 * @param callbacks.shell.payload.[payload_name] {function}
664 666 * @param callbacks.iopub.output {function}
665 667 * @param callbacks.iopub.clear_output {function}
666 668 * @param callbacks.input {function}
667 669 * @param {object} [options]
668 670 * @param [options.silent=false] {Boolean}
669 671 * @param [options.user_expressions=empty_dict] {Dict}
670 672 * @param [options.allow_stdin=false] {Boolean} true|false
671 673 *
672 674 * @example
673 675 *
674 676 * The options object should contain the options for the execute
675 677 * call. Its default values are:
676 678 *
677 679 * options = {
678 680 * silent : true,
679 681 * user_expressions : {},
680 682 * allow_stdin : false
681 683 * }
682 684 *
683 685 * When calling this method pass a callbacks structure of the
684 686 * form:
685 687 *
686 688 * callbacks = {
687 689 * shell : {
688 690 * reply : execute_reply_callback,
689 691 * payload : {
690 692 * set_next_input : set_next_input_callback,
691 693 * }
692 694 * },
693 695 * iopub : {
694 696 * output : output_callback,
695 697 * clear_output : clear_output_callback,
696 698 * },
697 699 * input : raw_input_callback
698 700 * }
699 701 *
700 702 * Each callback will be passed the entire message as a single
701 703 * arugment. Payload handlers will be passed the corresponding
702 704 * payload and the execute_reply message.
703 705 */
704 706 Kernel.prototype.execute = function (code, callbacks, options) {
705 707 var content = {
706 708 code : code,
707 709 silent : true,
708 710 store_history : false,
709 711 user_expressions : {},
710 712 allow_stdin : false
711 713 };
712 714 callbacks = callbacks || {};
713 715 if (callbacks.input !== undefined) {
714 716 content.allow_stdin = true;
715 717 }
716 718 $.extend(true, content, options);
717 719 this.events.trigger('execution_request.Kernel', {kernel: this, content: content});
718 720 return this.send_shell_message("execute_request", content, callbacks);
719 721 };
720 722
721 723 /**
722 724 * When calling this method, pass a function to be called with the
723 725 * `complete_reply` message as its only argument when it arrives.
724 726 *
725 727 * `complete_reply` is documented
726 728 * [here](http://ipython.org/ipython-doc/dev/development/messaging.html#complete)
727 729 *
728 730 * @function complete
729 731 * @param code {string}
730 732 * @param cursor_pos {integer}
731 733 * @param callback {function}
732 734 */
733 735 Kernel.prototype.complete = function (code, cursor_pos, callback) {
734 736 var callbacks;
735 737 if (callback) {
736 738 callbacks = { shell : { reply : callback } };
737 739 }
738 740 var content = {
739 741 code : code,
740 742 cursor_pos : cursor_pos
741 743 };
742 744 return this.send_shell_message("complete_request", content, callbacks);
743 745 };
744 746
745 747 /**
746 748 * @function send_input_reply
747 749 */
748 750 Kernel.prototype.send_input_reply = function (input) {
749 751 if (!this.is_connected()) {
750 752 throw new Error("kernel is not connected");
751 753 }
752 754 var content = {
753 755 value : input
754 756 };
755 757 this.events.trigger('input_reply.Kernel', {kernel: this, content: content});
756 758 var msg = this._get_msg("input_reply", content);
757 759 this.channels.stdin.send(serialize.serialize(msg));
758 760 return msg.header.msg_id;
759 761 };
760 762
761 763 /**
762 764 * @function register_iopub_handler
763 765 */
764 766 Kernel.prototype.register_iopub_handler = function (msg_type, callback) {
765 767 this._iopub_handlers[msg_type] = callback;
766 768 };
767 769
768 770 /**
769 771 * Get the iopub handler for a specific message type.
770 772 *
771 773 * @function get_iopub_handler
772 774 */
773 775 Kernel.prototype.get_iopub_handler = function (msg_type) {
774 776 return this._iopub_handlers[msg_type];
775 777 };
776 778
777 779 /**
778 780 * Get callbacks for a specific message.
779 781 *
780 782 * @function get_callbacks_for_msg
781 783 */
782 784 Kernel.prototype.get_callbacks_for_msg = function (msg_id) {
783 785 if (msg_id == this.last_msg_id) {
784 786 return this.last_msg_callbacks;
785 787 } else {
786 788 return this._msg_callbacks[msg_id];
787 789 }
788 790 };
789 791
790 792 /**
791 793 * Clear callbacks for a specific message.
792 794 *
793 795 * @function clear_callbacks_for_msg
794 796 */
795 797 Kernel.prototype.clear_callbacks_for_msg = function (msg_id) {
796 798 if (this._msg_callbacks[msg_id] !== undefined ) {
797 799 delete this._msg_callbacks[msg_id];
798 800 }
799 801 };
800 802
801 803 /**
802 804 * @function _finish_shell
803 805 */
804 806 Kernel.prototype._finish_shell = function (msg_id) {
805 807 var callbacks = this._msg_callbacks[msg_id];
806 808 if (callbacks !== undefined) {
807 809 callbacks.shell_done = true;
808 810 if (callbacks.iopub_done) {
809 811 this.clear_callbacks_for_msg(msg_id);
810 812 }
811 813 }
812 814 };
813 815
814 816 /**
815 817 * @function _finish_iopub
816 818 */
817 819 Kernel.prototype._finish_iopub = function (msg_id) {
818 820 var callbacks = this._msg_callbacks[msg_id];
819 821 if (callbacks !== undefined) {
820 822 callbacks.iopub_done = true;
821 823 if (callbacks.shell_done) {
822 824 this.clear_callbacks_for_msg(msg_id);
823 825 }
824 826 }
825 827 };
826 828
827 829 /**
828 830 * Set callbacks for a particular message.
829 831 * Callbacks should be a struct of the following form:
830 832 * shell : {
831 833 *
832 834 * }
833 835 *
834 836 * @function set_callbacks_for_msg
835 837 */
836 838 Kernel.prototype.set_callbacks_for_msg = function (msg_id, callbacks) {
837 839 this.last_msg_id = msg_id;
838 840 if (callbacks) {
839 841 // shallow-copy mapping, because we will modify it at the top level
840 842 var cbcopy = this._msg_callbacks[msg_id] = this.last_msg_callbacks = {};
841 843 cbcopy.shell = callbacks.shell;
842 844 cbcopy.iopub = callbacks.iopub;
843 845 cbcopy.input = callbacks.input;
844 846 cbcopy.shell_done = (!callbacks.shell);
845 847 cbcopy.iopub_done = (!callbacks.iopub);
846 848 } else {
847 849 this.last_msg_callbacks = {};
848 850 }
849 851 };
850 852
851 853 /**
852 854 * @function _handle_shell_reply
853 855 */
854 856 Kernel.prototype._handle_shell_reply = function (e) {
855 857 serialize.deserialize(e.data, $.proxy(this._finish_shell_reply, this));
856 858 };
857 859
858 860 Kernel.prototype._finish_shell_reply = function (reply) {
859 861 this.events.trigger('shell_reply.Kernel', {kernel: this, reply:reply});
860 862 var content = reply.content;
861 863 var metadata = reply.metadata;
862 864 var parent_id = reply.parent_header.msg_id;
863 865 var callbacks = this.get_callbacks_for_msg(parent_id);
864 866 if (!callbacks || !callbacks.shell) {
865 867 return;
866 868 }
867 869 var shell_callbacks = callbacks.shell;
868 870
869 871 // signal that shell callbacks are done
870 872 this._finish_shell(parent_id);
871 873
872 874 if (shell_callbacks.reply !== undefined) {
873 875 shell_callbacks.reply(reply);
874 876 }
875 877 if (content.payload && shell_callbacks.payload) {
876 878 this._handle_payloads(content.payload, shell_callbacks.payload, reply);
877 879 }
878 880 };
879 881
880 882 /**
881 883 * @function _handle_payloads
882 884 */
883 885 Kernel.prototype._handle_payloads = function (payloads, payload_callbacks, msg) {
884 886 var l = payloads.length;
885 887 // Payloads are handled by triggering events because we don't want the Kernel
886 888 // to depend on the Notebook or Pager classes.
887 889 for (var i=0; i<l; i++) {
888 890 var payload = payloads[i];
889 891 var callback = payload_callbacks[payload.source];
890 892 if (callback) {
891 893 callback(payload, msg);
892 894 }
893 895 }
894 896 };
895 897
896 898 /**
897 899 * @function _handle_status_message
898 900 */
899 901 Kernel.prototype._handle_status_message = function (msg) {
900 902 var execution_state = msg.content.execution_state;
901 903 var parent_id = msg.parent_header.msg_id;
902 904
903 905 // dispatch status msg callbacks, if any
904 906 var callbacks = this.get_callbacks_for_msg(parent_id);
905 907 if (callbacks && callbacks.iopub && callbacks.iopub.status) {
906 908 try {
907 909 callbacks.iopub.status(msg);
908 910 } catch (e) {
909 911 console.log("Exception in status msg handler", e, e.stack);
910 912 }
911 913 }
912 914
913 915 if (execution_state === 'busy') {
914 916 this.events.trigger('kernel_busy.Kernel', {kernel: this});
915 917
916 918 } else if (execution_state === 'idle') {
917 919 // signal that iopub callbacks are (probably) done
918 920 // async output may still arrive,
919 921 // but only for the most recent request
920 922 this._finish_iopub(parent_id);
921 923
922 924 // trigger status_idle event
923 925 this.events.trigger('kernel_idle.Kernel', {kernel: this});
924 926
925 927 } else if (execution_state === 'starting') {
926 928 this.events.trigger('kernel_starting.Kernel', {kernel: this});
927 929 var that = this;
928 this.kernel_info(function () {
930 this.kernel_info(function (reply) {
931 that.info_reply = reply.content;
929 932 that.events.trigger('kernel_ready.Kernel', {kernel: that});
930 933 });
931 934
932 935 } else if (execution_state === 'restarting') {
933 936 // autorestarting is distinct from restarting,
934 937 // in that it means the kernel died and the server is restarting it.
935 938 // kernel_restarting sets the notification widget,
936 939 // autorestart shows the more prominent dialog.
937 940 this._autorestart_attempt = this._autorestart_attempt + 1;
938 941 this.events.trigger('kernel_restarting.Kernel', {kernel: this});
939 942 this.events.trigger('kernel_autorestarting.Kernel', {kernel: this, attempt: this._autorestart_attempt});
940 943
941 944 } else if (execution_state === 'dead') {
942 945 this.events.trigger('kernel_dead.Kernel', {kernel: this});
943 946 this._kernel_dead();
944 947 }
945 948 };
946 949
947 950 /**
948 951 * Handle clear_output message
949 952 *
950 953 * @function _handle_clear_output
951 954 */
952 955 Kernel.prototype._handle_clear_output = function (msg) {
953 956 var callbacks = this.get_callbacks_for_msg(msg.parent_header.msg_id);
954 957 if (!callbacks || !callbacks.iopub) {
955 958 return;
956 959 }
957 960 var callback = callbacks.iopub.clear_output;
958 961 if (callback) {
959 962 callback(msg);
960 963 }
961 964 };
962 965
963 966 /**
964 967 * handle an output message (execute_result, display_data, etc.)
965 968 *
966 969 * @function _handle_output_message
967 970 */
968 971 Kernel.prototype._handle_output_message = function (msg) {
969 972 var callbacks = this.get_callbacks_for_msg(msg.parent_header.msg_id);
970 973 if (!callbacks || !callbacks.iopub) {
971 974 return;
972 975 }
973 976 var callback = callbacks.iopub.output;
974 977 if (callback) {
975 978 callback(msg);
976 979 }
977 980 };
978 981
979 982 /**
980 983 * Dispatch IOPub messages to respective handlers. Each message
981 984 * type should have a handler.
982 985 *
983 986 * @function _handle_iopub_message
984 987 */
985 988 Kernel.prototype._handle_iopub_message = function (e) {
986 989 serialize.deserialize(e.data, $.proxy(this._finish_iopub_message, this));
987 990 };
988 991
989 992
990 993 Kernel.prototype._finish_iopub_message = function (msg) {
991 994 var handler = this.get_iopub_handler(msg.header.msg_type);
992 995 if (handler !== undefined) {
993 996 handler(msg);
994 997 }
995 998 };
996 999
997 1000 /**
998 1001 * @function _handle_input_request
999 1002 */
1000 1003 Kernel.prototype._handle_input_request = function (e) {
1001 1004 serialize.deserialize(e.data, $.proxy(this._finish_input_request, this));
1002 1005 };
1003 1006
1004 1007
1005 1008 Kernel.prototype._finish_input_request = function (request) {
1006 1009 var header = request.header;
1007 1010 var content = request.content;
1008 1011 var metadata = request.metadata;
1009 1012 var msg_type = header.msg_type;
1010 1013 if (msg_type !== 'input_request') {
1011 1014 console.log("Invalid input request!", request);
1012 1015 return;
1013 1016 }
1014 1017 var callbacks = this.get_callbacks_for_msg(request.parent_header.msg_id);
1015 1018 if (callbacks) {
1016 1019 if (callbacks.input) {
1017 1020 callbacks.input(request);
1018 1021 }
1019 1022 }
1020 1023 };
1021 1024
1022 1025 // Backwards compatability.
1023 1026 IPython.Kernel = Kernel;
1024 1027
1025 1028 return {'Kernel': Kernel};
1026 1029 });
@@ -1,231 +1,213 b''
1 1 import io
2 2 import json
3 3 import os
4 4 import shutil
5 5 import sys
6 6
7 7 pjoin = os.path.join
8 8
9 9 from IPython.utils.path import get_ipython_dir
10 10 from IPython.utils.py3compat import PY3
11 11 from IPython.utils.traitlets import HasTraits, List, Unicode, Dict, Any
12 12 from .launcher import make_ipkernel_cmd
13 13
14 14 if os.name == 'nt':
15 15 programdata = os.environ.get('PROGRAMDATA', None)
16 16 if programdata:
17 17 SYSTEM_KERNEL_DIRS = [pjoin(programdata, 'ipython', 'kernels')]
18 18 else: # PROGRAMDATA is not defined by default on XP.
19 19 SYSTEM_KERNEL_DIRS = []
20 20 else:
21 21 SYSTEM_KERNEL_DIRS = ["/usr/share/ipython/kernels",
22 22 "/usr/local/share/ipython/kernels",
23 23 ]
24 24
25 25 NATIVE_KERNEL_NAME = 'python3' if PY3 else 'python2'
26 26
27 27 def _pythonfirst(s):
28 28 "Sort key function that will put strings starting with 'python' first."
29 29 if s == NATIVE_KERNEL_NAME:
30 30 return ' ' + s # Two spaces to sort this first of all
31 31 elif s.startswith('python'):
32 32 # Space is not valid in kernel names, so this should sort first
33 33 return ' ' + s
34 34 return s
35 35
36 36 class KernelSpec(HasTraits):
37 37 argv = List()
38 38 display_name = Unicode()
39 language = Unicode()
40 codemirror_mode = Any() # can be unicode or dict
41 pygments_lexer = Unicode()
42 39 env = Dict()
43 40 resource_dir = Unicode()
44 41
45 def _codemirror_mode_default(self):
46 return self.language
47
48 def _pygments_lexer_default(self):
49 return self.language
50
51 42 @classmethod
52 43 def from_resource_dir(cls, resource_dir):
53 44 """Create a KernelSpec object by reading kernel.json
54 45
55 46 Pass the path to the *directory* containing kernel.json.
56 47 """
57 48 kernel_file = pjoin(resource_dir, 'kernel.json')
58 49 with io.open(kernel_file, 'r', encoding='utf-8') as f:
59 50 kernel_dict = json.load(f)
60 51 return cls(resource_dir=resource_dir, **kernel_dict)
61 52
62 53 def to_dict(self):
63 54 d = dict(argv=self.argv,
64 55 env=self.env,
65 56 display_name=self.display_name,
66 language=self.language,
67 57 )
68 if self.codemirror_mode != self.language:
69 d['codemirror_mode'] = self.codemirror_mode
70 if self.pygments_lexer != self.language:
71 d['pygments_lexer'] = self.pygments_lexer
72 58
73 59 return d
74 60
75 61 def to_json(self):
76 62 return json.dumps(self.to_dict())
77 63
78 64 def _is_kernel_dir(path):
79 65 """Is ``path`` a kernel directory?"""
80 66 return os.path.isdir(path) and os.path.isfile(pjoin(path, 'kernel.json'))
81 67
82 68 def _list_kernels_in(dir):
83 69 """Return a mapping of kernel names to resource directories from dir.
84 70
85 71 If dir is None or does not exist, returns an empty dict.
86 72 """
87 73 if dir is None or not os.path.isdir(dir):
88 74 return {}
89 75 return {f.lower(): pjoin(dir, f) for f in os.listdir(dir)
90 76 if _is_kernel_dir(pjoin(dir, f))}
91 77
92 78 class NoSuchKernel(KeyError):
93 79 def __init__(self, name):
94 80 self.name = name
95 81
96 82 class KernelSpecManager(HasTraits):
97 83 ipython_dir = Unicode()
98 84 def _ipython_dir_default(self):
99 85 return get_ipython_dir()
100 86
101 87 user_kernel_dir = Unicode()
102 88 def _user_kernel_dir_default(self):
103 89 return pjoin(self.ipython_dir, 'kernels')
104 90
105 91 kernel_dirs = List(
106 92 help="List of kernel directories to search. Later ones take priority over earlier."
107 93 )
108 94 def _kernel_dirs_default(self):
109 95 return SYSTEM_KERNEL_DIRS + [
110 96 self.user_kernel_dir,
111 97 ]
112 98
113 99 @property
114 100 def _native_kernel_dict(self):
115 101 """Makes a kernel directory for the native kernel.
116 102
117 103 The native kernel is the kernel using the same Python runtime as this
118 104 process. This will put its informatino in the user kernels directory.
119 105 """
120 106 return {'argv': make_ipkernel_cmd(),
121 107 'display_name': 'IPython (Python %d)' % (3 if PY3 else 2),
122 'language': 'python',
123 'codemirror_mode': {'name': 'ipython',
124 'version': sys.version_info[0]},
125 'pygments_lexer': 'ipython%d' % (3 if PY3 else 2),
126 108 }
127 109
128 110 @property
129 111 def _native_kernel_resource_dir(self):
130 112 # TODO: This may be different when we actually have any resources
131 113 return os.path.dirname(__file__)
132 114
133 115 def find_kernel_specs(self):
134 116 """Returns a dict mapping kernel names to resource directories."""
135 117 d = {}
136 118 for kernel_dir in self.kernel_dirs:
137 119 d.update(_list_kernels_in(kernel_dir))
138 120
139 121 d[NATIVE_KERNEL_NAME] = self._native_kernel_resource_dir
140 122 return d
141 123 # TODO: Caching?
142 124
143 125 def get_kernel_spec(self, kernel_name):
144 126 """Returns a :class:`KernelSpec` instance for the given kernel_name.
145 127
146 128 Raises :exc:`NoSuchKernel` if the given kernel name is not found.
147 129 """
148 130 if kernel_name in {'python', NATIVE_KERNEL_NAME}:
149 131 return KernelSpec(self._native_kernel_resource_dir, **self._native_kernel_dict)
150 132
151 133 d = self.find_kernel_specs()
152 134 try:
153 135 resource_dir = d[kernel_name.lower()]
154 136 except KeyError:
155 137 raise NoSuchKernel(kernel_name)
156 138 return KernelSpec.from_resource_dir(resource_dir)
157 139
158 140 def _get_destination_dir(self, kernel_name, system=False):
159 141 if system:
160 142 if SYSTEM_KERNEL_DIRS:
161 143 return os.path.join(SYSTEM_KERNEL_DIRS[-1], kernel_name)
162 144 else:
163 145 raise EnvironmentError("No system kernel directory is available")
164 146 else:
165 147 return os.path.join(self.user_kernel_dir, kernel_name)
166 148
167 149 def install_kernel_spec(self, source_dir, kernel_name=None, system=False,
168 150 replace=False):
169 151 """Install a kernel spec by copying its directory.
170 152
171 153 If ``kernel_name`` is not given, the basename of ``source_dir`` will
172 154 be used.
173 155
174 156 If ``system`` is True, it will attempt to install into the systemwide
175 157 kernel registry. If the process does not have appropriate permissions,
176 158 an :exc:`OSError` will be raised.
177 159
178 160 If ``replace`` is True, this will replace an existing kernel of the same
179 161 name. Otherwise, if the destination already exists, an :exc:`OSError`
180 162 will be raised.
181 163 """
182 164 if not kernel_name:
183 165 kernel_name = os.path.basename(source_dir)
184 166 kernel_name = kernel_name.lower()
185 167
186 168 destination = self._get_destination_dir(kernel_name, system=system)
187 169
188 170 if replace and os.path.isdir(destination):
189 171 shutil.rmtree(destination)
190 172
191 173 shutil.copytree(source_dir, destination)
192 174
193 175 def install_native_kernel_spec(self, system=False):
194 176 """Install the native kernel spec to the filesystem
195 177
196 178 This allows a Python 3 frontend to use a Python 2 kernel, or vice versa.
197 179 The kernelspec will be written pointing to the Python executable on
198 180 which this is run.
199 181
200 182 If ``system`` is True, it will attempt to install into the systemwide
201 183 kernel registry. If the process does not have appropriate permissions,
202 184 an :exc:`OSError` will be raised.
203 185 """
204 186 path = self._get_destination_dir(NATIVE_KERNEL_NAME, system=system)
205 187 os.makedirs(path, mode=0o755)
206 188 with open(pjoin(path, 'kernel.json'), 'w') as f:
207 189 json.dump(self._native_kernel_dict, f, indent=1)
208 190 # TODO: Copy icons into directory
209 191 return path
210 192
211 193 def find_kernel_specs():
212 194 """Returns a dict mapping kernel names to resource directories."""
213 195 return KernelSpecManager().find_kernel_specs()
214 196
215 197 def get_kernel_spec(kernel_name):
216 198 """Returns a :class:`KernelSpec` instance for the given kernel_name.
217 199
218 200 Raises KeyError if the given kernel name is not found.
219 201 """
220 202 return KernelSpecManager().get_kernel_spec(kernel_name)
221 203
222 204 def install_kernel_spec(source_dir, kernel_name=None, system=False, replace=False):
223 205 return KernelSpecManager().install_kernel_spec(source_dir, kernel_name,
224 206 system, replace)
225 207
226 208 install_kernel_spec.__doc__ = KernelSpecManager.install_kernel_spec.__doc__
227 209
228 210 def install_native_kernel_spec(self, system=False):
229 211 return KernelSpecManager().install_native_kernel_spec(system=system)
230 212
231 213 install_native_kernel_spec.__doc__ = KernelSpecManager.install_native_kernel_spec.__doc__
@@ -1,65 +1,62 b''
1 1 import json
2 2 import os
3 3 from os.path import join as pjoin
4 4 import unittest
5 5
6 6 from IPython.testing.decorators import onlyif
7 7 from IPython.utils.tempdir import TemporaryDirectory
8 8 from IPython.kernel import kernelspec
9 9
10 10 sample_kernel_json = {'argv':['cat', '{connection_file}'],
11 11 'display_name':'Test kernel',
12 'language':'bash',
13 12 }
14 13
15 14 class KernelSpecTests(unittest.TestCase):
16 15 def setUp(self):
17 16 td = TemporaryDirectory()
18 17 self.addCleanup(td.cleanup)
19 18 self.sample_kernel_dir = pjoin(td.name, 'kernels', 'Sample')
20 19 os.makedirs(self.sample_kernel_dir)
21 20 json_file = pjoin(self.sample_kernel_dir, 'kernel.json')
22 21 with open(json_file, 'w') as f:
23 22 json.dump(sample_kernel_json, f)
24 23
25 24 self.ksm = kernelspec.KernelSpecManager(ipython_dir=td.name)
26 25
27 26 td2 = TemporaryDirectory()
28 27 self.addCleanup(td2.cleanup)
29 28 self.installable_kernel = td2.name
30 29 with open(pjoin(self.installable_kernel, 'kernel.json'), 'w') as f:
31 30 json.dump(sample_kernel_json, f)
32 31
33 32 def test_find_kernel_specs(self):
34 33 kernels = self.ksm.find_kernel_specs()
35 34 self.assertEqual(kernels['sample'], self.sample_kernel_dir)
36 35
37 36 def test_get_kernel_spec(self):
38 37 ks = self.ksm.get_kernel_spec('SAMPLE') # Case insensitive
39 38 self.assertEqual(ks.resource_dir, self.sample_kernel_dir)
40 39 self.assertEqual(ks.argv, sample_kernel_json['argv'])
41 40 self.assertEqual(ks.display_name, sample_kernel_json['display_name'])
42 self.assertEqual(ks.language, sample_kernel_json['language'])
43 self.assertEqual(ks.codemirror_mode, sample_kernel_json['language'])
44 41 self.assertEqual(ks.env, {})
45 42
46 43 def test_install_kernel_spec(self):
47 44 self.ksm.install_kernel_spec(self.installable_kernel,
48 45 kernel_name='tstinstalled')
49 46 self.assertIn('tstinstalled', self.ksm.find_kernel_specs())
50 47
51 48 with self.assertRaises(OSError):
52 49 self.ksm.install_kernel_spec(self.installable_kernel,
53 50 kernel_name='tstinstalled')
54 51
55 52 # Smoketest that this succeeds
56 53 self.ksm.install_kernel_spec(self.installable_kernel,
57 54 kernel_name='tstinstalled',
58 55 replace=True)
59 56
60 57 @onlyif(os.name != 'nt' and not os.access('/usr/local/share', os.W_OK), "needs Unix system without root privileges")
61 58 def test_cant_install_kernel_spec(self):
62 59 with self.assertRaises(OSError):
63 60 self.ksm.install_kernel_spec(self.installable_kernel,
64 61 kernel_name='tstinstalled',
65 62 system=True)
@@ -1,428 +1,429 b''
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 7 from distutils.version import LooseVersion as V
8 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 16 from IPython.kernel import KernelManager
17 17
18 18 from IPython.utils.traitlets import (
19 19 HasTraits, TraitError, Bool, Unicode, Dict, Integer, List, Enum, Any,
20 20 )
21 21 from IPython.utils.py3compat import string_types, iteritems
22 22
23 23 from .utils import TIMEOUT, start_global_kernel, flush_channels, execute
24 24
25 25 #-----------------------------------------------------------------------------
26 26 # Globals
27 27 #-----------------------------------------------------------------------------
28 28 KC = None
29 29
30 30 def setup():
31 31 global KC
32 32 KC = start_global_kernel()
33 33
34 34 #-----------------------------------------------------------------------------
35 35 # Message Spec References
36 36 #-----------------------------------------------------------------------------
37 37
38 38 class Reference(HasTraits):
39 39
40 40 """
41 41 Base class for message spec specification testing.
42 42
43 43 This class is the core of the message specification test. The
44 44 idea is that child classes implement trait attributes for each
45 45 message keys, so that message keys can be tested against these
46 46 traits using :meth:`check` method.
47 47
48 48 """
49 49
50 50 def check(self, d):
51 51 """validate a dict against our traits"""
52 52 for key in self.trait_names():
53 53 nt.assert_in(key, d)
54 54 # FIXME: always allow None, probably not a good idea
55 55 if d[key] is None:
56 56 continue
57 57 try:
58 58 setattr(self, key, d[key])
59 59 except TraitError as e:
60 60 assert False, str(e)
61 61
62 62
63 63 class Version(Unicode):
64 64 def __init__(self, *args, **kwargs):
65 65 self.min = kwargs.pop('min', None)
66 66 self.max = kwargs.pop('max', None)
67 67 kwargs['default_value'] = self.min
68 68 super(Version, self).__init__(*args, **kwargs)
69 69
70 70 def validate(self, obj, value):
71 71 if self.min and V(value) < V(self.min):
72 72 raise TraitError("bad version: %s < %s" % (value, self.min))
73 73 if self.max and (V(value) > V(self.max)):
74 74 raise TraitError("bad version: %s > %s" % (value, self.max))
75 75
76 76
77 77 class RMessage(Reference):
78 78 msg_id = Unicode()
79 79 msg_type = Unicode()
80 80 header = Dict()
81 81 parent_header = Dict()
82 82 content = Dict()
83 83
84 84 def check(self, d):
85 85 super(RMessage, self).check(d)
86 86 RHeader().check(self.header)
87 87 if self.parent_header:
88 88 RHeader().check(self.parent_header)
89 89
90 90 class RHeader(Reference):
91 91 msg_id = Unicode()
92 92 msg_type = Unicode()
93 93 session = Unicode()
94 94 username = Unicode()
95 95 version = Version(min='5.0')
96 96
97 97 mime_pat = re.compile(r'^[\w\-\+\.]+/[\w\-\+\.]+$')
98 98
99 99 class MimeBundle(Reference):
100 100 metadata = Dict()
101 101 data = Dict()
102 102 def _data_changed(self, name, old, new):
103 103 for k,v in iteritems(new):
104 104 assert mime_pat.match(k)
105 105 nt.assert_is_instance(v, string_types)
106 106
107 107 # shell replies
108 108
109 109 class ExecuteReply(Reference):
110 110 execution_count = Integer()
111 111 status = Enum((u'ok', u'error'))
112 112
113 113 def check(self, d):
114 114 Reference.check(self, d)
115 115 if d['status'] == 'ok':
116 116 ExecuteReplyOkay().check(d)
117 117 elif d['status'] == 'error':
118 118 ExecuteReplyError().check(d)
119 119
120 120
121 121 class ExecuteReplyOkay(Reference):
122 122 payload = List(Dict)
123 123 user_expressions = Dict()
124 124
125 125
126 126 class ExecuteReplyError(Reference):
127 127 ename = Unicode()
128 128 evalue = Unicode()
129 129 traceback = List(Unicode)
130 130
131 131
132 132 class InspectReply(MimeBundle):
133 133 found = Bool()
134 134
135 135
136 136 class ArgSpec(Reference):
137 137 args = List(Unicode)
138 138 varargs = Unicode()
139 139 varkw = Unicode()
140 140 defaults = List()
141 141
142 142
143 143 class Status(Reference):
144 144 execution_state = Enum((u'busy', u'idle', u'starting'))
145 145
146 146
147 147 class CompleteReply(Reference):
148 148 matches = List(Unicode)
149 149 cursor_start = Integer()
150 150 cursor_end = Integer()
151 151 status = Unicode()
152 152
153 153
154 154 class KernelInfoReply(Reference):
155 155 protocol_version = Version(min='5.0')
156 156 implementation = Unicode('ipython')
157 157 implementation_version = Version(min='2.1')
158 158 language_version = Version(min='2.7')
159 159 language = Unicode('python')
160 language_info = Dict()
160 161 banner = Unicode()
161 162
162 163
163 164 class IsCompleteReply(Reference):
164 165 status = Enum((u'complete', u'incomplete', u'invalid', u'unknown'))
165 166
166 167 def check(self, d):
167 168 Reference.check(self, d)
168 169 if d['status'] == 'incomplete':
169 170 IsCompleteReplyIncomplete().check(d)
170 171
171 172 class IsCompleteReplyIncomplete(Reference):
172 173 indent = Unicode()
173 174
174 175
175 176 # IOPub messages
176 177
177 178 class ExecuteInput(Reference):
178 179 code = Unicode()
179 180 execution_count = Integer()
180 181
181 182
182 183 Error = ExecuteReplyError
183 184
184 185
185 186 class Stream(Reference):
186 187 name = Enum((u'stdout', u'stderr'))
187 188 text = Unicode()
188 189
189 190
190 191 class DisplayData(MimeBundle):
191 192 pass
192 193
193 194
194 195 class ExecuteResult(MimeBundle):
195 196 execution_count = Integer()
196 197
197 198
198 199 references = {
199 200 'execute_reply' : ExecuteReply(),
200 201 'inspect_reply' : InspectReply(),
201 202 'status' : Status(),
202 203 'complete_reply' : CompleteReply(),
203 204 'kernel_info_reply': KernelInfoReply(),
204 205 'is_complete_reply': IsCompleteReply(),
205 206 'execute_input' : ExecuteInput(),
206 207 'execute_result' : ExecuteResult(),
207 208 'error' : Error(),
208 209 'stream' : Stream(),
209 210 'display_data' : DisplayData(),
210 211 'header' : RHeader(),
211 212 }
212 213 """
213 214 Specifications of `content` part of the reply messages.
214 215 """
215 216
216 217
217 218 def validate_message(msg, msg_type=None, parent=None):
218 219 """validate a message
219 220
220 221 This is a generator, and must be iterated through to actually
221 222 trigger each test.
222 223
223 224 If msg_type and/or parent are given, the msg_type and/or parent msg_id
224 225 are compared with the given values.
225 226 """
226 227 RMessage().check(msg)
227 228 if msg_type:
228 229 nt.assert_equal(msg['msg_type'], msg_type)
229 230 if parent:
230 231 nt.assert_equal(msg['parent_header']['msg_id'], parent)
231 232 content = msg['content']
232 233 ref = references[msg['msg_type']]
233 234 ref.check(content)
234 235
235 236
236 237 #-----------------------------------------------------------------------------
237 238 # Tests
238 239 #-----------------------------------------------------------------------------
239 240
240 241 # Shell channel
241 242
242 243 def test_execute():
243 244 flush_channels()
244 245
245 246 msg_id = KC.execute(code='x=1')
246 247 reply = KC.get_shell_msg(timeout=TIMEOUT)
247 248 validate_message(reply, 'execute_reply', msg_id)
248 249
249 250
250 251 def test_execute_silent():
251 252 flush_channels()
252 253 msg_id, reply = execute(code='x=1', silent=True)
253 254
254 255 # flush status=idle
255 256 status = KC.iopub_channel.get_msg(timeout=TIMEOUT)
256 257 validate_message(status, 'status', msg_id)
257 258 nt.assert_equal(status['content']['execution_state'], 'idle')
258 259
259 260 nt.assert_raises(Empty, KC.iopub_channel.get_msg, timeout=0.1)
260 261 count = reply['execution_count']
261 262
262 263 msg_id, reply = execute(code='x=2', silent=True)
263 264
264 265 # flush status=idle
265 266 status = KC.iopub_channel.get_msg(timeout=TIMEOUT)
266 267 validate_message(status, 'status', msg_id)
267 268 nt.assert_equal(status['content']['execution_state'], 'idle')
268 269
269 270 nt.assert_raises(Empty, KC.iopub_channel.get_msg, timeout=0.1)
270 271 count_2 = reply['execution_count']
271 272 nt.assert_equal(count_2, count)
272 273
273 274
274 275 def test_execute_error():
275 276 flush_channels()
276 277
277 278 msg_id, reply = execute(code='1/0')
278 279 nt.assert_equal(reply['status'], 'error')
279 280 nt.assert_equal(reply['ename'], 'ZeroDivisionError')
280 281
281 282 error = KC.iopub_channel.get_msg(timeout=TIMEOUT)
282 283 validate_message(error, 'error', msg_id)
283 284
284 285
285 286 def test_execute_inc():
286 287 """execute request should increment execution_count"""
287 288 flush_channels()
288 289
289 290 msg_id, reply = execute(code='x=1')
290 291 count = reply['execution_count']
291 292
292 293 flush_channels()
293 294
294 295 msg_id, reply = execute(code='x=2')
295 296 count_2 = reply['execution_count']
296 297 nt.assert_equal(count_2, count+1)
297 298
298 299
299 300 def test_user_expressions():
300 301 flush_channels()
301 302
302 303 msg_id, reply = execute(code='x=1', user_expressions=dict(foo='x+1'))
303 304 user_expressions = reply['user_expressions']
304 305 nt.assert_equal(user_expressions, {u'foo': {
305 306 u'status': u'ok',
306 307 u'data': {u'text/plain': u'2'},
307 308 u'metadata': {},
308 309 }})
309 310
310 311
311 312 def test_user_expressions_fail():
312 313 flush_channels()
313 314
314 315 msg_id, reply = execute(code='x=0', user_expressions=dict(foo='nosuchname'))
315 316 user_expressions = reply['user_expressions']
316 317 foo = user_expressions['foo']
317 318 nt.assert_equal(foo['status'], 'error')
318 319 nt.assert_equal(foo['ename'], 'NameError')
319 320
320 321
321 322 def test_oinfo():
322 323 flush_channels()
323 324
324 325 msg_id = KC.inspect('a')
325 326 reply = KC.get_shell_msg(timeout=TIMEOUT)
326 327 validate_message(reply, 'inspect_reply', msg_id)
327 328
328 329
329 330 def test_oinfo_found():
330 331 flush_channels()
331 332
332 333 msg_id, reply = execute(code='a=5')
333 334
334 335 msg_id = KC.inspect('a')
335 336 reply = KC.get_shell_msg(timeout=TIMEOUT)
336 337 validate_message(reply, 'inspect_reply', msg_id)
337 338 content = reply['content']
338 339 assert content['found']
339 340 text = content['data']['text/plain']
340 341 nt.assert_in('Type:', text)
341 342 nt.assert_in('Docstring:', text)
342 343
343 344
344 345 def test_oinfo_detail():
345 346 flush_channels()
346 347
347 348 msg_id, reply = execute(code='ip=get_ipython()')
348 349
349 350 msg_id = KC.inspect('ip.object_inspect', cursor_pos=10, detail_level=1)
350 351 reply = KC.get_shell_msg(timeout=TIMEOUT)
351 352 validate_message(reply, 'inspect_reply', msg_id)
352 353 content = reply['content']
353 354 assert content['found']
354 355 text = content['data']['text/plain']
355 356 nt.assert_in('Definition:', text)
356 357 nt.assert_in('Source:', text)
357 358
358 359
359 360 def test_oinfo_not_found():
360 361 flush_channels()
361 362
362 363 msg_id = KC.inspect('dne')
363 364 reply = KC.get_shell_msg(timeout=TIMEOUT)
364 365 validate_message(reply, 'inspect_reply', msg_id)
365 366 content = reply['content']
366 367 nt.assert_false(content['found'])
367 368
368 369
369 370 def test_complete():
370 371 flush_channels()
371 372
372 373 msg_id, reply = execute(code="alpha = albert = 5")
373 374
374 375 msg_id = KC.complete('al', 2)
375 376 reply = KC.get_shell_msg(timeout=TIMEOUT)
376 377 validate_message(reply, 'complete_reply', msg_id)
377 378 matches = reply['content']['matches']
378 379 for name in ('alpha', 'albert'):
379 380 nt.assert_in(name, matches)
380 381
381 382
382 383 def test_kernel_info_request():
383 384 flush_channels()
384 385
385 386 msg_id = KC.kernel_info()
386 387 reply = KC.get_shell_msg(timeout=TIMEOUT)
387 388 validate_message(reply, 'kernel_info_reply', msg_id)
388 389
389 390
390 391 def test_single_payload():
391 392 flush_channels()
392 393 msg_id, reply = execute(code="for i in range(3):\n"+
393 394 " x=range?\n")
394 395 payload = reply['payload']
395 396 next_input_pls = [pl for pl in payload if pl["source"] == "set_next_input"]
396 397 nt.assert_equal(len(next_input_pls), 1)
397 398
398 399 def test_is_complete():
399 400 flush_channels()
400 401
401 402 msg_id = KC.is_complete("a = 1")
402 403 reply = KC.get_shell_msg(timeout=TIMEOUT)
403 404 validate_message(reply, 'is_complete_reply', msg_id)
404 405
405 406 # IOPub channel
406 407
407 408
408 409 def test_stream():
409 410 flush_channels()
410 411
411 412 msg_id, reply = execute("print('hi')")
412 413
413 414 stdout = KC.iopub_channel.get_msg(timeout=TIMEOUT)
414 415 validate_message(stdout, 'stream', msg_id)
415 416 content = stdout['content']
416 417 nt.assert_equal(content['text'], u'hi\n')
417 418
418 419
419 420 def test_display_data():
420 421 flush_channels()
421 422
422 423 msg_id, reply = execute("from IPython.core.display import display; display(1)")
423 424
424 425 display = KC.iopub_channel.get_msg(timeout=TIMEOUT)
425 426 validate_message(display, 'display_data', parent=msg_id)
426 427 data = display['content']['data']
427 428 nt.assert_equal(data['text/plain'], u'1')
428 429
@@ -1,323 +1,328 b''
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 72 language = 'python'
73 73 language_version = sys.version.split()[0]
74 language_info = {'mimetype': 'text/x-python',
75 'codemirror_mode': {'name': 'ipython',
76 'version': sys.version_info[0]},
77 'pygments_lexer': 'ipython%d' % (3 if PY3 else 2),
78 }
74 79 @property
75 80 def banner(self):
76 81 return self.shell.banner
77 82
78 83 def start(self):
79 84 self.shell.exit_now = False
80 85 super(IPythonKernel, self).start()
81 86
82 87 def set_parent(self, ident, parent):
83 88 """Overridden from parent to tell the display hook and output streams
84 89 about the parent message.
85 90 """
86 91 super(IPythonKernel, self).set_parent(ident, parent)
87 92 self.shell.set_parent(parent)
88 93
89 94 def _forward_input(self, allow_stdin=False):
90 95 """Forward raw_input and getpass to the current frontend.
91 96
92 97 via input_request
93 98 """
94 99 self._allow_stdin = allow_stdin
95 100
96 101 if PY3:
97 102 self._sys_raw_input = builtin_mod.input
98 103 builtin_mod.input = self.raw_input
99 104 else:
100 105 self._sys_raw_input = builtin_mod.raw_input
101 106 self._sys_eval_input = builtin_mod.input
102 107 builtin_mod.raw_input = self.raw_input
103 108 builtin_mod.input = lambda prompt='': eval(self.raw_input(prompt))
104 109 self._save_getpass = getpass.getpass
105 110 getpass.getpass = self.getpass
106 111
107 112 def _restore_input(self):
108 113 """Restore raw_input, getpass"""
109 114 if PY3:
110 115 builtin_mod.input = self._sys_raw_input
111 116 else:
112 117 builtin_mod.raw_input = self._sys_raw_input
113 118 builtin_mod.input = self._sys_eval_input
114 119
115 120 getpass.getpass = self._save_getpass
116 121
117 122 @property
118 123 def execution_count(self):
119 124 return self.shell.execution_count
120 125
121 126 @execution_count.setter
122 127 def execution_count(self, value):
123 128 # Ignore the incrememnting done by KernelBase, in favour of our shell's
124 129 # execution counter.
125 130 pass
126 131
127 132 def do_execute(self, code, silent, store_history=True,
128 133 user_expressions=None, allow_stdin=False):
129 134 shell = self.shell # we'll need this a lot here
130 135
131 136 self._forward_input(allow_stdin)
132 137
133 138 reply_content = {}
134 139 # FIXME: the shell calls the exception handler itself.
135 140 shell._reply_content = None
136 141 try:
137 142 shell.run_cell(code, store_history=store_history, silent=silent)
138 143 except:
139 144 status = u'error'
140 145 # FIXME: this code right now isn't being used yet by default,
141 146 # because the run_cell() call above directly fires off exception
142 147 # reporting. This code, therefore, is only active in the scenario
143 148 # where runlines itself has an unhandled exception. We need to
144 149 # uniformize this, for all exception construction to come from a
145 150 # single location in the codbase.
146 151 etype, evalue, tb = sys.exc_info()
147 152 tb_list = traceback.format_exception(etype, evalue, tb)
148 153 reply_content.update(shell._showtraceback(etype, evalue, tb_list))
149 154 else:
150 155 status = u'ok'
151 156 finally:
152 157 self._restore_input()
153 158
154 159 reply_content[u'status'] = status
155 160
156 161 # Return the execution counter so clients can display prompts
157 162 reply_content['execution_count'] = shell.execution_count - 1
158 163
159 164 # FIXME - fish exception info out of shell, possibly left there by
160 165 # runlines. We'll need to clean up this logic later.
161 166 if shell._reply_content is not None:
162 167 reply_content.update(shell._reply_content)
163 168 e_info = dict(engine_uuid=self.ident, engine_id=self.int_id, method='execute')
164 169 reply_content['engine_info'] = e_info
165 170 # reset after use
166 171 shell._reply_content = None
167 172
168 173 if 'traceback' in reply_content:
169 174 self.log.info("Exception in execute request:\n%s", '\n'.join(reply_content['traceback']))
170 175
171 176
172 177 # At this point, we can tell whether the main code execution succeeded
173 178 # or not. If it did, we proceed to evaluate user_expressions
174 179 if reply_content['status'] == 'ok':
175 180 reply_content[u'user_expressions'] = \
176 181 shell.user_expressions(user_expressions or {})
177 182 else:
178 183 # If there was an error, don't even try to compute expressions
179 184 reply_content[u'user_expressions'] = {}
180 185
181 186 # Payloads should be retrieved regardless of outcome, so we can both
182 187 # recover partial output (that could have been generated early in a
183 188 # block, before an error) and clear the payload system always.
184 189 reply_content[u'payload'] = shell.payload_manager.read_payload()
185 190 # Be agressive about clearing the payload because we don't want
186 191 # it to sit in memory until the next execute_request comes in.
187 192 shell.payload_manager.clear_payload()
188 193
189 194 return reply_content
190 195
191 196 def do_complete(self, code, cursor_pos):
192 197 # FIXME: IPython completers currently assume single line,
193 198 # but completion messages give multi-line context
194 199 # For now, extract line from cell, based on cursor_pos:
195 200 if cursor_pos is None:
196 201 cursor_pos = len(code)
197 202 line, offset = line_at_cursor(code, cursor_pos)
198 203 line_cursor = cursor_pos - offset
199 204
200 205 txt, matches = self.shell.complete('', line, line_cursor)
201 206 return {'matches' : matches,
202 207 'cursor_end' : cursor_pos,
203 208 'cursor_start' : cursor_pos - len(txt),
204 209 'metadata' : {},
205 210 'status' : 'ok'}
206 211
207 212 def do_inspect(self, code, cursor_pos, detail_level=0):
208 213 name = token_at_cursor(code, cursor_pos)
209 214 info = self.shell.object_inspect(name)
210 215
211 216 reply_content = {'status' : 'ok'}
212 217 reply_content['data'] = data = {}
213 218 reply_content['metadata'] = {}
214 219 reply_content['found'] = info['found']
215 220 if info['found']:
216 221 info_text = self.shell.object_inspect_text(
217 222 name,
218 223 detail_level=detail_level,
219 224 )
220 225 data['text/plain'] = info_text
221 226
222 227 return reply_content
223 228
224 229 def do_history(self, hist_access_type, output, raw, session=None, start=None,
225 230 stop=None, n=None, pattern=None, unique=False):
226 231 if hist_access_type == 'tail':
227 232 hist = self.shell.history_manager.get_tail(n, raw=raw, output=output,
228 233 include_latest=True)
229 234
230 235 elif hist_access_type == 'range':
231 236 hist = self.shell.history_manager.get_range(session, start, stop,
232 237 raw=raw, output=output)
233 238
234 239 elif hist_access_type == 'search':
235 240 hist = self.shell.history_manager.search(
236 241 pattern, raw=raw, output=output, n=n, unique=unique)
237 242 else:
238 243 hist = []
239 244
240 245 return {'history' : list(hist)}
241 246
242 247 def do_shutdown(self, restart):
243 248 self.shell.exit_now = True
244 249 return dict(status='ok', restart=restart)
245 250
246 251 def do_is_complete(self, code):
247 252 status, indent_spaces = self.shell.input_transformer_manager.check_complete(code)
248 253 r = {'status': status}
249 254 if status == 'incomplete':
250 255 r['indent'] = ' ' * indent_spaces
251 256 return r
252 257
253 258 def do_apply(self, content, bufs, msg_id, reply_metadata):
254 259 shell = self.shell
255 260 try:
256 261 working = shell.user_ns
257 262
258 263 prefix = "_"+str(msg_id).replace("-","")+"_"
259 264
260 265 f,args,kwargs = unpack_apply_message(bufs, working, copy=False)
261 266
262 267 fname = getattr(f, '__name__', 'f')
263 268
264 269 fname = prefix+"f"
265 270 argname = prefix+"args"
266 271 kwargname = prefix+"kwargs"
267 272 resultname = prefix+"result"
268 273
269 274 ns = { fname : f, argname : args, kwargname : kwargs , resultname : None }
270 275 # print ns
271 276 working.update(ns)
272 277 code = "%s = %s(*%s,**%s)" % (resultname, fname, argname, kwargname)
273 278 try:
274 279 exec(code, shell.user_global_ns, shell.user_ns)
275 280 result = working.get(resultname)
276 281 finally:
277 282 for key in ns:
278 283 working.pop(key)
279 284
280 285 result_buf = serialize_object(result,
281 286 buffer_threshold=self.session.buffer_threshold,
282 287 item_threshold=self.session.item_threshold,
283 288 )
284 289
285 290 except:
286 291 # invoke IPython traceback formatting
287 292 shell.showtraceback()
288 293 # FIXME - fish exception info out of shell, possibly left there by
289 294 # run_code. We'll need to clean up this logic later.
290 295 reply_content = {}
291 296 if shell._reply_content is not None:
292 297 reply_content.update(shell._reply_content)
293 298 e_info = dict(engine_uuid=self.ident, engine_id=self.int_id, method='apply')
294 299 reply_content['engine_info'] = e_info
295 300 # reset after use
296 301 shell._reply_content = None
297 302
298 303 self.send_response(self.iopub_socket, u'error', reply_content,
299 304 ident=self._topic('error'))
300 305 self.log.info("Exception in apply request:\n%s", '\n'.join(reply_content['traceback']))
301 306 result_buf = []
302 307
303 308 if reply_content['ename'] == 'UnmetDependency':
304 309 reply_metadata['dependencies_met'] = False
305 310 else:
306 311 reply_content = {'status' : 'ok'}
307 312
308 313 return reply_content, result_buf
309 314
310 315 def do_clear(self):
311 316 self.shell.reset(False)
312 317 return dict(status='ok')
313 318
314 319
315 320 # This exists only for backwards compatibility - use IPythonKernel instead
316 321
317 322 @undoc
318 323 class Kernel(IPythonKernel):
319 324 def __init__(self, *args, **kwargs):
320 325 import warnings
321 326 warnings.warn('Kernel is a deprecated alias of IPython.kernel.zmq.ipkernel.IPythonKernel',
322 327 DeprecationWarning)
323 328 super(Kernel, self).__init__(*args, **kwargs) No newline at end of file
@@ -1,692 +1,697 b''
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 # This should be overridden by wrapper kernels that implement any real
64 # language.
65 language_info = {}
66
63 67 # Private interface
64 68
65 69 _darwin_app_nap = Bool(True, config=True,
66 70 help="""Whether to use appnope for compatiblity with OS X App Nap.
67 71
68 72 Only affects OS X >= 10.9.
69 73 """
70 74 )
71 75
72 76 # track associations with current request
73 77 _allow_stdin = Bool(False)
74 78 _parent_header = Dict()
75 79 _parent_ident = Any(b'')
76 80 # Time to sleep after flushing the stdout/err buffers in each execute
77 81 # cycle. While this introduces a hard limit on the minimal latency of the
78 82 # execute cycle, it helps prevent output synchronization problems for
79 83 # clients.
80 84 # Units are in seconds. The minimum zmq latency on local host is probably
81 85 # ~150 microseconds, set this to 500us for now. We may need to increase it
82 86 # a little if it's not enough after more interactive testing.
83 87 _execute_sleep = Float(0.0005, config=True)
84 88
85 89 # Frequency of the kernel's event loop.
86 90 # Units are in seconds, kernel subclasses for GUI toolkits may need to
87 91 # adapt to milliseconds.
88 92 _poll_interval = Float(0.05, config=True)
89 93
90 94 # If the shutdown was requested over the network, we leave here the
91 95 # necessary reply message so it can be sent by our registered atexit
92 96 # handler. This ensures that the reply is only sent to clients truly at
93 97 # the end of our shutdown process (which happens after the underlying
94 98 # IPython shell's own shutdown).
95 99 _shutdown_message = None
96 100
97 101 # This is a dict of port number that the kernel is listening on. It is set
98 102 # by record_ports and used by connect_request.
99 103 _recorded_ports = Dict()
100 104
101 105 # set of aborted msg_ids
102 106 aborted = Set()
103 107
104 108 # Track execution count here. For IPython, we override this to use the
105 109 # execution count we store in the shell.
106 110 execution_count = 0
107 111
108 112
109 113 def __init__(self, **kwargs):
110 114 super(Kernel, self).__init__(**kwargs)
111 115
112 116 # Build dict of handlers for message types
113 117 msg_types = [ 'execute_request', 'complete_request',
114 118 'inspect_request', 'history_request',
115 119 'kernel_info_request',
116 120 'connect_request', 'shutdown_request',
117 121 'apply_request', 'is_complete_request',
118 122 ]
119 123 self.shell_handlers = {}
120 124 for msg_type in msg_types:
121 125 self.shell_handlers[msg_type] = getattr(self, msg_type)
122 126
123 127 control_msg_types = msg_types + [ 'clear_request', 'abort_request' ]
124 128 self.control_handlers = {}
125 129 for msg_type in control_msg_types:
126 130 self.control_handlers[msg_type] = getattr(self, msg_type)
127 131
128 132
129 133 def dispatch_control(self, msg):
130 134 """dispatch control requests"""
131 135 idents,msg = self.session.feed_identities(msg, copy=False)
132 136 try:
133 137 msg = self.session.deserialize(msg, content=True, copy=False)
134 138 except:
135 139 self.log.error("Invalid Control Message", exc_info=True)
136 140 return
137 141
138 142 self.log.debug("Control received: %s", msg)
139 143
140 144 # Set the parent message for side effects.
141 145 self.set_parent(idents, msg)
142 146 self._publish_status(u'busy')
143 147
144 148 header = msg['header']
145 149 msg_type = header['msg_type']
146 150
147 151 handler = self.control_handlers.get(msg_type, None)
148 152 if handler is None:
149 153 self.log.error("UNKNOWN CONTROL MESSAGE TYPE: %r", msg_type)
150 154 else:
151 155 try:
152 156 handler(self.control_stream, idents, msg)
153 157 except Exception:
154 158 self.log.error("Exception in control handler:", exc_info=True)
155 159
156 160 sys.stdout.flush()
157 161 sys.stderr.flush()
158 162 self._publish_status(u'idle')
159 163
160 164 def dispatch_shell(self, stream, msg):
161 165 """dispatch shell requests"""
162 166 # flush control requests first
163 167 if self.control_stream:
164 168 self.control_stream.flush()
165 169
166 170 idents,msg = self.session.feed_identities(msg, copy=False)
167 171 try:
168 172 msg = self.session.deserialize(msg, content=True, copy=False)
169 173 except:
170 174 self.log.error("Invalid Message", exc_info=True)
171 175 return
172 176
173 177 # Set the parent message for side effects.
174 178 self.set_parent(idents, msg)
175 179 self._publish_status(u'busy')
176 180
177 181 header = msg['header']
178 182 msg_id = header['msg_id']
179 183 msg_type = msg['header']['msg_type']
180 184
181 185 # Print some info about this message and leave a '--->' marker, so it's
182 186 # easier to trace visually the message chain when debugging. Each
183 187 # handler prints its message at the end.
184 188 self.log.debug('\n*** MESSAGE TYPE:%s***', msg_type)
185 189 self.log.debug(' Content: %s\n --->\n ', msg['content'])
186 190
187 191 if msg_id in self.aborted:
188 192 self.aborted.remove(msg_id)
189 193 # is it safe to assume a msg_id will not be resubmitted?
190 194 reply_type = msg_type.split('_')[0] + '_reply'
191 195 status = {'status' : 'aborted'}
192 196 md = {'engine' : self.ident}
193 197 md.update(status)
194 198 self.session.send(stream, reply_type, metadata=md,
195 199 content=status, parent=msg, ident=idents)
196 200 return
197 201
198 202 handler = self.shell_handlers.get(msg_type, None)
199 203 if handler is None:
200 204 self.log.error("UNKNOWN MESSAGE TYPE: %r", msg_type)
201 205 else:
202 206 # ensure default_int_handler during handler call
203 207 sig = signal(SIGINT, default_int_handler)
204 208 self.log.debug("%s: %s", msg_type, msg)
205 209 try:
206 210 handler(stream, idents, msg)
207 211 except Exception:
208 212 self.log.error("Exception in message handler:", exc_info=True)
209 213 finally:
210 214 signal(SIGINT, sig)
211 215
212 216 sys.stdout.flush()
213 217 sys.stderr.flush()
214 218 self._publish_status(u'idle')
215 219
216 220 def enter_eventloop(self):
217 221 """enter eventloop"""
218 222 self.log.info("entering eventloop %s", self.eventloop)
219 223 for stream in self.shell_streams:
220 224 # flush any pending replies,
221 225 # which may be skipped by entering the eventloop
222 226 stream.flush(zmq.POLLOUT)
223 227 # restore default_int_handler
224 228 signal(SIGINT, default_int_handler)
225 229 while self.eventloop is not None:
226 230 try:
227 231 self.eventloop(self)
228 232 except KeyboardInterrupt:
229 233 # Ctrl-C shouldn't crash the kernel
230 234 self.log.error("KeyboardInterrupt caught in kernel")
231 235 continue
232 236 else:
233 237 # eventloop exited cleanly, this means we should stop (right?)
234 238 self.eventloop = None
235 239 break
236 240 self.log.info("exiting eventloop")
237 241
238 242 def start(self):
239 243 """register dispatchers for streams"""
240 244 if self.control_stream:
241 245 self.control_stream.on_recv(self.dispatch_control, copy=False)
242 246
243 247 def make_dispatcher(stream):
244 248 def dispatcher(msg):
245 249 return self.dispatch_shell(stream, msg)
246 250 return dispatcher
247 251
248 252 for s in self.shell_streams:
249 253 s.on_recv(make_dispatcher(s), copy=False)
250 254
251 255 # publish idle status
252 256 self._publish_status('starting')
253 257
254 258 def do_one_iteration(self):
255 259 """step eventloop just once"""
256 260 if self.control_stream:
257 261 self.control_stream.flush()
258 262 for stream in self.shell_streams:
259 263 # handle at most one request per iteration
260 264 stream.flush(zmq.POLLIN, 1)
261 265 stream.flush(zmq.POLLOUT)
262 266
263 267
264 268 def record_ports(self, ports):
265 269 """Record the ports that this kernel is using.
266 270
267 271 The creator of the Kernel instance must call this methods if they
268 272 want the :meth:`connect_request` method to return the port numbers.
269 273 """
270 274 self._recorded_ports = ports
271 275
272 276 #---------------------------------------------------------------------------
273 277 # Kernel request handlers
274 278 #---------------------------------------------------------------------------
275 279
276 280 def _make_metadata(self, other=None):
277 281 """init metadata dict, for execute/apply_reply"""
278 282 new_md = {
279 283 'dependencies_met' : True,
280 284 'engine' : self.ident,
281 285 'started': datetime.now(),
282 286 }
283 287 if other:
284 288 new_md.update(other)
285 289 return new_md
286 290
287 291 def _publish_execute_input(self, code, parent, execution_count):
288 292 """Publish the code request on the iopub stream."""
289 293
290 294 self.session.send(self.iopub_socket, u'execute_input',
291 295 {u'code':code, u'execution_count': execution_count},
292 296 parent=parent, ident=self._topic('execute_input')
293 297 )
294 298
295 299 def _publish_status(self, status, parent=None):
296 300 """send status (busy/idle) on IOPub"""
297 301 self.session.send(self.iopub_socket,
298 302 u'status',
299 303 {u'execution_state': status},
300 304 parent=parent or self._parent_header,
301 305 ident=self._topic('status'),
302 306 )
303 307
304 308 def set_parent(self, ident, parent):
305 309 """Set the current parent_header
306 310
307 311 Side effects (IOPub messages) and replies are associated with
308 312 the request that caused them via the parent_header.
309 313
310 314 The parent identity is used to route input_request messages
311 315 on the stdin channel.
312 316 """
313 317 self._parent_ident = ident
314 318 self._parent_header = parent
315 319
316 320 def send_response(self, stream, msg_or_type, content=None, ident=None,
317 321 buffers=None, track=False, header=None, metadata=None):
318 322 """Send a response to the message we're currently processing.
319 323
320 324 This accepts all the parameters of :meth:`IPython.kernel.zmq.session.Session.send`
321 325 except ``parent``.
322 326
323 327 This relies on :meth:`set_parent` having been called for the current
324 328 message.
325 329 """
326 330 return self.session.send(stream, msg_or_type, content, self._parent_header,
327 331 ident, buffers, track, header, metadata)
328 332
329 333 def execute_request(self, stream, ident, parent):
330 334 """handle an execute_request"""
331 335
332 336 try:
333 337 content = parent[u'content']
334 338 code = py3compat.cast_unicode_py2(content[u'code'])
335 339 silent = content[u'silent']
336 340 store_history = content.get(u'store_history', not silent)
337 341 user_expressions = content.get('user_expressions', {})
338 342 allow_stdin = content.get('allow_stdin', False)
339 343 except:
340 344 self.log.error("Got bad msg: ")
341 345 self.log.error("%s", parent)
342 346 return
343 347
344 348 md = self._make_metadata(parent['metadata'])
345 349
346 350 # Re-broadcast our input for the benefit of listening clients, and
347 351 # start computing output
348 352 if not silent:
349 353 self.execution_count += 1
350 354 self._publish_execute_input(code, parent, self.execution_count)
351 355
352 356 reply_content = self.do_execute(code, silent, store_history,
353 357 user_expressions, allow_stdin)
354 358
355 359 # Flush output before sending the reply.
356 360 sys.stdout.flush()
357 361 sys.stderr.flush()
358 362 # FIXME: on rare occasions, the flush doesn't seem to make it to the
359 363 # clients... This seems to mitigate the problem, but we definitely need
360 364 # to better understand what's going on.
361 365 if self._execute_sleep:
362 366 time.sleep(self._execute_sleep)
363 367
364 368 # Send the reply.
365 369 reply_content = json_clean(reply_content)
366 370
367 371 md['status'] = reply_content['status']
368 372 if reply_content['status'] == 'error' and \
369 373 reply_content['ename'] == 'UnmetDependency':
370 374 md['dependencies_met'] = False
371 375
372 376 reply_msg = self.session.send(stream, u'execute_reply',
373 377 reply_content, parent, metadata=md,
374 378 ident=ident)
375 379
376 380 self.log.debug("%s", reply_msg)
377 381
378 382 if not silent and reply_msg['content']['status'] == u'error':
379 383 self._abort_queues()
380 384
381 385 def do_execute(self, code, silent, store_history=True,
382 386 user_experssions=None, allow_stdin=False):
383 387 """Execute user code. Must be overridden by subclasses.
384 388 """
385 389 raise NotImplementedError
386 390
387 391 def complete_request(self, stream, ident, parent):
388 392 content = parent['content']
389 393 code = content['code']
390 394 cursor_pos = content['cursor_pos']
391 395
392 396 matches = self.do_complete(code, cursor_pos)
393 397 matches = json_clean(matches)
394 398 completion_msg = self.session.send(stream, 'complete_reply',
395 399 matches, parent, ident)
396 400 self.log.debug("%s", completion_msg)
397 401
398 402 def do_complete(self, code, cursor_pos):
399 403 """Override in subclasses to find completions.
400 404 """
401 405 return {'matches' : [],
402 406 'cursor_end' : cursor_pos,
403 407 'cursor_start' : cursor_pos,
404 408 'metadata' : {},
405 409 'status' : 'ok'}
406 410
407 411 def inspect_request(self, stream, ident, parent):
408 412 content = parent['content']
409 413
410 414 reply_content = self.do_inspect(content['code'], content['cursor_pos'],
411 415 content.get('detail_level', 0))
412 416 # Before we send this object over, we scrub it for JSON usage
413 417 reply_content = json_clean(reply_content)
414 418 msg = self.session.send(stream, 'inspect_reply',
415 419 reply_content, parent, ident)
416 420 self.log.debug("%s", msg)
417 421
418 422 def do_inspect(self, code, cursor_pos, detail_level=0):
419 423 """Override in subclasses to allow introspection.
420 424 """
421 425 return {'status': 'ok', 'data':{}, 'metadata':{}, 'found':False}
422 426
423 427 def history_request(self, stream, ident, parent):
424 428 content = parent['content']
425 429
426 430 reply_content = self.do_history(**content)
427 431
428 432 reply_content = json_clean(reply_content)
429 433 msg = self.session.send(stream, 'history_reply',
430 434 reply_content, parent, ident)
431 435 self.log.debug("%s", msg)
432 436
433 437 def do_history(self, hist_access_type, output, raw, session=None, start=None,
434 438 stop=None, n=None, pattern=None, unique=False):
435 439 """Override in subclasses to access history.
436 440 """
437 441 return {'history': []}
438 442
439 443 def connect_request(self, stream, ident, parent):
440 444 if self._recorded_ports is not None:
441 445 content = self._recorded_ports.copy()
442 446 else:
443 447 content = {}
444 448 msg = self.session.send(stream, 'connect_reply',
445 449 content, parent, ident)
446 450 self.log.debug("%s", msg)
447 451
448 452 @property
449 453 def kernel_info(self):
450 454 return {
451 455 'protocol_version': release.kernel_protocol_version,
452 456 'implementation': self.implementation,
453 457 'implementation_version': self.implementation_version,
454 458 'language': self.language,
455 459 'language_version': self.language_version,
460 'language_info': self.language_info,
456 461 'banner': self.banner,
457 462 }
458 463
459 464 def kernel_info_request(self, stream, ident, parent):
460 465 msg = self.session.send(stream, 'kernel_info_reply',
461 466 self.kernel_info, parent, ident)
462 467 self.log.debug("%s", msg)
463 468
464 469 def shutdown_request(self, stream, ident, parent):
465 470 content = self.do_shutdown(parent['content']['restart'])
466 471 self.session.send(stream, u'shutdown_reply', content, parent, ident=ident)
467 472 # same content, but different msg_id for broadcasting on IOPub
468 473 self._shutdown_message = self.session.msg(u'shutdown_reply',
469 474 content, parent
470 475 )
471 476
472 477 self._at_shutdown()
473 478 # call sys.exit after a short delay
474 479 loop = ioloop.IOLoop.instance()
475 480 loop.add_timeout(time.time()+0.1, loop.stop)
476 481
477 482 def do_shutdown(self, restart):
478 483 """Override in subclasses to do things when the frontend shuts down the
479 484 kernel.
480 485 """
481 486 return {'status': 'ok', 'restart': restart}
482 487
483 488 def is_complete_request(self, stream, ident, parent):
484 489 content = parent['content']
485 490 code = content['code']
486 491
487 492 reply_content = self.do_is_complete(code)
488 493 reply_content = json_clean(reply_content)
489 494 reply_msg = self.session.send(stream, 'is_complete_reply',
490 495 reply_content, parent, ident)
491 496 self.log.debug("%s", reply_msg)
492 497
493 498 def do_is_complete(self, code):
494 499 """Override in subclasses to find completions.
495 500 """
496 501 return {'status' : 'unknown',
497 502 }
498 503
499 504 #---------------------------------------------------------------------------
500 505 # Engine methods
501 506 #---------------------------------------------------------------------------
502 507
503 508 def apply_request(self, stream, ident, parent):
504 509 try:
505 510 content = parent[u'content']
506 511 bufs = parent[u'buffers']
507 512 msg_id = parent['header']['msg_id']
508 513 except:
509 514 self.log.error("Got bad msg: %s", parent, exc_info=True)
510 515 return
511 516
512 517 md = self._make_metadata(parent['metadata'])
513 518
514 519 reply_content, result_buf = self.do_apply(content, bufs, msg_id, md)
515 520
516 521 # put 'ok'/'error' status in header, for scheduler introspection:
517 522 md['status'] = reply_content['status']
518 523
519 524 # flush i/o
520 525 sys.stdout.flush()
521 526 sys.stderr.flush()
522 527
523 528 self.session.send(stream, u'apply_reply', reply_content,
524 529 parent=parent, ident=ident,buffers=result_buf, metadata=md)
525 530
526 531 def do_apply(self, content, bufs, msg_id, reply_metadata):
527 532 """Override in subclasses to support the IPython parallel framework.
528 533 """
529 534 raise NotImplementedError
530 535
531 536 #---------------------------------------------------------------------------
532 537 # Control messages
533 538 #---------------------------------------------------------------------------
534 539
535 540 def abort_request(self, stream, ident, parent):
536 541 """abort a specific msg by id"""
537 542 msg_ids = parent['content'].get('msg_ids', None)
538 543 if isinstance(msg_ids, string_types):
539 544 msg_ids = [msg_ids]
540 545 if not msg_ids:
541 546 self._abort_queues()
542 547 for mid in msg_ids:
543 548 self.aborted.add(str(mid))
544 549
545 550 content = dict(status='ok')
546 551 reply_msg = self.session.send(stream, 'abort_reply', content=content,
547 552 parent=parent, ident=ident)
548 553 self.log.debug("%s", reply_msg)
549 554
550 555 def clear_request(self, stream, idents, parent):
551 556 """Clear our namespace."""
552 557 content = self.do_clear()
553 558 self.session.send(stream, 'clear_reply', ident=idents, parent=parent,
554 559 content = content)
555 560
556 561 def do_clear(self):
557 562 """Override in subclasses to clear the namespace
558 563
559 564 This is only required for IPython.parallel.
560 565 """
561 566 raise NotImplementedError
562 567
563 568 #---------------------------------------------------------------------------
564 569 # Protected interface
565 570 #---------------------------------------------------------------------------
566 571
567 572 def _topic(self, topic):
568 573 """prefixed topic for IOPub messages"""
569 574 if self.int_id >= 0:
570 575 base = "engine.%i" % self.int_id
571 576 else:
572 577 base = "kernel.%s" % self.ident
573 578
574 579 return py3compat.cast_bytes("%s.%s" % (base, topic))
575 580
576 581 def _abort_queues(self):
577 582 for stream in self.shell_streams:
578 583 if stream:
579 584 self._abort_queue(stream)
580 585
581 586 def _abort_queue(self, stream):
582 587 poller = zmq.Poller()
583 588 poller.register(stream.socket, zmq.POLLIN)
584 589 while True:
585 590 idents,msg = self.session.recv(stream, zmq.NOBLOCK, content=True)
586 591 if msg is None:
587 592 return
588 593
589 594 self.log.info("Aborting:")
590 595 self.log.info("%s", msg)
591 596 msg_type = msg['header']['msg_type']
592 597 reply_type = msg_type.split('_')[0] + '_reply'
593 598
594 599 status = {'status' : 'aborted'}
595 600 md = {'engine' : self.ident}
596 601 md.update(status)
597 602 reply_msg = self.session.send(stream, reply_type, metadata=md,
598 603 content=status, parent=msg, ident=idents)
599 604 self.log.debug("%s", reply_msg)
600 605 # We need to wait a bit for requests to come in. This can probably
601 606 # be set shorter for true asynchronous clients.
602 607 poller.poll(50)
603 608
604 609
605 610 def _no_raw_input(self):
606 611 """Raise StdinNotImplentedError if active frontend doesn't support
607 612 stdin."""
608 613 raise StdinNotImplementedError("raw_input was called, but this "
609 614 "frontend does not support stdin.")
610 615
611 616 def getpass(self, prompt=''):
612 617 """Forward getpass to frontends
613 618
614 619 Raises
615 620 ------
616 621 StdinNotImplentedError if active frontend doesn't support stdin.
617 622 """
618 623 if not self._allow_stdin:
619 624 raise StdinNotImplementedError(
620 625 "getpass was called, but this frontend does not support input requests."
621 626 )
622 627 return self._input_request(prompt,
623 628 self._parent_ident,
624 629 self._parent_header,
625 630 password=True,
626 631 )
627 632
628 633 def raw_input(self, prompt=''):
629 634 """Forward raw_input to frontends
630 635
631 636 Raises
632 637 ------
633 638 StdinNotImplentedError if active frontend doesn't support stdin.
634 639 """
635 640 if not self._allow_stdin:
636 641 raise StdinNotImplementedError(
637 642 "raw_input was called, but this frontend does not support input requests."
638 643 )
639 644 return self._input_request(prompt,
640 645 self._parent_ident,
641 646 self._parent_header,
642 647 password=False,
643 648 )
644 649
645 650 def _input_request(self, prompt, ident, parent, password=False):
646 651 # Flush output before making the request.
647 652 sys.stderr.flush()
648 653 sys.stdout.flush()
649 654 # flush the stdin socket, to purge stale replies
650 655 while True:
651 656 try:
652 657 self.stdin_socket.recv_multipart(zmq.NOBLOCK)
653 658 except zmq.ZMQError as e:
654 659 if e.errno == zmq.EAGAIN:
655 660 break
656 661 else:
657 662 raise
658 663
659 664 # Send the input request.
660 665 content = json_clean(dict(prompt=prompt, password=password))
661 666 self.session.send(self.stdin_socket, u'input_request', content, parent,
662 667 ident=ident)
663 668
664 669 # Await a response.
665 670 while True:
666 671 try:
667 672 ident, reply = self.session.recv(self.stdin_socket, 0)
668 673 except Exception:
669 674 self.log.warn("Invalid Message:", exc_info=True)
670 675 except KeyboardInterrupt:
671 676 # re-raise KeyboardInterrupt, to truncate traceback
672 677 raise KeyboardInterrupt
673 678 else:
674 679 break
675 680 try:
676 681 value = py3compat.unicode_to_str(reply['content']['value'])
677 682 except:
678 683 self.log.error("Bad input_reply: %s", parent)
679 684 value = ''
680 685 if value == '\x04':
681 686 # EOF
682 687 raise EOFError
683 688 return value
684 689
685 690 def _at_shutdown(self):
686 691 """Actions taken at shutdown by the kernel, called by python's atexit.
687 692 """
688 693 # io.rprint("Kernel at_shutdown") # dbg
689 694 if self._shutdown_message is not None:
690 695 self.session.send(self.iopub_socket, self._shutdown_message, ident=self._topic('shutdown'))
691 696 self.log.debug("%s", self._shutdown_message)
692 697 [ s.flush(zmq.POLLOUT) for s in self.shell_streams ]
@@ -1,66 +1,66 b''
1 1 """HTML Exporter class"""
2 2
3 3 #-----------------------------------------------------------------------------
4 4 # Copyright (c) 2013, the IPython Development Team.
5 5 #
6 6 # Distributed under the terms of the Modified BSD License.
7 7 #
8 8 # The full license is in the file COPYING.txt, distributed with this software.
9 9 #-----------------------------------------------------------------------------
10 10
11 11 #-----------------------------------------------------------------------------
12 12 # Imports
13 13 #-----------------------------------------------------------------------------
14 14
15 15 import os
16 16
17 17 from IPython.nbconvert.filters.highlight import Highlight2HTML
18 18 from IPython.config import Config
19 19
20 20 from .templateexporter import TemplateExporter
21 21
22 22 #-----------------------------------------------------------------------------
23 23 # Classes
24 24 #-----------------------------------------------------------------------------
25 25
26 26 class HTMLExporter(TemplateExporter):
27 27 """
28 28 Exports a basic HTML document. This exporter assists with the export of
29 29 HTML. Inherit from it if you are writing your own HTML template and need
30 30 custom preprocessors/filters. If you don't need custom preprocessors/
31 31 filters, just change the 'template_file' config option.
32 32 """
33 33
34 34 def _file_extension_default(self):
35 35 return 'html'
36 36
37 37 def _default_template_path_default(self):
38 38 return os.path.join("..", "templates", "html")
39 39
40 40 def _template_file_default(self):
41 41 return 'full'
42 42
43 43 output_mimetype = 'text/html'
44 44
45 45 @property
46 46 def default_config(self):
47 47 c = Config({
48 48 'NbConvertBase': {
49 49 'display_data_priority' : ['javascript', 'html', 'application/pdf', 'svg', 'latex', 'png', 'jpg', 'jpeg' , 'text']
50 50 },
51 51 'CSSHTMLHeaderPreprocessor':{
52 52 'enabled':True
53 53 },
54 54 'HighlightMagicsPreprocessor': {
55 55 'enabled':True
56 56 }
57 57 })
58 58 c.merge(super(HTMLExporter,self).default_config)
59 59 return c
60 60
61 61 def from_notebook_node(self, nb, resources=None, **kw):
62 kernelspec = nb.metadata.get('kernelspec', {})
63 lexer = kernelspec.get('pygments_lexer', kernelspec.get('language', None))
62 langinfo = nb.metadata.get('language_info', {})
63 lexer = langinfo.get('pygments_lexer', langinfo.get('name', None))
64 64 self.register_filter('highlight_code',
65 65 Highlight2HTML(pygments_lexer=lexer, parent=self))
66 66 return super(HTMLExporter, self).from_notebook_node(nb, resources, **kw)
@@ -1,96 +1,96 b''
1 1 """LaTeX Exporter class"""
2 2
3 3 #-----------------------------------------------------------------------------
4 4 # Copyright (c) 2013, the IPython Development Team.
5 5 #
6 6 # Distributed under the terms of the Modified BSD License.
7 7 #
8 8 # The full license is in the file COPYING.txt, distributed with this software.
9 9 #-----------------------------------------------------------------------------
10 10
11 11 #-----------------------------------------------------------------------------
12 12 # Imports
13 13 #-----------------------------------------------------------------------------
14 14
15 15 # Stdlib imports
16 16 import os
17 17
18 18 # IPython imports
19 19 from IPython.utils.traitlets import Unicode
20 20 from IPython.config import Config
21 21
22 22 from IPython.nbconvert.filters.highlight import Highlight2Latex
23 23 from .templateexporter import TemplateExporter
24 24
25 25 #-----------------------------------------------------------------------------
26 26 # Classes and functions
27 27 #-----------------------------------------------------------------------------
28 28
29 29 class LatexExporter(TemplateExporter):
30 30 """
31 31 Exports to a Latex template. Inherit from this class if your template is
32 32 LaTeX based and you need custom tranformers/filters. Inherit from it if
33 33 you are writing your own HTML template and need custom tranformers/filters.
34 34 If you don't need custom tranformers/filters, just change the
35 35 'template_file' config option. Place your template in the special "/latex"
36 36 subfolder of the "../templates" folder.
37 37 """
38 38
39 39 def _file_extension_default(self):
40 40 return 'tex'
41 41
42 42 def _template_file_default(self):
43 43 return 'article'
44 44
45 45 #Latex constants
46 46 def _default_template_path_default(self):
47 47 return os.path.join("..", "templates", "latex")
48 48
49 49 def _template_skeleton_path_default(self):
50 50 return os.path.join("..", "templates", "latex", "skeleton")
51 51
52 52 #Special Jinja2 syntax that will not conflict when exporting latex.
53 53 jinja_comment_block_start = Unicode("((=", config=True)
54 54 jinja_comment_block_end = Unicode("=))", config=True)
55 55 jinja_variable_block_start = Unicode("(((", config=True)
56 56 jinja_variable_block_end = Unicode(")))", config=True)
57 57 jinja_logic_block_start = Unicode("((*", config=True)
58 58 jinja_logic_block_end = Unicode("*))", config=True)
59 59
60 60 #Extension that the template files use.
61 61 template_extension = Unicode(".tplx", config=True)
62 62
63 63 output_mimetype = 'text/latex'
64 64
65 65
66 66 @property
67 67 def default_config(self):
68 68 c = Config({
69 69 'NbConvertBase': {
70 70 'display_data_priority' : ['latex', 'application/pdf', 'png', 'jpg', 'svg', 'jpeg', 'text']
71 71 },
72 72 'ExtractOutputPreprocessor': {
73 73 'enabled':True
74 74 },
75 75 'SVG2PDFPreprocessor': {
76 76 'enabled':True
77 77 },
78 78 'LatexPreprocessor': {
79 79 'enabled':True
80 80 },
81 81 'SphinxPreprocessor': {
82 82 'enabled':True
83 83 },
84 84 'HighlightMagicsPreprocessor': {
85 85 'enabled':True
86 86 }
87 87 })
88 88 c.merge(super(LatexExporter,self).default_config)
89 89 return c
90 90
91 91 def from_notebook_node(self, nb, resources=None, **kw):
92 kernelspec = nb.metadata.get('kernelspec', {})
93 lexer = kernelspec.get('pygments_lexer', kernelspec.get('language', None))
92 langinfo = nb.metadata.get('language_info', {})
93 lexer = langinfo.get('pygments_lexer', langinfo.get('name', None))
94 94 self.register_filter('highlight_code',
95 95 Highlight2Latex(pygments_lexer=lexer, parent=self))
96 96 return super(LatexExporter, self).from_notebook_node(nb, resources, **kw)
@@ -1,135 +1,135 b''
1 1 """
2 2 Module containing filter functions that allow code to be highlighted
3 3 from within Jinja templates.
4 4 """
5 5
6 6 # Copyright (c) IPython Development Team.
7 7 # Distributed under the terms of the Modified BSD License.
8 8
9 9 # pygments must not be imported at the module level
10 10 # because errors should be raised at runtime if it's actually needed,
11 11 # not import time, when it may not be needed.
12 12
13 13 from IPython.nbconvert.utils.base import NbConvertBase
14 14 from warnings import warn
15 15
16 16 MULTILINE_OUTPUTS = ['text', 'html', 'svg', 'latex', 'javascript', 'json']
17 17
18 18 __all__ = [
19 19 'Highlight2HTML',
20 20 'Highlight2Latex'
21 21 ]
22 22
23 23 class Highlight2HTML(NbConvertBase):
24 24 def __init__(self, pygments_lexer=None, **kwargs):
25 25 self.pygments_lexer = pygments_lexer or 'ipython3'
26 26 super(Highlight2HTML, self).__init__(**kwargs)
27 27
28 28 def _default_language_changed(self, name, old, new):
29 29 warn('Setting default_language in config is deprecated, '
30 'please use kernelspecs instead.')
30 'please use language_info metadata instead.')
31 31 self.pygments_lexer = new
32 32
33 33 def __call__(self, source, language=None, metadata=None):
34 34 """
35 35 Return a syntax-highlighted version of the input source as html output.
36 36
37 37 Parameters
38 38 ----------
39 39 source : str
40 40 source of the cell to highlight
41 41 language : str
42 42 language to highlight the syntax of
43 43 metadata : NotebookNode cell metadata
44 44 metadata of the cell to highlight
45 45 """
46 46 from pygments.formatters import HtmlFormatter
47 47
48 48 if not language:
49 49 language=self.pygments_lexer
50 50
51 51 return _pygments_highlight(source if len(source) > 0 else ' ',
52 52 # needed to help post processors:
53 53 HtmlFormatter(cssclass=" highlight hl-"+language),
54 54 language, metadata)
55 55
56 56
57 57 class Highlight2Latex(NbConvertBase):
58 58 def __init__(self, pygments_lexer=None, **kwargs):
59 59 self.pygments_lexer = pygments_lexer or 'ipython3'
60 60 super(Highlight2Latex, self).__init__(**kwargs)
61 61
62 62 def _default_language_changed(self, name, old, new):
63 63 warn('Setting default_language in config is deprecated, '
64 'please use kernelspecs instead.')
64 'please use language_info metadata instead.')
65 65 self.pygments_lexer = new
66 66
67 67 def __call__(self, source, language=None, metadata=None, strip_verbatim=False):
68 68 """
69 69 Return a syntax-highlighted version of the input source as latex output.
70 70
71 71 Parameters
72 72 ----------
73 73 source : str
74 74 source of the cell to highlight
75 75 language : str
76 76 language to highlight the syntax of
77 77 metadata : NotebookNode cell metadata
78 78 metadata of the cell to highlight
79 79 strip_verbatim : bool
80 80 remove the Verbatim environment that pygments provides by default
81 81 """
82 82 from pygments.formatters import LatexFormatter
83 83 if not language:
84 84 language=self.pygments_lexer
85 85
86 86 latex = _pygments_highlight(source, LatexFormatter(), language, metadata)
87 87 if strip_verbatim:
88 88 latex = latex.replace(r'\begin{Verbatim}[commandchars=\\\{\}]' + '\n', '')
89 89 return latex.replace('\n\\end{Verbatim}\n', '')
90 90 else:
91 91 return latex
92 92
93 93
94 94
95 95 def _pygments_highlight(source, output_formatter, language='ipython', metadata=None):
96 96 """
97 97 Return a syntax-highlighted version of the input source
98 98
99 99 Parameters
100 100 ----------
101 101 source : str
102 102 source of the cell to highlight
103 103 output_formatter : Pygments formatter
104 104 language : str
105 105 language to highlight the syntax of
106 106 metadata : NotebookNode cell metadata
107 107 metadata of the cell to highlight
108 108 """
109 109 from pygments import highlight
110 110 from pygments.lexers import get_lexer_by_name
111 111 from pygments.util import ClassNotFound
112 112 from IPython.nbconvert.utils.lexers import IPythonLexer, IPython3Lexer
113 113
114 114 # If the cell uses a magic extension language,
115 115 # use the magic language instead.
116 116 if language == 'ipython' \
117 117 and metadata \
118 118 and 'magics_language' in metadata:
119 119
120 120 language = metadata['magics_language']
121 121
122 122 if language == 'ipython2':
123 123 lexer = IPythonLexer()
124 124 elif language == 'ipython3':
125 125 lexer = IPython3Lexer()
126 126 else:
127 127 try:
128 128 lexer = get_lexer_by_name(language, stripall=True)
129 129 except ClassNotFound:
130 130 warn("No lexer found for language %r. Treating as plain text." % language)
131 131 from pygments.lexers.special import TextLexer
132 132 lexer = TextLexer()
133 133
134 134
135 135 return highlight(source, lexer, output_formatter)
@@ -1,41 +1,41 b''
1 1 """Global configuration class."""
2 2 #-----------------------------------------------------------------------------
3 3 # Copyright (c) 2013, the IPython Development Team.
4 4 #
5 5 # Distributed under the terms of the Modified BSD License.
6 6 #
7 7 # The full license is in the file COPYING.txt, distributed with this software.
8 8 #-----------------------------------------------------------------------------
9 9
10 10 #-----------------------------------------------------------------------------
11 11 # Imports
12 12 #-----------------------------------------------------------------------------
13 13
14 14 from IPython.utils.traitlets import List
15 15 from IPython.config.configurable import LoggingConfigurable
16 16 from IPython.utils.traitlets import Unicode
17 17
18 18 #-----------------------------------------------------------------------------
19 19 # Classes and functions
20 20 #-----------------------------------------------------------------------------
21 21
22 22 class NbConvertBase(LoggingConfigurable):
23 23 """Global configurable class for shared config
24 24
25 25 Useful for display data priority that might be use by many transformers
26 26 """
27 27
28 28 display_data_priority = List(['html', 'application/pdf', 'svg', 'latex', 'png', 'jpg', 'jpeg' , 'text'],
29 29 config=True,
30 30 help= """
31 31 An ordered list of preferred output type, the first
32 32 encountered will usually be used when converting discarding
33 33 the others.
34 34 """
35 35 )
36 36
37 37 default_language = Unicode('ipython', config=True,
38 help='DEPRECATED default highlight language, please use kernelspecs instead')
38 help='DEPRECATED default highlight language, please use language_info metadata instead')
39 39
40 40 def __init__(self, **kw):
41 41 super(NbConvertBase, self).__init__(**kw)
@@ -1,155 +1,137 b''
1 1 ==========================
2 2 Making kernels for IPython
3 3 ==========================
4 4
5 5 A 'kernel' is a program that runs and introspects the user's code. IPython
6 6 includes a kernel for Python code, and people have written kernels for
7 7 `several other languages <https://github.com/ipython/ipython/wiki/Projects-using-IPython#list-of-some-ipython-compatible-kernels>`_.
8 8
9 9 When IPython starts a kernel, it passes it a connection file. This specifies
10 10 how to set up communications with the frontend.
11 11
12 12 There are two options for writing a kernel:
13 13
14 14 1. You can reuse the IPython kernel machinery to handle the communications, and
15 15 just describe how to execute your code. This is much simpler if the target
16 16 language can be driven from Python. See :doc:`wrapperkernels` for details.
17 17 2. You can implement the kernel machinery in your target language. This is more
18 18 work initially, but the people using your kernel might be more likely to
19 19 contribute to it if it's in the language they know.
20 20
21 21 Connection files
22 22 ================
23 23
24 24 Your kernel will be given the path to a connection file when it starts (see
25 25 :ref:`kernelspecs` for how to specify the command line arguments for your kernel).
26 26 This file, which is accessible only to the current user, will contain a JSON
27 27 dictionary looking something like this::
28 28
29 29 {
30 30 "control_port": 50160,
31 31 "shell_port": 57503,
32 32 "transport": "tcp",
33 33 "signature_scheme": "hmac-sha256",
34 34 "stdin_port": 52597,
35 35 "hb_port": 42540,
36 36 "ip": "127.0.0.1",
37 37 "iopub_port": 40885,
38 38 "key": "a0436f6c-1916-498b-8eb9-e81ab9368e84"
39 39 }
40 40
41 41 The ``transport``, ``ip`` and five ``_port`` fields specify five ports which the
42 42 kernel should bind to using `ZeroMQ <http://zeromq.org/>`_. For instance, the
43 43 address of the shell socket in the example above would be::
44 44
45 45 tcp://127.0.0.1:57503
46 46
47 47 New ports are chosen at random for each kernel started.
48 48
49 49 ``signature_scheme`` and ``key`` are used to cryptographically sign messages, so
50 50 that other users on the system can't send code to run in this kernel. See
51 51 :ref:`wire_protocol` for the details of how this signature is calculated.
52 52
53 53 Handling messages
54 54 =================
55 55
56 56 After reading the connection file and binding to the necessary sockets, the
57 57 kernel should go into an event loop, listening on the hb (heartbeat), control
58 58 and shell sockets.
59 59
60 60 :ref:`Heartbeat <kernel_heartbeat>` messages should be echoed back immediately
61 61 on the same socket - the frontend uses this to check that the kernel is still
62 62 alive.
63 63
64 64 Messages on the control and shell sockets should be parsed, and their signature
65 65 validated. See :ref:`wire_protocol` for how to do this.
66 66
67 67 The kernel will send messages on the iopub socket to display output, and on the
68 68 stdin socket to prompt the user for textual input.
69 69
70 70 .. seealso::
71 71
72 72 :doc:`messaging`
73 73 Details of the different sockets and the messages that come over them
74 74
75 75 `Creating Language Kernels for IPython <http://andrew.gibiansky.com/blog/ipython/ipython-kernels/>`_
76 76 A blog post by the author of `IHaskell <https://github.com/gibiansky/IHaskell>`_,
77 77 a Haskell kernel
78 78
79 79 `simple_kernel <https://github.com/dsblank/simple_kernel>`_
80 80 A simple example implementation of the kernel machinery in Python
81 81
82 82
83 83 .. _kernelspecs:
84 84
85 85 Kernel specs
86 86 ============
87 87
88 88 A kernel identifies itself to IPython by creating a directory, the name of which
89 89 is used as an identifier for the kernel. These may be created in a number of
90 90 locations:
91 91
92 92 +--------+--------------------------------------+-----------------------------------+
93 93 | | Unix | Windows |
94 94 +========+======================================+===================================+
95 95 | System | ``/usr/share/ipython/kernels`` | ``%PROGRAMDATA%\ipython\kernels`` |
96 96 | | | |
97 97 | | ``/usr/local/share/ipython/kernels`` | |
98 98 +--------+--------------------------------------+-----------------------------------+
99 99 | User | ``~/.ipython/kernels`` |
100 100 +--------+--------------------------------------+-----------------------------------+
101 101
102 102 The user location takes priority over the system locations, and the case of the
103 103 names is ignored, so selecting kernels works the same way whether or not the
104 104 filesystem is case sensitive.
105 105
106 106 Inside the directory, the most important file is *kernel.json*. This should be a
107 107 JSON serialised dictionary containing the following keys and values:
108 108
109 109 - **argv**: A list of command line arguments used to start the kernel. The text
110 110 ``{connection_file}`` in any argument will be replaced with the path to the
111 111 connection file.
112 112 - **display_name**: The kernel's name as it should be displayed in the UI.
113 113 Unlike the kernel name used in the API, this can contain arbitrary unicode
114 114 characters.
115 - **language**: The programming language which this kernel runs. This will be
116 stored in notebook metadata. This may be used by syntax highlighters to guess
117 how to parse code in a notebook, and frontends may eventually use it to
118 identify alternative kernels that can run some code.
119 - **codemirror_mode** (optional): The `codemirror mode <http://codemirror.net/mode/index.html>`_
120 to use for code in this language. This can be a string or a dictionary, as
121 passed to codemirror config. This only needs to be specified if it does not
122 match the value in *language*.
123 - **pygments_lexer** (optional): The name of a `Pygments lexer <http://pygments.org/docs/lexers/>`_
124 to use for code in this language, as a string. This only needs to be specified
125 if it does not match the value in *language*.
126 115 - **env** (optional): A dictionary of environment variables to set for the kernel.
127 116 These will be added to the current environment variables before the kernel is
128 117 started.
129 - **help_links** (optional): A list of dictionaries, each with keys 'text' and
130 'url'. These will be displayed in the help menu in the notebook UI.
131 118
132 119 For example, the kernel.json file for IPython looks like this::
133 120
134 121 {
135 122 "argv": ["python3", "-c", "from IPython.kernel.zmq.kernelapp import main; main()",
136 123 "-f", "{connection_file}"],
137 "codemirror_mode": {
138 "version": 3,
139 "name": "ipython"
140 },
141 124 "display_name": "IPython (Python 3)",
142 "language": "python"
143 125 }
144 126
145 127 To see the available kernel specs, run::
146 128
147 129 ipython kernelspec list
148 130
149 131 To start the terminal console or the Qt console with a specific kernel::
150 132
151 133 ipython console --kernel bash
152 134 ipython qtconsole --kernel bash
153 135
154 136 To use different kernels in the notebook, select a different kernel from the
155 137 dropdown menu in the top-right of the UI.
@@ -1,1115 +1,1138 b''
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.
359 359 # Each execution payload is a dict with string keys that may have been
360 360 # produced by the code being executed. It is retrieved by the kernel at
361 361 # the end of the execution and sent back to the front end, which can take
362 362 # action on it as needed.
363 363 # The only requirement of each payload dict is that it have a 'source' key,
364 364 # which is a string classifying the payload (e.g. 'pager').
365 365 'payload' : list(dict),
366 366
367 367 # Results for the user_expressions.
368 368 'user_expressions' : dict,
369 369 }
370 370
371 371 .. versionchanged:: 5.0
372 372
373 373 ``user_variables`` is removed, use user_expressions instead.
374 374
375 375 .. admonition:: Execution payloads
376 376
377 377 The notion of an 'execution payload' is different from a return value of a
378 378 given set of code, which normally is just displayed on the execute_result stream
379 379 through the PUB socket. The idea of a payload is to allow special types of
380 380 code, typically magics, to populate a data container in the IPython kernel
381 381 that will be shipped back to the caller via this channel. The kernel
382 382 has an API for this in the PayloadManager::
383 383
384 384 ip.payload_manager.write_payload(payload_dict)
385 385
386 386 which appends a dictionary to the list of payloads.
387 387
388 388 The payload API is not yet stabilized,
389 389 and should probably not be supported by non-Python kernels at this time.
390 390 In such cases, the payload list should always be empty.
391 391
392 392
393 393 When status is 'error', the following extra fields are present::
394 394
395 395 {
396 396 'ename' : str, # Exception name, as a string
397 397 'evalue' : str, # Exception value, as a string
398 398
399 399 # The traceback will contain a list of frames, represented each as a
400 400 # string. For now we'll stick to the existing design of ultraTB, which
401 401 # controls exception level of detail statefully. But eventually we'll
402 402 # want to grow into a model where more information is collected and
403 403 # packed into the traceback object, with clients deciding how little or
404 404 # how much of it to unpack. But for now, let's start with a simple list
405 405 # of strings, since that requires only minimal changes to ultratb as
406 406 # written.
407 407 'traceback' : list,
408 408 }
409 409
410 410
411 411 When status is 'abort', there are for now no additional data fields. This
412 412 happens when the kernel was interrupted by a signal.
413 413
414 414 .. _msging_inspection:
415 415
416 416 Introspection
417 417 -------------
418 418
419 419 Code can be inspected to show useful information to the user.
420 420 It is up to the Kernel to decide what information should be displayed, and its formatting.
421 421
422 422 Message type: ``inspect_request``::
423 423
424 424 content = {
425 425 # The code context in which introspection is requested
426 426 # this may be up to an entire multiline cell.
427 427 'code' : str,
428 428
429 429 # The cursor position within 'code' (in unicode characters) where inspection is requested
430 430 'cursor_pos' : int,
431 431
432 432 # The level of detail desired. In IPython, the default (0) is equivalent to typing
433 433 # 'x?' at the prompt, 1 is equivalent to 'x??'.
434 434 # The difference is up to kernels, but in IPython level 1 includes the source code
435 435 # if available.
436 436 'detail_level' : 0 or 1,
437 437 }
438 438
439 439 .. versionchanged:: 5.0
440 440
441 441 ``object_info_request`` renamed to ``inspect_request``.
442 442
443 443 .. versionchanged:: 5.0
444 444
445 445 ``name`` key replaced with ``code`` and ``cursor_pos``,
446 446 moving the lexing responsibility to the kernel.
447 447
448 448 The reply is a mime-bundle, like a `display_data`_ message,
449 449 which should be a formatted representation of information about the context.
450 450 In the notebook, this is used to show tooltips over function calls, etc.
451 451
452 452 Message type: ``inspect_reply``::
453 453
454 454 content = {
455 455 # 'ok' if the request succeeded or 'error', with error information as in all other replies.
456 456 'status' : 'ok',
457 457
458 458 # data can be empty if nothing is found
459 459 'data' : dict,
460 460 'metadata' : dict,
461 461 }
462 462
463 463 .. versionchanged:: 5.0
464 464
465 465 ``object_info_reply`` renamed to ``inspect_reply``.
466 466
467 467 .. versionchanged:: 5.0
468 468
469 469 Reply is changed from structured data to a mime bundle, allowing formatting decisions to be made by the kernel.
470 470
471 471 .. _msging_completion:
472 472
473 473 Completion
474 474 ----------
475 475
476 476 Message type: ``complete_request``::
477 477
478 478 content = {
479 479 # The code context in which completion is requested
480 480 # this may be up to an entire multiline cell, such as
481 481 # 'foo = a.isal'
482 482 'code' : str,
483 483
484 484 # The cursor position within 'code' (in unicode characters) where completion is requested
485 485 'cursor_pos' : int,
486 486 }
487 487
488 488 .. versionchanged:: 5.0
489 489
490 490 ``line``, ``block``, and ``text`` keys are removed in favor of a single ``code`` for context.
491 491 Lexing is up to the kernel.
492 492
493 493
494 494 Message type: ``complete_reply``::
495 495
496 496 content = {
497 497 # The list of all matches to the completion request, such as
498 498 # ['a.isalnum', 'a.isalpha'] for the above example.
499 499 'matches' : list,
500 500
501 501 # The range of text that should be replaced by the above matches when a completion is accepted.
502 502 # typically cursor_end is the same as cursor_pos in the request.
503 503 'cursor_start' : int,
504 504 'cursor_end' : int,
505 505
506 506 # Information that frontend plugins might use for extra display information about completions.
507 507 'metadata' : dict,
508 508
509 509 # status should be 'ok' unless an exception was raised during the request,
510 510 # in which case it should be 'error', along with the usual error message content
511 511 # in other messages.
512 512 'status' : 'ok'
513 513 }
514 514
515 515 .. versionchanged:: 5.0
516 516
517 517 - ``matched_text`` is removed in favor of ``cursor_start`` and ``cursor_end``.
518 518 - ``metadata`` is added for extended information.
519 519
520 520 .. _msging_history:
521 521
522 522 History
523 523 -------
524 524
525 525 For clients to explicitly request history from a kernel. The kernel has all
526 526 the actual execution history stored in a single location, so clients can
527 527 request it from the kernel when needed.
528 528
529 529 Message type: ``history_request``::
530 530
531 531 content = {
532 532
533 533 # If True, also return output history in the resulting dict.
534 534 'output' : bool,
535 535
536 536 # If True, return the raw input history, else the transformed input.
537 537 'raw' : bool,
538 538
539 539 # So far, this can be 'range', 'tail' or 'search'.
540 540 'hist_access_type' : str,
541 541
542 542 # If hist_access_type is 'range', get a range of input cells. session can
543 543 # be a positive session number, or a negative number to count back from
544 544 # the current session.
545 545 'session' : int,
546 546 # start and stop are line numbers within that session.
547 547 'start' : int,
548 548 'stop' : int,
549 549
550 550 # If hist_access_type is 'tail' or 'search', get the last n cells.
551 551 'n' : int,
552 552
553 553 # If hist_access_type is 'search', get cells matching the specified glob
554 554 # pattern (with * and ? as wildcards).
555 555 'pattern' : str,
556 556
557 557 # If hist_access_type is 'search' and unique is true, do not
558 558 # include duplicated history. Default is false.
559 559 'unique' : bool,
560 560
561 561 }
562 562
563 563 .. versionadded:: 4.0
564 564 The key ``unique`` for ``history_request``.
565 565
566 566 Message type: ``history_reply``::
567 567
568 568 content = {
569 569 # A list of 3 tuples, either:
570 570 # (session, line_number, input) or
571 571 # (session, line_number, (input, output)),
572 572 # depending on whether output was False or True, respectively.
573 573 'history' : list,
574 574 }
575 575
576 576 .. _msging_is_complete:
577 577
578 578 Code completeness
579 579 -----------------
580 580
581 581 .. versionadded:: 5.0
582 582
583 583 When the user enters a line in a console style interface, the console must
584 584 decide whether to immediately execute the current code, or whether to show a
585 585 continuation prompt for further input. For instance, in Python ``a = 5`` would
586 586 be executed immediately, while ``for i in range(5):`` would expect further input.
587 587
588 588 There are four possible replies:
589 589
590 590 - *complete* code is ready to be executed
591 591 - *incomplete* code should prompt for another line
592 592 - *invalid* code will typically be sent for execution, so that the user sees the
593 593 error soonest.
594 594 - *unknown* - if the kernel is not able to determine this. The frontend should
595 595 also handle the kernel not replying promptly. It may default to sending the
596 596 code for execution, or it may implement simple fallback heuristics for whether
597 597 to execute the code (e.g. execute after a blank line).
598 598
599 599 Frontends may have ways to override this, forcing the code to be sent for
600 600 execution or forcing a continuation prompt.
601 601
602 602 Message type: ``is_complete_request``::
603 603
604 604 content = {
605 605 # The code entered so far as a multiline string
606 606 'code' : str,
607 607 }
608 608
609 609 Message type: ``is_complete_reply``::
610 610
611 611 content = {
612 612 # One of 'complete', 'incomplete', 'invalid', 'unknown'
613 613 'status' : str,
614 614
615 615 # If status is 'incomplete', indent should contain the characters to use
616 616 # to indent the next line. This is only a hint: frontends may ignore it
617 617 # and use their own autoindentation rules. For other statuses, this
618 618 # field does not exist.
619 619 'indent': str,
620 620 }
621 621
622 622 Connect
623 623 -------
624 624
625 625 When a client connects to the request/reply socket of the kernel, it can issue
626 626 a connect request to get basic information about the kernel, such as the ports
627 627 the other ZeroMQ sockets are listening on. This allows clients to only have
628 628 to know about a single port (the shell channel) to connect to a kernel.
629 629
630 630 Message type: ``connect_request``::
631 631
632 632 content = {
633 633 }
634 634
635 635 Message type: ``connect_reply``::
636 636
637 637 content = {
638 638 'shell_port' : int, # The port the shell ROUTER socket is listening on.
639 639 'iopub_port' : int, # The port the PUB socket is listening on.
640 640 'stdin_port' : int, # The port the stdin ROUTER socket is listening on.
641 641 'hb_port' : int, # The port the heartbeat socket is listening on.
642 642 }
643 643
644 644 .. _msging_kernel_info:
645 645
646 646 Kernel info
647 647 -----------
648 648
649 649 If a client needs to know information about the kernel, it can
650 650 make a request of the kernel's information.
651 651 This message can be used to fetch core information of the
652 652 kernel, including language (e.g., Python), language version number and
653 653 IPython version number, and the IPython message spec version number.
654 654
655 655 Message type: ``kernel_info_request``::
656 656
657 657 content = {
658 658 }
659 659
660 660 Message type: ``kernel_info_reply``::
661 661
662 662 content = {
663 663 # Version of messaging protocol.
664 664 # The first integer indicates major version. It is incremented when
665 665 # there is any backward incompatible change.
666 666 # The second integer indicates minor version. It is incremented when
667 667 # there is any backward compatible change.
668 668 'protocol_version': 'X.Y.Z',
669 669
670 670 # The kernel implementation name
671 671 # (e.g. 'ipython' for the IPython kernel)
672 672 'implementation': str,
673 673
674 674 # Implementation version number.
675 675 # The version number of the kernel's implementation
676 676 # (e.g. IPython.__version__ for the IPython kernel)
677 677 'implementation_version': 'X.Y.Z',
678 678
679 679 # Programming language in which kernel is implemented.
680 680 # Kernel included in IPython returns 'python'.
681 681 'language': str,
682 682
683 683 # Language version number.
684 684 # It is Python version number (e.g., '2.7.3') for the kernel
685 685 # included in IPython.
686 686 'language_version': 'X.Y.Z',
687 687
688 # Information about the language of code for the kernel
689 'language_info': {
690 'mimetype': str,
691
692 # Pygments lexer, for highlighting
693 # Only needed if it differs from the top level 'language' field.
694 'pygments_lexer': str,
695
696 # Codemirror mode, for for highlighting in the notebook.
697 # Only needed if it differs from the top level 'language' field.
698 'codemirror_mode': str or dict,
699 },
700
688 701 # A banner of information about the kernel,
689 702 # which may be desplayed in console environments.
690 703 'banner' : str,
704
705 # Optional: A list of dictionaries, each with keys 'text' and 'url'.
706 # These will be displayed in the help menu in the notebook UI.
707 'help_links': [
708 {'text': str, 'url': str}
709 ],
691 710 }
692 711
712 Refer to the lists of available `Pygments lexers <http://pygments.org/docs/lexers/>`_
713 and `codemirror modes <http://codemirror.net/mode/index.html>`_ for those fields.
714
693 715 .. versionchanged:: 5.0
694 716
695 717 Versions changed from lists of integers to strings.
696 718
697 719 .. versionchanged:: 5.0
698 720
699 721 ``ipython_version`` is removed.
700 722
701 723 .. versionchanged:: 5.0
702 724
703 ``implementation``, ``implementation_version``, and ``banner`` keys are added.
725 ``language_info``, ``implementation``, ``implementation_version``, ``banner``
726 and ``help_links`` keys are added.
704 727
705 728 .. _msging_shutdown:
706 729
707 730 Kernel shutdown
708 731 ---------------
709 732
710 733 The clients can request the kernel to shut itself down; this is used in
711 734 multiple cases:
712 735
713 736 - when the user chooses to close the client application via a menu or window
714 737 control.
715 738 - when the user types 'exit' or 'quit' (or their uppercase magic equivalents).
716 739 - when the user chooses a GUI method (like the 'Ctrl-C' shortcut in the
717 740 IPythonQt client) to force a kernel restart to get a clean kernel without
718 741 losing client-side state like history or inlined figures.
719 742
720 743 The client sends a shutdown request to the kernel, and once it receives the
721 744 reply message (which is otherwise empty), it can assume that the kernel has
722 745 completed shutdown safely.
723 746
724 747 Upon their own shutdown, client applications will typically execute a last
725 748 minute sanity check and forcefully terminate any kernel that is still alive, to
726 749 avoid leaving stray processes in the user's machine.
727 750
728 751 Message type: ``shutdown_request``::
729 752
730 753 content = {
731 754 'restart' : bool # whether the shutdown is final, or precedes a restart
732 755 }
733 756
734 757 Message type: ``shutdown_reply``::
735 758
736 759 content = {
737 760 'restart' : bool # whether the shutdown is final, or precedes a restart
738 761 }
739 762
740 763 .. Note::
741 764
742 765 When the clients detect a dead kernel thanks to inactivity on the heartbeat
743 766 socket, they simply send a forceful process termination signal, since a dead
744 767 process is unlikely to respond in any useful way to messages.
745 768
746 769
747 770 Messages on the PUB/SUB socket
748 771 ==============================
749 772
750 773 Streams (stdout, stderr, etc)
751 774 ------------------------------
752 775
753 776 Message type: ``stream``::
754 777
755 778 content = {
756 779 # The name of the stream is one of 'stdout', 'stderr'
757 780 'name' : str,
758 781
759 782 # The text is an arbitrary string to be written to that stream
760 783 'text' : str,
761 784 }
762 785
763 786 .. versionchanged:: 5.0
764 787
765 788 'data' key renamed to 'text' for conistency with the notebook format.
766 789
767 790 Display Data
768 791 ------------
769 792
770 793 This type of message is used to bring back data that should be displayed (text,
771 794 html, svg, etc.) in the frontends. This data is published to all frontends.
772 795 Each message can have multiple representations of the data; it is up to the
773 796 frontend to decide which to use and how. A single message should contain all
774 797 possible representations of the same information. Each representation should
775 798 be a JSON'able data structure, and should be a valid MIME type.
776 799
777 800 Some questions remain about this design:
778 801
779 802 * Do we use this message type for execute_result/displayhook? Probably not, because
780 803 the displayhook also has to handle the Out prompt display. On the other hand
781 804 we could put that information into the metadata section.
782 805
783 806 .. _display_data:
784 807
785 808 Message type: ``display_data``::
786 809
787 810 content = {
788 811
789 812 # Who create the data
790 813 'source' : str,
791 814
792 815 # The data dict contains key/value pairs, where the keys are MIME
793 816 # types and the values are the raw data of the representation in that
794 817 # format.
795 818 'data' : dict,
796 819
797 820 # Any metadata that describes the data
798 821 'metadata' : dict
799 822 }
800 823
801 824
802 825 The ``metadata`` contains any metadata that describes the output.
803 826 Global keys are assumed to apply to the output as a whole.
804 827 The ``metadata`` dict can also contain mime-type keys, which will be sub-dictionaries,
805 828 which are interpreted as applying only to output of that type.
806 829 Third parties should put any data they write into a single dict
807 830 with a reasonably unique name to avoid conflicts.
808 831
809 832 The only metadata keys currently defined in IPython are the width and height
810 833 of images::
811 834
812 835 metadata = {
813 836 'image/png' : {
814 837 'width': 640,
815 838 'height': 480
816 839 }
817 840 }
818 841
819 842
820 843 .. versionchanged:: 5.0
821 844
822 845 `application/json` data should be unpacked JSON data,
823 846 not double-serialized as a JSON string.
824 847
825 848
826 849 Raw Data Publication
827 850 --------------------
828 851
829 852 ``display_data`` lets you publish *representations* of data, such as images and html.
830 853 This ``data_pub`` message lets you publish *actual raw data*, sent via message buffers.
831 854
832 855 data_pub messages are constructed via the :func:`IPython.lib.datapub.publish_data` function:
833 856
834 857 .. sourcecode:: python
835 858
836 859 from IPython.kernel.zmq.datapub import publish_data
837 860 ns = dict(x=my_array)
838 861 publish_data(ns)
839 862
840 863
841 864 Message type: ``data_pub``::
842 865
843 866 content = {
844 867 # the keys of the data dict, after it has been unserialized
845 868 'keys' : ['a', 'b']
846 869 }
847 870 # the namespace dict will be serialized in the message buffers,
848 871 # which will have a length of at least one
849 872 buffers = [b'pdict', ...]
850 873
851 874
852 875 The interpretation of a sequence of data_pub messages for a given parent request should be
853 876 to update a single namespace with subsequent results.
854 877
855 878 .. note::
856 879
857 880 No frontends directly handle data_pub messages at this time.
858 881 It is currently only used by the client/engines in :mod:`IPython.parallel`,
859 882 where engines may publish *data* to the Client,
860 883 of which the Client can then publish *representations* via ``display_data``
861 884 to various frontends.
862 885
863 886 Code inputs
864 887 -----------
865 888
866 889 To let all frontends know what code is being executed at any given time, these
867 890 messages contain a re-broadcast of the ``code`` portion of an
868 891 :ref:`execute_request <execute>`, along with the :ref:`execution_count
869 892 <execution_counter>`.
870 893
871 894 Message type: ``execute_input``::
872 895
873 896 content = {
874 897 'code' : str, # Source code to be executed, one or more lines
875 898
876 899 # The counter for this execution is also provided so that clients can
877 900 # display it, since IPython automatically creates variables called _iN
878 901 # (for input prompt In[N]).
879 902 'execution_count' : int
880 903 }
881 904
882 905 .. versionchanged:: 5.0
883 906
884 907 ``pyin`` is renamed to ``execute_input``.
885 908
886 909
887 910 Execution results
888 911 -----------------
889 912
890 913 Results of an execution are published as an ``execute_result``.
891 914 These are identical to `display_data`_ messages, with the addition of an ``execution_count`` key.
892 915
893 916 Results can have multiple simultaneous formats depending on its
894 917 configuration. A plain text representation should always be provided
895 918 in the ``text/plain`` mime-type. Frontends are free to display any or all of these
896 919 according to its capabilities.
897 920 Frontends should ignore mime-types they do not understand. The data itself is
898 921 any JSON object and depends on the format. It is often, but not always a string.
899 922
900 923 Message type: ``execute_result``::
901 924
902 925 content = {
903 926
904 927 # The counter for this execution is also provided so that clients can
905 928 # display it, since IPython automatically creates variables called _N
906 929 # (for prompt N).
907 930 'execution_count' : int,
908 931
909 932 # data and metadata are identical to a display_data message.
910 933 # the object being displayed is that passed to the display hook,
911 934 # i.e. the *result* of the execution.
912 935 'data' : dict,
913 936 'metadata' : dict,
914 937 }
915 938
916 939 Execution errors
917 940 ----------------
918 941
919 942 When an error occurs during code execution
920 943
921 944 Message type: ``error``::
922 945
923 946 content = {
924 947 # Similar content to the execute_reply messages for the 'error' case,
925 948 # except the 'status' field is omitted.
926 949 }
927 950
928 951 .. versionchanged:: 5.0
929 952
930 953 ``pyerr`` renamed to ``error``
931 954
932 955 Kernel status
933 956 -------------
934 957
935 958 This message type is used by frontends to monitor the status of the kernel.
936 959
937 960 Message type: ``status``::
938 961
939 962 content = {
940 963 # When the kernel starts to handle a message, it will enter the 'busy'
941 964 # state and when it finishes, it will enter the 'idle' state.
942 965 # The kernel will publish state 'starting' exactly once at process startup.
943 966 execution_state : ('busy', 'idle', 'starting')
944 967 }
945 968
946 969 .. versionchanged:: 5.0
947 970
948 971 Busy and idle messages should be sent before/after handling every message,
949 972 not just execution.
950 973
951 974 Clear output
952 975 ------------
953 976
954 977 This message type is used to clear the output that is visible on the frontend.
955 978
956 979 Message type: ``clear_output``::
957 980
958 981 content = {
959 982
960 983 # Wait to clear the output until new output is available. Clears the
961 984 # existing output immediately before the new output is displayed.
962 985 # Useful for creating simple animations with minimal flickering.
963 986 'wait' : bool,
964 987 }
965 988
966 989 .. versionchanged:: 4.1
967 990
968 991 ``stdout``, ``stderr``, and ``display`` boolean keys for selective clearing are removed,
969 992 and ``wait`` is added.
970 993 The selective clearing keys are ignored in v4 and the default behavior remains the same,
971 994 so v4 clear_output messages will be safely handled by a v4.1 frontend.
972 995
973 996
974 997 Messages on the stdin ROUTER/DEALER sockets
975 998 ===========================================
976 999
977 1000 This is a socket where the request/reply pattern goes in the opposite direction:
978 1001 from the kernel to a *single* frontend, and its purpose is to allow
979 1002 ``raw_input`` and similar operations that read from ``sys.stdin`` on the kernel
980 1003 to be fulfilled by the client. The request should be made to the frontend that
981 1004 made the execution request that prompted ``raw_input`` to be called. For now we
982 1005 will keep these messages as simple as possible, since they only mean to convey
983 1006 the ``raw_input(prompt)`` call.
984 1007
985 1008 Message type: ``input_request``::
986 1009
987 1010 content = {
988 1011 # the text to show at the prompt
989 1012 'prompt' : str,
990 1013 # Is the request for a password?
991 1014 # If so, the frontend shouldn't echo input.
992 1015 'password' : bool
993 1016 }
994 1017
995 1018 Message type: ``input_reply``::
996 1019
997 1020 content = { 'value' : str }
998 1021
999 1022
1000 1023 When ``password`` is True, the frontend should not echo the input as it is entered.
1001 1024
1002 1025 .. versionchanged:: 5.0
1003 1026
1004 1027 ``password`` key added.
1005 1028
1006 1029 .. note::
1007 1030
1008 1031 The stdin socket of the client is required to have the same zmq IDENTITY
1009 1032 as the client's shell socket.
1010 1033 Because of this, the ``input_request`` must be sent with the same IDENTITY
1011 1034 routing prefix as the ``execute_reply`` in order for the frontend to receive
1012 1035 the message.
1013 1036
1014 1037 .. note::
1015 1038
1016 1039 We do not explicitly try to forward the raw ``sys.stdin`` object, because in
1017 1040 practice the kernel should behave like an interactive program. When a
1018 1041 program is opened on the console, the keyboard effectively takes over the
1019 1042 ``stdin`` file descriptor, and it can't be used for raw reading anymore.
1020 1043 Since the IPython kernel effectively behaves like a console program (albeit
1021 1044 one whose "keyboard" is actually living in a separate process and
1022 1045 transported over the zmq connection), raw ``stdin`` isn't expected to be
1023 1046 available.
1024 1047
1025 1048 .. _kernel_heartbeat:
1026 1049
1027 1050 Heartbeat for kernels
1028 1051 =====================
1029 1052
1030 1053 Clients send ping messages on a REQ socket, which are echoed right back
1031 1054 from the Kernel's REP socket. These are simple bytestrings, not full JSON messages described above.
1032 1055
1033 1056
1034 1057 Custom Messages
1035 1058 ===============
1036 1059
1037 1060 .. versionadded:: 4.1
1038 1061
1039 1062 IPython 2.0 (msgspec v4.1) adds a messaging system for developers to add their own objects with Frontend
1040 1063 and Kernel-side components, and allow them to communicate with each other.
1041 1064 To do this, IPython adds a notion of a ``Comm``, which exists on both sides,
1042 1065 and can communicate in either direction.
1043 1066
1044 1067 These messages are fully symmetrical - both the Kernel and the Frontend can send each message,
1045 1068 and no messages expect a reply.
1046 1069 The Kernel listens for these messages on the Shell channel,
1047 1070 and the Frontend listens for them on the IOPub channel.
1048 1071
1049 1072 Opening a Comm
1050 1073 --------------
1051 1074
1052 1075 Opening a Comm produces a ``comm_open`` message, to be sent to the other side::
1053 1076
1054 1077 {
1055 1078 'comm_id' : 'u-u-i-d',
1056 1079 'target_name' : 'my_comm',
1057 1080 'data' : {}
1058 1081 }
1059 1082
1060 1083 Every Comm has an ID and a target name.
1061 1084 The code handling the message on the receiving side is responsible for maintaining a mapping
1062 1085 of target_name keys to constructors.
1063 1086 After a ``comm_open`` message has been sent,
1064 1087 there should be a corresponding Comm instance on both sides.
1065 1088 The ``data`` key is always a dict and can be any extra JSON information used in initialization of the comm.
1066 1089
1067 1090 If the ``target_name`` key is not found on the receiving side,
1068 1091 then it should immediately reply with a ``comm_close`` message to avoid an inconsistent state.
1069 1092
1070 1093 Comm Messages
1071 1094 -------------
1072 1095
1073 1096 Comm messages are one-way communications to update comm state,
1074 1097 used for synchronizing widget state, or simply requesting actions of a comm's counterpart.
1075 1098
1076 1099 Essentially, each comm pair defines their own message specification implemented inside the ``data`` dict.
1077 1100
1078 1101 There are no expected replies (of course, one side can send another ``comm_msg`` in reply).
1079 1102
1080 1103 Message type: ``comm_msg``::
1081 1104
1082 1105 {
1083 1106 'comm_id' : 'u-u-i-d',
1084 1107 'data' : {}
1085 1108 }
1086 1109
1087 1110 Tearing Down Comms
1088 1111 ------------------
1089 1112
1090 1113 Since comms live on both sides, when a comm is destroyed the other side must be notified.
1091 1114 This is done with a ``comm_close`` message.
1092 1115
1093 1116 Message type: ``comm_close``::
1094 1117
1095 1118 {
1096 1119 'comm_id' : 'u-u-i-d',
1097 1120 'data' : {}
1098 1121 }
1099 1122
1100 1123 Output Side Effects
1101 1124 -------------------
1102 1125
1103 1126 Since comm messages can execute arbitrary user code,
1104 1127 handlers should set the parent header and publish status busy / idle,
1105 1128 just like an execute request.
1106 1129
1107 1130
1108 1131 To Do
1109 1132 =====
1110 1133
1111 1134 Missing things include:
1112 1135
1113 1136 * Important: finish thinking through the payload concept and API.
1114 1137
1115 1138 .. include:: ../links.txt
@@ -1,164 +1,174 b''
1 1 Making simple Python wrapper kernels
2 2 ====================================
3 3
4 4 .. versionadded:: 3.0
5 5
6 6 You can now re-use the kernel machinery in IPython to easily make new kernels.
7 7 This is useful for languages that have Python bindings, such as `Octave
8 8 <http://www.gnu.org/software/octave/>`_ (via
9 9 `Oct2Py <http://blink1073.github.io/oct2py/docs/index.html>`_), or languages
10 10 where the REPL can be controlled in a tty using `pexpect <http://pexpect.readthedocs.org/en/latest/>`_,
11 11 such as bash.
12 12
13 13 .. seealso::
14 14
15 15 `bash_kernel <https://github.com/takluyver/bash_kernel>`_
16 16 A simple kernel for bash, written using this machinery
17 17
18 18 Required steps
19 19 --------------
20 20
21 21 Subclass :class:`IPython.kernel.zmq.kernelbase.Kernel`, and implement the
22 22 following methods and attributes:
23 23
24 24 .. class:: MyKernel
25 25
26 26 .. attribute:: implementation
27 27 implementation_version
28 28 language
29 29 language_version
30 30 banner
31 31
32 32 Information for :ref:`msging_kernel_info` replies. 'Implementation' refers
33 33 to the kernel (e.g. IPython), and 'language' refers to the language it
34 34 interprets (e.g. Python). The 'banner' is displayed to the user in console
35 35 UIs before the first prompt. All of these values are strings.
36 36
37 .. attribute:: language_info
38
39 Language information for :ref:`msging_kernel_info` replies, in a dictionary.
40 This should contain the key ``mimetype`` with the mimetype of code in the
41 target language (e.g. ``'text/x-python'``). It may also contain keys
42 ``codemirror_mode`` and ``pygments_lexer`` if they need to differ from
43 :attr:`language`.
44
45 Other keys may be added to this later.
46
37 47 .. method:: do_execute(code, silent, store_history=True, user_expressions=None, allow_stdin=False)
38 48
39 49 Execute user code.
40 50
41 51 :param str code: The code to be executed.
42 52 :param bool silent: Whether to display output.
43 53 :param bool store_history: Whether to record this code in history and
44 54 increase the execution count. If silent is True, this is implicitly
45 55 False.
46 56 :param dict user_expressions: Mapping of names to expressions to evaluate
47 57 after the code has run. You can ignore this if you need to.
48 58 :param bool allow_stdin: Whether the frontend can provide input on request
49 59 (e.g. for Python's :func:`raw_input`).
50 60
51 61 Your method should return a dict containing the fields described in
52 62 :ref:`execution_results`. To display output, it can send messages
53 63 using :meth:`~IPython.kernel.zmq.kernelbase.Kernel.send_response`.
54 64 See :doc:`messaging` for details of the different message types.
55 65
56 66 To launch your kernel, add this at the end of your module::
57 67
58 68 if __name__ == '__main__':
59 69 from IPython.kernel.zmq.kernelapp import IPKernelApp
60 70 IPKernelApp.launch_instance(kernel_class=MyKernel)
61 71
62 72 Example
63 73 -------
64 74
65 75 ``echokernel.py`` will simply echo any input it's given to stdout::
66 76
67 77 from IPython.kernel.zmq.kernelbase import Kernel
68 78
69 79 class EchoKernel(Kernel):
70 80 implementation = 'Echo'
71 81 implementation_version = '1.0'
72 82 language = 'no-op'
73 83 language_version = '0.1'
84 language_info = {'mimetype': 'text/plain'}
74 85 banner = "Echo kernel - as useful as a parrot"
75 86
76 87 def do_execute(self, code, silent, store_history=True, user_expressions=None,
77 88 allow_stdin=False):
78 89 if not silent:
79 90 stream_content = {'name': 'stdout', 'text': code}
80 91 self.send_response(self.iopub_socket, 'stream', stream_content)
81 92
82 93 return {'status': 'ok',
83 94 # The base class increments the execution count
84 95 'execution_count': self.execution_count,
85 96 'payload': [],
86 97 'user_expressions': {},
87 98 }
88 99
89 100 if __name__ == '__main__':
90 101 from IPython.kernel.zmq.kernelapp import IPKernelApp
91 102 IPKernelApp.launch_instance(kernel_class=EchoKernel)
92 103
93 104 Here's the Kernel spec ``kernel.json`` file for this::
94 105
95 106 {"argv":["python","-m","echokernel", "-f", "{connection_file}"],
96 107 "display_name":"Echo",
97 "language":"no-op"
98 108 }
99 109
100 110
101 111 Optional steps
102 112 --------------
103 113
104 114 You can override a number of other methods to improve the functionality of your
105 115 kernel. All of these methods should return a dictionary as described in the
106 116 relevant section of the :doc:`messaging spec <messaging>`.
107 117
108 118 .. class:: MyKernel
109 119
110 120 .. method:: do_complete(code, cusor_pos)
111 121
112 122 Code completion
113 123
114 124 :param str code: The code already present
115 125 :param int cursor_pos: The position in the code where completion is requested
116 126
117 127 .. seealso::
118 128
119 129 :ref:`msging_completion` messages
120 130
121 131 .. method:: do_inspect(code, cusor_pos, detail_level=0)
122 132
123 133 Object introspection
124 134
125 135 :param str code: The code
126 136 :param int cursor_pos: The position in the code where introspection is requested
127 137 :param int detail_level: 0 or 1 for more or less detail. In IPython, 1 gets
128 138 the source code.
129 139
130 140 .. seealso::
131 141
132 142 :ref:`msging_inspection` messages
133 143
134 144 .. method:: do_history(hist_access_type, output, raw, session=None, start=None, stop=None, n=None, pattern=None, unique=False)
135 145
136 146 History access. Only the relevant parameters for the type of history
137 147 request concerned will be passed, so your method definition must have defaults
138 148 for all the arguments shown with defaults here.
139 149
140 150 .. seealso::
141 151
142 152 :ref:`msging_history` messages
143 153
144 154 .. method:: do_is_complete(code)
145 155
146 156 Is code entered in a console-like interface complete and ready to execute,
147 157 or should a continuation prompt be shown?
148 158
149 159 :param str code: The code entered so far - possibly multiple lines
150 160
151 161 .. seealso::
152 162
153 163 :ref:`msging_is_complete` messages
154 164
155 165 .. method:: do_shutdown(restart)
156 166
157 167 Shutdown the kernel. You only need to handle your own clean up - the kernel
158 168 machinery will take care of cleaning up its own things before stopping.
159 169
160 170 :param bool restart: Whether the kernel will be started again afterwards
161 171
162 172 .. seealso::
163 173
164 174 :ref:`msging_shutdown` messages
General Comments 0
You need to be logged in to leave comments. Login now