##// END OF EJS Templates
Merge pull request #6978 from takluyver/nbconvert-script...
Min RK -
r19053:38e12544 merge
parent child Browse files
Show More
@@ -0,0 +1,14 b''
1 """Generic script exporter class for any kernel language"""
2
3 from .templateexporter import TemplateExporter
4
5 class ScriptExporter(TemplateExporter):
6 def _template_file_default(self):
7 return 'script'
8
9 def from_notebook_node(self, nb, resources=None, **kw):
10 langinfo = nb.metadata.get('language_info', {})
11 self.file_extension = langinfo.get('file_extension', '.txt')
12 self.output_mimetype = langinfo.get('mimetype', 'text/plain')
13
14 return super(ScriptExporter, self).from_notebook_node(nb, resources, **kw)
@@ -0,0 +1,5 b''
1 {%- extends 'null.tpl' -%}
2
3 {% block input %}
4 {{ cell.source }}
5 {% endblock input %}
@@ -1,149 +1,149 b''
1 1 """Tornado handlers for nbconvert."""
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 io
7 7 import os
8 8 import zipfile
9 9
10 10 from tornado import web
11 11
12 12 from ..base.handlers import (
13 13 IPythonHandler, FilesRedirectHandler,
14 14 notebook_path_regex, path_regex,
15 15 )
16 16 from IPython.nbformat import from_dict
17 17
18 18 from IPython.utils.py3compat import cast_bytes
19 19
20 20 def find_resource_files(output_files_dir):
21 21 files = []
22 22 for dirpath, dirnames, filenames in os.walk(output_files_dir):
23 23 files.extend([os.path.join(dirpath, f) for f in filenames])
24 24 return files
25 25
26 26 def respond_zip(handler, name, output, resources):
27 27 """Zip up the output and resource files and respond with the zip file.
28 28
29 29 Returns True if it has served a zip file, False if there are no resource
30 30 files, in which case we serve the plain output file.
31 31 """
32 32 # Check if we have resource files we need to zip
33 33 output_files = resources.get('outputs', None)
34 34 if not output_files:
35 35 return False
36 36
37 37 # Headers
38 38 zip_filename = os.path.splitext(name)[0] + '.zip'
39 39 handler.set_header('Content-Disposition',
40 40 'attachment; filename="%s"' % zip_filename)
41 41 handler.set_header('Content-Type', 'application/zip')
42 42
43 43 # Prepare the zip file
44 44 buffer = io.BytesIO()
45 45 zipf = zipfile.ZipFile(buffer, mode='w', compression=zipfile.ZIP_DEFLATED)
46 output_filename = os.path.splitext(name)[0] + '.' + resources['output_extension']
46 output_filename = os.path.splitext(name)[0] + resources['output_extension']
47 47 zipf.writestr(output_filename, cast_bytes(output, 'utf-8'))
48 48 for filename, data in output_files.items():
49 49 zipf.writestr(os.path.basename(filename), data)
50 50 zipf.close()
51 51
52 52 handler.finish(buffer.getvalue())
53 53 return True
54 54
55 55 def get_exporter(format, **kwargs):
56 56 """get an exporter, raising appropriate errors"""
57 57 # if this fails, will raise 500
58 58 try:
59 59 from IPython.nbconvert.exporters.export import exporter_map
60 60 except ImportError as e:
61 61 raise web.HTTPError(500, "Could not import nbconvert: %s" % e)
62 62
63 63 try:
64 64 Exporter = exporter_map[format]
65 65 except KeyError:
66 66 # should this be 400?
67 67 raise web.HTTPError(404, u"No exporter for format: %s" % format)
68 68
69 69 try:
70 70 return Exporter(**kwargs)
71 71 except Exception as e:
72 72 raise web.HTTPError(500, "Could not construct Exporter: %s" % e)
73 73
74 74 class NbconvertFileHandler(IPythonHandler):
75 75
76 76 SUPPORTED_METHODS = ('GET',)
77 77
78 78 @web.authenticated
79 79 def get(self, format, path):
80 80
81 81 exporter = get_exporter(format, config=self.config, log=self.log)
82 82
83 83 path = path.strip('/')
84 84 model = self.contents_manager.get(path=path)
85 85 name = model['name']
86 86
87 87 self.set_header('Last-Modified', model['last_modified'])
88 88
89 89 try:
90 90 output, resources = exporter.from_notebook_node(model['content'])
91 91 except Exception as e:
92 92 raise web.HTTPError(500, "nbconvert failed: %s" % e)
93 93
94 94 if respond_zip(self, name, output, resources):
95 95 return
96 96
97 97 # Force download if requested
98 98 if self.get_argument('download', 'false').lower() == 'true':
99 filename = os.path.splitext(name)[0] + '.' + resources['output_extension']
99 filename = os.path.splitext(name)[0] + resources['output_extension']
100 100 self.set_header('Content-Disposition',
101 101 'attachment; filename="%s"' % filename)
102 102
103 103 # MIME type
104 104 if exporter.output_mimetype:
105 105 self.set_header('Content-Type',
106 106 '%s; charset=utf-8' % exporter.output_mimetype)
107 107
108 108 self.finish(output)
109 109
110 110 class NbconvertPostHandler(IPythonHandler):
111 111 SUPPORTED_METHODS = ('POST',)
112 112
113 113 @web.authenticated
114 114 def post(self, format):
115 115 exporter = get_exporter(format, config=self.config)
116 116
117 117 model = self.get_json_body()
118 118 name = model.get('name', 'notebook.ipynb')
119 119 nbnode = from_dict(model['content'])
120 120
121 121 try:
122 122 output, resources = exporter.from_notebook_node(nbnode)
123 123 except Exception as e:
124 124 raise web.HTTPError(500, "nbconvert failed: %s" % e)
125 125
126 126 if respond_zip(self, name, output, resources):
127 127 return
128 128
129 129 # MIME type
130 130 if exporter.output_mimetype:
131 131 self.set_header('Content-Type',
132 132 '%s; charset=utf-8' % exporter.output_mimetype)
133 133
134 134 self.finish(output)
135 135
136 136
137 137 #-----------------------------------------------------------------------------
138 138 # URL to handler mappings
139 139 #-----------------------------------------------------------------------------
140 140
141 141 _format_regex = r"(?P<format>\w+)"
142 142
143 143
144 144 default_handlers = [
145 145 (r"/nbconvert/%s%s" % (_format_regex, notebook_path_regex),
146 146 NbconvertFileHandler),
147 147 (r"/nbconvert/%s" % _format_regex, NbconvertPostHandler),
148 148 (r"/nbconvert/html%s" % path_regex, FilesRedirectHandler),
149 149 ]
@@ -1,348 +1,383 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 'jquery',
6 6 'base/js/namespace',
7 7 'base/js/dialog',
8 8 'base/js/utils',
9 9 'notebook/js/tour',
10 10 'bootstrap',
11 11 'moment',
12 12 ], function($, IPython, dialog, utils, tour, bootstrap, moment) {
13 13 "use strict";
14 14
15 15 var MenuBar = function (selector, options) {
16 16 // Constructor
17 17 //
18 18 // A MenuBar Class to generate the menubar of IPython notebook
19 19 //
20 20 // Parameters:
21 21 // selector: string
22 22 // options: dictionary
23 23 // Dictionary of keyword arguments.
24 24 // notebook: Notebook instance
25 25 // contents: ContentManager instance
26 26 // layout_manager: LayoutManager instance
27 27 // events: $(Events) instance
28 28 // save_widget: SaveWidget instance
29 29 // quick_help: QuickHelp instance
30 30 // base_url : string
31 31 // notebook_path : string
32 32 // notebook_name : string
33 33 options = options || {};
34 34 this.base_url = options.base_url || utils.get_body_data("baseUrl");
35 35 this.selector = selector;
36 36 this.notebook = options.notebook;
37 37 this.contents = options.contents;
38 38 this.layout_manager = options.layout_manager;
39 39 this.events = options.events;
40 40 this.save_widget = options.save_widget;
41 41 this.quick_help = options.quick_help;
42 42
43 43 try {
44 44 this.tour = new tour.Tour(this.notebook, this.events);
45 45 } catch (e) {
46 46 this.tour = undefined;
47 47 console.log("Failed to instantiate Notebook Tour", e);
48 48 }
49 49
50 50 if (this.selector !== undefined) {
51 51 this.element = $(selector);
52 52 this.style();
53 53 this.bind_events();
54 54 }
55 55 };
56 56
57 57 // TODO: This has definitively nothing to do with style ...
58 58 MenuBar.prototype.style = function () {
59 59 var that = this;
60 60 this.element.find("li").click(function (event, ui) {
61 61 // The selected cell loses focus when the menu is entered, so we
62 62 // re-select it upon selection.
63 63 var i = that.notebook.get_selected_index();
64 64 that.notebook.select(i);
65 65 }
66 66 );
67 67 };
68 68
69 69 MenuBar.prototype._nbconvert = function (format, download) {
70 70 download = download || false;
71 71 var notebook_path = this.notebook.notebook_path;
72 if (this.notebook.dirty) {
73 this.notebook.save_notebook({async : false});
74 }
75 72 var url = utils.url_join_encode(
76 73 this.base_url,
77 74 'nbconvert',
78 75 format,
79 76 notebook_path
80 77 ) + "?download=" + download.toString();
81
82 window.open(url);
78
79 var w = window.open()
80 if (this.notebook.dirty) {
81 this.notebook.save_notebook().then(function() {
82 w.location = url;
83 });
84 } else {
85 w.location = url;
86 }
83 87 };
84 88
85 89 MenuBar.prototype.bind_events = function () {
86 90 // File
87 91 var that = this;
88 92 this.element.find('#new_notebook').click(function () {
89 93 var w = window.open();
90 94 // Create a new notebook in the same path as the current
91 95 // notebook's path.
92 96 var parent = utils.url_path_split(that.notebook.notebook_path)[0];
93 97 that.contents.new_untitled(parent, {type: "notebook"}).then(
94 98 function (data) {
95 99 w.location = utils.url_join_encode(
96 100 that.base_url, 'notebooks', data.path
97 101 );
98 102 },
99 103 function(error) {
100 104 w.close();
101 105 dialog.modal({
102 106 title : 'Creating Notebook Failed',
103 107 body : "The error was: " + error.message,
104 108 buttons : {'OK' : {'class' : 'btn-primary'}}
105 109 });
106 110 }
107 111 );
108 112 });
109 113 this.element.find('#open_notebook').click(function () {
110 114 var parent = utils.url_path_split(that.notebook.notebook_path)[0];
111 115 window.open(utils.url_join_encode(that.base_url, 'tree', parent));
112 116 });
113 117 this.element.find('#copy_notebook').click(function () {
114 118 that.notebook.copy_notebook();
115 119 return false;
116 120 });
117 121 this.element.find('#download_ipynb').click(function () {
118 122 var base_url = that.notebook.base_url;
119 123 var notebook_path = that.notebook.notebook_path;
120 124 if (that.notebook.dirty) {
121 125 that.notebook.save_notebook({async : false});
122 126 }
123 127
124 128 var url = utils.url_join_encode(base_url, 'files', notebook_path);
125 129 window.open(url + '?download=1');
126 130 });
127 131
128 132 this.element.find('#print_preview').click(function () {
129 133 that._nbconvert('html', false);
130 134 });
131 135
132 this.element.find('#download_py').click(function () {
133 that._nbconvert('python', true);
134 });
135
136 136 this.element.find('#download_html').click(function () {
137 137 that._nbconvert('html', true);
138 138 });
139 139
140 140 this.element.find('#download_rst').click(function () {
141 141 that._nbconvert('rst', true);
142 142 });
143 143
144 144 this.element.find('#download_pdf').click(function () {
145 145 that._nbconvert('pdf', true);
146 146 });
147 147
148 148 this.element.find('#rename_notebook').click(function () {
149 149 that.save_widget.rename_notebook({notebook: that.notebook});
150 150 });
151 151 this.element.find('#save_checkpoint').click(function () {
152 152 that.notebook.save_checkpoint();
153 153 });
154 154 this.element.find('#restore_checkpoint').click(function () {
155 155 });
156 156 this.element.find('#trust_notebook').click(function () {
157 157 that.notebook.trust_notebook();
158 158 });
159 159 this.events.on('trust_changed.Notebook', function (event, trusted) {
160 160 if (trusted) {
161 161 that.element.find('#trust_notebook')
162 162 .addClass("disabled")
163 163 .find("a").text("Trusted Notebook");
164 164 } else {
165 165 that.element.find('#trust_notebook')
166 166 .removeClass("disabled")
167 167 .find("a").text("Trust Notebook");
168 168 }
169 169 });
170 170 this.element.find('#kill_and_exit').click(function () {
171 171 var close_window = function () {
172 172 // allow closing of new tabs in Chromium, impossible in FF
173 173 window.open('', '_self', '');
174 174 window.close();
175 175 };
176 176 // finish with close on success or failure
177 177 that.notebook.session.delete(close_window, close_window);
178 178 });
179 179 // Edit
180 180 this.element.find('#cut_cell').click(function () {
181 181 that.notebook.cut_cell();
182 182 });
183 183 this.element.find('#copy_cell').click(function () {
184 184 that.notebook.copy_cell();
185 185 });
186 186 this.element.find('#delete_cell').click(function () {
187 187 that.notebook.delete_cell();
188 188 });
189 189 this.element.find('#undelete_cell').click(function () {
190 190 that.notebook.undelete_cell();
191 191 });
192 192 this.element.find('#split_cell').click(function () {
193 193 that.notebook.split_cell();
194 194 });
195 195 this.element.find('#merge_cell_above').click(function () {
196 196 that.notebook.merge_cell_above();
197 197 });
198 198 this.element.find('#merge_cell_below').click(function () {
199 199 that.notebook.merge_cell_below();
200 200 });
201 201 this.element.find('#move_cell_up').click(function () {
202 202 that.notebook.move_cell_up();
203 203 });
204 204 this.element.find('#move_cell_down').click(function () {
205 205 that.notebook.move_cell_down();
206 206 });
207 207 this.element.find('#edit_nb_metadata').click(function () {
208 208 that.notebook.edit_metadata({
209 209 notebook: that.notebook,
210 210 keyboard_manager: that.notebook.keyboard_manager});
211 211 });
212 212
213 213 // View
214 214 this.element.find('#toggle_header').click(function () {
215 215 $('div#header').toggle();
216 216 that.layout_manager.do_resize();
217 217 });
218 218 this.element.find('#toggle_toolbar').click(function () {
219 219 $('div#maintoolbar').toggle();
220 220 that.layout_manager.do_resize();
221 221 });
222 222 // Insert
223 223 this.element.find('#insert_cell_above').click(function () {
224 224 that.notebook.insert_cell_above('code');
225 225 that.notebook.select_prev();
226 226 });
227 227 this.element.find('#insert_cell_below').click(function () {
228 228 that.notebook.insert_cell_below('code');
229 229 that.notebook.select_next();
230 230 });
231 231 // Cell
232 232 this.element.find('#run_cell').click(function () {
233 233 that.notebook.execute_cell();
234 234 });
235 235 this.element.find('#run_cell_select_below').click(function () {
236 236 that.notebook.execute_cell_and_select_below();
237 237 });
238 238 this.element.find('#run_cell_insert_below').click(function () {
239 239 that.notebook.execute_cell_and_insert_below();
240 240 });
241 241 this.element.find('#run_all_cells').click(function () {
242 242 that.notebook.execute_all_cells();
243 243 });
244 244 this.element.find('#run_all_cells_above').click(function () {
245 245 that.notebook.execute_cells_above();
246 246 });
247 247 this.element.find('#run_all_cells_below').click(function () {
248 248 that.notebook.execute_cells_below();
249 249 });
250 250 this.element.find('#to_code').click(function () {
251 251 that.notebook.to_code();
252 252 });
253 253 this.element.find('#to_markdown').click(function () {
254 254 that.notebook.to_markdown();
255 255 });
256 256 this.element.find('#to_raw').click(function () {
257 257 that.notebook.to_raw();
258 258 });
259 259
260 260 this.element.find('#toggle_current_output').click(function () {
261 261 that.notebook.toggle_output();
262 262 });
263 263 this.element.find('#toggle_current_output_scroll').click(function () {
264 264 that.notebook.toggle_output_scroll();
265 265 });
266 266 this.element.find('#clear_current_output').click(function () {
267 267 that.notebook.clear_output();
268 268 });
269 269
270 270 this.element.find('#toggle_all_output').click(function () {
271 271 that.notebook.toggle_all_output();
272 272 });
273 273 this.element.find('#toggle_all_output_scroll').click(function () {
274 274 that.notebook.toggle_all_output_scroll();
275 275 });
276 276 this.element.find('#clear_all_output').click(function () {
277 277 that.notebook.clear_all_output();
278 278 });
279 279
280 280 // Kernel
281 281 this.element.find('#int_kernel').click(function () {
282 282 that.notebook.kernel.interrupt();
283 283 });
284 284 this.element.find('#restart_kernel').click(function () {
285 285 that.notebook.restart_kernel();
286 286 });
287 287 this.element.find('#reconnect_kernel').click(function () {
288 288 that.notebook.kernel.reconnect();
289 289 });
290 290 // Help
291 291 if (this.tour) {
292 292 this.element.find('#notebook_tour').click(function () {
293 293 that.tour.start();
294 294 });
295 295 } else {
296 296 this.element.find('#notebook_tour').addClass("disabled");
297 297 }
298 298 this.element.find('#keyboard_shortcuts').click(function () {
299 299 that.quick_help.show_keyboard_shortcuts();
300 300 });
301 301
302 302 this.update_restore_checkpoint(null);
303 303
304 304 this.events.on('checkpoints_listed.Notebook', function (event, data) {
305 305 that.update_restore_checkpoint(that.notebook.checkpoints);
306 306 });
307 307
308 308 this.events.on('checkpoint_created.Notebook', function (event, data) {
309 309 that.update_restore_checkpoint(that.notebook.checkpoints);
310 310 });
311
312 this.events.on('notebook_loaded.Notebook', function() {
313 var langinfo = that.notebook.metadata.language_info || {};
314 that.update_nbconvert_script(langinfo);
315 });
316
317 this.events.on('kernel_ready.Kernel', function(event, data) {
318 var langinfo = data.kernel.info_reply.language_info || {};
319 that.update_nbconvert_script(langinfo);
320 });
311 321 };
312 322
313 323 MenuBar.prototype.update_restore_checkpoint = function(checkpoints) {
314 324 var ul = this.element.find("#restore_checkpoint").find("ul");
315 325 ul.empty();
316 326 if (!checkpoints || checkpoints.length === 0) {
317 327 ul.append(
318 328 $("<li/>")
319 329 .addClass("disabled")
320 330 .append(
321 331 $("<a/>")
322 332 .text("No checkpoints")
323 333 )
324 334 );
325 335 return;
326 336 }
327 337
328 338 var that = this;
329 339 checkpoints.map(function (checkpoint) {
330 340 var d = new Date(checkpoint.last_modified);
331 341 ul.append(
332 342 $("<li/>").append(
333 343 $("<a/>")
334 344 .attr("href", "#")
335 345 .text(moment(d).format("LLLL"))
336 346 .click(function () {
337 347 that.notebook.restore_checkpoint_dialog(checkpoint);
338 348 })
339 349 )
340 350 );
341 351 });
342 352 };
353
354 MenuBar.prototype.update_nbconvert_script = function(langinfo) {
355 // Set the 'Download as foo' menu option for the relevant language.
356 var el = this.element.find('#download_script');
357 var that = this;
358
359 // Set menu entry text to e.g. "Python (.py)"
360 var langname = (langinfo.name || 'Script')
361 langname = langname.charAt(0).toUpperCase()+langname.substr(1) // Capitalise
362 el.find('a').text(langname + ' ('+(langinfo.file_extension || 'txt')+')');
363
364 // Unregister any previously registered handlers
365 el.off('click');
366 if (langinfo.nbconvert_exporter) {
367 // Metadata specifies a specific exporter, e.g. 'python'
368 el.click(function() {
369 that._nbconvert(langinfo.nbconvert_exporter, true);
370 });
371 } else {
372 // Use generic 'script' exporter
373 el.click(function() {
374 that._nbconvert('script', true);
375 });
376 }
377 };
343 378
344 379 // Backwards compatability.
345 380 IPython.MenuBar = MenuBar;
346 381
347 382 return {'MenuBar': MenuBar};
348 383 });
@@ -1,2495 +1,2495 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 'codemirror/lib/codemirror',
15 15 'codemirror/addon/runmode/runmode',
16 16 'notebook/js/mathjaxutils',
17 17 'base/js/keyboard',
18 18 'notebook/js/tooltip',
19 19 'notebook/js/celltoolbarpresets/default',
20 20 'notebook/js/celltoolbarpresets/rawcell',
21 21 'notebook/js/celltoolbarpresets/slideshow',
22 22 'notebook/js/scrollmanager'
23 23 ], function (
24 24 IPython,
25 25 $,
26 26 utils,
27 27 dialog,
28 28 textcell,
29 29 codecell,
30 30 session,
31 31 celltoolbar,
32 32 marked,
33 33 CodeMirror,
34 34 runMode,
35 35 mathjaxutils,
36 36 keyboard,
37 37 tooltip,
38 38 default_celltoolbar,
39 39 rawcell_celltoolbar,
40 40 slideshow_celltoolbar,
41 41 scrollmanager
42 42 ) {
43 43 "use strict";
44 44
45 45 var Notebook = function (selector, options) {
46 46 // Constructor
47 47 //
48 48 // A notebook contains and manages cells.
49 49 //
50 50 // Parameters:
51 51 // selector: string
52 52 // options: dictionary
53 53 // Dictionary of keyword arguments.
54 54 // events: $(Events) instance
55 55 // keyboard_manager: KeyboardManager instance
56 56 // contents: Contents instance
57 57 // save_widget: SaveWidget instance
58 58 // config: dictionary
59 59 // base_url : string
60 60 // notebook_path : string
61 61 // notebook_name : string
62 62 this.config = utils.mergeopt(Notebook, options.config);
63 63 this.base_url = options.base_url;
64 64 this.notebook_path = options.notebook_path;
65 65 this.notebook_name = options.notebook_name;
66 66 this.events = options.events;
67 67 this.keyboard_manager = options.keyboard_manager;
68 68 this.contents = options.contents;
69 69 this.save_widget = options.save_widget;
70 70 this.tooltip = new tooltip.Tooltip(this.events);
71 71 this.ws_url = options.ws_url;
72 72 this._session_starting = false;
73 73 this.default_cell_type = this.config.default_cell_type || 'code';
74 74
75 75 // Create default scroll manager.
76 76 this.scroll_manager = new scrollmanager.ScrollManager(this);
77 77
78 78 // TODO: This code smells (and the other `= this` line a couple lines down)
79 79 // We need a better way to deal with circular instance references.
80 80 this.keyboard_manager.notebook = this;
81 81 this.save_widget.notebook = this;
82 82
83 83 mathjaxutils.init();
84 84
85 85 if (marked) {
86 86 marked.setOptions({
87 87 gfm : true,
88 88 tables: true,
89 89 // FIXME: probably want central config for CodeMirror theme when we have js config
90 90 langPrefix: "cm-s-ipython language-",
91 91 highlight: function(code, lang, callback) {
92 92 if (!lang) {
93 93 // no language, no highlight
94 94 if (callback) {
95 95 callback(null, code);
96 96 return;
97 97 } else {
98 98 return code;
99 99 }
100 100 }
101 101 utils.requireCodeMirrorMode(lang, function () {
102 102 var el = document.createElement("div");
103 103 var mode = CodeMirror.getMode({}, lang);
104 104 if (!mode) {
105 105 console.log("No CodeMirror mode: " + lang);
106 106 callback(null, code);
107 107 return;
108 108 }
109 109 try {
110 110 CodeMirror.runMode(code, mode, el);
111 111 callback(null, el.innerHTML);
112 112 } catch (err) {
113 113 console.log("Failed to highlight " + lang + " code", err);
114 114 callback(err, code);
115 115 }
116 116 }, function (err) {
117 117 console.log("No CodeMirror mode: " + lang);
118 118 callback(err, code);
119 119 });
120 120 }
121 121 });
122 122 }
123 123
124 124 this.element = $(selector);
125 125 this.element.scroll();
126 126 this.element.data("notebook", this);
127 127 this.next_prompt_number = 1;
128 128 this.session = null;
129 129 this.kernel = null;
130 130 this.clipboard = null;
131 131 this.undelete_backup = null;
132 132 this.undelete_index = null;
133 133 this.undelete_below = false;
134 134 this.paste_enabled = false;
135 135 this.writable = false;
136 136 // It is important to start out in command mode to match the intial mode
137 137 // of the KeyboardManager.
138 138 this.mode = 'command';
139 139 this.set_dirty(false);
140 140 this.metadata = {};
141 141 this._checkpoint_after_save = false;
142 142 this.last_checkpoint = null;
143 143 this.checkpoints = [];
144 144 this.autosave_interval = 0;
145 145 this.autosave_timer = null;
146 146 // autosave *at most* every two minutes
147 147 this.minimum_autosave_interval = 120000;
148 148 this.notebook_name_blacklist_re = /[\/\\:]/;
149 149 this.nbformat = 4; // Increment this when changing the nbformat
150 150 this.nbformat_minor = 0; // Increment this when changing the nbformat
151 151 this.codemirror_mode = 'ipython';
152 152 this.create_elements();
153 153 this.bind_events();
154 154 this.kernel_selector = null;
155 155 this.dirty = null;
156 156 this.trusted = null;
157 157 this._fully_loaded = false;
158 158
159 159 // Trigger cell toolbar registration.
160 160 default_celltoolbar.register(this);
161 161 rawcell_celltoolbar.register(this);
162 162 slideshow_celltoolbar.register(this);
163 163
164 164 // prevent assign to miss-typed properties.
165 165 Object.seal(this);
166 166 };
167 167
168 168 Notebook.options_default = {
169 169 // can be any cell type, or the special values of
170 170 // 'above', 'below', or 'selected' to get the value from another cell.
171 171 Notebook: {
172 172 default_cell_type: 'code'
173 173 }
174 174 };
175 175
176 176
177 177 /**
178 178 * Create an HTML and CSS representation of the notebook.
179 179 *
180 180 * @method create_elements
181 181 */
182 182 Notebook.prototype.create_elements = function () {
183 183 var that = this;
184 184 this.element.attr('tabindex','-1');
185 185 this.container = $("<div/>").addClass("container").attr("id", "notebook-container");
186 186 // We add this end_space div to the end of the notebook div to:
187 187 // i) provide a margin between the last cell and the end of the notebook
188 188 // ii) to prevent the div from scrolling up when the last cell is being
189 189 // edited, but is too low on the page, which browsers will do automatically.
190 190 var end_space = $('<div/>').addClass('end_space');
191 191 end_space.dblclick(function (e) {
192 192 var ncells = that.ncells();
193 193 that.insert_cell_below('code',ncells-1);
194 194 });
195 195 this.element.append(this.container);
196 196 this.container.append(end_space);
197 197 };
198 198
199 199 /**
200 200 * Bind JavaScript events: key presses and custom IPython events.
201 201 *
202 202 * @method bind_events
203 203 */
204 204 Notebook.prototype.bind_events = function () {
205 205 var that = this;
206 206
207 207 this.events.on('set_next_input.Notebook', function (event, data) {
208 208 var index = that.find_cell_index(data.cell);
209 209 var new_cell = that.insert_cell_below('code',index);
210 210 new_cell.set_text(data.text);
211 211 that.dirty = true;
212 212 });
213 213
214 214 this.events.on('set_dirty.Notebook', function (event, data) {
215 215 that.dirty = data.value;
216 216 });
217 217
218 218 this.events.on('trust_changed.Notebook', function (event, trusted) {
219 219 that.trusted = trusted;
220 220 });
221 221
222 222 this.events.on('select.Cell', function (event, data) {
223 223 var index = that.find_cell_index(data.cell);
224 224 that.select(index);
225 225 });
226 226
227 227 this.events.on('edit_mode.Cell', function (event, data) {
228 228 that.handle_edit_mode(data.cell);
229 229 });
230 230
231 231 this.events.on('command_mode.Cell', function (event, data) {
232 232 that.handle_command_mode(data.cell);
233 233 });
234 234
235 235 this.events.on('spec_changed.Kernel', function(event, data) {
236 236 that.metadata.kernelspec =
237 237 {name: data.name, display_name: data.display_name};
238 238 });
239 239
240 240 this.events.on('kernel_ready.Kernel', function(event, data) {
241 241 var kinfo = data.kernel.info_reply;
242 242 var langinfo = kinfo.language_info || {};
243 243 if (!langinfo.name) langinfo.name = kinfo.language;
244 244
245 245 that.metadata.language_info = langinfo;
246 246 // Mode 'null' should be plain, unhighlighted text.
247 247 var cm_mode = langinfo.codemirror_mode || langinfo.language || 'null';
248 248 that.set_codemirror_mode(cm_mode);
249 249 });
250 250
251 251 var collapse_time = function (time) {
252 252 var app_height = $('#ipython-main-app').height(); // content height
253 253 var splitter_height = $('div#pager_splitter').outerHeight(true);
254 254 var new_height = app_height - splitter_height;
255 255 that.element.animate({height : new_height + 'px'}, time);
256 256 };
257 257
258 258 this.element.bind('collapse_pager', function (event, extrap) {
259 259 var time = (extrap !== undefined) ? ((extrap.duration !== undefined ) ? extrap.duration : 'fast') : 'fast';
260 260 collapse_time(time);
261 261 });
262 262
263 263 var expand_time = function (time) {
264 264 var app_height = $('#ipython-main-app').height(); // content height
265 265 var splitter_height = $('div#pager_splitter').outerHeight(true);
266 266 var pager_height = $('div#pager').outerHeight(true);
267 267 var new_height = app_height - pager_height - splitter_height;
268 268 that.element.animate({height : new_height + 'px'}, time);
269 269 };
270 270
271 271 this.element.bind('expand_pager', function (event, extrap) {
272 272 var time = (extrap !== undefined) ? ((extrap.duration !== undefined ) ? extrap.duration : 'fast') : 'fast';
273 273 expand_time(time);
274 274 });
275 275
276 276 // Firefox 22 broke $(window).on("beforeunload")
277 277 // I'm not sure why or how.
278 278 window.onbeforeunload = function (e) {
279 279 // TODO: Make killing the kernel configurable.
280 280 var kill_kernel = false;
281 281 if (kill_kernel) {
282 282 that.session.delete();
283 283 }
284 284 // if we are autosaving, trigger an autosave on nav-away.
285 285 // still warn, because if we don't the autosave may fail.
286 286 if (that.dirty) {
287 287 if ( that.autosave_interval ) {
288 288 // schedule autosave in a timeout
289 289 // this gives you a chance to forcefully discard changes
290 290 // by reloading the page if you *really* want to.
291 291 // the timer doesn't start until you *dismiss* the dialog.
292 292 setTimeout(function () {
293 293 if (that.dirty) {
294 294 that.save_notebook();
295 295 }
296 296 }, 1000);
297 297 return "Autosave in progress, latest changes may be lost.";
298 298 } else {
299 299 return "Unsaved changes will be lost.";
300 300 }
301 301 }
302 302 // Null is the *only* return value that will make the browser not
303 303 // pop up the "don't leave" dialog.
304 304 return null;
305 305 };
306 306 };
307 307
308 308 /**
309 309 * Set the dirty flag, and trigger the set_dirty.Notebook event
310 310 *
311 311 * @method set_dirty
312 312 */
313 313 Notebook.prototype.set_dirty = function (value) {
314 314 if (value === undefined) {
315 315 value = true;
316 316 }
317 317 if (this.dirty == value) {
318 318 return;
319 319 }
320 320 this.events.trigger('set_dirty.Notebook', {value: value});
321 321 };
322 322
323 323 /**
324 324 * Scroll the top of the page to a given cell.
325 325 *
326 326 * @method scroll_to_cell
327 327 * @param {Number} cell_number An index of the cell to view
328 328 * @param {Number} time Animation time in milliseconds
329 329 * @return {Number} Pixel offset from the top of the container
330 330 */
331 331 Notebook.prototype.scroll_to_cell = function (cell_number, time) {
332 332 var cells = this.get_cells();
333 333 time = time || 0;
334 334 cell_number = Math.min(cells.length-1,cell_number);
335 335 cell_number = Math.max(0 ,cell_number);
336 336 var scroll_value = cells[cell_number].element.position().top-cells[0].element.position().top ;
337 337 this.element.animate({scrollTop:scroll_value}, time);
338 338 return scroll_value;
339 339 };
340 340
341 341 /**
342 342 * Scroll to the bottom of the page.
343 343 *
344 344 * @method scroll_to_bottom
345 345 */
346 346 Notebook.prototype.scroll_to_bottom = function () {
347 347 this.element.animate({scrollTop:this.element.get(0).scrollHeight}, 0);
348 348 };
349 349
350 350 /**
351 351 * Scroll to the top of the page.
352 352 *
353 353 * @method scroll_to_top
354 354 */
355 355 Notebook.prototype.scroll_to_top = function () {
356 356 this.element.animate({scrollTop:0}, 0);
357 357 };
358 358
359 359 // Edit Notebook metadata
360 360
361 361 Notebook.prototype.edit_metadata = function () {
362 362 var that = this;
363 363 dialog.edit_metadata({
364 364 md: this.metadata,
365 365 callback: function (md) {
366 366 that.metadata = md;
367 367 },
368 368 name: 'Notebook',
369 369 notebook: this,
370 370 keyboard_manager: this.keyboard_manager});
371 371 };
372 372
373 373 // Cell indexing, retrieval, etc.
374 374
375 375 /**
376 376 * Get all cell elements in the notebook.
377 377 *
378 378 * @method get_cell_elements
379 379 * @return {jQuery} A selector of all cell elements
380 380 */
381 381 Notebook.prototype.get_cell_elements = function () {
382 382 return this.container.children("div.cell");
383 383 };
384 384
385 385 /**
386 386 * Get a particular cell element.
387 387 *
388 388 * @method get_cell_element
389 389 * @param {Number} index An index of a cell to select
390 390 * @return {jQuery} A selector of the given cell.
391 391 */
392 392 Notebook.prototype.get_cell_element = function (index) {
393 393 var result = null;
394 394 var e = this.get_cell_elements().eq(index);
395 395 if (e.length !== 0) {
396 396 result = e;
397 397 }
398 398 return result;
399 399 };
400 400
401 401 /**
402 402 * Try to get a particular cell by msg_id.
403 403 *
404 404 * @method get_msg_cell
405 405 * @param {String} msg_id A message UUID
406 406 * @return {Cell} Cell or null if no cell was found.
407 407 */
408 408 Notebook.prototype.get_msg_cell = function (msg_id) {
409 409 return codecell.CodeCell.msg_cells[msg_id] || null;
410 410 };
411 411
412 412 /**
413 413 * Count the cells in this notebook.
414 414 *
415 415 * @method ncells
416 416 * @return {Number} The number of cells in this notebook
417 417 */
418 418 Notebook.prototype.ncells = function () {
419 419 return this.get_cell_elements().length;
420 420 };
421 421
422 422 /**
423 423 * Get all Cell objects in this notebook.
424 424 *
425 425 * @method get_cells
426 426 * @return {Array} This notebook's Cell objects
427 427 */
428 428 // TODO: we are often calling cells as cells()[i], which we should optimize
429 429 // to cells(i) or a new method.
430 430 Notebook.prototype.get_cells = function () {
431 431 return this.get_cell_elements().toArray().map(function (e) {
432 432 return $(e).data("cell");
433 433 });
434 434 };
435 435
436 436 /**
437 437 * Get a Cell object from this notebook.
438 438 *
439 439 * @method get_cell
440 440 * @param {Number} index An index of a cell to retrieve
441 441 * @return {Cell} Cell or null if no cell was found.
442 442 */
443 443 Notebook.prototype.get_cell = function (index) {
444 444 var result = null;
445 445 var ce = this.get_cell_element(index);
446 446 if (ce !== null) {
447 447 result = ce.data('cell');
448 448 }
449 449 return result;
450 450 };
451 451
452 452 /**
453 453 * Get the cell below a given cell.
454 454 *
455 455 * @method get_next_cell
456 456 * @param {Cell} cell The provided cell
457 457 * @return {Cell} the next cell or null if no cell was found.
458 458 */
459 459 Notebook.prototype.get_next_cell = function (cell) {
460 460 var result = null;
461 461 var index = this.find_cell_index(cell);
462 462 if (this.is_valid_cell_index(index+1)) {
463 463 result = this.get_cell(index+1);
464 464 }
465 465 return result;
466 466 };
467 467
468 468 /**
469 469 * Get the cell above a given cell.
470 470 *
471 471 * @method get_prev_cell
472 472 * @param {Cell} cell The provided cell
473 473 * @return {Cell} The previous cell or null if no cell was found.
474 474 */
475 475 Notebook.prototype.get_prev_cell = function (cell) {
476 476 var result = null;
477 477 var index = this.find_cell_index(cell);
478 478 if (index !== null && index > 0) {
479 479 result = this.get_cell(index-1);
480 480 }
481 481 return result;
482 482 };
483 483
484 484 /**
485 485 * Get the numeric index of a given cell.
486 486 *
487 487 * @method find_cell_index
488 488 * @param {Cell} cell The provided cell
489 489 * @return {Number} The cell's numeric index or null if no cell was found.
490 490 */
491 491 Notebook.prototype.find_cell_index = function (cell) {
492 492 var result = null;
493 493 this.get_cell_elements().filter(function (index) {
494 494 if ($(this).data("cell") === cell) {
495 495 result = index;
496 496 }
497 497 });
498 498 return result;
499 499 };
500 500
501 501 /**
502 502 * Get a given index , or the selected index if none is provided.
503 503 *
504 504 * @method index_or_selected
505 505 * @param {Number} index A cell's index
506 506 * @return {Number} The given index, or selected index if none is provided.
507 507 */
508 508 Notebook.prototype.index_or_selected = function (index) {
509 509 var i;
510 510 if (index === undefined || index === null) {
511 511 i = this.get_selected_index();
512 512 if (i === null) {
513 513 i = 0;
514 514 }
515 515 } else {
516 516 i = index;
517 517 }
518 518 return i;
519 519 };
520 520
521 521 /**
522 522 * Get the currently selected cell.
523 523 * @method get_selected_cell
524 524 * @return {Cell} The selected cell
525 525 */
526 526 Notebook.prototype.get_selected_cell = function () {
527 527 var index = this.get_selected_index();
528 528 return this.get_cell(index);
529 529 };
530 530
531 531 /**
532 532 * Check whether a cell index is valid.
533 533 *
534 534 * @method is_valid_cell_index
535 535 * @param {Number} index A cell index
536 536 * @return True if the index is valid, false otherwise
537 537 */
538 538 Notebook.prototype.is_valid_cell_index = function (index) {
539 539 if (index !== null && index >= 0 && index < this.ncells()) {
540 540 return true;
541 541 } else {
542 542 return false;
543 543 }
544 544 };
545 545
546 546 /**
547 547 * Get the index of the currently selected cell.
548 548
549 549 * @method get_selected_index
550 550 * @return {Number} The selected cell's numeric index
551 551 */
552 552 Notebook.prototype.get_selected_index = function () {
553 553 var result = null;
554 554 this.get_cell_elements().filter(function (index) {
555 555 if ($(this).data("cell").selected === true) {
556 556 result = index;
557 557 }
558 558 });
559 559 return result;
560 560 };
561 561
562 562
563 563 // Cell selection.
564 564
565 565 /**
566 566 * Programmatically select a cell.
567 567 *
568 568 * @method select
569 569 * @param {Number} index A cell's index
570 570 * @return {Notebook} This notebook
571 571 */
572 572 Notebook.prototype.select = function (index) {
573 573 if (this.is_valid_cell_index(index)) {
574 574 var sindex = this.get_selected_index();
575 575 if (sindex !== null && index !== sindex) {
576 576 // If we are about to select a different cell, make sure we are
577 577 // first in command mode.
578 578 if (this.mode !== 'command') {
579 579 this.command_mode();
580 580 }
581 581 this.get_cell(sindex).unselect();
582 582 }
583 583 var cell = this.get_cell(index);
584 584 cell.select();
585 585 if (cell.cell_type === 'heading') {
586 586 this.events.trigger('selected_cell_type_changed.Notebook',
587 587 {'cell_type':cell.cell_type,level:cell.level}
588 588 );
589 589 } else {
590 590 this.events.trigger('selected_cell_type_changed.Notebook',
591 591 {'cell_type':cell.cell_type}
592 592 );
593 593 }
594 594 }
595 595 return this;
596 596 };
597 597
598 598 /**
599 599 * Programmatically select the next cell.
600 600 *
601 601 * @method select_next
602 602 * @return {Notebook} This notebook
603 603 */
604 604 Notebook.prototype.select_next = function () {
605 605 var index = this.get_selected_index();
606 606 this.select(index+1);
607 607 return this;
608 608 };
609 609
610 610 /**
611 611 * Programmatically select the previous cell.
612 612 *
613 613 * @method select_prev
614 614 * @return {Notebook} This notebook
615 615 */
616 616 Notebook.prototype.select_prev = function () {
617 617 var index = this.get_selected_index();
618 618 this.select(index-1);
619 619 return this;
620 620 };
621 621
622 622
623 623 // Edit/Command mode
624 624
625 625 /**
626 626 * Gets the index of the cell that is in edit mode.
627 627 *
628 628 * @method get_edit_index
629 629 *
630 630 * @return index {int}
631 631 **/
632 632 Notebook.prototype.get_edit_index = function () {
633 633 var result = null;
634 634 this.get_cell_elements().filter(function (index) {
635 635 if ($(this).data("cell").mode === 'edit') {
636 636 result = index;
637 637 }
638 638 });
639 639 return result;
640 640 };
641 641
642 642 /**
643 643 * Handle when a a cell blurs and the notebook should enter command mode.
644 644 *
645 645 * @method handle_command_mode
646 646 * @param [cell] {Cell} Cell to enter command mode on.
647 647 **/
648 648 Notebook.prototype.handle_command_mode = function (cell) {
649 649 if (this.mode !== 'command') {
650 650 cell.command_mode();
651 651 this.mode = 'command';
652 652 this.events.trigger('command_mode.Notebook');
653 653 this.keyboard_manager.command_mode();
654 654 }
655 655 };
656 656
657 657 /**
658 658 * Make the notebook enter command mode.
659 659 *
660 660 * @method command_mode
661 661 **/
662 662 Notebook.prototype.command_mode = function () {
663 663 var cell = this.get_cell(this.get_edit_index());
664 664 if (cell && this.mode !== 'command') {
665 665 // We don't call cell.command_mode, but rather call cell.focus_cell()
666 666 // which will blur and CM editor and trigger the call to
667 667 // handle_command_mode.
668 668 cell.focus_cell();
669 669 }
670 670 };
671 671
672 672 /**
673 673 * Handle when a cell fires it's edit_mode event.
674 674 *
675 675 * @method handle_edit_mode
676 676 * @param [cell] {Cell} Cell to enter edit mode on.
677 677 **/
678 678 Notebook.prototype.handle_edit_mode = function (cell) {
679 679 if (cell && this.mode !== 'edit') {
680 680 cell.edit_mode();
681 681 this.mode = 'edit';
682 682 this.events.trigger('edit_mode.Notebook');
683 683 this.keyboard_manager.edit_mode();
684 684 }
685 685 };
686 686
687 687 /**
688 688 * Make a cell enter edit mode.
689 689 *
690 690 * @method edit_mode
691 691 **/
692 692 Notebook.prototype.edit_mode = function () {
693 693 var cell = this.get_selected_cell();
694 694 if (cell && this.mode !== 'edit') {
695 695 cell.unrender();
696 696 cell.focus_editor();
697 697 }
698 698 };
699 699
700 700 /**
701 701 * Focus the currently selected cell.
702 702 *
703 703 * @method focus_cell
704 704 **/
705 705 Notebook.prototype.focus_cell = function () {
706 706 var cell = this.get_selected_cell();
707 707 if (cell === null) {return;} // No cell is selected
708 708 cell.focus_cell();
709 709 };
710 710
711 711 // Cell movement
712 712
713 713 /**
714 714 * Move given (or selected) cell up and select it.
715 715 *
716 716 * @method move_cell_up
717 717 * @param [index] {integer} cell index
718 718 * @return {Notebook} This notebook
719 719 **/
720 720 Notebook.prototype.move_cell_up = function (index) {
721 721 var i = this.index_or_selected(index);
722 722 if (this.is_valid_cell_index(i) && i > 0) {
723 723 var pivot = this.get_cell_element(i-1);
724 724 var tomove = this.get_cell_element(i);
725 725 if (pivot !== null && tomove !== null) {
726 726 tomove.detach();
727 727 pivot.before(tomove);
728 728 this.select(i-1);
729 729 var cell = this.get_selected_cell();
730 730 cell.focus_cell();
731 731 }
732 732 this.set_dirty(true);
733 733 }
734 734 return this;
735 735 };
736 736
737 737
738 738 /**
739 739 * Move given (or selected) cell down and select it
740 740 *
741 741 * @method move_cell_down
742 742 * @param [index] {integer} cell index
743 743 * @return {Notebook} This notebook
744 744 **/
745 745 Notebook.prototype.move_cell_down = function (index) {
746 746 var i = this.index_or_selected(index);
747 747 if (this.is_valid_cell_index(i) && this.is_valid_cell_index(i+1)) {
748 748 var pivot = this.get_cell_element(i+1);
749 749 var tomove = this.get_cell_element(i);
750 750 if (pivot !== null && tomove !== null) {
751 751 tomove.detach();
752 752 pivot.after(tomove);
753 753 this.select(i+1);
754 754 var cell = this.get_selected_cell();
755 755 cell.focus_cell();
756 756 }
757 757 }
758 758 this.set_dirty();
759 759 return this;
760 760 };
761 761
762 762
763 763 // Insertion, deletion.
764 764
765 765 /**
766 766 * Delete a cell from the notebook.
767 767 *
768 768 * @method delete_cell
769 769 * @param [index] A cell's numeric index
770 770 * @return {Notebook} This notebook
771 771 */
772 772 Notebook.prototype.delete_cell = function (index) {
773 773 var i = this.index_or_selected(index);
774 774 var cell = this.get_cell(i);
775 775 if (!cell.is_deletable()) {
776 776 return this;
777 777 }
778 778
779 779 this.undelete_backup = cell.toJSON();
780 780 $('#undelete_cell').removeClass('disabled');
781 781 if (this.is_valid_cell_index(i)) {
782 782 var old_ncells = this.ncells();
783 783 var ce = this.get_cell_element(i);
784 784 ce.remove();
785 785 if (i === 0) {
786 786 // Always make sure we have at least one cell.
787 787 if (old_ncells === 1) {
788 788 this.insert_cell_below('code');
789 789 }
790 790 this.select(0);
791 791 this.undelete_index = 0;
792 792 this.undelete_below = false;
793 793 } else if (i === old_ncells-1 && i !== 0) {
794 794 this.select(i-1);
795 795 this.undelete_index = i - 1;
796 796 this.undelete_below = true;
797 797 } else {
798 798 this.select(i);
799 799 this.undelete_index = i;
800 800 this.undelete_below = false;
801 801 }
802 802 this.events.trigger('delete.Cell', {'cell': cell, 'index': i});
803 803 this.set_dirty(true);
804 804 }
805 805 return this;
806 806 };
807 807
808 808 /**
809 809 * Restore the most recently deleted cell.
810 810 *
811 811 * @method undelete
812 812 */
813 813 Notebook.prototype.undelete_cell = function() {
814 814 if (this.undelete_backup !== null && this.undelete_index !== null) {
815 815 var current_index = this.get_selected_index();
816 816 if (this.undelete_index < current_index) {
817 817 current_index = current_index + 1;
818 818 }
819 819 if (this.undelete_index >= this.ncells()) {
820 820 this.select(this.ncells() - 1);
821 821 }
822 822 else {
823 823 this.select(this.undelete_index);
824 824 }
825 825 var cell_data = this.undelete_backup;
826 826 var new_cell = null;
827 827 if (this.undelete_below) {
828 828 new_cell = this.insert_cell_below(cell_data.cell_type);
829 829 } else {
830 830 new_cell = this.insert_cell_above(cell_data.cell_type);
831 831 }
832 832 new_cell.fromJSON(cell_data);
833 833 if (this.undelete_below) {
834 834 this.select(current_index+1);
835 835 } else {
836 836 this.select(current_index);
837 837 }
838 838 this.undelete_backup = null;
839 839 this.undelete_index = null;
840 840 }
841 841 $('#undelete_cell').addClass('disabled');
842 842 };
843 843
844 844 /**
845 845 * Insert a cell so that after insertion the cell is at given index.
846 846 *
847 847 * If cell type is not provided, it will default to the type of the
848 848 * currently active cell.
849 849 *
850 850 * Similar to insert_above, but index parameter is mandatory
851 851 *
852 852 * Index will be brought back into the accessible range [0,n]
853 853 *
854 854 * @method insert_cell_at_index
855 855 * @param [type] {string} in ['code','markdown', 'raw'], defaults to 'code'
856 856 * @param [index] {int} a valid index where to insert cell
857 857 *
858 858 * @return cell {cell|null} created cell or null
859 859 **/
860 860 Notebook.prototype.insert_cell_at_index = function(type, index){
861 861
862 862 var ncells = this.ncells();
863 863 index = Math.min(index, ncells);
864 864 index = Math.max(index, 0);
865 865 var cell = null;
866 866 type = type || this.default_cell_type;
867 867 if (type === 'above') {
868 868 if (index > 0) {
869 869 type = this.get_cell(index-1).cell_type;
870 870 } else {
871 871 type = 'code';
872 872 }
873 873 } else if (type === 'below') {
874 874 if (index < ncells) {
875 875 type = this.get_cell(index).cell_type;
876 876 } else {
877 877 type = 'code';
878 878 }
879 879 } else if (type === 'selected') {
880 880 type = this.get_selected_cell().cell_type;
881 881 }
882 882
883 883 if (ncells === 0 || this.is_valid_cell_index(index) || index === ncells) {
884 884 var cell_options = {
885 885 events: this.events,
886 886 config: this.config,
887 887 keyboard_manager: this.keyboard_manager,
888 888 notebook: this,
889 889 tooltip: this.tooltip
890 890 };
891 891 switch(type) {
892 892 case 'code':
893 893 cell = new codecell.CodeCell(this.kernel, cell_options);
894 894 cell.set_input_prompt();
895 895 break;
896 896 case 'markdown':
897 897 cell = new textcell.MarkdownCell(cell_options);
898 898 break;
899 899 case 'raw':
900 900 cell = new textcell.RawCell(cell_options);
901 901 break;
902 902 default:
903 903 console.log("invalid cell type: ", type);
904 904 }
905 905
906 906 if(this._insert_element_at_index(cell.element,index)) {
907 907 cell.render();
908 908 this.events.trigger('create.Cell', {'cell': cell, 'index': index});
909 909 cell.refresh();
910 910 // We used to select the cell after we refresh it, but there
911 911 // are now cases were this method is called where select is
912 912 // not appropriate. The selection logic should be handled by the
913 913 // caller of the the top level insert_cell methods.
914 914 this.set_dirty(true);
915 915 }
916 916 }
917 917 return cell;
918 918
919 919 };
920 920
921 921 /**
922 922 * Insert an element at given cell index.
923 923 *
924 924 * @method _insert_element_at_index
925 925 * @param element {dom_element} a cell element
926 926 * @param [index] {int} a valid index where to inser cell
927 927 * @private
928 928 *
929 929 * return true if everything whent fine.
930 930 **/
931 931 Notebook.prototype._insert_element_at_index = function(element, index){
932 932 if (element === undefined){
933 933 return false;
934 934 }
935 935
936 936 var ncells = this.ncells();
937 937
938 938 if (ncells === 0) {
939 939 // special case append if empty
940 940 this.element.find('div.end_space').before(element);
941 941 } else if ( ncells === index ) {
942 942 // special case append it the end, but not empty
943 943 this.get_cell_element(index-1).after(element);
944 944 } else if (this.is_valid_cell_index(index)) {
945 945 // otherwise always somewhere to append to
946 946 this.get_cell_element(index).before(element);
947 947 } else {
948 948 return false;
949 949 }
950 950
951 951 if (this.undelete_index !== null && index <= this.undelete_index) {
952 952 this.undelete_index = this.undelete_index + 1;
953 953 this.set_dirty(true);
954 954 }
955 955 return true;
956 956 };
957 957
958 958 /**
959 959 * Insert a cell of given type above given index, or at top
960 960 * of notebook if index smaller than 0.
961 961 *
962 962 * default index value is the one of currently selected cell
963 963 *
964 964 * @method insert_cell_above
965 965 * @param [type] {string} cell type
966 966 * @param [index] {integer}
967 967 *
968 968 * @return handle to created cell or null
969 969 **/
970 970 Notebook.prototype.insert_cell_above = function (type, index) {
971 971 index = this.index_or_selected(index);
972 972 return this.insert_cell_at_index(type, index);
973 973 };
974 974
975 975 /**
976 976 * Insert a cell of given type below given index, or at bottom
977 977 * of notebook if index greater than number of cells
978 978 *
979 979 * default index value is the one of currently selected cell
980 980 *
981 981 * @method insert_cell_below
982 982 * @param [type] {string} cell type
983 983 * @param [index] {integer}
984 984 *
985 985 * @return handle to created cell or null
986 986 *
987 987 **/
988 988 Notebook.prototype.insert_cell_below = function (type, index) {
989 989 index = this.index_or_selected(index);
990 990 return this.insert_cell_at_index(type, index+1);
991 991 };
992 992
993 993
994 994 /**
995 995 * Insert cell at end of notebook
996 996 *
997 997 * @method insert_cell_at_bottom
998 998 * @param {String} type cell type
999 999 *
1000 1000 * @return the added cell; or null
1001 1001 **/
1002 1002 Notebook.prototype.insert_cell_at_bottom = function (type){
1003 1003 var len = this.ncells();
1004 1004 return this.insert_cell_below(type,len-1);
1005 1005 };
1006 1006
1007 1007 /**
1008 1008 * Turn a cell into a code cell.
1009 1009 *
1010 1010 * @method to_code
1011 1011 * @param {Number} [index] A cell's index
1012 1012 */
1013 1013 Notebook.prototype.to_code = function (index) {
1014 1014 var i = this.index_or_selected(index);
1015 1015 if (this.is_valid_cell_index(i)) {
1016 1016 var source_cell = this.get_cell(i);
1017 1017 if (!(source_cell instanceof codecell.CodeCell)) {
1018 1018 var target_cell = this.insert_cell_below('code',i);
1019 1019 var text = source_cell.get_text();
1020 1020 if (text === source_cell.placeholder) {
1021 1021 text = '';
1022 1022 }
1023 1023 //metadata
1024 1024 target_cell.metadata = source_cell.metadata;
1025 1025
1026 1026 target_cell.set_text(text);
1027 1027 // make this value the starting point, so that we can only undo
1028 1028 // to this state, instead of a blank cell
1029 1029 target_cell.code_mirror.clearHistory();
1030 1030 source_cell.element.remove();
1031 1031 this.select(i);
1032 1032 var cursor = source_cell.code_mirror.getCursor();
1033 1033 target_cell.code_mirror.setCursor(cursor);
1034 1034 this.set_dirty(true);
1035 1035 }
1036 1036 }
1037 1037 };
1038 1038
1039 1039 /**
1040 1040 * Turn a cell into a Markdown cell.
1041 1041 *
1042 1042 * @method to_markdown
1043 1043 * @param {Number} [index] A cell's index
1044 1044 */
1045 1045 Notebook.prototype.to_markdown = function (index) {
1046 1046 var i = this.index_or_selected(index);
1047 1047 if (this.is_valid_cell_index(i)) {
1048 1048 var source_cell = this.get_cell(i);
1049 1049
1050 1050 if (!(source_cell instanceof textcell.MarkdownCell)) {
1051 1051 var target_cell = this.insert_cell_below('markdown',i);
1052 1052 var text = source_cell.get_text();
1053 1053
1054 1054 if (text === source_cell.placeholder) {
1055 1055 text = '';
1056 1056 }
1057 1057 // metadata
1058 1058 target_cell.metadata = source_cell.metadata;
1059 1059 // We must show the editor before setting its contents
1060 1060 target_cell.unrender();
1061 1061 target_cell.set_text(text);
1062 1062 // make this value the starting point, so that we can only undo
1063 1063 // to this state, instead of a blank cell
1064 1064 target_cell.code_mirror.clearHistory();
1065 1065 source_cell.element.remove();
1066 1066 this.select(i);
1067 1067 if ((source_cell instanceof textcell.TextCell) && source_cell.rendered) {
1068 1068 target_cell.render();
1069 1069 }
1070 1070 var cursor = source_cell.code_mirror.getCursor();
1071 1071 target_cell.code_mirror.setCursor(cursor);
1072 1072 this.set_dirty(true);
1073 1073 }
1074 1074 }
1075 1075 };
1076 1076
1077 1077 /**
1078 1078 * Turn a cell into a raw text cell.
1079 1079 *
1080 1080 * @method to_raw
1081 1081 * @param {Number} [index] A cell's index
1082 1082 */
1083 1083 Notebook.prototype.to_raw = function (index) {
1084 1084 var i = this.index_or_selected(index);
1085 1085 if (this.is_valid_cell_index(i)) {
1086 1086 var target_cell = null;
1087 1087 var source_cell = this.get_cell(i);
1088 1088
1089 1089 if (!(source_cell instanceof textcell.RawCell)) {
1090 1090 target_cell = this.insert_cell_below('raw',i);
1091 1091 var text = source_cell.get_text();
1092 1092 if (text === source_cell.placeholder) {
1093 1093 text = '';
1094 1094 }
1095 1095 //metadata
1096 1096 target_cell.metadata = source_cell.metadata;
1097 1097 // We must show the editor before setting its contents
1098 1098 target_cell.unrender();
1099 1099 target_cell.set_text(text);
1100 1100 // make this value the starting point, so that we can only undo
1101 1101 // to this state, instead of a blank cell
1102 1102 target_cell.code_mirror.clearHistory();
1103 1103 source_cell.element.remove();
1104 1104 this.select(i);
1105 1105 var cursor = source_cell.code_mirror.getCursor();
1106 1106 target_cell.code_mirror.setCursor(cursor);
1107 1107 this.set_dirty(true);
1108 1108 }
1109 1109 }
1110 1110 };
1111 1111
1112 1112 Notebook.prototype._warn_heading = function () {
1113 1113 // warn about heading cells being removed
1114 1114 dialog.modal({
1115 1115 notebook: this,
1116 1116 keyboard_manager: this.keyboard_manager,
1117 1117 title : "Use markdown headings",
1118 1118 body : $("<p/>").text(
1119 1119 'IPython no longer uses special heading cells. ' +
1120 1120 'Instead, write your headings in Markdown cells using # characters:'
1121 1121 ).append($('<pre/>').text(
1122 1122 '## This is a level 2 heading'
1123 1123 )),
1124 1124 buttons : {
1125 1125 "OK" : {}
1126 1126 }
1127 1127 });
1128 1128 };
1129 1129
1130 1130 /**
1131 1131 * Turn a cell into a markdown cell with a heading.
1132 1132 *
1133 1133 * @method to_heading
1134 1134 * @param {Number} [index] A cell's index
1135 1135 * @param {Number} [level] A heading level (e.g., 1 for h1)
1136 1136 */
1137 1137 Notebook.prototype.to_heading = function (index, level) {
1138 1138 this.to_markdown(index);
1139 1139 level = level || 1;
1140 1140 var i = this.index_or_selected(index);
1141 1141 if (this.is_valid_cell_index(i)) {
1142 1142 var cell = this.get_cell(i);
1143 1143 cell.set_heading_level(level);
1144 1144 this.set_dirty(true);
1145 1145 }
1146 1146 };
1147 1147
1148 1148
1149 1149 // Cut/Copy/Paste
1150 1150
1151 1151 /**
1152 1152 * Enable UI elements for pasting cells.
1153 1153 *
1154 1154 * @method enable_paste
1155 1155 */
1156 1156 Notebook.prototype.enable_paste = function () {
1157 1157 var that = this;
1158 1158 if (!this.paste_enabled) {
1159 1159 $('#paste_cell_replace').removeClass('disabled')
1160 1160 .on('click', function () {that.paste_cell_replace();});
1161 1161 $('#paste_cell_above').removeClass('disabled')
1162 1162 .on('click', function () {that.paste_cell_above();});
1163 1163 $('#paste_cell_below').removeClass('disabled')
1164 1164 .on('click', function () {that.paste_cell_below();});
1165 1165 this.paste_enabled = true;
1166 1166 }
1167 1167 };
1168 1168
1169 1169 /**
1170 1170 * Disable UI elements for pasting cells.
1171 1171 *
1172 1172 * @method disable_paste
1173 1173 */
1174 1174 Notebook.prototype.disable_paste = function () {
1175 1175 if (this.paste_enabled) {
1176 1176 $('#paste_cell_replace').addClass('disabled').off('click');
1177 1177 $('#paste_cell_above').addClass('disabled').off('click');
1178 1178 $('#paste_cell_below').addClass('disabled').off('click');
1179 1179 this.paste_enabled = false;
1180 1180 }
1181 1181 };
1182 1182
1183 1183 /**
1184 1184 * Cut a cell.
1185 1185 *
1186 1186 * @method cut_cell
1187 1187 */
1188 1188 Notebook.prototype.cut_cell = function () {
1189 1189 this.copy_cell();
1190 1190 this.delete_cell();
1191 1191 };
1192 1192
1193 1193 /**
1194 1194 * Copy a cell.
1195 1195 *
1196 1196 * @method copy_cell
1197 1197 */
1198 1198 Notebook.prototype.copy_cell = function () {
1199 1199 var cell = this.get_selected_cell();
1200 1200 this.clipboard = cell.toJSON();
1201 1201 // remove undeletable status from the copied cell
1202 1202 if (this.clipboard.metadata.deletable !== undefined) {
1203 1203 delete this.clipboard.metadata.deletable;
1204 1204 }
1205 1205 this.enable_paste();
1206 1206 };
1207 1207
1208 1208 /**
1209 1209 * Replace the selected cell with a cell in the clipboard.
1210 1210 *
1211 1211 * @method paste_cell_replace
1212 1212 */
1213 1213 Notebook.prototype.paste_cell_replace = function () {
1214 1214 if (this.clipboard !== null && this.paste_enabled) {
1215 1215 var cell_data = this.clipboard;
1216 1216 var new_cell = this.insert_cell_above(cell_data.cell_type);
1217 1217 new_cell.fromJSON(cell_data);
1218 1218 var old_cell = this.get_next_cell(new_cell);
1219 1219 this.delete_cell(this.find_cell_index(old_cell));
1220 1220 this.select(this.find_cell_index(new_cell));
1221 1221 }
1222 1222 };
1223 1223
1224 1224 /**
1225 1225 * Paste a cell from the clipboard above the selected cell.
1226 1226 *
1227 1227 * @method paste_cell_above
1228 1228 */
1229 1229 Notebook.prototype.paste_cell_above = function () {
1230 1230 if (this.clipboard !== null && this.paste_enabled) {
1231 1231 var cell_data = this.clipboard;
1232 1232 var new_cell = this.insert_cell_above(cell_data.cell_type);
1233 1233 new_cell.fromJSON(cell_data);
1234 1234 new_cell.focus_cell();
1235 1235 }
1236 1236 };
1237 1237
1238 1238 /**
1239 1239 * Paste a cell from the clipboard below the selected cell.
1240 1240 *
1241 1241 * @method paste_cell_below
1242 1242 */
1243 1243 Notebook.prototype.paste_cell_below = function () {
1244 1244 if (this.clipboard !== null && this.paste_enabled) {
1245 1245 var cell_data = this.clipboard;
1246 1246 var new_cell = this.insert_cell_below(cell_data.cell_type);
1247 1247 new_cell.fromJSON(cell_data);
1248 1248 new_cell.focus_cell();
1249 1249 }
1250 1250 };
1251 1251
1252 1252 // Split/merge
1253 1253
1254 1254 /**
1255 1255 * Split the selected cell into two, at the cursor.
1256 1256 *
1257 1257 * @method split_cell
1258 1258 */
1259 1259 Notebook.prototype.split_cell = function () {
1260 1260 var cell = this.get_selected_cell();
1261 1261 if (cell.is_splittable()) {
1262 1262 var texta = cell.get_pre_cursor();
1263 1263 var textb = cell.get_post_cursor();
1264 1264 cell.set_text(textb);
1265 1265 var new_cell = this.insert_cell_above(cell.cell_type);
1266 1266 // Unrender the new cell so we can call set_text.
1267 1267 new_cell.unrender();
1268 1268 new_cell.set_text(texta);
1269 1269 }
1270 1270 };
1271 1271
1272 1272 /**
1273 1273 * Combine the selected cell into the cell above it.
1274 1274 *
1275 1275 * @method merge_cell_above
1276 1276 */
1277 1277 Notebook.prototype.merge_cell_above = function () {
1278 1278 var index = this.get_selected_index();
1279 1279 var cell = this.get_cell(index);
1280 1280 var render = cell.rendered;
1281 1281 if (!cell.is_mergeable()) {
1282 1282 return;
1283 1283 }
1284 1284 if (index > 0) {
1285 1285 var upper_cell = this.get_cell(index-1);
1286 1286 if (!upper_cell.is_mergeable()) {
1287 1287 return;
1288 1288 }
1289 1289 var upper_text = upper_cell.get_text();
1290 1290 var text = cell.get_text();
1291 1291 if (cell instanceof codecell.CodeCell) {
1292 1292 cell.set_text(upper_text+'\n'+text);
1293 1293 } else {
1294 1294 cell.unrender(); // Must unrender before we set_text.
1295 1295 cell.set_text(upper_text+'\n\n'+text);
1296 1296 if (render) {
1297 1297 // The rendered state of the final cell should match
1298 1298 // that of the original selected cell;
1299 1299 cell.render();
1300 1300 }
1301 1301 }
1302 1302 this.delete_cell(index-1);
1303 1303 this.select(this.find_cell_index(cell));
1304 1304 }
1305 1305 };
1306 1306
1307 1307 /**
1308 1308 * Combine the selected cell into the cell below it.
1309 1309 *
1310 1310 * @method merge_cell_below
1311 1311 */
1312 1312 Notebook.prototype.merge_cell_below = function () {
1313 1313 var index = this.get_selected_index();
1314 1314 var cell = this.get_cell(index);
1315 1315 var render = cell.rendered;
1316 1316 if (!cell.is_mergeable()) {
1317 1317 return;
1318 1318 }
1319 1319 if (index < this.ncells()-1) {
1320 1320 var lower_cell = this.get_cell(index+1);
1321 1321 if (!lower_cell.is_mergeable()) {
1322 1322 return;
1323 1323 }
1324 1324 var lower_text = lower_cell.get_text();
1325 1325 var text = cell.get_text();
1326 1326 if (cell instanceof codecell.CodeCell) {
1327 1327 cell.set_text(text+'\n'+lower_text);
1328 1328 } else {
1329 1329 cell.unrender(); // Must unrender before we set_text.
1330 1330 cell.set_text(text+'\n\n'+lower_text);
1331 1331 if (render) {
1332 1332 // The rendered state of the final cell should match
1333 1333 // that of the original selected cell;
1334 1334 cell.render();
1335 1335 }
1336 1336 }
1337 1337 this.delete_cell(index+1);
1338 1338 this.select(this.find_cell_index(cell));
1339 1339 }
1340 1340 };
1341 1341
1342 1342
1343 1343 // Cell collapsing and output clearing
1344 1344
1345 1345 /**
1346 1346 * Hide a cell's output.
1347 1347 *
1348 1348 * @method collapse_output
1349 1349 * @param {Number} index A cell's numeric index
1350 1350 */
1351 1351 Notebook.prototype.collapse_output = function (index) {
1352 1352 var i = this.index_or_selected(index);
1353 1353 var cell = this.get_cell(i);
1354 1354 if (cell !== null && (cell instanceof codecell.CodeCell)) {
1355 1355 cell.collapse_output();
1356 1356 this.set_dirty(true);
1357 1357 }
1358 1358 };
1359 1359
1360 1360 /**
1361 1361 * Hide each code cell's output area.
1362 1362 *
1363 1363 * @method collapse_all_output
1364 1364 */
1365 1365 Notebook.prototype.collapse_all_output = function () {
1366 1366 this.get_cells().map(function (cell, i) {
1367 1367 if (cell instanceof codecell.CodeCell) {
1368 1368 cell.collapse_output();
1369 1369 }
1370 1370 });
1371 1371 // this should not be set if the `collapse` key is removed from nbformat
1372 1372 this.set_dirty(true);
1373 1373 };
1374 1374
1375 1375 /**
1376 1376 * Show a cell's output.
1377 1377 *
1378 1378 * @method expand_output
1379 1379 * @param {Number} index A cell's numeric index
1380 1380 */
1381 1381 Notebook.prototype.expand_output = function (index) {
1382 1382 var i = this.index_or_selected(index);
1383 1383 var cell = this.get_cell(i);
1384 1384 if (cell !== null && (cell instanceof codecell.CodeCell)) {
1385 1385 cell.expand_output();
1386 1386 this.set_dirty(true);
1387 1387 }
1388 1388 };
1389 1389
1390 1390 /**
1391 1391 * Expand each code cell's output area, and remove scrollbars.
1392 1392 *
1393 1393 * @method expand_all_output
1394 1394 */
1395 1395 Notebook.prototype.expand_all_output = function () {
1396 1396 this.get_cells().map(function (cell, i) {
1397 1397 if (cell instanceof codecell.CodeCell) {
1398 1398 cell.expand_output();
1399 1399 }
1400 1400 });
1401 1401 // this should not be set if the `collapse` key is removed from nbformat
1402 1402 this.set_dirty(true);
1403 1403 };
1404 1404
1405 1405 /**
1406 1406 * Clear the selected CodeCell's output area.
1407 1407 *
1408 1408 * @method clear_output
1409 1409 * @param {Number} index A cell's numeric index
1410 1410 */
1411 1411 Notebook.prototype.clear_output = function (index) {
1412 1412 var i = this.index_or_selected(index);
1413 1413 var cell = this.get_cell(i);
1414 1414 if (cell !== null && (cell instanceof codecell.CodeCell)) {
1415 1415 cell.clear_output();
1416 1416 this.set_dirty(true);
1417 1417 }
1418 1418 };
1419 1419
1420 1420 /**
1421 1421 * Clear each code cell's output area.
1422 1422 *
1423 1423 * @method clear_all_output
1424 1424 */
1425 1425 Notebook.prototype.clear_all_output = function () {
1426 1426 this.get_cells().map(function (cell, i) {
1427 1427 if (cell instanceof codecell.CodeCell) {
1428 1428 cell.clear_output();
1429 1429 }
1430 1430 });
1431 1431 this.set_dirty(true);
1432 1432 };
1433 1433
1434 1434 /**
1435 1435 * Scroll the selected CodeCell's output area.
1436 1436 *
1437 1437 * @method scroll_output
1438 1438 * @param {Number} index A cell's numeric index
1439 1439 */
1440 1440 Notebook.prototype.scroll_output = function (index) {
1441 1441 var i = this.index_or_selected(index);
1442 1442 var cell = this.get_cell(i);
1443 1443 if (cell !== null && (cell instanceof codecell.CodeCell)) {
1444 1444 cell.scroll_output();
1445 1445 this.set_dirty(true);
1446 1446 }
1447 1447 };
1448 1448
1449 1449 /**
1450 1450 * Expand each code cell's output area, and add a scrollbar for long output.
1451 1451 *
1452 1452 * @method scroll_all_output
1453 1453 */
1454 1454 Notebook.prototype.scroll_all_output = function () {
1455 1455 this.get_cells().map(function (cell, i) {
1456 1456 if (cell instanceof codecell.CodeCell) {
1457 1457 cell.scroll_output();
1458 1458 }
1459 1459 });
1460 1460 // this should not be set if the `collapse` key is removed from nbformat
1461 1461 this.set_dirty(true);
1462 1462 };
1463 1463
1464 1464 /** Toggle whether a cell's output is collapsed or expanded.
1465 1465 *
1466 1466 * @method toggle_output
1467 1467 * @param {Number} index A cell's numeric index
1468 1468 */
1469 1469 Notebook.prototype.toggle_output = function (index) {
1470 1470 var i = this.index_or_selected(index);
1471 1471 var cell = this.get_cell(i);
1472 1472 if (cell !== null && (cell instanceof codecell.CodeCell)) {
1473 1473 cell.toggle_output();
1474 1474 this.set_dirty(true);
1475 1475 }
1476 1476 };
1477 1477
1478 1478 /**
1479 1479 * Hide/show the output of all cells.
1480 1480 *
1481 1481 * @method toggle_all_output
1482 1482 */
1483 1483 Notebook.prototype.toggle_all_output = function () {
1484 1484 this.get_cells().map(function (cell, i) {
1485 1485 if (cell instanceof codecell.CodeCell) {
1486 1486 cell.toggle_output();
1487 1487 }
1488 1488 });
1489 1489 // this should not be set if the `collapse` key is removed from nbformat
1490 1490 this.set_dirty(true);
1491 1491 };
1492 1492
1493 1493 /**
1494 1494 * Toggle a scrollbar for long cell outputs.
1495 1495 *
1496 1496 * @method toggle_output_scroll
1497 1497 * @param {Number} index A cell's numeric index
1498 1498 */
1499 1499 Notebook.prototype.toggle_output_scroll = function (index) {
1500 1500 var i = this.index_or_selected(index);
1501 1501 var cell = this.get_cell(i);
1502 1502 if (cell !== null && (cell instanceof codecell.CodeCell)) {
1503 1503 cell.toggle_output_scroll();
1504 1504 this.set_dirty(true);
1505 1505 }
1506 1506 };
1507 1507
1508 1508 /**
1509 1509 * Toggle the scrolling of long output on all cells.
1510 1510 *
1511 1511 * @method toggle_all_output_scrolling
1512 1512 */
1513 1513 Notebook.prototype.toggle_all_output_scroll = function () {
1514 1514 this.get_cells().map(function (cell, i) {
1515 1515 if (cell instanceof codecell.CodeCell) {
1516 1516 cell.toggle_output_scroll();
1517 1517 }
1518 1518 });
1519 1519 // this should not be set if the `collapse` key is removed from nbformat
1520 1520 this.set_dirty(true);
1521 1521 };
1522 1522
1523 1523 // Other cell functions: line numbers, ...
1524 1524
1525 1525 /**
1526 1526 * Toggle line numbers in the selected cell's input area.
1527 1527 *
1528 1528 * @method cell_toggle_line_numbers
1529 1529 */
1530 1530 Notebook.prototype.cell_toggle_line_numbers = function() {
1531 1531 this.get_selected_cell().toggle_line_numbers();
1532 1532 };
1533 1533
1534 1534 /**
1535 1535 * Set the codemirror mode for all code cells, including the default for
1536 1536 * new code cells.
1537 1537 *
1538 1538 * @method set_codemirror_mode
1539 1539 */
1540 1540 Notebook.prototype.set_codemirror_mode = function(newmode){
1541 1541 if (newmode === this.codemirror_mode) {
1542 1542 return;
1543 1543 }
1544 1544 this.codemirror_mode = newmode;
1545 1545 codecell.CodeCell.options_default.cm_config.mode = newmode;
1546 1546 var modename = newmode.mode || newmode.name || newmode;
1547 1547
1548 1548 var that = this;
1549 1549 utils.requireCodeMirrorMode(modename, function () {
1550 1550 that.get_cells().map(function(cell, i) {
1551 1551 if (cell.cell_type === 'code'){
1552 1552 cell.code_mirror.setOption('mode', newmode);
1553 1553 // This is currently redundant, because cm_config ends up as
1554 1554 // codemirror's own .options object, but I don't want to
1555 1555 // rely on that.
1556 1556 cell.cm_config.mode = newmode;
1557 1557 }
1558 1558 });
1559 1559 });
1560 1560 };
1561 1561
1562 1562 // Session related things
1563 1563
1564 1564 /**
1565 1565 * Start a new session and set it on each code cell.
1566 1566 *
1567 1567 * @method start_session
1568 1568 */
1569 1569 Notebook.prototype.start_session = function (kernel_name) {
1570 1570 if (this._session_starting) {
1571 1571 throw new session.SessionAlreadyStarting();
1572 1572 }
1573 1573 this._session_starting = true;
1574 1574
1575 1575 var options = {
1576 1576 base_url: this.base_url,
1577 1577 ws_url: this.ws_url,
1578 1578 notebook_path: this.notebook_path,
1579 1579 notebook_name: this.notebook_name,
1580 1580 kernel_name: kernel_name,
1581 1581 notebook: this
1582 1582 };
1583 1583
1584 1584 var success = $.proxy(this._session_started, this);
1585 1585 var failure = $.proxy(this._session_start_failed, this);
1586 1586
1587 1587 if (this.session !== null) {
1588 1588 this.session.restart(options, success, failure);
1589 1589 } else {
1590 1590 this.session = new session.Session(options);
1591 1591 this.session.start(success, failure);
1592 1592 }
1593 1593 };
1594 1594
1595 1595
1596 1596 /**
1597 1597 * Once a session is started, link the code cells to the kernel and pass the
1598 1598 * comm manager to the widget manager
1599 1599 *
1600 1600 */
1601 1601 Notebook.prototype._session_started = function (){
1602 1602 this._session_starting = false;
1603 1603 this.kernel = this.session.kernel;
1604 1604 var ncells = this.ncells();
1605 1605 for (var i=0; i<ncells; i++) {
1606 1606 var cell = this.get_cell(i);
1607 1607 if (cell instanceof codecell.CodeCell) {
1608 1608 cell.set_kernel(this.session.kernel);
1609 1609 }
1610 1610 }
1611 1611 };
1612 1612 Notebook.prototype._session_start_failed = function (jqxhr, status, error){
1613 1613 this._session_starting = false;
1614 1614 utils.log_ajax_error(jqxhr, status, error);
1615 1615 };
1616 1616
1617 1617 /**
1618 1618 * Prompt the user to restart the IPython kernel.
1619 1619 *
1620 1620 * @method restart_kernel
1621 1621 */
1622 1622 Notebook.prototype.restart_kernel = function () {
1623 1623 var that = this;
1624 1624 dialog.modal({
1625 1625 notebook: this,
1626 1626 keyboard_manager: this.keyboard_manager,
1627 1627 title : "Restart kernel or continue running?",
1628 1628 body : $("<p/>").text(
1629 1629 'Do you want to restart the current kernel? You will lose all variables defined in it.'
1630 1630 ),
1631 1631 buttons : {
1632 1632 "Continue running" : {},
1633 1633 "Restart" : {
1634 1634 "class" : "btn-danger",
1635 1635 "click" : function() {
1636 1636 that.kernel.restart();
1637 1637 }
1638 1638 }
1639 1639 }
1640 1640 });
1641 1641 };
1642 1642
1643 1643 /**
1644 1644 * Execute or render cell outputs and go into command mode.
1645 1645 *
1646 1646 * @method execute_cell
1647 1647 */
1648 1648 Notebook.prototype.execute_cell = function () {
1649 1649 // mode = shift, ctrl, alt
1650 1650 var cell = this.get_selected_cell();
1651 1651
1652 1652 cell.execute();
1653 1653 this.command_mode();
1654 1654 this.set_dirty(true);
1655 1655 };
1656 1656
1657 1657 /**
1658 1658 * Execute or render cell outputs and insert a new cell below.
1659 1659 *
1660 1660 * @method execute_cell_and_insert_below
1661 1661 */
1662 1662 Notebook.prototype.execute_cell_and_insert_below = function () {
1663 1663 var cell = this.get_selected_cell();
1664 1664 var cell_index = this.find_cell_index(cell);
1665 1665
1666 1666 cell.execute();
1667 1667
1668 1668 // If we are at the end always insert a new cell and return
1669 1669 if (cell_index === (this.ncells()-1)) {
1670 1670 this.command_mode();
1671 1671 this.insert_cell_below();
1672 1672 this.select(cell_index+1);
1673 1673 this.edit_mode();
1674 1674 this.scroll_to_bottom();
1675 1675 this.set_dirty(true);
1676 1676 return;
1677 1677 }
1678 1678
1679 1679 this.command_mode();
1680 1680 this.insert_cell_below();
1681 1681 this.select(cell_index+1);
1682 1682 this.edit_mode();
1683 1683 this.set_dirty(true);
1684 1684 };
1685 1685
1686 1686 /**
1687 1687 * Execute or render cell outputs and select the next cell.
1688 1688 *
1689 1689 * @method execute_cell_and_select_below
1690 1690 */
1691 1691 Notebook.prototype.execute_cell_and_select_below = function () {
1692 1692
1693 1693 var cell = this.get_selected_cell();
1694 1694 var cell_index = this.find_cell_index(cell);
1695 1695
1696 1696 cell.execute();
1697 1697
1698 1698 // If we are at the end always insert a new cell and return
1699 1699 if (cell_index === (this.ncells()-1)) {
1700 1700 this.command_mode();
1701 1701 this.insert_cell_below();
1702 1702 this.select(cell_index+1);
1703 1703 this.edit_mode();
1704 1704 this.scroll_to_bottom();
1705 1705 this.set_dirty(true);
1706 1706 return;
1707 1707 }
1708 1708
1709 1709 this.command_mode();
1710 1710 this.select(cell_index+1);
1711 1711 this.focus_cell();
1712 1712 this.set_dirty(true);
1713 1713 };
1714 1714
1715 1715 /**
1716 1716 * Execute all cells below the selected cell.
1717 1717 *
1718 1718 * @method execute_cells_below
1719 1719 */
1720 1720 Notebook.prototype.execute_cells_below = function () {
1721 1721 this.execute_cell_range(this.get_selected_index(), this.ncells());
1722 1722 this.scroll_to_bottom();
1723 1723 };
1724 1724
1725 1725 /**
1726 1726 * Execute all cells above the selected cell.
1727 1727 *
1728 1728 * @method execute_cells_above
1729 1729 */
1730 1730 Notebook.prototype.execute_cells_above = function () {
1731 1731 this.execute_cell_range(0, this.get_selected_index());
1732 1732 };
1733 1733
1734 1734 /**
1735 1735 * Execute all cells.
1736 1736 *
1737 1737 * @method execute_all_cells
1738 1738 */
1739 1739 Notebook.prototype.execute_all_cells = function () {
1740 1740 this.execute_cell_range(0, this.ncells());
1741 1741 this.scroll_to_bottom();
1742 1742 };
1743 1743
1744 1744 /**
1745 1745 * Execute a contiguous range of cells.
1746 1746 *
1747 1747 * @method execute_cell_range
1748 1748 * @param {Number} start Index of the first cell to execute (inclusive)
1749 1749 * @param {Number} end Index of the last cell to execute (exclusive)
1750 1750 */
1751 1751 Notebook.prototype.execute_cell_range = function (start, end) {
1752 1752 this.command_mode();
1753 1753 for (var i=start; i<end; i++) {
1754 1754 this.select(i);
1755 1755 this.execute_cell();
1756 1756 }
1757 1757 };
1758 1758
1759 1759 // Persistance and loading
1760 1760
1761 1761 /**
1762 1762 * Getter method for this notebook's name.
1763 1763 *
1764 1764 * @method get_notebook_name
1765 1765 * @return {String} This notebook's name (excluding file extension)
1766 1766 */
1767 1767 Notebook.prototype.get_notebook_name = function () {
1768 1768 var nbname = this.notebook_name.substring(0,this.notebook_name.length-6);
1769 1769 return nbname;
1770 1770 };
1771 1771
1772 1772 /**
1773 1773 * Setter method for this notebook's name.
1774 1774 *
1775 1775 * @method set_notebook_name
1776 1776 * @param {String} name A new name for this notebook
1777 1777 */
1778 1778 Notebook.prototype.set_notebook_name = function (name) {
1779 1779 var parent = utils.url_path_split(this.notebook_path)[0];
1780 1780 this.notebook_name = name;
1781 1781 this.notebook_path = utils.url_path_join(parent, name);
1782 1782 };
1783 1783
1784 1784 /**
1785 1785 * Check that a notebook's name is valid.
1786 1786 *
1787 1787 * @method test_notebook_name
1788 1788 * @param {String} nbname A name for this notebook
1789 1789 * @return {Boolean} True if the name is valid, false if invalid
1790 1790 */
1791 1791 Notebook.prototype.test_notebook_name = function (nbname) {
1792 1792 nbname = nbname || '';
1793 1793 if (nbname.length>0 && !this.notebook_name_blacklist_re.test(nbname)) {
1794 1794 return true;
1795 1795 } else {
1796 1796 return false;
1797 1797 }
1798 1798 };
1799 1799
1800 1800 /**
1801 1801 * Load a notebook from JSON (.ipynb).
1802 1802 *
1803 1803 * @method fromJSON
1804 1804 * @param {Object} data JSON representation of a notebook
1805 1805 */
1806 1806 Notebook.prototype.fromJSON = function (data) {
1807 1807
1808 1808 var content = data.content;
1809 1809 var ncells = this.ncells();
1810 1810 var i;
1811 1811 for (i=0; i<ncells; i++) {
1812 1812 // Always delete cell 0 as they get renumbered as they are deleted.
1813 1813 this.delete_cell(0);
1814 1814 }
1815 1815 // Save the metadata and name.
1816 1816 this.metadata = content.metadata;
1817 1817 this.notebook_name = data.name;
1818 1818 this.notebook_path = data.path;
1819 1819 var trusted = true;
1820 1820
1821 1821 // Trigger an event changing the kernel spec - this will set the default
1822 1822 // codemirror mode
1823 1823 if (this.metadata.kernelspec !== undefined) {
1824 1824 this.events.trigger('spec_changed.Kernel', this.metadata.kernelspec);
1825 1825 }
1826 1826
1827 1827 // Set the codemirror mode from language_info metadata
1828 1828 if (this.metadata.language_info !== undefined) {
1829 1829 var langinfo = this.metadata.language_info;
1830 1830 // Mode 'null' should be plain, unhighlighted text.
1831 1831 var cm_mode = langinfo.codemirror_mode || langinfo.language || 'null';
1832 1832 this.set_codemirror_mode(cm_mode);
1833 1833 }
1834 1834
1835 1835 var new_cells = content.cells;
1836 1836 ncells = new_cells.length;
1837 1837 var cell_data = null;
1838 1838 var new_cell = null;
1839 1839 for (i=0; i<ncells; i++) {
1840 1840 cell_data = new_cells[i];
1841 1841 new_cell = this.insert_cell_at_index(cell_data.cell_type, i);
1842 1842 new_cell.fromJSON(cell_data);
1843 1843 if (new_cell.cell_type == 'code' && !new_cell.output_area.trusted) {
1844 1844 trusted = false;
1845 1845 }
1846 1846 }
1847 1847 if (trusted !== this.trusted) {
1848 1848 this.trusted = trusted;
1849 1849 this.events.trigger("trust_changed.Notebook", trusted);
1850 1850 }
1851 1851 };
1852 1852
1853 1853 /**
1854 1854 * Dump this notebook into a JSON-friendly object.
1855 1855 *
1856 1856 * @method toJSON
1857 1857 * @return {Object} A JSON-friendly representation of this notebook.
1858 1858 */
1859 1859 Notebook.prototype.toJSON = function () {
1860 1860 // remove the conversion indicator, which only belongs in-memory
1861 1861 delete this.metadata.orig_nbformat;
1862 1862 delete this.metadata.orig_nbformat_minor;
1863 1863
1864 1864 var cells = this.get_cells();
1865 1865 var ncells = cells.length;
1866 1866 var cell_array = new Array(ncells);
1867 1867 var trusted = true;
1868 1868 for (var i=0; i<ncells; i++) {
1869 1869 var cell = cells[i];
1870 1870 if (cell.cell_type == 'code' && !cell.output_area.trusted) {
1871 1871 trusted = false;
1872 1872 }
1873 1873 cell_array[i] = cell.toJSON();
1874 1874 }
1875 1875 var data = {
1876 1876 cells: cell_array,
1877 1877 metadata: this.metadata,
1878 1878 nbformat: this.nbformat,
1879 1879 nbformat_minor: this.nbformat_minor
1880 1880 };
1881 1881 if (trusted != this.trusted) {
1882 1882 this.trusted = trusted;
1883 1883 this.events.trigger("trust_changed.Notebook", trusted);
1884 1884 }
1885 1885 return data;
1886 1886 };
1887 1887
1888 1888 /**
1889 1889 * Start an autosave timer, for periodically saving the notebook.
1890 1890 *
1891 1891 * @method set_autosave_interval
1892 1892 * @param {Integer} interval the autosave interval in milliseconds
1893 1893 */
1894 1894 Notebook.prototype.set_autosave_interval = function (interval) {
1895 1895 var that = this;
1896 1896 // clear previous interval, so we don't get simultaneous timers
1897 1897 if (this.autosave_timer) {
1898 1898 clearInterval(this.autosave_timer);
1899 1899 }
1900 1900 if (!this.writable) {
1901 1901 // disable autosave if not writable
1902 1902 interval = 0;
1903 1903 }
1904 1904
1905 1905 this.autosave_interval = this.minimum_autosave_interval = interval;
1906 1906 if (interval) {
1907 1907 this.autosave_timer = setInterval(function() {
1908 1908 if (that.dirty) {
1909 1909 that.save_notebook();
1910 1910 }
1911 1911 }, interval);
1912 1912 this.events.trigger("autosave_enabled.Notebook", interval);
1913 1913 } else {
1914 1914 this.autosave_timer = null;
1915 1915 this.events.trigger("autosave_disabled.Notebook");
1916 1916 }
1917 1917 };
1918 1918
1919 1919 /**
1920 1920 * Save this notebook on the server. This becomes a notebook instance's
1921 1921 * .save_notebook method *after* the entire notebook has been loaded.
1922 1922 *
1923 1923 * @method save_notebook
1924 1924 */
1925 1925 Notebook.prototype.save_notebook = function () {
1926 1926 if (!this._fully_loaded) {
1927 1927 this.events.trigger('notebook_save_failed.Notebook',
1928 1928 new Error("Load failed, save is disabled")
1929 1929 );
1930 1930 return;
1931 1931 } else if (!this.writable) {
1932 1932 this.events.trigger('notebook_save_failed.Notebook',
1933 1933 new Error("Notebook is read-only")
1934 1934 );
1935 1935 return;
1936 1936 }
1937 1937
1938 1938 // Create a JSON model to be sent to the server.
1939 1939 var model = {
1940 1940 type : "notebook",
1941 1941 content : this.toJSON()
1942 1942 };
1943 1943 // time the ajax call for autosave tuning purposes.
1944 1944 var start = new Date().getTime();
1945 1945
1946 1946 var that = this;
1947 this.contents.save(this.notebook_path, model).then(
1947 return this.contents.save(this.notebook_path, model).then(
1948 1948 $.proxy(this.save_notebook_success, this, start),
1949 1949 function (error) {
1950 1950 that.events.trigger('notebook_save_failed.Notebook', error);
1951 1951 }
1952 1952 );
1953 1953 };
1954 1954
1955 1955 /**
1956 1956 * Success callback for saving a notebook.
1957 1957 *
1958 1958 * @method save_notebook_success
1959 1959 * @param {Integer} start Time when the save request start
1960 1960 * @param {Object} data JSON representation of a notebook
1961 1961 */
1962 1962 Notebook.prototype.save_notebook_success = function (start, data) {
1963 1963 this.set_dirty(false);
1964 1964 if (data.message) {
1965 1965 // save succeeded, but validation failed.
1966 1966 var body = $("<div>");
1967 1967 var title = "Notebook validation failed";
1968 1968
1969 1969 body.append($("<p>").text(
1970 1970 "The save operation succeeded," +
1971 1971 " but the notebook does not appear to be valid." +
1972 1972 " The validation error was:"
1973 1973 )).append($("<div>").addClass("validation-error").append(
1974 1974 $("<pre>").text(data.message)
1975 1975 ));
1976 1976 dialog.modal({
1977 1977 notebook: this,
1978 1978 keyboard_manager: this.keyboard_manager,
1979 1979 title: title,
1980 1980 body: body,
1981 1981 buttons : {
1982 1982 OK : {
1983 1983 "class" : "btn-primary"
1984 1984 }
1985 1985 }
1986 1986 });
1987 1987 }
1988 1988 this.events.trigger('notebook_saved.Notebook');
1989 1989 this._update_autosave_interval(start);
1990 1990 if (this._checkpoint_after_save) {
1991 1991 this.create_checkpoint();
1992 1992 this._checkpoint_after_save = false;
1993 1993 }
1994 1994 };
1995 1995
1996 1996 /**
1997 1997 * update the autosave interval based on how long the last save took
1998 1998 *
1999 1999 * @method _update_autosave_interval
2000 2000 * @param {Integer} timestamp when the save request started
2001 2001 */
2002 2002 Notebook.prototype._update_autosave_interval = function (start) {
2003 2003 var duration = (new Date().getTime() - start);
2004 2004 if (this.autosave_interval) {
2005 2005 // new save interval: higher of 10x save duration or parameter (default 30 seconds)
2006 2006 var interval = Math.max(10 * duration, this.minimum_autosave_interval);
2007 2007 // round to 10 seconds, otherwise we will be setting a new interval too often
2008 2008 interval = 10000 * Math.round(interval / 10000);
2009 2009 // set new interval, if it's changed
2010 2010 if (interval != this.autosave_interval) {
2011 2011 this.set_autosave_interval(interval);
2012 2012 }
2013 2013 }
2014 2014 };
2015 2015
2016 2016 /**
2017 2017 * Explicitly trust the output of this notebook.
2018 2018 *
2019 2019 * @method trust_notebook
2020 2020 */
2021 2021 Notebook.prototype.trust_notebook = function () {
2022 2022 var body = $("<div>").append($("<p>")
2023 2023 .text("A trusted IPython notebook may execute hidden malicious code ")
2024 2024 .append($("<strong>")
2025 2025 .append(
2026 2026 $("<em>").text("when you open it")
2027 2027 )
2028 2028 ).append(".").append(
2029 2029 " Selecting trust will immediately reload this notebook in a trusted state."
2030 2030 ).append(
2031 2031 " For more information, see the "
2032 2032 ).append($("<a>").attr("href", "http://ipython.org/ipython-doc/2/notebook/security.html")
2033 2033 .text("IPython security documentation")
2034 2034 ).append(".")
2035 2035 );
2036 2036
2037 2037 var nb = this;
2038 2038 dialog.modal({
2039 2039 notebook: this,
2040 2040 keyboard_manager: this.keyboard_manager,
2041 2041 title: "Trust this notebook?",
2042 2042 body: body,
2043 2043
2044 2044 buttons: {
2045 2045 Cancel : {},
2046 2046 Trust : {
2047 2047 class : "btn-danger",
2048 2048 click : function () {
2049 2049 var cells = nb.get_cells();
2050 2050 for (var i = 0; i < cells.length; i++) {
2051 2051 var cell = cells[i];
2052 2052 if (cell.cell_type == 'code') {
2053 2053 cell.output_area.trusted = true;
2054 2054 }
2055 2055 }
2056 2056 nb.events.on('notebook_saved.Notebook', function () {
2057 2057 window.location.reload();
2058 2058 });
2059 2059 nb.save_notebook();
2060 2060 }
2061 2061 }
2062 2062 }
2063 2063 });
2064 2064 };
2065 2065
2066 2066 Notebook.prototype.copy_notebook = function () {
2067 2067 var that = this;
2068 2068 var base_url = this.base_url;
2069 2069 var w = window.open();
2070 2070 var parent = utils.url_path_split(this.notebook_path)[0];
2071 2071 this.contents.copy(this.notebook_path, parent).then(
2072 2072 function (data) {
2073 2073 w.location = utils.url_join_encode(
2074 2074 base_url, 'notebooks', data.path
2075 2075 );
2076 2076 },
2077 2077 function(error) {
2078 2078 w.close();
2079 2079 that.events.trigger('notebook_copy_failed', error);
2080 2080 }
2081 2081 );
2082 2082 };
2083 2083
2084 2084 Notebook.prototype.rename = function (new_name) {
2085 2085 if (!new_name.match(/\.ipynb$/)) {
2086 2086 new_name = new_name + ".ipynb";
2087 2087 }
2088 2088
2089 2089 var that = this;
2090 2090 var parent = utils.url_path_split(this.notebook_path)[0];
2091 2091 var new_path = utils.url_path_join(parent, new_name);
2092 2092 return this.contents.rename(this.notebook_path, new_path).then(
2093 2093 function (json) {
2094 2094 that.notebook_name = json.name;
2095 2095 that.notebook_path = json.path;
2096 2096 that.session.rename_notebook(json.path);
2097 2097 that.events.trigger('notebook_renamed.Notebook', json);
2098 2098 }
2099 2099 );
2100 2100 };
2101 2101
2102 2102 Notebook.prototype.delete = function () {
2103 2103 this.contents.delete(this.notebook_path);
2104 2104 };
2105 2105
2106 2106 /**
2107 2107 * Request a notebook's data from the server.
2108 2108 *
2109 2109 * @method load_notebook
2110 2110 * @param {String} notebook_path A notebook to load
2111 2111 */
2112 2112 Notebook.prototype.load_notebook = function (notebook_path) {
2113 2113 this.notebook_path = notebook_path;
2114 2114 this.notebook_name = utils.url_path_split(this.notebook_path)[1];
2115 2115 this.events.trigger('notebook_loading.Notebook');
2116 2116 this.contents.get(notebook_path, {type: 'notebook'}).then(
2117 2117 $.proxy(this.load_notebook_success, this),
2118 2118 $.proxy(this.load_notebook_error, this)
2119 2119 );
2120 2120 };
2121 2121
2122 2122 /**
2123 2123 * Success callback for loading a notebook from the server.
2124 2124 *
2125 2125 * Load notebook data from the JSON response.
2126 2126 *
2127 2127 * @method load_notebook_success
2128 2128 * @param {Object} data JSON representation of a notebook
2129 2129 */
2130 2130 Notebook.prototype.load_notebook_success = function (data) {
2131 2131 var failed, msg;
2132 2132 try {
2133 2133 this.fromJSON(data);
2134 2134 } catch (e) {
2135 2135 failed = e;
2136 2136 console.log("Notebook failed to load from JSON:", e);
2137 2137 }
2138 2138 if (failed || data.message) {
2139 2139 // *either* fromJSON failed or validation failed
2140 2140 var body = $("<div>");
2141 2141 var title;
2142 2142 if (failed) {
2143 2143 title = "Notebook failed to load";
2144 2144 body.append($("<p>").text(
2145 2145 "The error was: "
2146 2146 )).append($("<div>").addClass("js-error").text(
2147 2147 failed.toString()
2148 2148 )).append($("<p>").text(
2149 2149 "See the error console for details."
2150 2150 ));
2151 2151 } else {
2152 2152 title = "Notebook validation failed";
2153 2153 }
2154 2154
2155 2155 if (data.message) {
2156 2156 if (failed) {
2157 2157 msg = "The notebook also failed validation:";
2158 2158 } else {
2159 2159 msg = "An invalid notebook may not function properly." +
2160 2160 " The validation error was:";
2161 2161 }
2162 2162 body.append($("<p>").text(
2163 2163 msg
2164 2164 )).append($("<div>").addClass("validation-error").append(
2165 2165 $("<pre>").text(data.message)
2166 2166 ));
2167 2167 }
2168 2168
2169 2169 dialog.modal({
2170 2170 notebook: this,
2171 2171 keyboard_manager: this.keyboard_manager,
2172 2172 title: title,
2173 2173 body: body,
2174 2174 buttons : {
2175 2175 OK : {
2176 2176 "class" : "btn-primary"
2177 2177 }
2178 2178 }
2179 2179 });
2180 2180 }
2181 2181 if (this.ncells() === 0) {
2182 2182 this.insert_cell_below('code');
2183 2183 this.edit_mode(0);
2184 2184 } else {
2185 2185 this.select(0);
2186 2186 this.handle_command_mode(this.get_cell(0));
2187 2187 }
2188 2188 this.set_dirty(false);
2189 2189 this.scroll_to_top();
2190 2190 this.writable = data.writable || false;
2191 2191 var nbmodel = data.content;
2192 2192 var orig_nbformat = nbmodel.metadata.orig_nbformat;
2193 2193 var orig_nbformat_minor = nbmodel.metadata.orig_nbformat_minor;
2194 2194 if (orig_nbformat !== undefined && nbmodel.nbformat !== orig_nbformat) {
2195 2195 var src;
2196 2196 if (nbmodel.nbformat > orig_nbformat) {
2197 2197 src = " an older notebook format ";
2198 2198 } else {
2199 2199 src = " a newer notebook format ";
2200 2200 }
2201 2201
2202 2202 msg = "This notebook has been converted from" + src +
2203 2203 "(v"+orig_nbformat+") to the current notebook " +
2204 2204 "format (v"+nbmodel.nbformat+"). The next time you save this notebook, the " +
2205 2205 "current notebook format will be used.";
2206 2206
2207 2207 if (nbmodel.nbformat > orig_nbformat) {
2208 2208 msg += " Older versions of IPython may not be able to read the new format.";
2209 2209 } else {
2210 2210 msg += " Some features of the original notebook may not be available.";
2211 2211 }
2212 2212 msg += " To preserve the original version, close the " +
2213 2213 "notebook without saving it.";
2214 2214 dialog.modal({
2215 2215 notebook: this,
2216 2216 keyboard_manager: this.keyboard_manager,
2217 2217 title : "Notebook converted",
2218 2218 body : msg,
2219 2219 buttons : {
2220 2220 OK : {
2221 2221 class : "btn-primary"
2222 2222 }
2223 2223 }
2224 2224 });
2225 2225 } else if (orig_nbformat_minor !== undefined && nbmodel.nbformat_minor < orig_nbformat_minor) {
2226 2226 var that = this;
2227 2227 var orig_vs = 'v' + nbmodel.nbformat + '.' + orig_nbformat_minor;
2228 2228 var this_vs = 'v' + nbmodel.nbformat + '.' + this.nbformat_minor;
2229 2229 msg = "This notebook is version " + orig_vs + ", but we only fully support up to " +
2230 2230 this_vs + ". You can still work with this notebook, but some features " +
2231 2231 "introduced in later notebook versions may not be available.";
2232 2232
2233 2233 dialog.modal({
2234 2234 notebook: this,
2235 2235 keyboard_manager: this.keyboard_manager,
2236 2236 title : "Newer Notebook",
2237 2237 body : msg,
2238 2238 buttons : {
2239 2239 OK : {
2240 2240 class : "btn-danger"
2241 2241 }
2242 2242 }
2243 2243 });
2244 2244
2245 2245 }
2246 2246
2247 2247 // Create the session after the notebook is completely loaded to prevent
2248 2248 // code execution upon loading, which is a security risk.
2249 2249 if (this.session === null) {
2250 2250 var kernelspec = this.metadata.kernelspec || {};
2251 2251 var kernel_name = kernelspec.name;
2252 2252
2253 2253 this.start_session(kernel_name);
2254 2254 }
2255 2255 // load our checkpoint list
2256 2256 this.list_checkpoints();
2257 2257
2258 2258 // load toolbar state
2259 2259 if (this.metadata.celltoolbar) {
2260 2260 celltoolbar.CellToolbar.global_show();
2261 2261 celltoolbar.CellToolbar.activate_preset(this.metadata.celltoolbar);
2262 2262 } else {
2263 2263 celltoolbar.CellToolbar.global_hide();
2264 2264 }
2265 2265
2266 2266 if (!this.writable) {
2267 2267 this.set_autosave_interval(0);
2268 2268 this.events.trigger('notebook_read_only.Notebook');
2269 2269 }
2270 2270
2271 2271 // now that we're fully loaded, it is safe to restore save functionality
2272 2272 this._fully_loaded = true;
2273 2273 this.events.trigger('notebook_loaded.Notebook');
2274 2274 };
2275 2275
2276 2276 /**
2277 2277 * Failure callback for loading a notebook from the server.
2278 2278 *
2279 2279 * @method load_notebook_error
2280 2280 * @param {Error} error
2281 2281 */
2282 2282 Notebook.prototype.load_notebook_error = function (error) {
2283 2283 this.events.trigger('notebook_load_failed.Notebook', error);
2284 2284 var msg;
2285 2285 if (error.name === utils.XHR_ERROR && error.xhr.status === 500) {
2286 2286 utils.log_ajax_error(error.xhr, error.xhr_status, error.xhr_error);
2287 2287 msg = "An unknown error occurred while loading this notebook. " +
2288 2288 "This version can load notebook formats " +
2289 2289 "v" + this.nbformat + " or earlier. See the server log for details.";
2290 2290 } else {
2291 2291 msg = error.message;
2292 2292 }
2293 2293 dialog.modal({
2294 2294 notebook: this,
2295 2295 keyboard_manager: this.keyboard_manager,
2296 2296 title: "Error loading notebook",
2297 2297 body : msg,
2298 2298 buttons : {
2299 2299 "OK": {}
2300 2300 }
2301 2301 });
2302 2302 };
2303 2303
2304 2304 /********************* checkpoint-related *********************/
2305 2305
2306 2306 /**
2307 2307 * Save the notebook then immediately create a checkpoint.
2308 2308 *
2309 2309 * @method save_checkpoint
2310 2310 */
2311 2311 Notebook.prototype.save_checkpoint = function () {
2312 2312 this._checkpoint_after_save = true;
2313 2313 this.save_notebook();
2314 2314 };
2315 2315
2316 2316 /**
2317 2317 * Add a checkpoint for this notebook.
2318 2318 * for use as a callback from checkpoint creation.
2319 2319 *
2320 2320 * @method add_checkpoint
2321 2321 */
2322 2322 Notebook.prototype.add_checkpoint = function (checkpoint) {
2323 2323 var found = false;
2324 2324 for (var i = 0; i < this.checkpoints.length; i++) {
2325 2325 var existing = this.checkpoints[i];
2326 2326 if (existing.id == checkpoint.id) {
2327 2327 found = true;
2328 2328 this.checkpoints[i] = checkpoint;
2329 2329 break;
2330 2330 }
2331 2331 }
2332 2332 if (!found) {
2333 2333 this.checkpoints.push(checkpoint);
2334 2334 }
2335 2335 this.last_checkpoint = this.checkpoints[this.checkpoints.length - 1];
2336 2336 };
2337 2337
2338 2338 /**
2339 2339 * List checkpoints for this notebook.
2340 2340 *
2341 2341 * @method list_checkpoints
2342 2342 */
2343 2343 Notebook.prototype.list_checkpoints = function () {
2344 2344 var that = this;
2345 2345 this.contents.list_checkpoints(this.notebook_path).then(
2346 2346 $.proxy(this.list_checkpoints_success, this),
2347 2347 function(error) {
2348 2348 that.events.trigger('list_checkpoints_failed.Notebook', error);
2349 2349 }
2350 2350 );
2351 2351 };
2352 2352
2353 2353 /**
2354 2354 * Success callback for listing checkpoints.
2355 2355 *
2356 2356 * @method list_checkpoint_success
2357 2357 * @param {Object} data JSON representation of a checkpoint
2358 2358 */
2359 2359 Notebook.prototype.list_checkpoints_success = function (data) {
2360 2360 this.checkpoints = data;
2361 2361 if (data.length) {
2362 2362 this.last_checkpoint = data[data.length - 1];
2363 2363 } else {
2364 2364 this.last_checkpoint = null;
2365 2365 }
2366 2366 this.events.trigger('checkpoints_listed.Notebook', [data]);
2367 2367 };
2368 2368
2369 2369 /**
2370 2370 * Create a checkpoint of this notebook on the server from the most recent save.
2371 2371 *
2372 2372 * @method create_checkpoint
2373 2373 */
2374 2374 Notebook.prototype.create_checkpoint = function () {
2375 2375 var that = this;
2376 2376 this.contents.create_checkpoint(this.notebook_path).then(
2377 2377 $.proxy(this.create_checkpoint_success, this),
2378 2378 function (error) {
2379 2379 that.events.trigger('checkpoint_failed.Notebook', error);
2380 2380 }
2381 2381 );
2382 2382 };
2383 2383
2384 2384 /**
2385 2385 * Success callback for creating a checkpoint.
2386 2386 *
2387 2387 * @method create_checkpoint_success
2388 2388 * @param {Object} data JSON representation of a checkpoint
2389 2389 */
2390 2390 Notebook.prototype.create_checkpoint_success = function (data) {
2391 2391 this.add_checkpoint(data);
2392 2392 this.events.trigger('checkpoint_created.Notebook', data);
2393 2393 };
2394 2394
2395 2395 Notebook.prototype.restore_checkpoint_dialog = function (checkpoint) {
2396 2396 var that = this;
2397 2397 checkpoint = checkpoint || this.last_checkpoint;
2398 2398 if ( ! checkpoint ) {
2399 2399 console.log("restore dialog, but no checkpoint to restore to!");
2400 2400 return;
2401 2401 }
2402 2402 var body = $('<div/>').append(
2403 2403 $('<p/>').addClass("p-space").text(
2404 2404 "Are you sure you want to revert the notebook to " +
2405 2405 "the latest checkpoint?"
2406 2406 ).append(
2407 2407 $("<strong/>").text(
2408 2408 " This cannot be undone."
2409 2409 )
2410 2410 )
2411 2411 ).append(
2412 2412 $('<p/>').addClass("p-space").text("The checkpoint was last updated at:")
2413 2413 ).append(
2414 2414 $('<p/>').addClass("p-space").text(
2415 2415 Date(checkpoint.last_modified)
2416 2416 ).css("text-align", "center")
2417 2417 );
2418 2418
2419 2419 dialog.modal({
2420 2420 notebook: this,
2421 2421 keyboard_manager: this.keyboard_manager,
2422 2422 title : "Revert notebook to checkpoint",
2423 2423 body : body,
2424 2424 buttons : {
2425 2425 Revert : {
2426 2426 class : "btn-danger",
2427 2427 click : function () {
2428 2428 that.restore_checkpoint(checkpoint.id);
2429 2429 }
2430 2430 },
2431 2431 Cancel : {}
2432 2432 }
2433 2433 });
2434 2434 };
2435 2435
2436 2436 /**
2437 2437 * Restore the notebook to a checkpoint state.
2438 2438 *
2439 2439 * @method restore_checkpoint
2440 2440 * @param {String} checkpoint ID
2441 2441 */
2442 2442 Notebook.prototype.restore_checkpoint = function (checkpoint) {
2443 2443 this.events.trigger('notebook_restoring.Notebook', checkpoint);
2444 2444 var that = this;
2445 2445 this.contents.restore_checkpoint(this.notebook_path, checkpoint).then(
2446 2446 $.proxy(this.restore_checkpoint_success, this),
2447 2447 function (error) {
2448 2448 that.events.trigger('checkpoint_restore_failed.Notebook', error);
2449 2449 }
2450 2450 );
2451 2451 };
2452 2452
2453 2453 /**
2454 2454 * Success callback for restoring a notebook to a checkpoint.
2455 2455 *
2456 2456 * @method restore_checkpoint_success
2457 2457 */
2458 2458 Notebook.prototype.restore_checkpoint_success = function () {
2459 2459 this.events.trigger('checkpoint_restored.Notebook');
2460 2460 this.load_notebook(this.notebook_path);
2461 2461 };
2462 2462
2463 2463 /**
2464 2464 * Delete a notebook checkpoint.
2465 2465 *
2466 2466 * @method delete_checkpoint
2467 2467 * @param {String} checkpoint ID
2468 2468 */
2469 2469 Notebook.prototype.delete_checkpoint = function (checkpoint) {
2470 2470 this.events.trigger('notebook_restoring.Notebook', checkpoint);
2471 2471 var that = this;
2472 2472 this.contents.delete_checkpoint(this.notebook_path, checkpoint).then(
2473 2473 $.proxy(this.delete_checkpoint_success, this),
2474 2474 function (error) {
2475 2475 that.events.trigger('checkpoint_delete_failed.Notebook', error);
2476 2476 }
2477 2477 );
2478 2478 };
2479 2479
2480 2480 /**
2481 2481 * Success callback for deleting a notebook checkpoint
2482 2482 *
2483 2483 * @method delete_checkpoint_success
2484 2484 */
2485 2485 Notebook.prototype.delete_checkpoint_success = function () {
2486 2486 this.events.trigger('checkpoint_deleted.Notebook');
2487 2487 this.load_notebook(this.notebook_path);
2488 2488 };
2489 2489
2490 2490
2491 2491 // For backwards compatability.
2492 2492 IPython.Notebook = Notebook;
2493 2493
2494 2494 return {'Notebook': Notebook};
2495 2495 });
@@ -1,328 +1,328 b''
1 1 {% extends "page.html" %}
2 2
3 3 {% block stylesheet %}
4 4
5 5 {% if mathjax_url %}
6 6 <script type="text/javascript" src="{{mathjax_url}}?config=TeX-AMS_HTML-full&delayStartupUntil=configured" charset="utf-8"></script>
7 7 {% endif %}
8 8 <script type="text/javascript">
9 9 // MathJax disabled, set as null to distingish from *missing* MathJax,
10 10 // where it will be undefined, and should prompt a dialog later.
11 11 window.mathjax_url = "{{mathjax_url}}";
12 12 </script>
13 13
14 14 <link rel="stylesheet" href="{{ static_url("components/bootstrap-tour/build/css/bootstrap-tour.min.css") }}" type="text/css" />
15 15 <link rel="stylesheet" href="{{ static_url("components/codemirror/lib/codemirror.css") }}">
16 16
17 17 {{super()}}
18 18
19 19 <link rel="stylesheet" href="{{ static_url("notebook/css/override.css") }}" type="text/css" />
20 20
21 21 {% endblock %}
22 22
23 23 {% block params %}
24 24
25 25 data-project="{{project}}"
26 26 data-base-url="{{base_url}}"
27 27 data-ws-url="{{ws_url}}"
28 28 data-notebook-name="{{notebook_name}}"
29 29 data-notebook-path="{{notebook_path}}"
30 30 class="notebook_app"
31 31
32 32 {% endblock %}
33 33
34 34
35 35 {% block header %}
36 36
37 37
38 38 <span id="save_widget" class="nav pull-left">
39 39 <span id="notebook_name"></span>
40 40 <span id="checkpoint_status"></span>
41 41 <span id="autosave_status"></span>
42 42 </span>
43 43
44 44 <span id="kernel_selector_widget" class="pull-right dropdown">
45 45 <button class="dropdown-toggle" data-toggle="dropdown" type='button' id="current_kernel_spec">
46 46 <span class='kernel_name'>Python</span>
47 47 <span class="caret"></span>
48 48 </button>
49 49 <ul id="kernel_selector" class="dropdown-menu">
50 50 </ul>
51 51 </span>
52 52
53 53 {% endblock %}
54 54
55 55
56 56 {% block site %}
57 57
58 58 <div id="menubar-container" class="container">
59 59 <div id="menubar">
60 60 <div id="menus" class="navbar navbar-default" role="navigation">
61 61 <div class="container-fluid">
62 62 <button type="button" class="btn btn-default navbar-toggle" data-toggle="collapse" data-target=".navbar-collapse">
63 63 <i class="fa fa-bars"></i>
64 64 <span class="navbar-text">Menu</span>
65 65 </button>
66 66 <ul class="nav navbar-nav navbar-right">
67 67 <li id="kernel_indicator">
68 68 <i id="kernel_indicator_icon"></i>
69 69 </li>
70 70 <li id="modal_indicator">
71 71 <i id="modal_indicator_icon"></i>
72 72 </li>
73 73 <li id="notification_area"></li>
74 74 </ul>
75 75 <div class="navbar-collapse collapse">
76 76 <ul class="nav navbar-nav">
77 77 <li class="dropdown"><a href="#" class="dropdown-toggle" data-toggle="dropdown">File</a>
78 78 <ul id="file_menu" class="dropdown-menu">
79 79 <li id="new_notebook"
80 80 title="Make a new notebook (Opens a new window)">
81 81 <a href="#">New</a></li>
82 82 <li id="open_notebook"
83 83 title="Opens a new window with the Dashboard view">
84 84 <a href="#">Open...</a></li>
85 85 <!-- <hr/> -->
86 86 <li class="divider"></li>
87 87 <li id="copy_notebook"
88 88 title="Open a copy of this notebook's contents and start a new kernel">
89 89 <a href="#">Make a Copy...</a></li>
90 90 <li id="rename_notebook"><a href="#">Rename...</a></li>
91 91 <li id="save_checkpoint"><a href="#">Save and Checkpoint</a></li>
92 92 <!-- <hr/> -->
93 93 <li class="divider"></li>
94 94 <li id="restore_checkpoint" class="dropdown-submenu"><a href="#">Revert to Checkpoint</a>
95 95 <ul class="dropdown-menu">
96 96 <li><a href="#"></a></li>
97 97 <li><a href="#"></a></li>
98 98 <li><a href="#"></a></li>
99 99 <li><a href="#"></a></li>
100 100 <li><a href="#"></a></li>
101 101 </ul>
102 102 </li>
103 103 <li class="divider"></li>
104 104 <li id="print_preview"><a href="#">Print Preview</a></li>
105 105 <li class="dropdown-submenu"><a href="#">Download as</a>
106 106 <ul class="dropdown-menu">
107 107 <li id="download_ipynb"><a href="#">IPython Notebook (.ipynb)</a></li>
108 <li id="download_py"><a href="#">Python (.py)</a></li>
108 <li id="download_script"><a href="#">Script</a></li>
109 109 <li id="download_html"><a href="#">HTML (.html)</a></li>
110 110 <li id="download_rst"><a href="#">reST (.rst)</a></li>
111 111 <li id="download_pdf"><a href="#">PDF (.pdf)</a></li>
112 112 </ul>
113 113 </li>
114 114 <li class="divider"></li>
115 115 <li id="trust_notebook"
116 116 title="Trust the output of this notebook">
117 117 <a href="#" >Trust Notebook</a></li>
118 118 <li class="divider"></li>
119 119 <li id="kill_and_exit"
120 120 title="Shutdown this notebook's kernel, and close this window">
121 121 <a href="#" >Close and halt</a></li>
122 122 </ul>
123 123 </li>
124 124 <li class="dropdown"><a href="#" class="dropdown-toggle" data-toggle="dropdown">Edit</a>
125 125 <ul id="edit_menu" class="dropdown-menu">
126 126 <li id="cut_cell"><a href="#">Cut Cell</a></li>
127 127 <li id="copy_cell"><a href="#">Copy Cell</a></li>
128 128 <li id="paste_cell_above" class="disabled"><a href="#">Paste Cell Above</a></li>
129 129 <li id="paste_cell_below" class="disabled"><a href="#">Paste Cell Below</a></li>
130 130 <li id="paste_cell_replace" class="disabled"><a href="#">Paste Cell &amp; Replace</a></li>
131 131 <li id="delete_cell"><a href="#">Delete Cell</a></li>
132 132 <li id="undelete_cell" class="disabled"><a href="#">Undo Delete Cell</a></li>
133 133 <li class="divider"></li>
134 134 <li id="split_cell"><a href="#">Split Cell</a></li>
135 135 <li id="merge_cell_above"><a href="#">Merge Cell Above</a></li>
136 136 <li id="merge_cell_below"><a href="#">Merge Cell Below</a></li>
137 137 <li class="divider"></li>
138 138 <li id="move_cell_up"><a href="#">Move Cell Up</a></li>
139 139 <li id="move_cell_down"><a href="#">Move Cell Down</a></li>
140 140 <li class="divider"></li>
141 141 <li id="edit_nb_metadata"><a href="#">Edit Notebook Metadata</a></li>
142 142 </ul>
143 143 </li>
144 144 <li class="dropdown"><a href="#" class="dropdown-toggle" data-toggle="dropdown">View</a>
145 145 <ul id="view_menu" class="dropdown-menu">
146 146 <li id="toggle_header"
147 147 title="Show/Hide the IPython Notebook logo and notebook title (above menu bar)">
148 148 <a href="#">Toggle Header</a></li>
149 149 <li id="toggle_toolbar"
150 150 title="Show/Hide the action icons (below menu bar)">
151 151 <a href="#">Toggle Toolbar</a></li>
152 152 </ul>
153 153 </li>
154 154 <li class="dropdown"><a href="#" class="dropdown-toggle" data-toggle="dropdown">Insert</a>
155 155 <ul id="insert_menu" class="dropdown-menu">
156 156 <li id="insert_cell_above"
157 157 title="Insert an empty Code cell above the currently active cell">
158 158 <a href="#">Insert Cell Above</a></li>
159 159 <li id="insert_cell_below"
160 160 title="Insert an empty Code cell below the currently active cell">
161 161 <a href="#">Insert Cell Below</a></li>
162 162 </ul>
163 163 </li>
164 164 <li class="dropdown"><a href="#" class="dropdown-toggle" data-toggle="dropdown">Cell</a>
165 165 <ul id="cell_menu" class="dropdown-menu">
166 166 <li id="run_cell" title="Run this cell, and move cursor to the next one">
167 167 <a href="#">Run</a></li>
168 168 <li id="run_cell_select_below" title="Run this cell, select below">
169 169 <a href="#">Run and Select Below</a></li>
170 170 <li id="run_cell_insert_below" title="Run this cell, insert below">
171 171 <a href="#">Run and Insert Below</a></li>
172 172 <li id="run_all_cells" title="Run all cells in the notebook">
173 173 <a href="#">Run All</a></li>
174 174 <li id="run_all_cells_above" title="Run all cells above (but not including) this cell">
175 175 <a href="#">Run All Above</a></li>
176 176 <li id="run_all_cells_below" title="Run this cell and all cells below it">
177 177 <a href="#">Run All Below</a></li>
178 178 <li class="divider"></li>
179 179 <li id="change_cell_type" class="dropdown-submenu"
180 180 title="All cells in the notebook have a cell type. By default, new cells are created as 'Code' cells">
181 181 <a href="#">Cell Type</a>
182 182 <ul class="dropdown-menu">
183 183 <li id="to_code"
184 184 title="Contents will be sent to the kernel for execution, and output will display in the footer of cell">
185 185 <a href="#">Code</a></li>
186 186 <li id="to_markdown"
187 187 title="Contents will be rendered as HTML and serve as explanatory text">
188 188 <a href="#">Markdown</a></li>
189 189 <li id="to_raw"
190 190 title="Contents will pass through nbconvert unmodified">
191 191 <a href="#">Raw NBConvert</a></li>
192 192 </ul>
193 193 </li>
194 194 <li class="divider"></li>
195 195 <li id="current_outputs" class="dropdown-submenu"><a href="#">Current Output</a>
196 196 <ul class="dropdown-menu">
197 197 <li id="toggle_current_output"
198 198 title="Hide/Show the output of the current cell">
199 199 <a href="#">Toggle</a>
200 200 </li>
201 201 <li id="toggle_current_output_scroll"
202 202 title="Scroll the output of the current cell">
203 203 <a href="#">Toggle Scrolling</a>
204 204 </li>
205 205 <li id="clear_current_output"
206 206 title="Clear the output of the current cell">
207 207 <a href="#">Clear</a>
208 208 </li>
209 209 </ul>
210 210 </li>
211 211 <li id="all_outputs" class="dropdown-submenu"><a href="#">All Output</a>
212 212 <ul class="dropdown-menu">
213 213 <li id="toggle_all_output"
214 214 title="Hide/Show the output of all cells">
215 215 <a href="#">Toggle</a>
216 216 </li>
217 217 <li id="toggle_all_output_scroll"
218 218 title="Scroll the output of all cells">
219 219 <a href="#">Toggle Scrolling</a>
220 220 </li>
221 221 <li id="clear_all_output"
222 222 title="Clear the output of all cells">
223 223 <a href="#">Clear</a>
224 224 </li>
225 225 </ul>
226 226 </li>
227 227 </ul>
228 228 </li>
229 229 <li class="dropdown"><a href="#" class="dropdown-toggle" data-toggle="dropdown">Kernel</a>
230 230 <ul id="kernel_menu" class="dropdown-menu">
231 231 <li id="int_kernel"
232 232 title="Send KeyboardInterrupt (CTRL-C) to the Kernel">
233 233 <a href="#">Interrupt</a>
234 234 </li>
235 235 <li id="restart_kernel"
236 236 title="Restart the Kernel">
237 237 <a href="#">Restart</a>
238 238 </li>
239 239 <li id="reconnect_kernel"
240 240 title="Reconnect to the Kernel">
241 241 <a href="#">Reconnect</a>
242 242 </li>
243 243 <li class="divider"></li>
244 244 <li id="menu-change-kernel" class="dropdown-submenu">
245 245 <a href="#">Change kernel</a>
246 246 <ul class="dropdown-menu" id="menu-change-kernel-submenu"></ul>
247 247 </li>
248 248 </ul>
249 249 </li>
250 250 <li class="dropdown"><a href="#" class="dropdown-toggle" data-toggle="dropdown">Help</a>
251 251 <ul id="help_menu" class="dropdown-menu">
252 252 <li id="notebook_tour" title="A quick tour of the notebook user interface"><a href="#">User Interface Tour</a></li>
253 253 <li id="keyboard_shortcuts" title="Opens a tooltip with all keyboard shortcuts"><a href="#">Keyboard Shortcuts</a></li>
254 254 <li class="divider"></li>
255 255 {% set
256 256 sections = (
257 257 (
258 258 ("http://ipython.org/documentation.html","IPython Help",True),
259 259 ("http://nbviewer.ipython.org/github/ipython/ipython/tree/2.x/examples/Index.ipynb", "Notebook Help", True),
260 260 ),(
261 261 ("http://docs.python.org","Python",True),
262 262 ("http://help.github.com/articles/github-flavored-markdown","Markdown",True),
263 263 ("http://docs.scipy.org/doc/numpy/reference/","NumPy",True),
264 264 ("http://docs.scipy.org/doc/scipy/reference/","SciPy",True),
265 265 ("http://matplotlib.org/contents.html","Matplotlib",True),
266 266 ("http://docs.sympy.org/latest/index.html","SymPy",True),
267 267 ("http://pandas.pydata.org/pandas-docs/stable/","pandas", True)
268 268 )
269 269 )
270 270 %}
271 271
272 272 {% for helplinks in sections %}
273 273 {% for link in helplinks %}
274 274 <li><a href="{{link[0]}}" {{'target="_blank" title="Opens in a new window"' if link[2]}}>
275 275 {{'<i class="fa fa-external-link menu-icon pull-right"></i>' if link[2]}}
276 276 {{link[1]}}
277 277 </a></li>
278 278 {% endfor %}
279 279 {% if not loop.last %}
280 280 <li class="divider"></li>
281 281 {% endif %}
282 282 {% endfor %}
283 283 <li class="divider"></li>
284 284 <li title="About IPython Notebook"><a id="notebook_about" href="#">About</a></li>
285 285 </ul>
286 286 </li>
287 287 </ul>
288 288 </div>
289 289 </div>
290 290 </div>
291 291 </div>
292 292 <div id="maintoolbar" class="navbar">
293 293 <div class="toolbar-inner navbar-inner navbar-nobg">
294 294 <div id="maintoolbar-container" class="container"></div>
295 295 </div>
296 296 </div>
297 297 </div>
298 298
299 299 <div id="ipython-main-app">
300 300
301 301 <div id="notebook_panel">
302 302 <div id="notebook"></div>
303 303 <div id="pager_splitter"></div>
304 304 <div id="pager">
305 305 <div id='pager_button_area'>
306 306 </div>
307 307 <div id="pager-container" class="container"></div>
308 308 </div>
309 309 </div>
310 310
311 311 </div>
312 312 <div id='tooltip' class='ipython_tooltip' style='display:none'></div>
313 313
314 314
315 315 {% endblock %}
316 316
317 317
318 318 {% block script %}
319 319 {{super()}}
320 320 <script type="text/javascript">
321 321 sys_info = {{sys_info}};
322 322 </script>
323 323
324 324 <script src="{{ static_url("components/text-encoding/lib/encoding.js") }}" charset="utf-8"></script>
325 325
326 326 <script src="{{ static_url("notebook/js/main.js") }}" charset="utf-8"></script>
327 327
328 328 {% endblock %}
@@ -1,328 +1,330 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 74 language_info = {'mimetype': 'text/x-python',
75 75 'codemirror_mode': {'name': 'ipython',
76 76 'version': sys.version_info[0]},
77 77 'pygments_lexer': 'ipython%d' % (3 if PY3 else 2),
78 'nbconvert_exporter': 'python',
79 'file_extension': '.py'
78 80 }
79 81 @property
80 82 def banner(self):
81 83 return self.shell.banner
82 84
83 85 def start(self):
84 86 self.shell.exit_now = False
85 87 super(IPythonKernel, self).start()
86 88
87 89 def set_parent(self, ident, parent):
88 90 """Overridden from parent to tell the display hook and output streams
89 91 about the parent message.
90 92 """
91 93 super(IPythonKernel, self).set_parent(ident, parent)
92 94 self.shell.set_parent(parent)
93 95
94 96 def _forward_input(self, allow_stdin=False):
95 97 """Forward raw_input and getpass to the current frontend.
96 98
97 99 via input_request
98 100 """
99 101 self._allow_stdin = allow_stdin
100 102
101 103 if PY3:
102 104 self._sys_raw_input = builtin_mod.input
103 105 builtin_mod.input = self.raw_input
104 106 else:
105 107 self._sys_raw_input = builtin_mod.raw_input
106 108 self._sys_eval_input = builtin_mod.input
107 109 builtin_mod.raw_input = self.raw_input
108 110 builtin_mod.input = lambda prompt='': eval(self.raw_input(prompt))
109 111 self._save_getpass = getpass.getpass
110 112 getpass.getpass = self.getpass
111 113
112 114 def _restore_input(self):
113 115 """Restore raw_input, getpass"""
114 116 if PY3:
115 117 builtin_mod.input = self._sys_raw_input
116 118 else:
117 119 builtin_mod.raw_input = self._sys_raw_input
118 120 builtin_mod.input = self._sys_eval_input
119 121
120 122 getpass.getpass = self._save_getpass
121 123
122 124 @property
123 125 def execution_count(self):
124 126 return self.shell.execution_count
125 127
126 128 @execution_count.setter
127 129 def execution_count(self, value):
128 130 # Ignore the incrememnting done by KernelBase, in favour of our shell's
129 131 # execution counter.
130 132 pass
131 133
132 134 def do_execute(self, code, silent, store_history=True,
133 135 user_expressions=None, allow_stdin=False):
134 136 shell = self.shell # we'll need this a lot here
135 137
136 138 self._forward_input(allow_stdin)
137 139
138 140 reply_content = {}
139 141 # FIXME: the shell calls the exception handler itself.
140 142 shell._reply_content = None
141 143 try:
142 144 shell.run_cell(code, store_history=store_history, silent=silent)
143 145 except:
144 146 status = u'error'
145 147 # FIXME: this code right now isn't being used yet by default,
146 148 # because the run_cell() call above directly fires off exception
147 149 # reporting. This code, therefore, is only active in the scenario
148 150 # where runlines itself has an unhandled exception. We need to
149 151 # uniformize this, for all exception construction to come from a
150 152 # single location in the codbase.
151 153 etype, evalue, tb = sys.exc_info()
152 154 tb_list = traceback.format_exception(etype, evalue, tb)
153 155 reply_content.update(shell._showtraceback(etype, evalue, tb_list))
154 156 else:
155 157 status = u'ok'
156 158 finally:
157 159 self._restore_input()
158 160
159 161 reply_content[u'status'] = status
160 162
161 163 # Return the execution counter so clients can display prompts
162 164 reply_content['execution_count'] = shell.execution_count - 1
163 165
164 166 # FIXME - fish exception info out of shell, possibly left there by
165 167 # runlines. We'll need to clean up this logic later.
166 168 if shell._reply_content is not None:
167 169 reply_content.update(shell._reply_content)
168 170 e_info = dict(engine_uuid=self.ident, engine_id=self.int_id, method='execute')
169 171 reply_content['engine_info'] = e_info
170 172 # reset after use
171 173 shell._reply_content = None
172 174
173 175 if 'traceback' in reply_content:
174 176 self.log.info("Exception in execute request:\n%s", '\n'.join(reply_content['traceback']))
175 177
176 178
177 179 # At this point, we can tell whether the main code execution succeeded
178 180 # or not. If it did, we proceed to evaluate user_expressions
179 181 if reply_content['status'] == 'ok':
180 182 reply_content[u'user_expressions'] = \
181 183 shell.user_expressions(user_expressions or {})
182 184 else:
183 185 # If there was an error, don't even try to compute expressions
184 186 reply_content[u'user_expressions'] = {}
185 187
186 188 # Payloads should be retrieved regardless of outcome, so we can both
187 189 # recover partial output (that could have been generated early in a
188 190 # block, before an error) and clear the payload system always.
189 191 reply_content[u'payload'] = shell.payload_manager.read_payload()
190 192 # Be agressive about clearing the payload because we don't want
191 193 # it to sit in memory until the next execute_request comes in.
192 194 shell.payload_manager.clear_payload()
193 195
194 196 return reply_content
195 197
196 198 def do_complete(self, code, cursor_pos):
197 199 # FIXME: IPython completers currently assume single line,
198 200 # but completion messages give multi-line context
199 201 # For now, extract line from cell, based on cursor_pos:
200 202 if cursor_pos is None:
201 203 cursor_pos = len(code)
202 204 line, offset = line_at_cursor(code, cursor_pos)
203 205 line_cursor = cursor_pos - offset
204 206
205 207 txt, matches = self.shell.complete('', line, line_cursor)
206 208 return {'matches' : matches,
207 209 'cursor_end' : cursor_pos,
208 210 'cursor_start' : cursor_pos - len(txt),
209 211 'metadata' : {},
210 212 'status' : 'ok'}
211 213
212 214 def do_inspect(self, code, cursor_pos, detail_level=0):
213 215 name = token_at_cursor(code, cursor_pos)
214 216 info = self.shell.object_inspect(name)
215 217
216 218 reply_content = {'status' : 'ok'}
217 219 reply_content['data'] = data = {}
218 220 reply_content['metadata'] = {}
219 221 reply_content['found'] = info['found']
220 222 if info['found']:
221 223 info_text = self.shell.object_inspect_text(
222 224 name,
223 225 detail_level=detail_level,
224 226 )
225 227 data['text/plain'] = info_text
226 228
227 229 return reply_content
228 230
229 231 def do_history(self, hist_access_type, output, raw, session=None, start=None,
230 232 stop=None, n=None, pattern=None, unique=False):
231 233 if hist_access_type == 'tail':
232 234 hist = self.shell.history_manager.get_tail(n, raw=raw, output=output,
233 235 include_latest=True)
234 236
235 237 elif hist_access_type == 'range':
236 238 hist = self.shell.history_manager.get_range(session, start, stop,
237 239 raw=raw, output=output)
238 240
239 241 elif hist_access_type == 'search':
240 242 hist = self.shell.history_manager.search(
241 243 pattern, raw=raw, output=output, n=n, unique=unique)
242 244 else:
243 245 hist = []
244 246
245 247 return {'history' : list(hist)}
246 248
247 249 def do_shutdown(self, restart):
248 250 self.shell.exit_now = True
249 251 return dict(status='ok', restart=restart)
250 252
251 253 def do_is_complete(self, code):
252 254 status, indent_spaces = self.shell.input_transformer_manager.check_complete(code)
253 255 r = {'status': status}
254 256 if status == 'incomplete':
255 257 r['indent'] = ' ' * indent_spaces
256 258 return r
257 259
258 260 def do_apply(self, content, bufs, msg_id, reply_metadata):
259 261 shell = self.shell
260 262 try:
261 263 working = shell.user_ns
262 264
263 265 prefix = "_"+str(msg_id).replace("-","")+"_"
264 266
265 267 f,args,kwargs = unpack_apply_message(bufs, working, copy=False)
266 268
267 269 fname = getattr(f, '__name__', 'f')
268 270
269 271 fname = prefix+"f"
270 272 argname = prefix+"args"
271 273 kwargname = prefix+"kwargs"
272 274 resultname = prefix+"result"
273 275
274 276 ns = { fname : f, argname : args, kwargname : kwargs , resultname : None }
275 277 # print ns
276 278 working.update(ns)
277 279 code = "%s = %s(*%s,**%s)" % (resultname, fname, argname, kwargname)
278 280 try:
279 281 exec(code, shell.user_global_ns, shell.user_ns)
280 282 result = working.get(resultname)
281 283 finally:
282 284 for key in ns:
283 285 working.pop(key)
284 286
285 287 result_buf = serialize_object(result,
286 288 buffer_threshold=self.session.buffer_threshold,
287 289 item_threshold=self.session.item_threshold,
288 290 )
289 291
290 292 except:
291 293 # invoke IPython traceback formatting
292 294 shell.showtraceback()
293 295 # FIXME - fish exception info out of shell, possibly left there by
294 296 # run_code. We'll need to clean up this logic later.
295 297 reply_content = {}
296 298 if shell._reply_content is not None:
297 299 reply_content.update(shell._reply_content)
298 300 e_info = dict(engine_uuid=self.ident, engine_id=self.int_id, method='apply')
299 301 reply_content['engine_info'] = e_info
300 302 # reset after use
301 303 shell._reply_content = None
302 304
303 305 self.send_response(self.iopub_socket, u'error', reply_content,
304 306 ident=self._topic('error'))
305 307 self.log.info("Exception in apply request:\n%s", '\n'.join(reply_content['traceback']))
306 308 result_buf = []
307 309
308 310 if reply_content['ename'] == 'UnmetDependency':
309 311 reply_metadata['dependencies_met'] = False
310 312 else:
311 313 reply_content = {'status' : 'ok'}
312 314
313 315 return reply_content, result_buf
314 316
315 317 def do_clear(self):
316 318 self.shell.reset(False)
317 319 return dict(status='ok')
318 320
319 321
320 322 # This exists only for backwards compatibility - use IPythonKernel instead
321 323
322 324 @undoc
323 325 class Kernel(IPythonKernel):
324 326 def __init__(self, *args, **kwargs):
325 327 import warnings
326 328 warnings.warn('Kernel is a deprecated alias of IPython.kernel.zmq.ipkernel.IPythonKernel',
327 329 DeprecationWarning)
328 330 super(Kernel, self).__init__(*args, **kwargs) No newline at end of file
@@ -1,174 +1,177 b''
1 1 """Module containing single call export functions."""
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 functools import wraps
7 7
8 8 from IPython.nbformat import NotebookNode
9 9 from IPython.utils.decorators import undoc
10 10 from IPython.utils.py3compat import string_types
11 11
12 12 from .exporter import Exporter
13 13 from .templateexporter import TemplateExporter
14 14 from .html import HTMLExporter
15 15 from .slides import SlidesExporter
16 16 from .latex import LatexExporter
17 17 from .pdf import PDFExporter
18 18 from .markdown import MarkdownExporter
19 19 from .python import PythonExporter
20 20 from .rst import RSTExporter
21 21 from .notebook import NotebookExporter
22 from .script import ScriptExporter
22 23
23 24 #-----------------------------------------------------------------------------
24 25 # Classes
25 26 #-----------------------------------------------------------------------------
26 27
27 28 @undoc
28 29 def DocDecorator(f):
29 30
30 31 #Set docstring of function
31 32 f.__doc__ = f.__doc__ + """
32 33 nb : :class:`~IPython.nbformat.NotebookNode`
33 34 The notebook to export.
34 35 config : config (optional, keyword arg)
35 36 User configuration instance.
36 37 resources : dict (optional, keyword arg)
37 38 Resources used in the conversion process.
38 39
39 40 Returns
40 41 -------
41 42 tuple- output, resources, exporter_instance
42 43 output : str
43 44 Jinja 2 output. This is the resulting converted notebook.
44 45 resources : dictionary
45 46 Dictionary of resources used prior to and during the conversion
46 47 process.
47 48 exporter_instance : Exporter
48 49 Instance of the Exporter class used to export the document. Useful
49 50 to caller because it provides a 'file_extension' property which
50 51 specifies what extension the output should be saved as.
51 52
52 53 Notes
53 54 -----
54 55 WARNING: API WILL CHANGE IN FUTURE RELEASES OF NBCONVERT
55 56 """
56 57
57 58 @wraps(f)
58 59 def decorator(*args, **kwargs):
59 60 return f(*args, **kwargs)
60 61
61 62 return decorator
62 63
63 64
64 65 #-----------------------------------------------------------------------------
65 66 # Functions
66 67 #-----------------------------------------------------------------------------
67 68
68 69 __all__ = [
69 70 'export',
70 71 'export_html',
71 72 'export_custom',
72 73 'export_slides',
73 74 'export_latex',
74 75 'export_pdf',
75 76 'export_markdown',
76 77 'export_python',
78 'export_script',
77 79 'export_rst',
78 80 'export_by_name',
79 81 'get_export_names',
80 82 'ExporterNameError'
81 83 ]
82 84
83 85
84 86 class ExporterNameError(NameError):
85 87 pass
86 88
87 89 @DocDecorator
88 90 def export(exporter, nb, **kw):
89 91 """
90 92 Export a notebook object using specific exporter class.
91 93
92 94 Parameters
93 95 ----------
94 96 exporter : class:`~IPython.nbconvert.exporters.exporter.Exporter` class or instance
95 97 Class type or instance of the exporter that should be used. If the
96 98 method initializes it's own instance of the class, it is ASSUMED that
97 99 the class type provided exposes a constructor (``__init__``) with the same
98 100 signature as the base Exporter class.
99 101 """
100 102
101 103 #Check arguments
102 104 if exporter is None:
103 105 raise TypeError("Exporter is None")
104 106 elif not isinstance(exporter, Exporter) and not issubclass(exporter, Exporter):
105 107 raise TypeError("exporter does not inherit from Exporter (base)")
106 108 if nb is None:
107 109 raise TypeError("nb is None")
108 110
109 111 #Create the exporter
110 112 resources = kw.pop('resources', None)
111 113 if isinstance(exporter, Exporter):
112 114 exporter_instance = exporter
113 115 else:
114 116 exporter_instance = exporter(**kw)
115 117
116 118 #Try to convert the notebook using the appropriate conversion function.
117 119 if isinstance(nb, NotebookNode):
118 120 output, resources = exporter_instance.from_notebook_node(nb, resources)
119 121 elif isinstance(nb, string_types):
120 122 output, resources = exporter_instance.from_filename(nb, resources)
121 123 else:
122 124 output, resources = exporter_instance.from_file(nb, resources)
123 125 return output, resources
124 126
125 127 exporter_map = dict(
126 128 custom=TemplateExporter,
127 129 html=HTMLExporter,
128 130 slides=SlidesExporter,
129 131 latex=LatexExporter,
130 132 pdf=PDFExporter,
131 133 markdown=MarkdownExporter,
132 134 python=PythonExporter,
133 135 rst=RSTExporter,
134 136 notebook=NotebookExporter,
137 script=ScriptExporter,
135 138 )
136 139
137 140 def _make_exporter(name, E):
138 141 """make an export_foo function from a short key and Exporter class E"""
139 142 def _export(nb, **kw):
140 143 return export(E, nb, **kw)
141 144 _export.__doc__ = """Export a notebook object to {0} format""".format(name)
142 145 return _export
143 146
144 147 g = globals()
145 148
146 149 for name, E in exporter_map.items():
147 150 g['export_%s' % name] = DocDecorator(_make_exporter(name, E))
148 151
149 152 @DocDecorator
150 153 def export_by_name(format_name, nb, **kw):
151 154 """
152 155 Export a notebook object to a template type by its name. Reflection
153 156 (Inspect) is used to find the template's corresponding explicit export
154 157 method defined in this module. That method is then called directly.
155 158
156 159 Parameters
157 160 ----------
158 161 format_name : str
159 162 Name of the template style to export to.
160 163 """
161 164
162 165 function_name = "export_" + format_name.lower()
163 166
164 167 if function_name in globals():
165 168 return globals()[function_name](nb, **kw)
166 169 else:
167 170 raise ExporterNameError("template for `%s` not found" % function_name)
168 171
169 172
170 173 def get_export_names():
171 174 """Return a list of the currently supported export targets
172 175
173 176 WARNING: API WILL CHANGE IN FUTURE RELEASES OF NBCONVERT"""
174 177 return sorted(exporter_map.keys())
@@ -1,259 +1,259 b''
1 1 """This module defines a base Exporter class. For Jinja template-based export,
2 2 see templateexporter.py.
3 3 """
4 4
5 5
6 6 from __future__ import print_function, absolute_import
7 7
8 8 import io
9 9 import os
10 10 import copy
11 11 import collections
12 12 import datetime
13 13
14 14 from IPython.config.configurable import LoggingConfigurable
15 15 from IPython.config import Config
16 16 from IPython import nbformat
17 17 from IPython.utils.traitlets import MetaHasTraits, Unicode, List
18 18 from IPython.utils.importstring import import_item
19 19 from IPython.utils import text, py3compat
20 20
21 21
22 22 class ResourcesDict(collections.defaultdict):
23 23 def __missing__(self, key):
24 24 return ''
25 25
26 26
27 27 class Exporter(LoggingConfigurable):
28 28 """
29 29 Class containing methods that sequentially run a list of preprocessors on a
30 30 NotebookNode object and then return the modified NotebookNode object and
31 31 accompanying resources dict.
32 32 """
33 33
34 34 file_extension = Unicode(
35 'txt', config=True,
35 '.txt', config=True,
36 36 help="Extension of the file that should be written to disk"
37 37 )
38 38
39 39 # MIME type of the result file, for HTTP response headers.
40 40 # This is *not* a traitlet, because we want to be able to access it from
41 41 # the class, not just on instances.
42 42 output_mimetype = ''
43 43
44 44 #Configurability, allows the user to easily add filters and preprocessors.
45 45 preprocessors = List(config=True,
46 46 help="""List of preprocessors, by name or namespace, to enable.""")
47 47
48 48 _preprocessors = List()
49 49
50 50 default_preprocessors = List(['IPython.nbconvert.preprocessors.coalesce_streams',
51 51 'IPython.nbconvert.preprocessors.SVG2PDFPreprocessor',
52 52 'IPython.nbconvert.preprocessors.ExtractOutputPreprocessor',
53 53 'IPython.nbconvert.preprocessors.CSSHTMLHeaderPreprocessor',
54 54 'IPython.nbconvert.preprocessors.RevealHelpPreprocessor',
55 55 'IPython.nbconvert.preprocessors.LatexPreprocessor',
56 56 'IPython.nbconvert.preprocessors.ClearOutputPreprocessor',
57 57 'IPython.nbconvert.preprocessors.ExecutePreprocessor',
58 58 'IPython.nbconvert.preprocessors.HighlightMagicsPreprocessor'],
59 59 config=True,
60 60 help="""List of preprocessors available by default, by name, namespace,
61 61 instance, or type.""")
62 62
63 63
64 64 def __init__(self, config=None, **kw):
65 65 """
66 66 Public constructor
67 67
68 68 Parameters
69 69 ----------
70 70 config : config
71 71 User configuration instance.
72 72 """
73 73 with_default_config = self.default_config
74 74 if config:
75 75 with_default_config.merge(config)
76 76
77 77 super(Exporter, self).__init__(config=with_default_config, **kw)
78 78
79 79 self._init_preprocessors()
80 80
81 81
82 82 @property
83 83 def default_config(self):
84 84 return Config()
85 85
86 86 def from_notebook_node(self, nb, resources=None, **kw):
87 87 """
88 88 Convert a notebook from a notebook node instance.
89 89
90 90 Parameters
91 91 ----------
92 92 nb : :class:`~IPython.nbformat.NotebookNode`
93 93 Notebook node (dict-like with attr-access)
94 94 resources : dict
95 95 Additional resources that can be accessed read/write by
96 96 preprocessors and filters.
97 97 **kw
98 98 Ignored (?)
99 99 """
100 100 nb_copy = copy.deepcopy(nb)
101 101 resources = self._init_resources(resources)
102 102
103 103 if 'language' in nb['metadata']:
104 104 resources['language'] = nb['metadata']['language'].lower()
105 105
106 106 # Preprocess
107 107 nb_copy, resources = self._preprocess(nb_copy, resources)
108 108
109 109 return nb_copy, resources
110 110
111 111
112 112 def from_filename(self, filename, resources=None, **kw):
113 113 """
114 114 Convert a notebook from a notebook file.
115 115
116 116 Parameters
117 117 ----------
118 118 filename : str
119 119 Full filename of the notebook file to open and convert.
120 120 """
121 121
122 122 # Pull the metadata from the filesystem.
123 123 if resources is None:
124 124 resources = ResourcesDict()
125 125 if not 'metadata' in resources or resources['metadata'] == '':
126 126 resources['metadata'] = ResourcesDict()
127 127 basename = os.path.basename(filename)
128 128 notebook_name = basename[:basename.rfind('.')]
129 129 resources['metadata']['name'] = notebook_name
130 130
131 131 modified_date = datetime.datetime.fromtimestamp(os.path.getmtime(filename))
132 132 resources['metadata']['modified_date'] = modified_date.strftime(text.date_format)
133 133
134 134 with io.open(filename, encoding='utf-8') as f:
135 135 return self.from_notebook_node(nbformat.read(f, as_version=4), resources=resources, **kw)
136 136
137 137
138 138 def from_file(self, file_stream, resources=None, **kw):
139 139 """
140 140 Convert a notebook from a notebook file.
141 141
142 142 Parameters
143 143 ----------
144 144 file_stream : file-like object
145 145 Notebook file-like object to convert.
146 146 """
147 147 return self.from_notebook_node(nbformat.read(file_stream, as_version=4), resources=resources, **kw)
148 148
149 149
150 150 def register_preprocessor(self, preprocessor, enabled=False):
151 151 """
152 152 Register a preprocessor.
153 153 Preprocessors are classes that act upon the notebook before it is
154 154 passed into the Jinja templating engine. preprocessors are also
155 155 capable of passing additional information to the Jinja
156 156 templating engine.
157 157
158 158 Parameters
159 159 ----------
160 160 preprocessor : preprocessor
161 161 """
162 162 if preprocessor is None:
163 163 raise TypeError('preprocessor')
164 164 isclass = isinstance(preprocessor, type)
165 165 constructed = not isclass
166 166
167 167 # Handle preprocessor's registration based on it's type
168 168 if constructed and isinstance(preprocessor, py3compat.string_types):
169 169 # Preprocessor is a string, import the namespace and recursively call
170 170 # this register_preprocessor method
171 171 preprocessor_cls = import_item(preprocessor)
172 172 return self.register_preprocessor(preprocessor_cls, enabled)
173 173
174 174 if constructed and hasattr(preprocessor, '__call__'):
175 175 # Preprocessor is a function, no need to construct it.
176 176 # Register and return the preprocessor.
177 177 if enabled:
178 178 preprocessor.enabled = True
179 179 self._preprocessors.append(preprocessor)
180 180 return preprocessor
181 181
182 182 elif isclass and isinstance(preprocessor, MetaHasTraits):
183 183 # Preprocessor is configurable. Make sure to pass in new default for
184 184 # the enabled flag if one was specified.
185 185 self.register_preprocessor(preprocessor(parent=self), enabled)
186 186
187 187 elif isclass:
188 188 # Preprocessor is not configurable, construct it
189 189 self.register_preprocessor(preprocessor(), enabled)
190 190
191 191 else:
192 192 # Preprocessor is an instance of something without a __call__
193 193 # attribute.
194 194 raise TypeError('preprocessor')
195 195
196 196
197 197 def _init_preprocessors(self):
198 198 """
199 199 Register all of the preprocessors needed for this exporter, disabled
200 200 unless specified explicitly.
201 201 """
202 202 self._preprocessors = []
203 203
204 204 # Load default preprocessors (not necessarly enabled by default).
205 205 for preprocessor in self.default_preprocessors:
206 206 self.register_preprocessor(preprocessor)
207 207
208 208 # Load user-specified preprocessors. Enable by default.
209 209 for preprocessor in self.preprocessors:
210 210 self.register_preprocessor(preprocessor, enabled=True)
211 211
212 212
213 213 def _init_resources(self, resources):
214 214
215 215 #Make sure the resources dict is of ResourcesDict type.
216 216 if resources is None:
217 217 resources = ResourcesDict()
218 218 if not isinstance(resources, ResourcesDict):
219 219 new_resources = ResourcesDict()
220 220 new_resources.update(resources)
221 221 resources = new_resources
222 222
223 223 #Make sure the metadata extension exists in resources
224 224 if 'metadata' in resources:
225 225 if not isinstance(resources['metadata'], ResourcesDict):
226 226 resources['metadata'] = ResourcesDict(resources['metadata'])
227 227 else:
228 228 resources['metadata'] = ResourcesDict()
229 229 if not resources['metadata']['name']:
230 230 resources['metadata']['name'] = 'Notebook'
231 231
232 232 #Set the output extension
233 233 resources['output_extension'] = self.file_extension
234 234 return resources
235 235
236 236
237 237 def _preprocess(self, nb, resources):
238 238 """
239 239 Preprocess the notebook before passing it into the Jinja engine.
240 240 To preprocess the notebook is to apply all of the
241 241
242 242 Parameters
243 243 ----------
244 244 nb : notebook node
245 245 notebook that is being exported.
246 246 resources : a dict of additional resources that
247 247 can be accessed read/write by preprocessors
248 248 """
249 249
250 250 # Do a copy.deepcopy first,
251 251 # we are never safe enough with what the preprocessors could do.
252 252 nbc = copy.deepcopy(nb)
253 253 resc = copy.deepcopy(resources)
254 254
255 255 #Run each preprocessor on the notebook. Carry the output along
256 256 #to each preprocessor
257 257 for preprocessor in self._preprocessors:
258 258 nbc, resc = preprocessor(nbc, resc)
259 259 return nbc, resc
@@ -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 return 'html'
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' : ['text/javascript', 'text/html', 'application/pdf', 'image/svg+xml', 'text/latex', 'image/png', 'image/jpeg', 'text/plain']
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 62 langinfo = nb.metadata.get('language_info', {})
63 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 return 'tex'
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' : ['text/latex', 'application/pdf', 'image/png', 'image/jpeg', 'image/svg+xml', 'text/plain']
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 92 langinfo = nb.metadata.get('language_info', {})
93 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,141 +1,141 b''
1 1 """Export to PDF via latex"""
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 subprocess
7 7 import os
8 8 import sys
9 9
10 10 from IPython.utils.traitlets import Integer, List, Bool, Instance
11 11 from IPython.utils.tempdir import TemporaryWorkingDirectory
12 12 from .latex import LatexExporter
13 13
14 14
15 15 class PDFExporter(LatexExporter):
16 16 """Writer designed to write to PDF files"""
17 17
18 18 latex_count = Integer(3, config=True,
19 19 help="How many times latex will be called."
20 20 )
21 21
22 22 latex_command = List([u"pdflatex", u"{filename}"], config=True,
23 23 help="Shell command used to compile latex."
24 24 )
25 25
26 26 bib_command = List([u"bibtex", u"{filename}"], config=True,
27 27 help="Shell command used to run bibtex."
28 28 )
29 29
30 30 verbose = Bool(False, config=True,
31 31 help="Whether to display the output of latex commands."
32 32 )
33 33
34 34 temp_file_exts = List(['.aux', '.bbl', '.blg', '.idx', '.log', '.out'], config=True,
35 35 help="File extensions of temp files to remove after running."
36 36 )
37 37
38 38 writer = Instance("IPython.nbconvert.writers.FilesWriter", args=())
39 39
40 40 def run_command(self, command_list, filename, count, log_function):
41 41 """Run command_list count times.
42 42
43 43 Parameters
44 44 ----------
45 45 command_list : list
46 46 A list of args to provide to Popen. Each element of this
47 47 list will be interpolated with the filename to convert.
48 48 filename : unicode
49 49 The name of the file to convert.
50 50 count : int
51 51 How many times to run the command.
52 52
53 53 Returns
54 54 -------
55 55 success : bool
56 56 A boolean indicating if the command was successful (True)
57 57 or failed (False).
58 58 """
59 59 command = [c.format(filename=filename) for c in command_list]
60 60 #In windows and python 2.x there is a bug in subprocess.Popen and
61 61 # unicode commands are not supported
62 62 if sys.platform == 'win32' and sys.version_info < (3,0):
63 63 #We must use cp1252 encoding for calling subprocess.Popen
64 64 #Note that sys.stdin.encoding and encoding.DEFAULT_ENCODING
65 65 # could be different (cp437 in case of dos console)
66 66 command = [c.encode('cp1252') for c in command]
67 67 times = 'time' if count == 1 else 'times'
68 68 self.log.info("Running %s %i %s: %s", command_list[0], count, times, command)
69 69 with open(os.devnull, 'rb') as null:
70 70 stdout = subprocess.PIPE if not self.verbose else None
71 71 for index in range(count):
72 72 p = subprocess.Popen(command, stdout=stdout, stdin=null)
73 73 out, err = p.communicate()
74 74 if p.returncode:
75 75 if self.verbose:
76 76 # verbose means I didn't capture stdout with PIPE,
77 77 # so it's already been displayed and `out` is None.
78 78 out = u''
79 79 else:
80 80 out = out.decode('utf-8', 'replace')
81 81 log_function(command, out)
82 82 return False # failure
83 83 return True # success
84 84
85 85 def run_latex(self, filename):
86 86 """Run pdflatex self.latex_count times."""
87 87
88 88 def log_error(command, out):
89 89 self.log.critical(u"%s failed: %s\n%s", command[0], command, out)
90 90
91 91 return self.run_command(self.latex_command, filename,
92 92 self.latex_count, log_error)
93 93
94 94 def run_bib(self, filename):
95 95 """Run bibtex self.latex_count times."""
96 96 filename = os.path.splitext(filename)[0]
97 97
98 98 def log_error(command, out):
99 99 self.log.warn('%s had problems, most likely because there were no citations',
100 100 command[0])
101 101 self.log.debug(u"%s output: %s\n%s", command[0], command, out)
102 102
103 103 return self.run_command(self.bib_command, filename, 1, log_error)
104 104
105 105 def clean_temp_files(self, filename):
106 106 """Remove temporary files created by pdflatex/bibtex."""
107 107 self.log.info("Removing temporary LaTeX files")
108 108 filename = os.path.splitext(filename)[0]
109 109 for ext in self.temp_file_exts:
110 110 try:
111 111 os.remove(filename+ext)
112 112 except OSError:
113 113 pass
114 114
115 115 def from_notebook_node(self, nb, resources=None, **kw):
116 116 latex, resources = super(PDFExporter, self).from_notebook_node(
117 117 nb, resources=resources, **kw
118 118 )
119 119 with TemporaryWorkingDirectory() as td:
120 120 notebook_name = "notebook"
121 121 tex_file = self.writer.write(latex, resources, notebook_name=notebook_name)
122 122 self.log.info("Building PDF")
123 123 rc = self.run_latex(tex_file)
124 124 if not rc:
125 125 rc = self.run_bib(tex_file)
126 126 if not rc:
127 127 rc = self.run_latex(tex_file)
128 128
129 129 pdf_file = notebook_name + '.pdf'
130 130 if not os.path.isfile(pdf_file):
131 131 raise RuntimeError("PDF creating failed")
132 132 self.log.info('PDF successfully created')
133 133 with open(pdf_file, 'rb') as f:
134 134 pdf_data = f.read()
135 135
136 136 # convert output extension to pdf
137 137 # the writer above required it to be tex
138 resources['output_extension'] = 'pdf'
138 resources['output_extension'] = '.pdf'
139 139
140 140 return pdf_data, resources
141 141
@@ -1,31 +1,31 b''
1 1 """Python script 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 from .templateexporter import TemplateExporter
16 16
17 17 #-----------------------------------------------------------------------------
18 18 # Classes
19 19 #-----------------------------------------------------------------------------
20 20
21 21 class PythonExporter(TemplateExporter):
22 22 """
23 23 Exports a Python code file.
24 24 """
25 25 def _file_extension_default(self):
26 return 'py'
26 return '.py'
27 27
28 28 def _template_file_default(self):
29 29 return 'python'
30 30
31 31 output_mimetype = 'text/x-python'
@@ -1,40 +1,40 b''
1 1 """restructuredText 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 from IPython.config import Config
16 16
17 17 from .templateexporter import TemplateExporter
18 18
19 19 #-----------------------------------------------------------------------------
20 20 # Classes
21 21 #-----------------------------------------------------------------------------
22 22
23 23 class RSTExporter(TemplateExporter):
24 24 """
25 25 Exports restructured text documents.
26 26 """
27 27
28 28 def _file_extension_default(self):
29 return 'rst'
29 return '.rst'
30 30
31 31 def _template_file_default(self):
32 32 return 'rst'
33 33
34 34 output_mimetype = 'text/restructuredtext'
35 35
36 36 @property
37 37 def default_config(self):
38 38 c = Config({'ExtractOutputPreprocessor':{'enabled':True}})
39 39 c.merge(super(RSTExporter,self).default_config)
40 40 return c
@@ -1,43 +1,43 b''
1 1 """HTML slide show 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 from IPython.nbconvert import preprocessors
16 16 from IPython.config import Config
17 17
18 18 from .html import HTMLExporter
19 19
20 20 #-----------------------------------------------------------------------------
21 21 # Classes
22 22 #-----------------------------------------------------------------------------
23 23
24 24 class SlidesExporter(HTMLExporter):
25 25 """Exports HTML slides with reveal.js"""
26 26
27 27 def _file_extension_default(self):
28 return 'slides.html'
28 return '.slides.html'
29 29
30 30 def _template_file_default(self):
31 31 return 'slides_reveal'
32 32
33 33 output_mimetype = 'text/html'
34 34
35 35 @property
36 36 def default_config(self):
37 37 c = Config({
38 38 'RevealHelpPreprocessor': {
39 39 'enabled': True,
40 40 },
41 41 })
42 42 c.merge(super(SlidesExporter,self).default_config)
43 43 return c
@@ -1,324 +1,324 b''
1 1 #!/usr/bin/env python
2 2 """NbConvert is a utility for conversion of .ipynb files.
3 3
4 4 Command-line interface for the NbConvert conversion utility.
5 5 """
6 6
7 7 # Copyright (c) IPython Development Team.
8 8 # Distributed under the terms of the Modified BSD License.
9 9
10 10 from __future__ import print_function
11 11
12 12 import logging
13 13 import sys
14 14 import os
15 15 import glob
16 16
17 17 from IPython.core.application import BaseIPythonApplication, base_aliases, base_flags
18 18 from IPython.core.profiledir import ProfileDir
19 19 from IPython.config import catch_config_error, Configurable
20 20 from IPython.utils.traitlets import (
21 21 Unicode, List, Instance, DottedObjectName, Type, CaselessStrEnum,
22 22 )
23 23 from IPython.utils.importstring import import_item
24 24
25 25 from .exporters.export import get_export_names, exporter_map
26 26 from IPython.nbconvert import exporters, preprocessors, writers, postprocessors
27 27 from .utils.base import NbConvertBase
28 28 from .utils.exceptions import ConversionException
29 29
30 30 #-----------------------------------------------------------------------------
31 31 #Classes and functions
32 32 #-----------------------------------------------------------------------------
33 33
34 34 class DottedOrNone(DottedObjectName):
35 35 """
36 36 A string holding a valid dotted object name in Python, such as A.b3._c
37 37 Also allows for None type."""
38 38
39 39 default_value = u''
40 40
41 41 def validate(self, obj, value):
42 42 if value is not None and len(value) > 0:
43 43 return super(DottedOrNone, self).validate(obj, value)
44 44 else:
45 45 return value
46 46
47 47 nbconvert_aliases = {}
48 48 nbconvert_aliases.update(base_aliases)
49 49 nbconvert_aliases.update({
50 50 'to' : 'NbConvertApp.export_format',
51 51 'template' : 'TemplateExporter.template_file',
52 52 'writer' : 'NbConvertApp.writer_class',
53 53 'post': 'NbConvertApp.postprocessor_class',
54 54 'output': 'NbConvertApp.output_base',
55 55 'reveal-prefix': 'RevealHelpPreprocessor.url_prefix',
56 56 'nbformat': 'NotebookExporter.nbformat_version',
57 57 })
58 58
59 59 nbconvert_flags = {}
60 60 nbconvert_flags.update(base_flags)
61 61 nbconvert_flags.update({
62 62 'execute' : (
63 63 {'ExecutePreprocessor' : {'enabled' : True}},
64 64 "Execute the notebook prior to export."
65 65 ),
66 66 'stdout' : (
67 67 {'NbConvertApp' : {'writer_class' : "StdoutWriter"}},
68 68 "Write notebook output to stdout instead of files."
69 69 )
70 70 })
71 71
72 72
73 73 class NbConvertApp(BaseIPythonApplication):
74 74 """Application used to convert from notebook file type (``*.ipynb``)"""
75 75
76 76 name = 'ipython-nbconvert'
77 77 aliases = nbconvert_aliases
78 78 flags = nbconvert_flags
79 79
80 80 def _log_level_default(self):
81 81 return logging.INFO
82 82
83 83 def _classes_default(self):
84 84 classes = [NbConvertBase, ProfileDir]
85 85 for pkg in (exporters, preprocessors, writers, postprocessors):
86 86 for name in dir(pkg):
87 87 cls = getattr(pkg, name)
88 88 if isinstance(cls, type) and issubclass(cls, Configurable):
89 89 classes.append(cls)
90 90
91 91 return classes
92 92
93 93 description = Unicode(
94 94 u"""This application is used to convert notebook files (*.ipynb)
95 95 to various other formats.
96 96
97 97 WARNING: THE COMMANDLINE INTERFACE MAY CHANGE IN FUTURE RELEASES.""")
98 98
99 99 output_base = Unicode('', config=True, help='''overwrite base name use for output files.
100 100 can only be used when converting one notebook at a time.
101 101 ''')
102 102
103 103 examples = Unicode(u"""
104 104 The simplest way to use nbconvert is
105 105
106 106 > ipython nbconvert mynotebook.ipynb
107 107
108 108 which will convert mynotebook.ipynb to the default format (probably HTML).
109 109
110 110 You can specify the export format with `--to`.
111 111 Options include {0}
112 112
113 113 > ipython nbconvert --to latex mynotebook.ipynb
114 114
115 115 Both HTML and LaTeX support multiple output templates. LaTeX includes
116 116 'base', 'article' and 'report'. HTML includes 'basic' and 'full'. You
117 117 can specify the flavor of the format used.
118 118
119 119 > ipython nbconvert --to html --template basic mynotebook.ipynb
120 120
121 121 You can also pipe the output to stdout, rather than a file
122 122
123 123 > ipython nbconvert mynotebook.ipynb --stdout
124 124
125 125 PDF is generated via latex
126 126
127 127 > ipython nbconvert mynotebook.ipynb --to pdf
128 128
129 129 You can get (and serve) a Reveal.js-powered slideshow
130 130
131 131 > ipython nbconvert myslides.ipynb --to slides --post serve
132 132
133 133 Multiple notebooks can be given at the command line in a couple of
134 134 different ways:
135 135
136 136 > ipython nbconvert notebook*.ipynb
137 137 > ipython nbconvert notebook1.ipynb notebook2.ipynb
138 138
139 139 or you can specify the notebooks list in a config file, containing::
140 140
141 141 c.NbConvertApp.notebooks = ["my_notebook.ipynb"]
142 142
143 143 > ipython nbconvert --config mycfg.py
144 144 """.format(get_export_names()))
145 145
146 146 # Writer specific variables
147 147 writer = Instance('IPython.nbconvert.writers.base.WriterBase',
148 148 help="""Instance of the writer class used to write the
149 149 results of the conversion.""")
150 150 writer_class = DottedObjectName('FilesWriter', config=True,
151 151 help="""Writer class used to write the
152 152 results of the conversion""")
153 153 writer_aliases = {'fileswriter': 'IPython.nbconvert.writers.files.FilesWriter',
154 154 'debugwriter': 'IPython.nbconvert.writers.debug.DebugWriter',
155 155 'stdoutwriter': 'IPython.nbconvert.writers.stdout.StdoutWriter'}
156 156 writer_factory = Type()
157 157
158 158 def _writer_class_changed(self, name, old, new):
159 159 if new.lower() in self.writer_aliases:
160 160 new = self.writer_aliases[new.lower()]
161 161 self.writer_factory = import_item(new)
162 162
163 163 # Post-processor specific variables
164 164 postprocessor = Instance('IPython.nbconvert.postprocessors.base.PostProcessorBase',
165 165 help="""Instance of the PostProcessor class used to write the
166 166 results of the conversion.""")
167 167
168 168 postprocessor_class = DottedOrNone(config=True,
169 169 help="""PostProcessor class used to write the
170 170 results of the conversion""")
171 171 postprocessor_aliases = {'serve': 'IPython.nbconvert.postprocessors.serve.ServePostProcessor'}
172 172 postprocessor_factory = Type()
173 173
174 174 def _postprocessor_class_changed(self, name, old, new):
175 175 if new.lower() in self.postprocessor_aliases:
176 176 new = self.postprocessor_aliases[new.lower()]
177 177 if new:
178 178 self.postprocessor_factory = import_item(new)
179 179
180 180
181 181 # Other configurable variables
182 182 export_format = CaselessStrEnum(get_export_names(),
183 183 default_value="html",
184 184 config=True,
185 185 help="""The export format to be used."""
186 186 )
187 187
188 188 notebooks = List([], config=True, help="""List of notebooks to convert.
189 189 Wildcards are supported.
190 190 Filenames passed positionally will be added to the list.
191 191 """)
192 192
193 193 @catch_config_error
194 194 def initialize(self, argv=None):
195 195 self.init_syspath()
196 196 super(NbConvertApp, self).initialize(argv)
197 197 self.init_notebooks()
198 198 self.init_writer()
199 199 self.init_postprocessor()
200 200
201 201
202 202
203 203 def init_syspath(self):
204 204 """
205 205 Add the cwd to the sys.path ($PYTHONPATH)
206 206 """
207 207 sys.path.insert(0, os.getcwd())
208 208
209 209
210 210 def init_notebooks(self):
211 211 """Construct the list of notebooks.
212 212 If notebooks are passed on the command-line,
213 213 they override notebooks specified in config files.
214 214 Glob each notebook to replace notebook patterns with filenames.
215 215 """
216 216
217 217 # Specifying notebooks on the command-line overrides (rather than adds)
218 218 # the notebook list
219 219 if self.extra_args:
220 220 patterns = self.extra_args
221 221 else:
222 222 patterns = self.notebooks
223 223
224 224 # Use glob to replace all the notebook patterns with filenames.
225 225 filenames = []
226 226 for pattern in patterns:
227 227
228 228 # Use glob to find matching filenames. Allow the user to convert
229 229 # notebooks without having to type the extension.
230 230 globbed_files = glob.glob(pattern)
231 231 globbed_files.extend(glob.glob(pattern + '.ipynb'))
232 232 if not globbed_files:
233 233 self.log.warn("pattern %r matched no files", pattern)
234 234
235 235 for filename in globbed_files:
236 236 if not filename in filenames:
237 237 filenames.append(filename)
238 238 self.notebooks = filenames
239 239
240 240 def init_writer(self):
241 241 """
242 242 Initialize the writer (which is stateless)
243 243 """
244 244 self._writer_class_changed(None, self.writer_class, self.writer_class)
245 245 self.writer = self.writer_factory(parent=self)
246 246
247 247 def init_postprocessor(self):
248 248 """
249 249 Initialize the postprocessor (which is stateless)
250 250 """
251 251 self._postprocessor_class_changed(None, self.postprocessor_class,
252 252 self.postprocessor_class)
253 253 if self.postprocessor_factory:
254 254 self.postprocessor = self.postprocessor_factory(parent=self)
255 255
256 256 def start(self):
257 257 """
258 258 Ran after initialization completed
259 259 """
260 260 super(NbConvertApp, self).start()
261 261 self.convert_notebooks()
262 262
263 263 def convert_notebooks(self):
264 264 """
265 265 Convert the notebooks in the self.notebook traitlet
266 266 """
267 267 # Export each notebook
268 268 conversion_success = 0
269 269
270 270 if self.output_base != '' and len(self.notebooks) > 1:
271 271 self.log.error(
272 272 """UsageError: --output flag or `NbConvertApp.output_base` config option
273 273 cannot be used when converting multiple notebooks.
274 274 """)
275 275 self.exit(1)
276 276
277 277 exporter = exporter_map[self.export_format](config=self.config)
278 278
279 279 for notebook_filename in self.notebooks:
280 280 self.log.info("Converting notebook %s to %s", notebook_filename, self.export_format)
281 281
282 282 # Get a unique key for the notebook and set it in the resources object.
283 283 basename = os.path.basename(notebook_filename)
284 284 notebook_name = basename[:basename.rfind('.')]
285 285 if self.output_base:
286 286 # strip duplicate extension from output_base, to avoid Basname.ext.ext
287 287 if getattr(exporter, 'file_extension', False):
288 288 base, ext = os.path.splitext(self.output_base)
289 if ext == '.' + exporter.file_extension:
289 if ext == exporter.file_extension:
290 290 self.output_base = base
291 291 notebook_name = self.output_base
292 292 resources = {}
293 293 resources['profile_dir'] = self.profile_dir.location
294 294 resources['unique_key'] = notebook_name
295 295 resources['output_files_dir'] = '%s_files' % notebook_name
296 296 self.log.info("Support files will be in %s", os.path.join(resources['output_files_dir'], ''))
297 297
298 298 # Try to export
299 299 try:
300 300 output, resources = exporter.from_filename(notebook_filename, resources=resources)
301 301 except ConversionException as e:
302 302 self.log.error("Error while converting '%s'", notebook_filename,
303 303 exc_info=True)
304 304 self.exit(1)
305 305 else:
306 306 if 'output_suffix' in resources and not self.output_base:
307 307 notebook_name += resources['output_suffix']
308 308 write_results = self.writer.write(output, resources, notebook_name=notebook_name)
309 309
310 310 #Post-process if post processor has been defined.
311 311 if hasattr(self, 'postprocessor') and self.postprocessor:
312 312 self.postprocessor(write_results)
313 313 conversion_success += 1
314 314
315 315 # If nothing was converted successfully, help the user.
316 316 if conversion_success == 0:
317 317 self.print_help()
318 318 sys.exit(-1)
319 319
320 320 #-----------------------------------------------------------------------------
321 321 # Main entry point
322 322 #-----------------------------------------------------------------------------
323 323
324 324 launch_new_instance = NbConvertApp.launch_instance
@@ -1,111 +1,111 b''
1 1 """Contains writer for writing nbconvert output to filesystem."""
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 io
7 7 import os
8 8 import glob
9 9
10 10 from IPython.utils.traitlets import Unicode
11 11 from IPython.utils.path import link_or_copy, ensure_dir_exists
12 12 from IPython.utils.py3compat import unicode_type
13 13
14 14 from .base import WriterBase
15 15
16 16 #-----------------------------------------------------------------------------
17 17 # Classes
18 18 #-----------------------------------------------------------------------------
19 19
20 20 class FilesWriter(WriterBase):
21 21 """Consumes nbconvert output and produces files."""
22 22
23 23
24 24 build_directory = Unicode("", config=True,
25 25 help="""Directory to write output to. Leave blank
26 26 to output to the current directory""")
27 27
28 28
29 29 # Make sure that the output directory exists.
30 30 def _build_directory_changed(self, name, old, new):
31 31 if new:
32 32 ensure_dir_exists(new)
33 33
34 34
35 35 def __init__(self, **kw):
36 36 super(FilesWriter, self).__init__(**kw)
37 37 self._build_directory_changed('build_directory', self.build_directory,
38 38 self.build_directory)
39 39
40 40 def _makedir(self, path):
41 41 """Make a directory if it doesn't already exist"""
42 42 if path:
43 43 self.log.info("Making directory %s", path)
44 44 ensure_dir_exists(path)
45 45
46 46 def write(self, output, resources, notebook_name=None, **kw):
47 47 """
48 48 Consume and write Jinja output to the file system. Output directory
49 49 is set via the 'build_directory' variable of this instance (a
50 50 configurable).
51 51
52 52 See base for more...
53 53 """
54 54
55 55 # Verify that a notebook name is provided.
56 56 if notebook_name is None:
57 57 raise TypeError('notebook_name')
58 58
59 59 # Pull the extension and subdir from the resources dict.
60 60 output_extension = resources.get('output_extension', None)
61 61
62 62 # Write all of the extracted resources to the destination directory.
63 63 # NOTE: WE WRITE EVERYTHING AS-IF IT'S BINARY. THE EXTRACT FIG
64 64 # PREPROCESSOR SHOULD HANDLE UNIX/WINDOWS LINE ENDINGS...
65 65 for filename, data in resources.get('outputs', {}).items():
66 66
67 67 # Determine where to write the file to
68 68 dest = os.path.join(self.build_directory, filename)
69 69 path = os.path.dirname(dest)
70 70 self._makedir(path)
71 71
72 72 # Write file
73 73 self.log.debug("Writing %i bytes to support file %s", len(data), dest)
74 74 with io.open(dest, 'wb') as f:
75 75 f.write(data)
76 76
77 77 # Copy referenced files to output directory
78 78 if self.build_directory:
79 79 for filename in self.files:
80 80
81 81 # Copy files that match search pattern
82 82 for matching_filename in glob.glob(filename):
83 83
84 84 # Make sure folder exists.
85 85 dest = os.path.join(self.build_directory, matching_filename)
86 86 path = os.path.dirname(dest)
87 87 self._makedir(path)
88 88
89 89 # Copy if destination is different.
90 90 if not os.path.normpath(dest) == os.path.normpath(matching_filename):
91 91 self.log.info("Linking %s -> %s", matching_filename, dest)
92 92 link_or_copy(matching_filename, dest)
93 93
94 94 # Determine where to write conversion results.
95 95 if output_extension is not None:
96 dest = notebook_name + '.' + output_extension
96 dest = notebook_name + output_extension
97 97 else:
98 98 dest = notebook_name
99 99 if self.build_directory:
100 100 dest = os.path.join(self.build_directory, dest)
101 101
102 102 # Write conversion results.
103 103 self.log.info("Writing %i bytes to %s", len(output), dest)
104 104 if isinstance(output, unicode_type):
105 105 with io.open(dest, 'w', encoding='utf-8') as f:
106 106 f.write(output)
107 107 else:
108 108 with io.open(dest, 'wb') as f:
109 109 f.write(output)
110 110
111 111 return dest
@@ -1,203 +1,203 b''
1 1 """
2 2 Module with tests for files
3 3 """
4 4
5 5 #-----------------------------------------------------------------------------
6 6 # Copyright (c) 2013, the IPython Development Team.
7 7 #
8 8 # Distributed under the terms of the Modified BSD License.
9 9 #
10 10 # The full license is in the file COPYING.txt, distributed with this software.
11 11 #-----------------------------------------------------------------------------
12 12
13 13 #-----------------------------------------------------------------------------
14 14 # Imports
15 15 #-----------------------------------------------------------------------------
16 16
17 17 import sys
18 18 import os
19 19
20 20 from ...tests.base import TestsBase
21 21 from ..files import FilesWriter
22 22 from IPython.utils.py3compat import PY3
23 23
24 24 if PY3:
25 25 from io import StringIO
26 26 else:
27 27 from StringIO import StringIO
28 28
29 29
30 30 #-----------------------------------------------------------------------------
31 31 # Class
32 32 #-----------------------------------------------------------------------------
33 33
34 34 class Testfiles(TestsBase):
35 35 """Contains test functions for files.py"""
36 36
37 37 def test_basic_output(self):
38 38 """Is FilesWriter basic output correct?"""
39 39
40 40 # Work in a temporary directory.
41 41 with self.create_temp_cwd():
42 42
43 43 # Create the resoruces dictionary
44 44 res = {}
45 45
46 46 # Create files writer, test output
47 47 writer = FilesWriter()
48 48 writer.write(u'y', res, notebook_name="z")
49 49
50 50 # Check the output of the file
51 51 with open('z', 'r') as f:
52 52 output = f.read()
53 53 self.assertEqual(output, u'y')
54 54
55 55 def test_ext(self):
56 56 """Does the FilesWriter add the correct extension to the output?"""
57 57
58 58 # Work in a temporary directory.
59 59 with self.create_temp_cwd():
60 60
61 61 # Create the resoruces dictionary
62 res = {'output_extension': 'txt'}
62 res = {'output_extension': '.txt'}
63 63
64 64 # Create files writer, test output
65 65 writer = FilesWriter()
66 66 writer.write(u'y', res, notebook_name="z")
67 67
68 68 # Check the output of the file
69 69 assert os.path.isfile('z.txt')
70 70 with open('z.txt', 'r') as f:
71 71 output = f.read()
72 72 self.assertEqual(output, u'y')
73 73
74 74
75 75 def test_extract(self):
76 76 """Can FilesWriter write extracted figures correctly?"""
77 77
78 78 # Work in a temporary directory.
79 79 with self.create_temp_cwd():
80 80
81 81 # Create the resoruces dictionary
82 82 res = {'outputs': {os.path.join('z_files', 'a'): b'b'}}
83 83
84 84 # Create files writer, test output
85 85 writer = FilesWriter()
86 86 writer.write(u'y', res, notebook_name="z")
87 87
88 88 # Check the output of the file
89 89 with open('z', 'r') as f:
90 90 output = f.read()
91 91 self.assertEqual(output, u'y')
92 92
93 93 # Check the output of the extracted file
94 94 extracted_file_dest = os.path.join('z_files', 'a')
95 95 assert os.path.isfile(extracted_file_dest)
96 96 with open(extracted_file_dest, 'r') as f:
97 97 output = f.read()
98 98 self.assertEqual(output, 'b')
99 99
100 100
101 101 def test_builddir(self):
102 102 """Can FilesWriter write to a build dir correctly?"""
103 103
104 104 # Work in a temporary directory.
105 105 with self.create_temp_cwd():
106 106
107 107 # Create the resoruces dictionary
108 108 res = {'outputs': {os.path.join('z_files', 'a'): b'b'}}
109 109
110 110 # Create files writer, test output
111 111 writer = FilesWriter()
112 112 writer.build_directory = u'build'
113 113 writer.write(u'y', res, notebook_name="z")
114 114
115 115 # Check the output of the file
116 116 assert os.path.isdir(writer.build_directory)
117 117 dest = os.path.join(writer.build_directory, 'z')
118 118 with open(dest, 'r') as f:
119 119 output = f.read()
120 120 self.assertEqual(output, u'y')
121 121
122 122 # Check the output of the extracted file
123 123 extracted_file_dest = os.path.join(writer.build_directory, 'z_files', 'a')
124 124 assert os.path.isfile(extracted_file_dest)
125 125 with open(extracted_file_dest, 'r') as f:
126 126 output = f.read()
127 127 self.assertEqual(output, 'b')
128 128
129 129
130 130 def test_links(self):
131 131 """Can the FilesWriter handle linked files correctly?"""
132 132
133 133 # Work in a temporary directory.
134 134 with self.create_temp_cwd():
135 135
136 136 # Create test file
137 137 os.mkdir('sub')
138 138 with open(os.path.join('sub', 'c'), 'w') as f:
139 139 f.write('d')
140 140
141 141 # Create the resoruces dictionary
142 142 res = {}
143 143
144 144 # Create files writer, test output
145 145 writer = FilesWriter()
146 146 writer.files = [os.path.join('sub', 'c')]
147 147 writer.build_directory = u'build'
148 148 writer.write(u'y', res, notebook_name="z")
149 149
150 150 # Check the output of the file
151 151 assert os.path.isdir(writer.build_directory)
152 152 dest = os.path.join(writer.build_directory, 'z')
153 153 with open(dest, 'r') as f:
154 154 output = f.read()
155 155 self.assertEqual(output, u'y')
156 156
157 157 # Check to make sure the linked file was copied
158 158 path = os.path.join(writer.build_directory, 'sub')
159 159 assert os.path.isdir(path)
160 160 dest = os.path.join(path, 'c')
161 161 assert os.path.isfile(dest)
162 162 with open(dest, 'r') as f:
163 163 output = f.read()
164 164 self.assertEqual(output, 'd')
165 165
166 166 def test_glob(self):
167 167 """Can the FilesWriter handle globbed files correctly?"""
168 168
169 169 # Work in a temporary directory.
170 170 with self.create_temp_cwd():
171 171
172 172 # Create test files
173 173 os.mkdir('sub')
174 174 with open(os.path.join('sub', 'c'), 'w') as f:
175 175 f.write('e')
176 176 with open(os.path.join('sub', 'd'), 'w') as f:
177 177 f.write('e')
178 178
179 179 # Create the resoruces dictionary
180 180 res = {}
181 181
182 182 # Create files writer, test output
183 183 writer = FilesWriter()
184 184 writer.files = ['sub/*']
185 185 writer.build_directory = u'build'
186 186 writer.write(u'y', res, notebook_name="z")
187 187
188 188 # Check the output of the file
189 189 assert os.path.isdir(writer.build_directory)
190 190 dest = os.path.join(writer.build_directory, 'z')
191 191 with open(dest, 'r') as f:
192 192 output = f.read()
193 193 self.assertEqual(output, u'y')
194 194
195 195 # Check to make sure the globbed files were copied
196 196 path = os.path.join(writer.build_directory, 'sub')
197 197 assert os.path.isdir(path)
198 198 for filename in ['c', 'd']:
199 199 dest = os.path.join(path, filename)
200 200 assert os.path.isfile(dest)
201 201 with open(dest, 'r') as f:
202 202 output = f.read()
203 203 self.assertEqual(output, 'e')
@@ -1,1184 +1,1192 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, and is optional.
359 359 # payloads are considered deprecated.
360 360 # The only requirement of each payload dict is that it have a 'source' key,
361 361 # which is a string classifying the payload (e.g. 'page').
362 362
363 363 'payload' : list(dict),
364 364
365 365 # Results for the user_expressions.
366 366 'user_expressions' : dict,
367 367 }
368 368
369 369 .. versionchanged:: 5.0
370 370
371 371 ``user_variables`` is removed, use user_expressions instead.
372 372
373 373 When status is 'error', the following extra fields are present::
374 374
375 375 {
376 376 'ename' : str, # Exception name, as a string
377 377 'evalue' : str, # Exception value, as a string
378 378
379 379 # The traceback will contain a list of frames, represented each as a
380 380 # string. For now we'll stick to the existing design of ultraTB, which
381 381 # controls exception level of detail statefully. But eventually we'll
382 382 # want to grow into a model where more information is collected and
383 383 # packed into the traceback object, with clients deciding how little or
384 384 # how much of it to unpack. But for now, let's start with a simple list
385 385 # of strings, since that requires only minimal changes to ultratb as
386 386 # written.
387 387 'traceback' : list,
388 388 }
389 389
390 390
391 391 When status is 'abort', there are for now no additional data fields. This
392 392 happens when the kernel was interrupted by a signal.
393 393
394 394 Payloads
395 395 ********
396 396
397 397 .. admonition:: Execution payloads
398 398
399 399 Payloads are considered deprecated, though their replacement is not yet implemented.
400 400
401 401 Payloads are a way to trigger frontend actions from the kernel. Current payloads:
402 402
403 403 **page**: display data in a pager.
404 404
405 405 Pager output is used for introspection, or other displayed information that's not considered output.
406 406 Pager payloads are generally displayed in a separate pane, that can be viewed alongside code,
407 407 and are not included in notebook documents.
408 408
409 409 .. sourcecode:: python
410 410
411 411 {
412 412 "source": "page",
413 413 # mime-bundle of data to display in the pager.
414 414 # Must include text/plain.
415 415 "data": mimebundle,
416 416 # line offset to start from
417 417 "start": int,
418 418 }
419 419
420 420 **set_next_input**: create a new output
421 421
422 422 used to create new cells in the notebook,
423 423 or set the next input in a console interface.
424 424 The main example being ``%load``.
425 425
426 426 .. sourcecode:: python
427 427
428 428 {
429 429 "source": "set_next_input",
430 430 # the text contents of the cell to create
431 431 "text": "some cell content",
432 432 }
433 433
434 434 **edit**: open a file for editing.
435 435
436 436 Triggered by `%edit`. Only the QtConsole currently supports edit payloads.
437 437
438 438 .. sourcecode:: python
439 439
440 440 {
441 441 "source": "edit",
442 442 "filename": "/path/to/file.py", # the file to edit
443 443 "line_number": int, # the line number to start with
444 444 }
445 445
446 446 **ask_exit**: instruct the frontend to prompt the user for exit
447 447
448 448 Allows the kernel to request exit, e.g. via ``%exit`` in IPython.
449 449 Only for console frontends.
450 450
451 451 .. sourcecode:: python
452 452
453 453 {
454 454 "source": "ask_exit",
455 455 # whether the kernel should be left running, only closing the client
456 456 "keepkernel": bool,
457 457 }
458 458
459 459
460 460 .. _msging_inspection:
461 461
462 462 Introspection
463 463 -------------
464 464
465 465 Code can be inspected to show useful information to the user.
466 466 It is up to the Kernel to decide what information should be displayed, and its formatting.
467 467
468 468 Message type: ``inspect_request``::
469 469
470 470 content = {
471 471 # The code context in which introspection is requested
472 472 # this may be up to an entire multiline cell.
473 473 'code' : str,
474 474
475 475 # The cursor position within 'code' (in unicode characters) where inspection is requested
476 476 'cursor_pos' : int,
477 477
478 478 # The level of detail desired. In IPython, the default (0) is equivalent to typing
479 479 # 'x?' at the prompt, 1 is equivalent to 'x??'.
480 480 # The difference is up to kernels, but in IPython level 1 includes the source code
481 481 # if available.
482 482 'detail_level' : 0 or 1,
483 483 }
484 484
485 485 .. versionchanged:: 5.0
486 486
487 487 ``object_info_request`` renamed to ``inspect_request``.
488 488
489 489 .. versionchanged:: 5.0
490 490
491 491 ``name`` key replaced with ``code`` and ``cursor_pos``,
492 492 moving the lexing responsibility to the kernel.
493 493
494 494 The reply is a mime-bundle, like a `display_data`_ message,
495 495 which should be a formatted representation of information about the context.
496 496 In the notebook, this is used to show tooltips over function calls, etc.
497 497
498 498 Message type: ``inspect_reply``::
499 499
500 500 content = {
501 501 # 'ok' if the request succeeded or 'error', with error information as in all other replies.
502 502 'status' : 'ok',
503 503
504 504 # data can be empty if nothing is found
505 505 'data' : dict,
506 506 'metadata' : dict,
507 507 }
508 508
509 509 .. versionchanged:: 5.0
510 510
511 511 ``object_info_reply`` renamed to ``inspect_reply``.
512 512
513 513 .. versionchanged:: 5.0
514 514
515 515 Reply is changed from structured data to a mime bundle, allowing formatting decisions to be made by the kernel.
516 516
517 517 .. _msging_completion:
518 518
519 519 Completion
520 520 ----------
521 521
522 522 Message type: ``complete_request``::
523 523
524 524 content = {
525 525 # The code context in which completion is requested
526 526 # this may be up to an entire multiline cell, such as
527 527 # 'foo = a.isal'
528 528 'code' : str,
529 529
530 530 # The cursor position within 'code' (in unicode characters) where completion is requested
531 531 'cursor_pos' : int,
532 532 }
533 533
534 534 .. versionchanged:: 5.0
535 535
536 536 ``line``, ``block``, and ``text`` keys are removed in favor of a single ``code`` for context.
537 537 Lexing is up to the kernel.
538 538
539 539
540 540 Message type: ``complete_reply``::
541 541
542 542 content = {
543 543 # The list of all matches to the completion request, such as
544 544 # ['a.isalnum', 'a.isalpha'] for the above example.
545 545 'matches' : list,
546 546
547 547 # The range of text that should be replaced by the above matches when a completion is accepted.
548 548 # typically cursor_end is the same as cursor_pos in the request.
549 549 'cursor_start' : int,
550 550 'cursor_end' : int,
551 551
552 552 # Information that frontend plugins might use for extra display information about completions.
553 553 'metadata' : dict,
554 554
555 555 # status should be 'ok' unless an exception was raised during the request,
556 556 # in which case it should be 'error', along with the usual error message content
557 557 # in other messages.
558 558 'status' : 'ok'
559 559 }
560 560
561 561 .. versionchanged:: 5.0
562 562
563 563 - ``matched_text`` is removed in favor of ``cursor_start`` and ``cursor_end``.
564 564 - ``metadata`` is added for extended information.
565 565
566 566 .. _msging_history:
567 567
568 568 History
569 569 -------
570 570
571 571 For clients to explicitly request history from a kernel. The kernel has all
572 572 the actual execution history stored in a single location, so clients can
573 573 request it from the kernel when needed.
574 574
575 575 Message type: ``history_request``::
576 576
577 577 content = {
578 578
579 579 # If True, also return output history in the resulting dict.
580 580 'output' : bool,
581 581
582 582 # If True, return the raw input history, else the transformed input.
583 583 'raw' : bool,
584 584
585 585 # So far, this can be 'range', 'tail' or 'search'.
586 586 'hist_access_type' : str,
587 587
588 588 # If hist_access_type is 'range', get a range of input cells. session can
589 589 # be a positive session number, or a negative number to count back from
590 590 # the current session.
591 591 'session' : int,
592 592 # start and stop are line numbers within that session.
593 593 'start' : int,
594 594 'stop' : int,
595 595
596 596 # If hist_access_type is 'tail' or 'search', get the last n cells.
597 597 'n' : int,
598 598
599 599 # If hist_access_type is 'search', get cells matching the specified glob
600 600 # pattern (with * and ? as wildcards).
601 601 'pattern' : str,
602 602
603 603 # If hist_access_type is 'search' and unique is true, do not
604 604 # include duplicated history. Default is false.
605 605 'unique' : bool,
606 606
607 607 }
608 608
609 609 .. versionadded:: 4.0
610 610 The key ``unique`` for ``history_request``.
611 611
612 612 Message type: ``history_reply``::
613 613
614 614 content = {
615 615 # A list of 3 tuples, either:
616 616 # (session, line_number, input) or
617 617 # (session, line_number, (input, output)),
618 618 # depending on whether output was False or True, respectively.
619 619 'history' : list,
620 620 }
621 621
622 622 .. _msging_is_complete:
623 623
624 624 Code completeness
625 625 -----------------
626 626
627 627 .. versionadded:: 5.0
628 628
629 629 When the user enters a line in a console style interface, the console must
630 630 decide whether to immediately execute the current code, or whether to show a
631 631 continuation prompt for further input. For instance, in Python ``a = 5`` would
632 632 be executed immediately, while ``for i in range(5):`` would expect further input.
633 633
634 634 There are four possible replies:
635 635
636 636 - *complete* code is ready to be executed
637 637 - *incomplete* code should prompt for another line
638 638 - *invalid* code will typically be sent for execution, so that the user sees the
639 639 error soonest.
640 640 - *unknown* - if the kernel is not able to determine this. The frontend should
641 641 also handle the kernel not replying promptly. It may default to sending the
642 642 code for execution, or it may implement simple fallback heuristics for whether
643 643 to execute the code (e.g. execute after a blank line).
644 644
645 645 Frontends may have ways to override this, forcing the code to be sent for
646 646 execution or forcing a continuation prompt.
647 647
648 648 Message type: ``is_complete_request``::
649 649
650 650 content = {
651 651 # The code entered so far as a multiline string
652 652 'code' : str,
653 653 }
654 654
655 655 Message type: ``is_complete_reply``::
656 656
657 657 content = {
658 658 # One of 'complete', 'incomplete', 'invalid', 'unknown'
659 659 'status' : str,
660 660
661 661 # If status is 'incomplete', indent should contain the characters to use
662 662 # to indent the next line. This is only a hint: frontends may ignore it
663 663 # and use their own autoindentation rules. For other statuses, this
664 664 # field does not exist.
665 665 'indent': str,
666 666 }
667 667
668 668 Connect
669 669 -------
670 670
671 671 When a client connects to the request/reply socket of the kernel, it can issue
672 672 a connect request to get basic information about the kernel, such as the ports
673 673 the other ZeroMQ sockets are listening on. This allows clients to only have
674 674 to know about a single port (the shell channel) to connect to a kernel.
675 675
676 676 Message type: ``connect_request``::
677 677
678 678 content = {
679 679 }
680 680
681 681 Message type: ``connect_reply``::
682 682
683 683 content = {
684 684 'shell_port' : int, # The port the shell ROUTER socket is listening on.
685 685 'iopub_port' : int, # The port the PUB socket is listening on.
686 686 'stdin_port' : int, # The port the stdin ROUTER socket is listening on.
687 687 'hb_port' : int, # The port the heartbeat socket is listening on.
688 688 }
689 689
690 690 .. _msging_kernel_info:
691 691
692 692 Kernel info
693 693 -----------
694 694
695 695 If a client needs to know information about the kernel, it can
696 696 make a request of the kernel's information.
697 697 This message can be used to fetch core information of the
698 698 kernel, including language (e.g., Python), language version number and
699 699 IPython version number, and the IPython message spec version number.
700 700
701 701 Message type: ``kernel_info_request``::
702 702
703 703 content = {
704 704 }
705 705
706 706 Message type: ``kernel_info_reply``::
707 707
708 708 content = {
709 709 # Version of messaging protocol.
710 710 # The first integer indicates major version. It is incremented when
711 711 # there is any backward incompatible change.
712 712 # The second integer indicates minor version. It is incremented when
713 713 # there is any backward compatible change.
714 714 'protocol_version': 'X.Y.Z',
715 715
716 716 # The kernel implementation name
717 717 # (e.g. 'ipython' for the IPython kernel)
718 718 'implementation': str,
719 719
720 720 # Implementation version number.
721 721 # The version number of the kernel's implementation
722 722 # (e.g. IPython.__version__ for the IPython kernel)
723 723 'implementation_version': 'X.Y.Z',
724 724
725 725 # Programming language in which kernel is implemented.
726 726 # Kernel included in IPython returns 'python'.
727 727 'language': str,
728 728
729 729 # Language version number.
730 730 # It is Python version number (e.g., '2.7.3') for the kernel
731 731 # included in IPython.
732 732 'language_version': 'X.Y.Z',
733 733
734 734 # Information about the language of code for the kernel
735 735 'language_info': {
736 736 'mimetype': str,
737 737
738 # Extension without the dot, e.g. 'py'
739 'file_extension': str,
740
738 741 # Pygments lexer, for highlighting
739 742 # Only needed if it differs from the top level 'language' field.
740 743 'pygments_lexer': str,
741 744
742 745 # Codemirror mode, for for highlighting in the notebook.
743 746 # Only needed if it differs from the top level 'language' field.
744 747 'codemirror_mode': str or dict,
748
749 # Nbconvert exporter, if notebooks written with this kernel should
750 # be exported with something other than the general 'script'
751 # exporter.
752 'nbconvert_exporter': str,
745 753 },
746 754
747 755 # A banner of information about the kernel,
748 756 # which may be desplayed in console environments.
749 757 'banner' : str,
750 758
751 759 # Optional: A list of dictionaries, each with keys 'text' and 'url'.
752 760 # These will be displayed in the help menu in the notebook UI.
753 761 'help_links': [
754 762 {'text': str, 'url': str}
755 763 ],
756 764 }
757 765
758 766 Refer to the lists of available `Pygments lexers <http://pygments.org/docs/lexers/>`_
759 767 and `codemirror modes <http://codemirror.net/mode/index.html>`_ for those fields.
760 768
761 769 .. versionchanged:: 5.0
762 770
763 771 Versions changed from lists of integers to strings.
764 772
765 773 .. versionchanged:: 5.0
766 774
767 775 ``ipython_version`` is removed.
768 776
769 777 .. versionchanged:: 5.0
770 778
771 779 ``language_info``, ``implementation``, ``implementation_version``, ``banner``
772 780 and ``help_links`` keys are added.
773 781
774 782 .. _msging_shutdown:
775 783
776 784 Kernel shutdown
777 785 ---------------
778 786
779 787 The clients can request the kernel to shut itself down; this is used in
780 788 multiple cases:
781 789
782 790 - when the user chooses to close the client application via a menu or window
783 791 control.
784 792 - when the user types 'exit' or 'quit' (or their uppercase magic equivalents).
785 793 - when the user chooses a GUI method (like the 'Ctrl-C' shortcut in the
786 794 IPythonQt client) to force a kernel restart to get a clean kernel without
787 795 losing client-side state like history or inlined figures.
788 796
789 797 The client sends a shutdown request to the kernel, and once it receives the
790 798 reply message (which is otherwise empty), it can assume that the kernel has
791 799 completed shutdown safely.
792 800
793 801 Upon their own shutdown, client applications will typically execute a last
794 802 minute sanity check and forcefully terminate any kernel that is still alive, to
795 803 avoid leaving stray processes in the user's machine.
796 804
797 805 Message type: ``shutdown_request``::
798 806
799 807 content = {
800 808 'restart' : bool # whether the shutdown is final, or precedes a restart
801 809 }
802 810
803 811 Message type: ``shutdown_reply``::
804 812
805 813 content = {
806 814 'restart' : bool # whether the shutdown is final, or precedes a restart
807 815 }
808 816
809 817 .. Note::
810 818
811 819 When the clients detect a dead kernel thanks to inactivity on the heartbeat
812 820 socket, they simply send a forceful process termination signal, since a dead
813 821 process is unlikely to respond in any useful way to messages.
814 822
815 823
816 824 Messages on the PUB/SUB socket
817 825 ==============================
818 826
819 827 Streams (stdout, stderr, etc)
820 828 ------------------------------
821 829
822 830 Message type: ``stream``::
823 831
824 832 content = {
825 833 # The name of the stream is one of 'stdout', 'stderr'
826 834 'name' : str,
827 835
828 836 # The text is an arbitrary string to be written to that stream
829 837 'text' : str,
830 838 }
831 839
832 840 .. versionchanged:: 5.0
833 841
834 842 'data' key renamed to 'text' for conistency with the notebook format.
835 843
836 844 Display Data
837 845 ------------
838 846
839 847 This type of message is used to bring back data that should be displayed (text,
840 848 html, svg, etc.) in the frontends. This data is published to all frontends.
841 849 Each message can have multiple representations of the data; it is up to the
842 850 frontend to decide which to use and how. A single message should contain all
843 851 possible representations of the same information. Each representation should
844 852 be a JSON'able data structure, and should be a valid MIME type.
845 853
846 854 Some questions remain about this design:
847 855
848 856 * Do we use this message type for execute_result/displayhook? Probably not, because
849 857 the displayhook also has to handle the Out prompt display. On the other hand
850 858 we could put that information into the metadata section.
851 859
852 860 .. _display_data:
853 861
854 862 Message type: ``display_data``::
855 863
856 864 content = {
857 865
858 866 # Who create the data
859 867 'source' : str,
860 868
861 869 # The data dict contains key/value pairs, where the keys are MIME
862 870 # types and the values are the raw data of the representation in that
863 871 # format.
864 872 'data' : dict,
865 873
866 874 # Any metadata that describes the data
867 875 'metadata' : dict
868 876 }
869 877
870 878
871 879 The ``metadata`` contains any metadata that describes the output.
872 880 Global keys are assumed to apply to the output as a whole.
873 881 The ``metadata`` dict can also contain mime-type keys, which will be sub-dictionaries,
874 882 which are interpreted as applying only to output of that type.
875 883 Third parties should put any data they write into a single dict
876 884 with a reasonably unique name to avoid conflicts.
877 885
878 886 The only metadata keys currently defined in IPython are the width and height
879 887 of images::
880 888
881 889 metadata = {
882 890 'image/png' : {
883 891 'width': 640,
884 892 'height': 480
885 893 }
886 894 }
887 895
888 896
889 897 .. versionchanged:: 5.0
890 898
891 899 `application/json` data should be unpacked JSON data,
892 900 not double-serialized as a JSON string.
893 901
894 902
895 903 Raw Data Publication
896 904 --------------------
897 905
898 906 ``display_data`` lets you publish *representations* of data, such as images and html.
899 907 This ``data_pub`` message lets you publish *actual raw data*, sent via message buffers.
900 908
901 909 data_pub messages are constructed via the :func:`IPython.lib.datapub.publish_data` function:
902 910
903 911 .. sourcecode:: python
904 912
905 913 from IPython.kernel.zmq.datapub import publish_data
906 914 ns = dict(x=my_array)
907 915 publish_data(ns)
908 916
909 917
910 918 Message type: ``data_pub``::
911 919
912 920 content = {
913 921 # the keys of the data dict, after it has been unserialized
914 922 'keys' : ['a', 'b']
915 923 }
916 924 # the namespace dict will be serialized in the message buffers,
917 925 # which will have a length of at least one
918 926 buffers = [b'pdict', ...]
919 927
920 928
921 929 The interpretation of a sequence of data_pub messages for a given parent request should be
922 930 to update a single namespace with subsequent results.
923 931
924 932 .. note::
925 933
926 934 No frontends directly handle data_pub messages at this time.
927 935 It is currently only used by the client/engines in :mod:`IPython.parallel`,
928 936 where engines may publish *data* to the Client,
929 937 of which the Client can then publish *representations* via ``display_data``
930 938 to various frontends.
931 939
932 940 Code inputs
933 941 -----------
934 942
935 943 To let all frontends know what code is being executed at any given time, these
936 944 messages contain a re-broadcast of the ``code`` portion of an
937 945 :ref:`execute_request <execute>`, along with the :ref:`execution_count
938 946 <execution_counter>`.
939 947
940 948 Message type: ``execute_input``::
941 949
942 950 content = {
943 951 'code' : str, # Source code to be executed, one or more lines
944 952
945 953 # The counter for this execution is also provided so that clients can
946 954 # display it, since IPython automatically creates variables called _iN
947 955 # (for input prompt In[N]).
948 956 'execution_count' : int
949 957 }
950 958
951 959 .. versionchanged:: 5.0
952 960
953 961 ``pyin`` is renamed to ``execute_input``.
954 962
955 963
956 964 Execution results
957 965 -----------------
958 966
959 967 Results of an execution are published as an ``execute_result``.
960 968 These are identical to `display_data`_ messages, with the addition of an ``execution_count`` key.
961 969
962 970 Results can have multiple simultaneous formats depending on its
963 971 configuration. A plain text representation should always be provided
964 972 in the ``text/plain`` mime-type. Frontends are free to display any or all of these
965 973 according to its capabilities.
966 974 Frontends should ignore mime-types they do not understand. The data itself is
967 975 any JSON object and depends on the format. It is often, but not always a string.
968 976
969 977 Message type: ``execute_result``::
970 978
971 979 content = {
972 980
973 981 # The counter for this execution is also provided so that clients can
974 982 # display it, since IPython automatically creates variables called _N
975 983 # (for prompt N).
976 984 'execution_count' : int,
977 985
978 986 # data and metadata are identical to a display_data message.
979 987 # the object being displayed is that passed to the display hook,
980 988 # i.e. the *result* of the execution.
981 989 'data' : dict,
982 990 'metadata' : dict,
983 991 }
984 992
985 993 Execution errors
986 994 ----------------
987 995
988 996 When an error occurs during code execution
989 997
990 998 Message type: ``error``::
991 999
992 1000 content = {
993 1001 # Similar content to the execute_reply messages for the 'error' case,
994 1002 # except the 'status' field is omitted.
995 1003 }
996 1004
997 1005 .. versionchanged:: 5.0
998 1006
999 1007 ``pyerr`` renamed to ``error``
1000 1008
1001 1009 Kernel status
1002 1010 -------------
1003 1011
1004 1012 This message type is used by frontends to monitor the status of the kernel.
1005 1013
1006 1014 Message type: ``status``::
1007 1015
1008 1016 content = {
1009 1017 # When the kernel starts to handle a message, it will enter the 'busy'
1010 1018 # state and when it finishes, it will enter the 'idle' state.
1011 1019 # The kernel will publish state 'starting' exactly once at process startup.
1012 1020 execution_state : ('busy', 'idle', 'starting')
1013 1021 }
1014 1022
1015 1023 .. versionchanged:: 5.0
1016 1024
1017 1025 Busy and idle messages should be sent before/after handling every message,
1018 1026 not just execution.
1019 1027
1020 1028 Clear output
1021 1029 ------------
1022 1030
1023 1031 This message type is used to clear the output that is visible on the frontend.
1024 1032
1025 1033 Message type: ``clear_output``::
1026 1034
1027 1035 content = {
1028 1036
1029 1037 # Wait to clear the output until new output is available. Clears the
1030 1038 # existing output immediately before the new output is displayed.
1031 1039 # Useful for creating simple animations with minimal flickering.
1032 1040 'wait' : bool,
1033 1041 }
1034 1042
1035 1043 .. versionchanged:: 4.1
1036 1044
1037 1045 ``stdout``, ``stderr``, and ``display`` boolean keys for selective clearing are removed,
1038 1046 and ``wait`` is added.
1039 1047 The selective clearing keys are ignored in v4 and the default behavior remains the same,
1040 1048 so v4 clear_output messages will be safely handled by a v4.1 frontend.
1041 1049
1042 1050
1043 1051 Messages on the stdin ROUTER/DEALER sockets
1044 1052 ===========================================
1045 1053
1046 1054 This is a socket where the request/reply pattern goes in the opposite direction:
1047 1055 from the kernel to a *single* frontend, and its purpose is to allow
1048 1056 ``raw_input`` and similar operations that read from ``sys.stdin`` on the kernel
1049 1057 to be fulfilled by the client. The request should be made to the frontend that
1050 1058 made the execution request that prompted ``raw_input`` to be called. For now we
1051 1059 will keep these messages as simple as possible, since they only mean to convey
1052 1060 the ``raw_input(prompt)`` call.
1053 1061
1054 1062 Message type: ``input_request``::
1055 1063
1056 1064 content = {
1057 1065 # the text to show at the prompt
1058 1066 'prompt' : str,
1059 1067 # Is the request for a password?
1060 1068 # If so, the frontend shouldn't echo input.
1061 1069 'password' : bool
1062 1070 }
1063 1071
1064 1072 Message type: ``input_reply``::
1065 1073
1066 1074 content = { 'value' : str }
1067 1075
1068 1076
1069 1077 When ``password`` is True, the frontend should not echo the input as it is entered.
1070 1078
1071 1079 .. versionchanged:: 5.0
1072 1080
1073 1081 ``password`` key added.
1074 1082
1075 1083 .. note::
1076 1084
1077 1085 The stdin socket of the client is required to have the same zmq IDENTITY
1078 1086 as the client's shell socket.
1079 1087 Because of this, the ``input_request`` must be sent with the same IDENTITY
1080 1088 routing prefix as the ``execute_reply`` in order for the frontend to receive
1081 1089 the message.
1082 1090
1083 1091 .. note::
1084 1092
1085 1093 We do not explicitly try to forward the raw ``sys.stdin`` object, because in
1086 1094 practice the kernel should behave like an interactive program. When a
1087 1095 program is opened on the console, the keyboard effectively takes over the
1088 1096 ``stdin`` file descriptor, and it can't be used for raw reading anymore.
1089 1097 Since the IPython kernel effectively behaves like a console program (albeit
1090 1098 one whose "keyboard" is actually living in a separate process and
1091 1099 transported over the zmq connection), raw ``stdin`` isn't expected to be
1092 1100 available.
1093 1101
1094 1102 .. _kernel_heartbeat:
1095 1103
1096 1104 Heartbeat for kernels
1097 1105 =====================
1098 1106
1099 1107 Clients send ping messages on a REQ socket, which are echoed right back
1100 1108 from the Kernel's REP socket. These are simple bytestrings, not full JSON messages described above.
1101 1109
1102 1110
1103 1111 Custom Messages
1104 1112 ===============
1105 1113
1106 1114 .. versionadded:: 4.1
1107 1115
1108 1116 IPython 2.0 (msgspec v4.1) adds a messaging system for developers to add their own objects with Frontend
1109 1117 and Kernel-side components, and allow them to communicate with each other.
1110 1118 To do this, IPython adds a notion of a ``Comm``, which exists on both sides,
1111 1119 and can communicate in either direction.
1112 1120
1113 1121 These messages are fully symmetrical - both the Kernel and the Frontend can send each message,
1114 1122 and no messages expect a reply.
1115 1123 The Kernel listens for these messages on the Shell channel,
1116 1124 and the Frontend listens for them on the IOPub channel.
1117 1125
1118 1126 Opening a Comm
1119 1127 --------------
1120 1128
1121 1129 Opening a Comm produces a ``comm_open`` message, to be sent to the other side::
1122 1130
1123 1131 {
1124 1132 'comm_id' : 'u-u-i-d',
1125 1133 'target_name' : 'my_comm',
1126 1134 'data' : {}
1127 1135 }
1128 1136
1129 1137 Every Comm has an ID and a target name.
1130 1138 The code handling the message on the receiving side is responsible for maintaining a mapping
1131 1139 of target_name keys to constructors.
1132 1140 After a ``comm_open`` message has been sent,
1133 1141 there should be a corresponding Comm instance on both sides.
1134 1142 The ``data`` key is always a dict and can be any extra JSON information used in initialization of the comm.
1135 1143
1136 1144 If the ``target_name`` key is not found on the receiving side,
1137 1145 then it should immediately reply with a ``comm_close`` message to avoid an inconsistent state.
1138 1146
1139 1147 Comm Messages
1140 1148 -------------
1141 1149
1142 1150 Comm messages are one-way communications to update comm state,
1143 1151 used for synchronizing widget state, or simply requesting actions of a comm's counterpart.
1144 1152
1145 1153 Essentially, each comm pair defines their own message specification implemented inside the ``data`` dict.
1146 1154
1147 1155 There are no expected replies (of course, one side can send another ``comm_msg`` in reply).
1148 1156
1149 1157 Message type: ``comm_msg``::
1150 1158
1151 1159 {
1152 1160 'comm_id' : 'u-u-i-d',
1153 1161 'data' : {}
1154 1162 }
1155 1163
1156 1164 Tearing Down Comms
1157 1165 ------------------
1158 1166
1159 1167 Since comms live on both sides, when a comm is destroyed the other side must be notified.
1160 1168 This is done with a ``comm_close`` message.
1161 1169
1162 1170 Message type: ``comm_close``::
1163 1171
1164 1172 {
1165 1173 'comm_id' : 'u-u-i-d',
1166 1174 'data' : {}
1167 1175 }
1168 1176
1169 1177 Output Side Effects
1170 1178 -------------------
1171 1179
1172 1180 Since comm messages can execute arbitrary user code,
1173 1181 handlers should set the parent header and publish status busy / idle,
1174 1182 just like an execute request.
1175 1183
1176 1184
1177 1185 To Do
1178 1186 =====
1179 1187
1180 1188 Missing things include:
1181 1189
1182 1190 * Important: finish thinking through the payload concept and API.
1183 1191
1184 1192 .. include:: ../links.txt
@@ -1,174 +1,175 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 37 .. attribute:: language_info
38 38
39 39 Language information for :ref:`msging_kernel_info` replies, in a dictionary.
40 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`.
41 target language (e.g. ``'text/x-python'``), and ``file_extension`` (e.g.
42 ``'py'``).
43 It may also contain keys ``codemirror_mode`` and ``pygments_lexer`` if they
44 need to differ from :attr:`language`.
44 45
45 46 Other keys may be added to this later.
46 47
47 48 .. method:: do_execute(code, silent, store_history=True, user_expressions=None, allow_stdin=False)
48 49
49 50 Execute user code.
50 51
51 52 :param str code: The code to be executed.
52 53 :param bool silent: Whether to display output.
53 54 :param bool store_history: Whether to record this code in history and
54 55 increase the execution count. If silent is True, this is implicitly
55 56 False.
56 57 :param dict user_expressions: Mapping of names to expressions to evaluate
57 58 after the code has run. You can ignore this if you need to.
58 59 :param bool allow_stdin: Whether the frontend can provide input on request
59 60 (e.g. for Python's :func:`raw_input`).
60 61
61 62 Your method should return a dict containing the fields described in
62 63 :ref:`execution_results`. To display output, it can send messages
63 64 using :meth:`~IPython.kernel.zmq.kernelbase.Kernel.send_response`.
64 65 See :doc:`messaging` for details of the different message types.
65 66
66 67 To launch your kernel, add this at the end of your module::
67 68
68 69 if __name__ == '__main__':
69 70 from IPython.kernel.zmq.kernelapp import IPKernelApp
70 71 IPKernelApp.launch_instance(kernel_class=MyKernel)
71 72
72 73 Example
73 74 -------
74 75
75 76 ``echokernel.py`` will simply echo any input it's given to stdout::
76 77
77 78 from IPython.kernel.zmq.kernelbase import Kernel
78 79
79 80 class EchoKernel(Kernel):
80 81 implementation = 'Echo'
81 82 implementation_version = '1.0'
82 83 language = 'no-op'
83 84 language_version = '0.1'
84 85 language_info = {'mimetype': 'text/plain'}
85 86 banner = "Echo kernel - as useful as a parrot"
86 87
87 88 def do_execute(self, code, silent, store_history=True, user_expressions=None,
88 89 allow_stdin=False):
89 90 if not silent:
90 91 stream_content = {'name': 'stdout', 'text': code}
91 92 self.send_response(self.iopub_socket, 'stream', stream_content)
92 93
93 94 return {'status': 'ok',
94 95 # The base class increments the execution count
95 96 'execution_count': self.execution_count,
96 97 'payload': [],
97 98 'user_expressions': {},
98 99 }
99 100
100 101 if __name__ == '__main__':
101 102 from IPython.kernel.zmq.kernelapp import IPKernelApp
102 103 IPKernelApp.launch_instance(kernel_class=EchoKernel)
103 104
104 105 Here's the Kernel spec ``kernel.json`` file for this::
105 106
106 107 {"argv":["python","-m","echokernel", "-f", "{connection_file}"],
107 108 "display_name":"Echo",
108 109 }
109 110
110 111
111 112 Optional steps
112 113 --------------
113 114
114 115 You can override a number of other methods to improve the functionality of your
115 116 kernel. All of these methods should return a dictionary as described in the
116 117 relevant section of the :doc:`messaging spec <messaging>`.
117 118
118 119 .. class:: MyKernel
119 120
120 121 .. method:: do_complete(code, cusor_pos)
121 122
122 123 Code completion
123 124
124 125 :param str code: The code already present
125 126 :param int cursor_pos: The position in the code where completion is requested
126 127
127 128 .. seealso::
128 129
129 130 :ref:`msging_completion` messages
130 131
131 132 .. method:: do_inspect(code, cusor_pos, detail_level=0)
132 133
133 134 Object introspection
134 135
135 136 :param str code: The code
136 137 :param int cursor_pos: The position in the code where introspection is requested
137 138 :param int detail_level: 0 or 1 for more or less detail. In IPython, 1 gets
138 139 the source code.
139 140
140 141 .. seealso::
141 142
142 143 :ref:`msging_inspection` messages
143 144
144 145 .. method:: do_history(hist_access_type, output, raw, session=None, start=None, stop=None, n=None, pattern=None, unique=False)
145 146
146 147 History access. Only the relevant parameters for the type of history
147 148 request concerned will be passed, so your method definition must have defaults
148 149 for all the arguments shown with defaults here.
149 150
150 151 .. seealso::
151 152
152 153 :ref:`msging_history` messages
153 154
154 155 .. method:: do_is_complete(code)
155 156
156 157 Is code entered in a console-like interface complete and ready to execute,
157 158 or should a continuation prompt be shown?
158 159
159 160 :param str code: The code entered so far - possibly multiple lines
160 161
161 162 .. seealso::
162 163
163 164 :ref:`msging_is_complete` messages
164 165
165 166 .. method:: do_shutdown(restart)
166 167
167 168 Shutdown the kernel. You only need to handle your own clean up - the kernel
168 169 machinery will take care of cleaning up its own things before stopping.
169 170
170 171 :param bool restart: Whether the kernel will be started again afterwards
171 172
172 173 .. seealso::
173 174
174 175 :ref:`msging_shutdown` messages
General Comments 0
You need to be logged in to leave comments. Login now