diff --git a/IPython/html/nbconvert/handlers.py b/IPython/html/nbconvert/handlers.py index f6e1094..40c820e 100644 --- a/IPython/html/nbconvert/handlers.py +++ b/IPython/html/nbconvert/handlers.py @@ -114,6 +114,7 @@ class NbconvertPostHandler(IPythonHandler): exporter = get_exporter(format, config=self.config) model = self.get_json_body() + name = model.get('name', 'notebook.ipynb') nbnode = to_notebook_json(model['content']) try: @@ -121,7 +122,7 @@ class NbconvertPostHandler(IPythonHandler): except Exception as e: raise web.HTTPError(500, "nbconvert failed: %s" % e) - if respond_zip(self, nbnode.metadata.name, output, resources): + if respond_zip(self, name, output, resources): return # MIME type diff --git a/IPython/html/nbconvert/tests/test_nbconvert_handlers.py b/IPython/html/nbconvert/tests/test_nbconvert_handlers.py index ea44217..904bc98 100644 --- a/IPython/html/nbconvert/tests/test_nbconvert_handlers.py +++ b/IPython/html/nbconvert/tests/test_nbconvert_handlers.py @@ -10,7 +10,7 @@ import requests from IPython.html.utils import url_path_join from IPython.html.tests.launchnotebook import NotebookTestBase, assert_http_error -from IPython.nbformat.current import (new_notebook, write, new_worksheet, +from IPython.nbformat.current import (new_notebook, write, new_heading_cell, new_code_cell, new_output) @@ -43,7 +43,8 @@ class NbconvertAPI(object): png_green_pixel = base64.encodestring(b'\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00' b'\x00\x00\x01\x00\x00x00\x01\x08\x02\x00\x00\x00\x90wS\xde\x00\x00\x00\x0cIDAT' -b'\x08\xd7c\x90\xfb\xcf\x00\x00\x02\\\x01\x1e.~d\x87\x00\x00\x00\x00IEND\xaeB`\x82') +b'\x08\xd7c\x90\xfb\xcf\x00\x00\x02\\\x01\x1e.~d\x87\x00\x00\x00\x00IEND\xaeB`\x82' +).decode('ascii') class APITest(NotebookTestBase): def setUp(self): @@ -52,19 +53,20 @@ class APITest(NotebookTestBase): if not os.path.isdir(pjoin(nbdir, 'foo')): os.mkdir(pjoin(nbdir, 'foo')) - nb = new_notebook(name='testnb') + nb = new_notebook() - ws = new_worksheet() - nb.worksheets = [ws] - ws.cells.append(new_heading_cell(u'Created by test ³')) - cc1 = new_code_cell(input=u'print(2*6)') - cc1.outputs.append(new_output(output_text=u'12', output_type='stream')) - cc1.outputs.append(new_output(output_png=png_green_pixel, output_type='pyout')) - ws.cells.append(cc1) + nb.cells.append(new_heading_cell(u'Created by test ³')) + cc1 = new_code_cell(source=u'print(2*6)') + cc1.outputs.append(new_output(output_type="stream", data=u'12')) + cc1.outputs.append(new_output(output_type="execute_result", + mime_bundle={'image/png' : png_green_pixel}, + prompt_number=1, + )) + nb.cells.append(cc1) with io.open(pjoin(nbdir, 'foo', 'testnb.ipynb'), 'w', encoding='utf-8') as f: - write(nb, f, format='ipynb') + write(nb, f) self.nbconvert_api = NbconvertAPI(self.base_url()) diff --git a/IPython/html/services/contents/manager.py b/IPython/html/services/contents/manager.py index bdff6e8..ba06072 100644 --- a/IPython/html/services/contents/manager.py +++ b/IPython/html/services/contents/manager.py @@ -234,8 +234,7 @@ class ContentsManager(LoggingConfigurable): model = {} if 'content' not in model and model.get('type', None) != 'directory': if ext == '.ipynb': - metadata = current.new_metadata(name=u'') - model['content'] = current.new_notebook(metadata=metadata) + model['content'] = current.new_notebook() model['type'] = 'notebook' model['format'] = 'json' else: diff --git a/IPython/html/services/contents/tests/test_contents_api.py b/IPython/html/services/contents/tests/test_contents_api.py index bac91de..bb5a175 100644 --- a/IPython/html/services/contents/tests/test_contents_api.py +++ b/IPython/html/services/contents/tests/test_contents_api.py @@ -15,7 +15,7 @@ import requests from IPython.html.utils import url_path_join, url_escape from IPython.html.tests.launchnotebook import NotebookTestBase, assert_http_error from IPython.nbformat import current -from IPython.nbformat.current import (new_notebook, write, read, new_worksheet, +from IPython.nbformat.current import (new_notebook, write, read, new_heading_cell, to_notebook_json) from IPython.nbformat import v2 from IPython.utils import py3compat @@ -142,7 +142,7 @@ class APITest(NotebookTestBase): # create a notebook with io.open(pjoin(nbdir, d, '%s.ipynb' % name), 'w', encoding='utf-8') as f: - nb = new_notebook(name=name) + nb = new_notebook() write(nb, f, format='ipynb') # create a text file @@ -286,14 +286,14 @@ class APITest(NotebookTestBase): self.assertEqual(model['content'], '') def test_upload_untitled(self): - nb = new_notebook(name='Upload test') + nb = new_notebook() nbmodel = {'content': nb, 'type': 'notebook'} resp = self.api.upload_untitled(path=u'å b', body=json.dumps(nbmodel)) self._check_created(resp, 'Untitled0.ipynb', u'å b') def test_upload(self): - nb = new_notebook(name=u'ignored') + nb = new_notebook() nbmodel = {'content': nb, 'type': 'notebook'} resp = self.api.upload(u'Upload tést.ipynb', path=u'å b', body=json.dumps(nbmodel)) @@ -355,7 +355,6 @@ class APITest(NotebookTestBase): resp = self.api.read(u'Upload tést.ipynb', u'å b') data = resp.json() self.assertEqual(data['content']['nbformat'], current.nbformat) - self.assertEqual(data['content']['orig_nbformat'], 2) def test_copy_untitled(self): resp = self.api.copy_untitled(u'ç d.ipynb', path=u'å b') @@ -416,9 +415,7 @@ class APITest(NotebookTestBase): resp = self.api.read('a.ipynb', 'foo') nbcontent = json.loads(resp.text)['content'] nb = to_notebook_json(nbcontent) - ws = new_worksheet() - nb.worksheets = [ws] - ws.cells.append(new_heading_cell(u'Created by test ³')) + nb.cells.append(new_heading_cell(u'Created by test ³')) nbmodel= {'name': 'a.ipynb', 'path':'foo', 'content': nb, 'type': 'notebook'} resp = self.api.save('a.ipynb', path='foo', body=json.dumps(nbmodel)) @@ -426,11 +423,11 @@ class APITest(NotebookTestBase): nbfile = pjoin(self.notebook_dir.name, 'foo', 'a.ipynb') with io.open(nbfile, 'r', encoding='utf-8') as f: newnb = read(f, format='ipynb') - self.assertEqual(newnb.worksheets[0].cells[0].source, + self.assertEqual(newnb.cells[0].source, u'Created by test ³') nbcontent = self.api.read('a.ipynb', 'foo').json()['content'] newnb = to_notebook_json(nbcontent) - self.assertEqual(newnb.worksheets[0].cells[0].source, + self.assertEqual(newnb.cells[0].source, u'Created by test ³') # Save and rename @@ -455,10 +452,8 @@ class APITest(NotebookTestBase): # Modify it nbcontent = json.loads(resp.text)['content'] nb = to_notebook_json(nbcontent) - ws = new_worksheet() - nb.worksheets = [ws] hcell = new_heading_cell('Created by test') - ws.cells.append(hcell) + nb.cells.append(hcell) # Save nbmodel= {'name': 'a.ipynb', 'path':'foo', 'content': nb, 'type': 'notebook'} resp = self.api.save('a.ipynb', path='foo', body=json.dumps(nbmodel)) @@ -469,14 +464,14 @@ class APITest(NotebookTestBase): nbcontent = self.api.read('a.ipynb', 'foo').json()['content'] nb = to_notebook_json(nbcontent) - self.assertEqual(nb.worksheets[0].cells[0].source, 'Created by test') + self.assertEqual(nb.cells[0].source, 'Created by test') # Restore cp1 r = self.api.restore_checkpoint('a.ipynb', 'foo', cp1['id']) self.assertEqual(r.status_code, 204) nbcontent = self.api.read('a.ipynb', 'foo').json()['content'] nb = to_notebook_json(nbcontent) - self.assertEqual(nb.worksheets, []) + self.assertEqual(nb.cells, []) # Delete cp1 r = self.api.delete_checkpoint('a.ipynb', 'foo', cp1['id']) diff --git a/IPython/html/services/contents/tests/test_manager.py b/IPython/html/services/contents/tests/test_manager.py index d5dfff4..558f73a 100644 --- a/IPython/html/services/contents/tests/test_manager.py +++ b/IPython/html/services/contents/tests/test_manager.py @@ -95,11 +95,9 @@ class TestContentsManager(TestCase): return os_path def add_code_cell(self, nb): - output = current.new_output("display_data", output_javascript="alert('hi');") + output = current.new_output("display_data", {'application/javascript': "alert('hi');"}) cell = current.new_code_cell("print('hi')", outputs=[output]) - if not nb.worksheets: - nb.worksheets.append(current.new_worksheet()) - nb.worksheets[0].cells.append(cell) + nb.cells.append(cell) def new_notebook(self): cm = self.contents_manager @@ -309,13 +307,13 @@ class TestContentsManager(TestCase): nb, name, path = self.new_notebook() cm.mark_trusted_cells(nb, name, path) - for cell in nb.worksheets[0].cells: + for cell in nb.cells: if cell.cell_type == 'code': assert not cell.metadata.trusted cm.trust_notebook(name, path) nb = cm.get_model(name, path)['content'] - for cell in nb.worksheets[0].cells: + for cell in nb.cells: if cell.cell_type == 'code': assert cell.metadata.trusted diff --git a/IPython/html/services/sessions/tests/test_sessions_api.py b/IPython/html/services/sessions/tests/test_sessions_api.py index 9623415..643fd68 100644 --- a/IPython/html/services/sessions/tests/test_sessions_api.py +++ b/IPython/html/services/sessions/tests/test_sessions_api.py @@ -62,7 +62,7 @@ class SessionAPITest(NotebookTestBase): with io.open(pjoin(nbdir, 'foo', 'nb1.ipynb'), 'w', encoding='utf-8') as f: - nb = new_notebook(name='nb1') + nb = new_notebook() write(nb, f, format='ipynb') self.sess_api = SessionAPI(self.base_url()) diff --git a/IPython/html/static/notebook/js/codecell.js b/IPython/html/static/notebook/js/codecell.js index c6d19c8..ca5b6f3 100644 --- a/IPython/html/static/notebook/js/codecell.js +++ b/IPython/html/static/notebook/js/codecell.js @@ -382,13 +382,11 @@ define([ CodeCell.prototype.collapse_output = function () { - this.collapsed = true; this.output_area.collapse(); }; CodeCell.prototype.expand_output = function () { - this.collapsed = false; this.output_area.expand(); this.output_area.unscroll_area(); }; @@ -399,7 +397,6 @@ define([ }; CodeCell.prototype.toggle_output = function () { - this.collapsed = Boolean(1 - this.collapsed); this.output_area.toggle_output(); }; @@ -467,22 +464,18 @@ define([ CodeCell.prototype.fromJSON = function (data) { Cell.prototype.fromJSON.apply(this, arguments); if (data.cell_type === 'code') { - if (data.input !== undefined) { - this.set_text(data.input); + if (data.source !== undefined) { + this.set_text(data.source); // make this value the starting point, so that we can only undo // to this state, instead of a blank cell this.code_mirror.clearHistory(); this.auto_highlight(); } - if (data.prompt_number !== undefined) { - this.set_input_prompt(data.prompt_number); - } else { - this.set_input_prompt(); - } + this.set_input_prompt(data.prompt_number); this.output_area.trusted = data.metadata.trusted || false; this.output_area.fromJSON(data.outputs); - if (data.collapsed !== undefined) { - if (data.collapsed) { + if (data.metadata.collapsed !== undefined) { + if (data.metadata.collapsed) { this.collapse_output(); } else { this.expand_output(); @@ -494,16 +487,17 @@ define([ CodeCell.prototype.toJSON = function () { var data = Cell.prototype.toJSON.apply(this); - data.input = this.get_text(); + data.source = this.get_text(); // is finite protect against undefined and '*' value if (isFinite(this.input_prompt_number)) { data.prompt_number = this.input_prompt_number; + } else { + data.prompt_number = null; } var outputs = this.output_area.toJSON(); data.outputs = outputs; - data.language = 'python'; data.metadata.trusted = this.output_area.trusted; - data.collapsed = this.output_area.collapsed; + data.metadata.collapsed = this.output_area.collapsed; return data; }; diff --git a/IPython/html/static/notebook/js/notebook.js b/IPython/html/static/notebook/js/notebook.js index 8181138..98c6111 100644 --- a/IPython/html/static/notebook/js/notebook.js +++ b/IPython/html/static/notebook/js/notebook.js @@ -121,10 +121,8 @@ define([ this.autosave_timer = null; // autosave *at most* every two minutes this.minimum_autosave_interval = 120000; - // single worksheet for now - this.worksheet_metadata = {}; this.notebook_name_blacklist_re = /[\/\\:]/; - this.nbformat = 3; // Increment this when changing the nbformat + this.nbformat = 4; // Increment this when changing the nbformat this.nbformat_minor = 0; // Increment this when changing the nbformat this.codemirror_mode = 'ipython'; this.create_elements(); @@ -1785,8 +1783,6 @@ define([ /** * Load a notebook from JSON (.ipynb). * - * This currently handles one worksheet: others are deleted. - * * @method fromJSON * @param {Object} data JSON representation of a notebook */ @@ -1818,50 +1814,22 @@ define([ this.set_codemirror_mode(cm_mode); } - // Only handle 1 worksheet for now. - var worksheet = content.worksheets[0]; - if (worksheet !== undefined) { - if (worksheet.metadata) { - this.worksheet_metadata = worksheet.metadata; - } - var new_cells = worksheet.cells; - ncells = new_cells.length; - var cell_data = null; - var new_cell = null; - for (i=0; i raw - // handle never-released plaintext name for raw cells - if (cell_data.cell_type === 'plaintext'){ - cell_data.cell_type = 'raw'; - } - - new_cell = this.insert_cell_at_index(cell_data.cell_type, i); - new_cell.fromJSON(cell_data); - if (new_cell.cell_type == 'code' && !new_cell.output_area.trusted) { - trusted = false; - } + var new_cells = content.cells; + ncells = new_cells.length; + var cell_data = null; + var new_cell = null; + for (i=0; i 1) { - dialog.modal({ - notebook: this, - keyboard_manager: this.keyboard_manager, - title : "Multiple worksheets", - body : "This notebook has " + data.worksheets.length + " worksheets, " + - "but this version of IPython can only handle the first. " + - "If you save this notebook, worksheets after the first will be lost.", - buttons : { - OK : { - class : "btn-danger" - } - } - }); - } }; /** @@ -1871,6 +1839,10 @@ define([ * @return {Object} A JSON-friendly representation of this notebook. */ Notebook.prototype.toJSON = function () { + // remove the conversion indicator, which only belongs in-memory + delete this.metadata.orig_nbformat; + delete this.metadata.orig_nbformat_minor; + var cells = this.get_cells(); var ncells = cells.length; var cell_array = new Array(ncells); @@ -1883,11 +1855,7 @@ define([ cell_array[i] = cell.toJSON(); } var data = { - // Only handle 1 worksheet for now. - worksheets : [{ - cells: cell_array, - metadata: this.worksheet_metadata - }], + cells: cell_array, metadata : this.metadata }; if (trusted != this.trusted) { @@ -2337,10 +2305,13 @@ define([ } this.set_dirty(false); this.scroll_to_top(); - if (data.orig_nbformat !== undefined && data.nbformat !== data.orig_nbformat) { + var nbmodel = data.content; + var orig_nbformat = nbmodel.metadata.orig_nbformat; + var orig_nbformat_minor = nbmodel.metadata.orig_nbformat_minor; + if (orig_nbformat !== undefined && nbmodel.nbformat !== orig_nbformat) { var msg = "This notebook has been converted from an older " + - "notebook format (v"+data.orig_nbformat+") to the current notebook " + - "format (v"+data.nbformat+"). The next time you save this notebook, the " + + "notebook format (v"+orig_nbformat+") to the current notebook " + + "format (v"+nbmodel.nbformat+"). The next time you save this notebook, the " + "newer notebook format will be used and older versions of IPython " + "may not be able to read it. To keep the older version, close the " + "notebook without saving it."; @@ -2355,10 +2326,10 @@ define([ } } }); - } else if (data.orig_nbformat_minor !== undefined && data.nbformat_minor !== data.orig_nbformat_minor) { + } else if (orig_nbformat_minor !== undefined && nbmodel.nbformat_minor !== orig_nbformat_minor) { var that = this; - var orig_vs = 'v' + data.nbformat + '.' + data.orig_nbformat_minor; - var this_vs = 'v' + data.nbformat + '.' + this.nbformat_minor; + var orig_vs = 'v' + nbmodel.nbformat + '.' + orig_nbformat_minor; + var this_vs = 'v' + nbmodel.nbformat + '.' + this.nbformat_minor; var msg = "This notebook is version " + orig_vs + ", but we only fully support up to " + this_vs + ". You can still work with this notebook, but some features " + "introduced in later notebook versions may not be available."; diff --git a/IPython/html/static/notebook/js/outputarea.js b/IPython/html/static/notebook/js/outputarea.js index 3be41f5..3732563 100644 --- a/IPython/html/static/notebook/js/outputarea.js +++ b/IPython/html/static/notebook/js/outputarea.js @@ -211,7 +211,7 @@ define([ var content = msg.content; if (msg_type === "stream") { json.text = content.text; - json.stream = content.name; + json.name = content.name; } else if (msg_type === "display_data") { json = content.data; json.output_type = msg_type; @@ -234,6 +234,7 @@ define([ OutputArea.prototype.rename_keys = function (data, key_map) { + // TODO: This is now unused, should it be removed? var remapped = {}; for (var key in data) { var new_key = key_map[key] || key; @@ -260,7 +261,10 @@ define([ // TODO: right now everything is a string, but JSON really shouldn't be. // nbformat 4 will fix that. $.map(OutputArea.output_types, function(key){ - if (json[key] !== undefined && typeof json[key] !== 'string') { + if (key !== 'application/json' && + json[key] !== undefined && + typeof json[key] !== 'string' + ) { console.log("Invalid type for " + key, json[key]); delete json[key]; } @@ -449,23 +453,18 @@ define([ OutputArea.prototype.append_stream = function (json) { - // temporary fix: if stream undefined (json file written prior to this patch), - // default to most likely stdout: - if (json.stream === undefined){ - json.stream = 'stdout'; - } - var text = json.text; - var subclass = "output_"+json.stream; + var text = json.data; + var subclass = "output_"+json.name; if (this.outputs.length > 0){ // have at least one output to consider var last = this.outputs[this.outputs.length-1]; - if (last.output_type == 'stream' && json.stream == last.stream){ + if (last.output_type == 'stream' && json.name == last.name){ // latest output was in the same stream, // so append directly into its pre tag // escape ANSI & HTML specials: - last.text = utils.fixCarriageReturn(last.text + json.text); + last.data = utils.fixCarriageReturn(last.data + json.data); var pre = this.element.find('div.'+subclass).last().find('pre'); - var html = utils.fixConsole(last.text); + var html = utils.fixConsole(last.data); // The only user content injected with this HTML call is // escaped by the fixConsole() method. pre.html(html); @@ -852,70 +851,33 @@ define([ // JSON serialization - OutputArea.prototype.fromJSON = function (outputs) { + OutputArea.prototype.fromJSON = function (outputs, metadata) { var len = outputs.length; - var data; + metadata = metadata || {}; for (var i=0; i\""; @@ -24,8 +24,10 @@ var svg = "\"