diff --git a/IPython/frontend/html/notebook/static/js/cell.js b/IPython/frontend/html/notebook/static/js/cell.js index 68c8769..9536c9b 100644 --- a/IPython/frontend/html/notebook/static/js/cell.js +++ b/IPython/frontend/html/notebook/static/js/cell.js @@ -19,6 +19,7 @@ var IPython = (function (IPython) { this.read_only = false; this.selected = false; this.element = null; + this.metadata = {}; this.create_element(); if (this.element !== null) { this.element.data("cell", this); @@ -90,10 +91,16 @@ var IPython = (function (IPython) { Cell.prototype.toJSON = function () { + var data = {}; + data.metadata = this.metadata; + return data; }; Cell.prototype.fromJSON = function (data) { + if (data.metadata !== undefined) { + this.metadata = data.metadata; + } }; diff --git a/IPython/frontend/html/notebook/static/js/codecell.js b/IPython/frontend/html/notebook/static/js/codecell.js index 41e7156..5c5ead2 100644 --- a/IPython/frontend/html/notebook/static/js/codecell.js +++ b/IPython/frontend/html/notebook/static/js/codecell.js @@ -22,6 +22,7 @@ var IPython = (function (IPython) { this.code_mirror = null; this.input_prompt_number = null; this.tooltip_on_tab = true; + this.collapsed = false; IPython.Cell.apply(this, arguments); }; @@ -200,17 +201,20 @@ var IPython = (function (IPython) { CodeCell.prototype.collapse = function () { - this.output_area.collapse(); + this.collapsed = true; + this.output_area.collapse(); }; CodeCell.prototype.expand = function () { - this.output_area.expand(); + this.collapsed = false; + this.output_area.expand(); }; CodeCell.prototype.toggle_output = function () { - this.output_area.toggle_output(); + this.collapsed = Boolean(1 - this.collapsed); + this.output_area.toggle_output(); }; @@ -264,6 +268,7 @@ var IPython = (function (IPython) { // JSON serialization CodeCell.prototype.fromJSON = function (data) { + IPython.Cell.prototype.fromJSON.apply(this, arguments); if (data.cell_type === 'code') { if (data.input !== undefined) { this.set_text(data.input); @@ -289,7 +294,7 @@ var IPython = (function (IPython) { CodeCell.prototype.toJSON = function () { - var data = {}; + var data = IPython.Cell.prototype.toJSON.apply(this); data.input = this.get_text(); data.cell_type = 'code'; if (this.input_prompt_number) { diff --git a/IPython/frontend/html/notebook/static/js/notebook.js b/IPython/frontend/html/notebook/static/js/notebook.js index 12268e9..7c02bf7 100644 --- a/IPython/frontend/html/notebook/static/js/notebook.js +++ b/IPython/frontend/html/notebook/static/js/notebook.js @@ -25,11 +25,14 @@ var IPython = (function (IPython) { this.paste_enabled = false; this.dirty = false; this.metadata = {}; + // single worksheet for now + this.worksheet_metadata = {}; this.control_key_active = false; this.notebook_id = null; this.notebook_name = null; this.notebook_name_blacklist_re = /[\/\\:]/; this.nbformat = 3 // Increment this when changing the nbformat + this.nbformat_minor = 0 // Increment this when changing the nbformat this.style(); this.create_elements(); this.bind_events(); @@ -1018,6 +1021,9 @@ var IPython = (function (IPython) { // Only handle 1 worksheet for now. var worksheet = data.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; @@ -1034,6 +1040,27 @@ var IPython = (function (IPython) { new_cell.fromJSON(cell_data); }; }; + if (data.worksheets.length > 1) { + var dialog = $('
'); + dialog.html("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." + ); + this.element.append(dialog); + dialog.dialog({ + resizable: false, + modal: true, + title: "Multiple worksheets", + closeText: "", + close: function(event, ui) {$(this).dialog('destroy').remove();}, + buttons : { + "OK": function () { + $(this).dialog('close'); + } + }, + width: 400 + }); + } }; @@ -1046,7 +1073,10 @@ var IPython = (function (IPython) { }; var data = { // Only handle 1 worksheet for now. - worksheets : [{cells:cell_array}], + worksheets : [{ + cells: cell_array, + metadata: this.worksheet_metadata + }], metadata : this.metadata }; return data; @@ -1057,6 +1087,7 @@ var IPython = (function (IPython) { var data = this.toJSON(); data.metadata.name = this.notebook_name; data.nbformat = this.nbformat; + data.nbformat_minor = this.nbformat_minor; // We do the call with settings so we can set cache to false. var settings = { processData : false, @@ -1133,6 +1164,31 @@ var IPython = (function (IPython) { }, width: 400 }); + } else if (data.orig_nbformat_minor !== undefined && data.nbformat_minor !== data.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; + 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." + + var dialog = $('
'); + dialog.html(msg); + this.element.append(dialog); + dialog.dialog({ + resizable: false, + modal: true, + title: "Newer Notebook", + closeText: "", + close: function(event, ui) {$(this).dialog('destroy').remove();}, + buttons : { + "OK": function () { + $(this).dialog('close'); + } + }, + width: 400 + }); + } // Create the kernel after the notebook is completely loaded to prevent // code execution upon loading, which is a security risk. diff --git a/IPython/frontend/html/notebook/static/js/textcell.js b/IPython/frontend/html/notebook/static/js/textcell.js index 2e52a75..3d07d3a 100644 --- a/IPython/frontend/html/notebook/static/js/textcell.js +++ b/IPython/frontend/html/notebook/static/js/textcell.js @@ -155,6 +155,7 @@ var IPython = (function (IPython) { TextCell.prototype.fromJSON = function (data) { + IPython.Cell.prototype.fromJSON.apply(this, arguments); if (data.cell_type === this.cell_type) { if (data.source !== undefined) { this.set_text(data.source); @@ -170,7 +171,7 @@ var IPython = (function (IPython) { TextCell.prototype.toJSON = function () { - var data = {}; + var data = IPython.Cell.prototype.toJSON.apply(this); data.cell_type = this.cell_type; data.source = this.get_text(); return data; diff --git a/IPython/nbformat/current.py b/IPython/nbformat/current.py index c16efc4..6f89356 100644 --- a/IPython/nbformat/current.py +++ b/IPython/nbformat/current.py @@ -28,7 +28,8 @@ from IPython.nbformat import v1 from IPython.nbformat.v3 import ( NotebookNode, new_code_cell, new_text_cell, new_notebook, new_output, new_worksheet, - parse_filename, new_metadata, new_author, new_heading_cell, nbformat + parse_filename, new_metadata, new_author, new_heading_cell, nbformat, + nbformat_minor, ) #----------------------------------------------------------------------------- @@ -36,6 +37,8 @@ from IPython.nbformat.v3 import ( #----------------------------------------------------------------------------- current_nbformat = nbformat +current_nbformat_minor = nbformat_minor + class NBFormatError(Exception): @@ -45,24 +48,30 @@ class NBFormatError(Exception): def parse_json(s, **kwargs): """Parse a string into a (nbformat, dict) tuple.""" d = json.loads(s, **kwargs) - nbf = d.get('nbformat',1) - return nbf, d + nbf = d.get('nbformat', 1) + nbm = d.get('nbformat_minor', 0) + return nbf, nbm, d def parse_py(s, **kwargs): """Parse a string into a (nbformat, string) tuple.""" - pattern = r'# (?P\d+)' + nbf = current_nbformat + nbm = current_nbformat_minor + + pattern = r'# (?P\d+[\.\d+]*)' m = re.search(pattern,s) if m is not None: - nbf = int(m.group('nbformat')) - else: - nbf = current_nbformat - return nbf, s + digits = m.group('nbformat').split('.') + nbf = int(digits[0]) + if len(digits) > 1: + nbm = int(digits[1]) + + return nbf, nbm, s def reads_json(s, **kwargs): """Read a JSON notebook from a string and return the NotebookNode object.""" - nbf, d = parse_json(s, **kwargs) + nbf, minor, d = parse_json(s, **kwargs) if nbf == 1: nb = v1.to_notebook_json(d, **kwargs) nb = v3.convert_to_this_nbformat(nb, orig_version=1) @@ -71,6 +80,7 @@ def reads_json(s, **kwargs): nb = v3.convert_to_this_nbformat(nb, orig_version=2) elif nbf == 3: nb = v3.to_notebook_json(d, **kwargs) + nb = v3.convert_to_this_nbformat(nb, orig_version=3, orig_minor=minor) else: raise NBFormatError('Unsupported JSON nbformat version: %i' % nbf) return nb @@ -82,7 +92,7 @@ def writes_json(nb, **kwargs): def reads_py(s, **kwargs): """Read a .py notebook from a string and return the NotebookNode object.""" - nbf, s = parse_py(s, **kwargs) + nbf, nbm, s = parse_py(s, **kwargs) if nbf == 2: nb = v2.to_notebook_py(s, **kwargs) elif nbf == 3: diff --git a/IPython/nbformat/v3/__init__.py b/IPython/nbformat/v3/__init__.py index c8fa25d..d3838e3 100644 --- a/IPython/nbformat/v3/__init__.py +++ b/IPython/nbformat/v3/__init__.py @@ -19,7 +19,7 @@ Authors: from .nbbase import ( NotebookNode, new_code_cell, new_text_cell, new_notebook, new_output, new_worksheet, - new_metadata, new_author, new_heading_cell, nbformat + new_metadata, new_author, new_heading_cell, nbformat, nbformat_minor ) from .nbjson import reads as reads_json, writes as writes_json diff --git a/IPython/nbformat/v3/convert.py b/IPython/nbformat/v3/convert.py index 8914fef..6421b80 100644 --- a/IPython/nbformat/v3/convert.py +++ b/IPython/nbformat/v3/convert.py @@ -17,7 +17,8 @@ Authors: #----------------------------------------------------------------------------- from .nbbase import ( - new_code_cell, new_text_cell, new_worksheet, new_notebook, new_output + new_code_cell, new_text_cell, new_worksheet, new_notebook, new_output, + nbformat, nbformat_minor ) from IPython.nbformat import v2 @@ -26,7 +27,7 @@ from IPython.nbformat import v2 # Code #----------------------------------------------------------------------------- -def convert_to_this_nbformat(nb, orig_version=2): +def convert_to_this_nbformat(nb, orig_version=2, orig_minor=0): """Convert a notebook to the v3 format. Parameters @@ -35,16 +36,23 @@ def convert_to_this_nbformat(nb, orig_version=2): The Python representation of the notebook to convert. orig_version : int The original version of the notebook to convert. + orig_minor : int + The original minor version of the notebook to convert (only relevant for v >= 3). """ if orig_version == 1: nb = v2.convert_to_this_nbformat(nb) orig_version = 2 if orig_version == 2: # Mark the original nbformat so consumers know it has been converted. - nb.nbformat = 3 + nb.nbformat = nbformat + nb.nbformat_minor = nbformat_minor + nb.orig_nbformat = 2 return nb elif orig_version == 3: + if orig_minor != nbformat_minor: + nb.orig_nbformat_minor = orig_minor + nb.nbformat_minor = nbformat_minor return nb else: raise ValueError('Cannot convert a notebook from v%s to v3' % orig_version) diff --git a/IPython/nbformat/v3/nbbase.py b/IPython/nbformat/v3/nbbase.py index 2bc2fd4..f4e312b 100644 --- a/IPython/nbformat/v3/nbbase.py +++ b/IPython/nbformat/v3/nbbase.py @@ -32,6 +32,7 @@ from IPython.utils.ipstruct import Struct # Change this when incrementing the nbformat version nbformat = 3 +nbformat_minor = 0 class NotebookNode(Struct): pass @@ -92,7 +93,7 @@ def new_output(output_type=None, output_text=None, output_png=None, def new_code_cell(input=None, prompt_number=None, outputs=None, - language=u'python', collapsed=False): + language=u'python', collapsed=False, metadata=None): """Create a new code cell with input and output""" cell = NotebookNode() cell.cell_type = u'code' @@ -108,10 +109,11 @@ def new_code_cell(input=None, prompt_number=None, outputs=None, cell.outputs = outputs if collapsed is not None: cell.collapsed = bool(collapsed) + cell.metadata = NotebookNode(metadata or {}) return cell -def new_text_cell(cell_type, source=None, rendered=None): +def new_text_cell(cell_type, source=None, rendered=None, metadata=None): """Create a new text cell.""" cell = NotebookNode() # VERSIONHACK: plaintext -> raw @@ -122,11 +124,12 @@ def new_text_cell(cell_type, source=None, rendered=None): cell.source = unicode(source) if rendered is not None: cell.rendered = unicode(rendered) + cell.metadata = NotebookNode(metadata or {}) cell.cell_type = cell_type return cell -def new_heading_cell(source=None, rendered=None, level=1): +def new_heading_cell(source=None, rendered=None, level=1, metadata=None): """Create a new section cell with a given integer level.""" cell = NotebookNode() cell.cell_type = u'heading' @@ -135,10 +138,11 @@ def new_heading_cell(source=None, rendered=None, level=1): if rendered is not None: cell.rendered = unicode(rendered) cell.level = int(level) + cell.metadata = NotebookNode(metadata or {}) return cell -def new_worksheet(name=None, cells=None): +def new_worksheet(name=None, cells=None, metadata=None): """Create a worksheet by name with with a list of cells.""" ws = NotebookNode() if name is not None: @@ -147,6 +151,7 @@ def new_worksheet(name=None, cells=None): ws.cells = [] else: ws.cells = list(cells) + ws.metadata = NotebookNode(metadata or {}) return ws @@ -154,6 +159,7 @@ def new_notebook(name=None, metadata=None, worksheets=None): """Create a notebook by name, id and a list of worksheets.""" nb = NotebookNode() nb.nbformat = nbformat + nb.nbformat_minor = nbformat_minor if worksheets is None: nb.worksheets = [] else: diff --git a/IPython/nbformat/v3/nbpy.py b/IPython/nbformat/v3/nbpy.py index 1130cee..23fc2f0 100644 --- a/IPython/nbformat/v3/nbpy.py +++ b/IPython/nbformat/v3/nbpy.py @@ -20,7 +20,7 @@ import re from .rwbase import NotebookReader, NotebookWriter from .nbbase import ( new_code_cell, new_text_cell, new_worksheet, - new_notebook, new_heading_cell, nbformat + new_notebook, new_heading_cell, nbformat, nbformat_minor, ) #----------------------------------------------------------------------------- @@ -152,7 +152,10 @@ class PyWriter(NotebookWriter): def writes(self, nb, **kwargs): lines = [u'# -*- coding: utf-8 -*-'] - lines.extend([u'# %i' % nbformat,'']) + lines.extend([ + u'# %i.%i' % (nbformat, nbformat_minor), + u'', + ]) for ws in nb.worksheets: for cell in ws.cells: if cell.cell_type == u'code': diff --git a/IPython/nbformat/v3/tests/nbexamples.py b/IPython/nbformat/v3/tests/nbexamples.py index a34d831..efdd38e 100644 --- a/IPython/nbformat/v3/tests/nbexamples.py +++ b/IPython/nbformat/v3/tests/nbexamples.py @@ -6,7 +6,7 @@ from base64 import encodestring from ..nbbase import ( NotebookNode, new_code_cell, new_text_cell, new_worksheet, new_notebook, new_output, - new_metadata, new_author, new_heading_cell, nbformat + new_metadata, new_author, new_heading_cell, nbformat, nbformat_minor ) # some random base64-encoded *bytes* @@ -108,7 +108,7 @@ nb0 = new_notebook( ) nb0_py = u"""# -*- coding: utf-8 -*- -# %i +# %i.%i # @@ -148,6 +148,6 @@ b = 5 print "ünîcødé" -""" % nbformat +""" % (nbformat, nbformat_minor)