diff --git a/IPython/frontend/html/notebook/handlers.py b/IPython/frontend/html/notebook/handlers.py index f9a43b5..7984ef3 100644 --- a/IPython/frontend/html/notebook/handlers.py +++ b/IPython/frontend/html/notebook/handlers.py @@ -4,15 +4,14 @@ # Imports #----------------------------------------------------------------------------- -import datetime import json import logging -import os import urllib from tornado import web from tornado import websocket + #----------------------------------------------------------------------------- # Handlers #----------------------------------------------------------------------------- @@ -20,7 +19,16 @@ from tornado import websocket class MainHandler(web.RequestHandler): def get(self): - self.render('notebook.html') + notebook_id = self.application.notebook_manager.new_notebook() + self.render('notebook.html', notebook_id=notebook_id) + + +class NamedNotebookHandler(web.RequestHandler): + def get(self, notebook_id): + nbm = self.application.notebook_manager + if not nbm.notebook_exists(notebook_id): + raise web.HTTPError(404) + self.render('notebook.html', notebook_id=notebook_id) class KernelHandler(web.RequestHandler): @@ -30,6 +38,7 @@ class KernelHandler(web.RequestHandler): def post(self): kernel_id = self.application.start_kernel() + self.set_header('Location', '/'+kernel_id) self.write(json.dumps(kernel_id)) @@ -65,52 +74,52 @@ class ZMQStreamHandler(websocket.WebSocketHandler): class NotebookRootHandler(web.RequestHandler): def get(self): - files = os.listdir(os.getcwd()) - files = [file for file in files if file.endswith(".ipynb")] + nbm = self.application.notebook_manager + files = nbm.list_notebooks() self.write(json.dumps(files)) + def post(self): + nbm = self.application.notebook_manager + body = self.request.body.strip() + format = self.get_argument('format', default='json') + if body: + notebook_id = nbm.save_new_notebook(body, format) + else: + notebook_id = nbm.new_notebook() + self.set_header('Location', '/'+notebook_id) + self.write(json.dumps(notebook_id)) -class NotebookHandler(web.RequestHandler): - - SUPPORTED_METHODS = ("GET", "DELETE", "PUT") - def find_path(self, filename): - filename = urllib.unquote(filename) - if not filename.endswith('.ipynb'): - raise web.HTTPError(400) - path = os.path.join(os.getcwd(), filename) - return path +class NotebookHandler(web.RequestHandler): - def get(self, filename): - path = self.find_path(filename) - if not os.path.isfile(path): - raise web.HTTPError(404) - info = os.stat(path) - self.set_header("Content-Type", "application/unknown") - self.set_header("Last-Modified", datetime.datetime.utcfromtimestamp( - info.st_mtime)) - f = open(path, "r") - try: - self.finish(f.read()) - finally: - f.close() - - def put(self, filename): - path = self.find_path(filename) - f = open(path, "w") - f.write(self.request.body) - f.close() + SUPPORTED_METHODS = ('GET', 'PUT', 'DELETE') + + def get(self, notebook_id): + nbm = self.application.notebook_manager + format = self.get_argument('format', default='json') + last_mod, name, data = nbm.get_notebook(notebook_id, format) + if format == u'json': + self.set_header('Content-Type', 'application/json') + self.set_header('Content-Disposition','attachment; filename=%s.json' % name) + elif format == u'xml': + self.set_header('Content-Type', 'text/xml') + self.set_header('Content-Disposition','attachment; filename=%s.ipynb' % name) + elif format == u'py': + self.set_header('Content-Type', 'text/plain') + self.set_header('Content-Disposition','attachment; filename=%s.py' % name) + self.set_header('Last-Modified', last_mod) + self.finish(data) + + def put(self, notebook_id): + nbm = self.application.notebook_manager + format = self.get_argument('format', default='json') + nbm.save_notebook(notebook_id, self.request.body, format) + self.set_status(204) self.finish() - def delete(self, filename): - path = self.find_path(filename) - if not os.path.isfile(path): - raise web.HTTPError(404) - os.unlink(path) + def delete(self, notebook_id): + nbm = self.application.notebook_manager + nbm.delete_notebook(notebook_id) self.set_status(204) self.finish() - - - - diff --git a/IPython/frontend/html/notebook/notebookapp.py b/IPython/frontend/html/notebook/notebookapp.py index caf3a38..f21a002 100644 --- a/IPython/frontend/html/notebook/notebookapp.py +++ b/IPython/frontend/html/notebook/notebookapp.py @@ -27,13 +27,15 @@ tornado.ioloop = ioloop from tornado import httpserver from tornado import web -from kernelmanager import KernelManager -from sessionmanager import SessionManager -from handlers import ( - MainHandler, KernelHandler, KernelActionHandler, ZMQStreamHandler, +from .kernelmanager import KernelManager +from .sessionmanager import SessionManager +from .handlers import ( + MainHandler, NamedNotebookHandler, + KernelHandler, KernelActionHandler, ZMQStreamHandler, NotebookRootHandler, NotebookHandler ) -from routers import IOPubStreamRouter, ShellStreamRouter +from .routers import IOPubStreamRouter, ShellStreamRouter +from .notebookmanager import NotebookManager from IPython.core.application import BaseIPythonApplication from IPython.core.profiledir import ProfileDir @@ -53,6 +55,7 @@ from IPython.utils.traitlets import Dict, Unicode, Int, Any, List, Enum _kernel_id_regex = r"(?P\w+-\w+-\w+-\w+-\w+)" _kernel_action_regex = r"(?Prestart|interrupt)" +_notebook_id_regex = r"(?P\w+-\w+-\w+-\w+-\w+)" LOCALHOST = '127.0.0.1' @@ -65,12 +68,13 @@ class NotebookWebApplication(web.Application): def __init__(self, kernel_manager, log, kernel_argv, config): handlers = [ (r"/", MainHandler), + (r"/%s" % _notebook_id_regex, NamedNotebookHandler), (r"/kernels", KernelHandler), (r"/kernels/%s/%s" % (_kernel_id_regex, _kernel_action_regex), KernelActionHandler), (r"/kernels/%s/iopub" % _kernel_id_regex, ZMQStreamHandler, dict(stream_name='iopub')), (r"/kernels/%s/shell" % _kernel_id_regex, ZMQStreamHandler, dict(stream_name='shell')), (r"/notebooks", NotebookRootHandler), - (r"/notebooks/([^/]+)", NotebookHandler) + (r"/notebooks/%s" % _notebook_id_regex, NotebookHandler) ] settings = dict( template_path=os.path.join(os.path.dirname(__file__), "templates"), @@ -84,6 +88,7 @@ class NotebookWebApplication(web.Application): self.config = config self._routers = {} self._session_dict = {} + self.notebook_manager = NotebookManager(config=self.config) #------------------------------------------------------------------------- # Methods for managing kernels and sessions diff --git a/IPython/frontend/html/notebook/notebookmanager.py b/IPython/frontend/html/notebook/notebookmanager.py new file mode 100644 index 0000000..f0b434f --- /dev/null +++ b/IPython/frontend/html/notebook/notebookmanager.py @@ -0,0 +1,195 @@ +#----------------------------------------------------------------------------- +# Copyright (C) 2011 The IPython Development Team +# +# Distributed under the terms of the BSD License. The full license is in +# the file COPYING.txt, distributed as part of this software. +#----------------------------------------------------------------------------- + +#----------------------------------------------------------------------------- +# Imports +#----------------------------------------------------------------------------- + +import datetime +import os +import uuid + +from tornado import web + +from IPython.config.configurable import Configurable +from IPython.nbformat import current +from IPython.utils.traitlets import Unicode, List, Dict + + +#----------------------------------------------------------------------------- +# Code +#----------------------------------------------------------------------------- + + +class NotebookManager(Configurable): + + notebook_dir = Unicode(os.getcwd()) + filename_ext = Unicode(u'.ipynb') + allowed_formats = List([u'json',u'xml',u'py']) + + # Map notebook_ids to notebook names + mapping = Dict() + # Map notebook names to notebook_ids + rev_mapping = Dict() + + def list_notebooks(self): + """List all notebooks in the notebook dir. + + This returns a list of dicts of the form:: + + dict(notebook_id=notebook,name=name) + """ + names = os.listdir(self.notebook_dir) + names = [name.strip(self.filename_ext)\ + for name in names if name.endswith(self.filename_ext)] + data = [] + for name in names: + if name not in self.rev_mapping: + notebook_id = self.new_notebook_id(name) + else: + notebook_id = self.rev_mapping[name] + data.append(dict(notebook_id=notebook_id,name=name)) + return data + + def new_notebook_id(self, name): + """Generate a new notebook_id for a name and store its mappings.""" + notebook_id = unicode(uuid.uuid4()) + self.mapping[notebook_id] = name + self.rev_mapping[name] = notebook_id + return notebook_id + + def delete_notebook_id(self, notebook_id): + """Delete a notebook's id only. This doesn't delete the actual notebook.""" + name = self.mapping[notebook_id] + del self.mapping[notebook_id] + del self.rev_mapping[name] + + def notebook_exists(self, notebook_id): + """Does a notebook exist?""" + if notebook_id not in self.mapping: + return False + path = self.get_path_by_name(self.mapping[notebook_id]) + if not os.path.isfile(path): + return False + return True + + def find_path(self, notebook_id): + """Return a full path to a notebook given its notebook_id.""" + try: + name = self.mapping[notebook_id] + except KeyError: + raise web.HTTPError(404) + return self.get_path_by_name(name) + + def get_path_by_name(self, name): + """Return a full path to a notebook given its name.""" + filename = name + self.filename_ext + path = os.path.join(self.notebook_dir, filename) + return path + + def get_notebook(self, notebook_id, format=u'json'): + """Get the representation of a notebook in format by notebook_id.""" + format = unicode(format) + if format not in self.allowed_formats: + raise web.HTTPError(415) + last_modified, nb = self.get_notebook_object(notebook_id) + data = current.writes(nb, format) + name = nb.get('name','notebook') + return last_modified, name, data + + def get_notebook_object(self, notebook_id): + """Get the NotebookNode representation of a notebook by notebook_id.""" + path = self.find_path(notebook_id) + if not os.path.isfile(path): + raise web.HTTPError(404) + info = os.stat(path) + last_modified = datetime.datetime.utcfromtimestamp(info.st_mtime) + try: + with open(path,'r') as f: + s = f.read() + try: + # v2 and later have xml in the .ipynb files + nb = current.reads(s, 'xml') + except: + # v1 had json in the .ipynb files + nb = current.reads(s, 'json') + except: + raise web.HTTPError(404) + return last_modified, nb + + def save_new_notebook(self, data, format=u'json'): + """Save a new notebook and return its notebook_id.""" + if format not in self.allowed_formats: + raise web.HTTPError(415) + try: + nb = current.reads(data, format) + except: + raise web.HTTPError(400) + try: + name = nb.name + except AttributeError: + raise web.HTTPError(400) + notebook_id = self.new_notebook_id(name) + self.save_notebook_object(notebook_id, nb) + return notebook_id + + def save_notebook(self, notebook_id, data, format=u'json'): + """Save an existing notebook by notebook_id.""" + if format not in self.allowed_formats: + raise web.HTTPError(415) + try: + nb = current.reads(data, format) + except: + raise web.HTTPError(400) + self.save_notebook_object(notebook_id, nb) + + def save_notebook_object(self, notebook_id, nb): + """Save an existing notebook object by notebook_id.""" + if notebook_id not in self.mapping: + raise web.HTTPError(404) + old_name = self.mapping[notebook_id] + try: + new_name = nb.name + except AttributeError: + raise web.HTTPError(400) + path = self.get_path_by_name(new_name) + try: + with open(path,'w') as f: + current.write(nb, f, u'xml') + except: + raise web.HTTPError(400) + if old_name != new_name: + old_path = self.get_path_by_name(old_name) + if os.path.isfile(old_path): + os.unlink(old_path) + self.mapping[notebook_id] = new_name + self.rev_mapping[new_name] = notebook_id + + def delete_notebook(self, notebook_id): + """Delete notebook by notebook_id.""" + path = self.find_path(notebook_id) + if not os.path.isfile(path): + raise web.HTTPError(404) + os.unlink(path) + self.delete_notebook_id(notebook_id) + + def new_notebook(self): + """Create a new notebook and returns its notebook_id.""" + i = 0 + while True: + name = u'Untitled%i' % i + path = self.get_path_by_name(name) + if not os.path.isfile(path): + break + else: + i = i+1 + notebook_id = self.new_notebook_id(name) + nb = current.new_notebook(name=name, id=notebook_id) + with open(path,'w') as f: + current.write(nb, f, u'xml') + return notebook_id + diff --git a/IPython/frontend/html/notebook/routers.py b/IPython/frontend/html/notebook/routers.py index eca982a..babc8c4 100644 --- a/IPython/frontend/html/notebook/routers.py +++ b/IPython/frontend/html/notebook/routers.py @@ -112,7 +112,7 @@ class ShellStreamRouter(ZMQStreamRouter): def forward_msg(self, client_id, msg): if len(msg) < self.max_msg_size: msg = json.loads(msg) - to_send = self.session.serialize(msg) + # to_send = self.session.serialize(msg) self._request_queue.put(client_id) self.session.send(self.zmq_stream, msg) diff --git a/IPython/frontend/html/notebook/static/css/notebook.css b/IPython/frontend/html/notebook/static/css/notebook.css index d138af8..d1f0b50 100644 --- a/IPython/frontend/html/notebook/static/css/notebook.css +++ b/IPython/frontend/html/notebook/static/css/notebook.css @@ -206,6 +206,13 @@ span.button_label { font-size: 77%; } +#download_format { + float: right; + font-size: 85%; + width: 60px; + margin: 1px 5px; +} + div#left_panel_splitter { width: 8px; top: 0px; diff --git a/IPython/frontend/html/notebook/static/js/codecell.js b/IPython/frontend/html/notebook/static/js/codecell.js index 88fb5a1..de3030e 100644 --- a/IPython/frontend/html/notebook/static/js/codecell.js +++ b/IPython/frontend/html/notebook/static/js/codecell.js @@ -302,18 +302,28 @@ var IPython = (function (IPython) { CodeCell.prototype.fromJSON = function (data) { if (data.cell_type === 'code') { - this.set_code(data.code); - this.set_input_prompt(data.prompt_number); + if (data.input !== undefined) { + this.set_code(data.input); + } + if (data.prompt_number !== undefined) { + this.set_input_prompt(data.prompt_number); + } else { + this.set_input_prompt(); + }; }; }; CodeCell.prototype.toJSON = function () { - return { - code : this.get_code(), - cell_type : 'code', - prompt_number : this.input_prompt_number + var data = {} + data.input = this.get_code(); + data.cell_type = 'code'; + if (this.input_prompt_number !== ' ') { + data.prompt_number = this.input_prompt_number }; + data.outputs = []; + data.language = 'python'; + return data; }; IPython.CodeCell = CodeCell; diff --git a/IPython/frontend/html/notebook/static/js/notebook.js b/IPython/frontend/html/notebook/static/js/notebook.js index 0c31597..68cc7a5 100644 --- a/IPython/frontend/html/notebook/static/js/notebook.js +++ b/IPython/frontend/html/notebook/static/js/notebook.js @@ -14,14 +14,9 @@ var IPython = (function (IPython) { this.next_prompt_number = 1; this.kernel = null; this.msg_cell_map = {}; - this.filename = null; - this.notebook_load_re = /%notebook load/ - this.notebook_save_re = /%notebook save/ - this.notebook_filename_re = /(\w)+.ipynb/ this.style(); this.create_elements(); this.bind_events(); - this.start_kernel(); }; @@ -473,24 +468,8 @@ var IPython = (function (IPython) { if (cell instanceof IPython.CodeCell) { cell.clear_output(); var code = cell.get_code(); - if (that.notebook_load_re.test(code)) { - // %notebook load - var code_parts = code.split(' '); - if (code_parts.length === 3) { - that.load_notebook(code_parts[2]); - }; - } else if (that.notebook_save_re.test(code)) { - // %notebook save - var code_parts = code.split(' '); - if (code_parts.length === 3) { - that.save_notebook(code_parts[2]); - } else { - that.save_notebook() - }; - } else { - var msg_id = that.kernel.execute(cell.get_code()); - that.msg_cell_map[msg_id] = cell.cell_id; - }; + var msg_id = that.kernel.execute(cell.get_code()); + that.msg_cell_map[msg_id] = cell.cell_id; } else if (cell instanceof IPython.TextCell) { cell.render(); } @@ -532,18 +511,22 @@ var IPython = (function (IPython) { // Always delete cell 0 as they get renumbered as they are deleted. this.delete_cell(0); }; - var new_cells = data.cells; - ncells = new_cells.length; - var cell_data = null; - for (var i=0; i'); - bad_filename.html( - "The filename you entered (" + filename + ") is not valid. Notebook filenames must have the following form: foo.ipynb" - ); - bad_filename.dialog({title: 'Invalid filename', modal: true}); - return false; - }; - }; - - Notebook.prototype.save_notebook = function (filename) { - this.filename = filename || this.filename || ''; - if (this.filename === '') { - var no_filename = $('
'); - no_filename.html( - "This notebook has no filename, please specify a filename of the form: foo.ipynb" - ); - no_filename.dialog({title: 'Missing filename', modal: true}); - return; - } - if (!this.test_filename(this.filename)) {return;} - var thedata = this.toJSON(); - var settings = { - processData : false, - cache : false, - type : "PUT", - data : JSON.stringify(thedata), - success : function (data, status, xhr) {console.log(data);} - }; - $.ajax("/notebooks/" + this.filename, settings); - }; + Notebook.prototype.notebook_saved = function (data, status, xhr) { + IPython.save_widget.status_save(); + } - Notebook.prototype.load_notebook = function (filename) { - if (!this.test_filename(filename)) {return;} - var that = this; + Notebook.prototype.load_notebook = function () { + var notebook_id = IPython.save_widget.get_notebook_id(); // We do the call with settings so we can set cache to false. var settings = { processData : false, cache : false, type : "GET", dataType : "json", - success : function (data, status, xhr) { - that.fromJSON(data); - that.filename = filename; - that.kernel.restart(); - } + success : $.proxy(this.notebook_loaded,this) }; - $.ajax("/notebooks/" + filename, settings); + IPython.save_widget.status_loading(); + $.ajax("/notebooks/" + notebook_id, settings); } + + Notebook.prototype.notebook_loaded = function (data, status, xhr) { + this.fromJSON(data); + if (this.ncells() === 0) { + this.insert_code_cell_after(); + }; + IPython.save_widget.status_save(); + IPython.save_widget.set_notebook_name(data.name); + this.start_kernel(); + }; + IPython.Notebook = Notebook; return IPython; diff --git a/IPython/frontend/html/notebook/static/js/notebook_main.js b/IPython/frontend/html/notebook/static/js/notebook_main.js index 57d8909..0d5071d 100644 --- a/IPython/frontend/html/notebook/static/js/notebook_main.js +++ b/IPython/frontend/html/notebook/static/js/notebook_main.js @@ -30,14 +30,18 @@ $(document).ready(function () { IPython.kernel_status_widget.status_idle(); IPython.layout_manager.do_resize(); - IPython.notebook.insert_code_cell_after(); - IPython.layout_manager.do_resize(); // These have display: none in the css file and are made visible here to prevent FLOUC. $('div#header').css('display','block'); $('div#notebook_app').css('display','block'); - IPython.layout_manager.do_resize(); - IPython.pager.collapse(); - IPython.layout_manager.do_resize(); + + IPython.notebook.load_notebook(); + + // Perform these actions after the notebook has been loaded. + setTimeout(function () { + IPython.save_widget.update_url(); + IPython.layout_manager.do_resize(); + IPython.pager.collapse(); + }, 100); }); diff --git a/IPython/frontend/html/notebook/static/js/panelsection.js b/IPython/frontend/html/notebook/static/js/panelsection.js index f95b872..82a76c3 100644 --- a/IPython/frontend/html/notebook/static/js/panelsection.js +++ b/IPython/frontend/html/notebook/static/js/panelsection.js @@ -82,20 +82,31 @@ var IPython = (function (IPython) { this.content.addClass('ui-helper-clearfix'); this.content.find('div.section_row').addClass('ui-helper-clearfix'); this.content.find('#new_open').buttonset(); + this.content.find('#download_notebook').button(); + this.content.find('#upload_notebook').button(); + this.content.find('#download_format').addClass('ui-widget ui-widget-content'); + this.content.find('#download_format option').addClass('ui-widget ui-widget-content'); }; NotebookSection.prototype.bind_events = function () { PanelSection.prototype.bind_events.apply(this); + var that = this; this.content.find('#new_notebook').click(function () { - alert('Not Implemented'); + console.log('click!') + window.open('/'); }); this.content.find('#open_notebook').click(function () { alert('Not Implemented'); }); + this.content.find('#download_notebook').click(function () { + var format = that.content.find('#download_format').val(); + var notebook_id = IPython.save_widget.get_notebook_id(); + var url = '/notebooks/' + notebook_id + '?format=' + format; + window.open(url,'_newtab'); + }); }; - // CellSection var CellSection = function () { diff --git a/IPython/frontend/html/notebook/static/js/savewidget.js b/IPython/frontend/html/notebook/static/js/savewidget.js index 57c3420..bf0e22e 100644 --- a/IPython/frontend/html/notebook/static/js/savewidget.js +++ b/IPython/frontend/html/notebook/static/js/savewidget.js @@ -9,6 +9,7 @@ var IPython = (function (IPython) { var SaveWidget = function (selector) { this.selector = selector; + this.notebook_name_re = /[^/\\]+/ if (this.selector !== undefined) { this.element = $(selector); this.style(); @@ -29,7 +30,7 @@ var IPython = (function (IPython) { SaveWidget.prototype.bind_events = function () { var that = this; this.element.find('button#save_notebook').click(function () { - IPython.notebook.save_notebook(that.get_notebook_name()); + IPython.notebook.save_notebook(); }); }; @@ -39,11 +40,59 @@ var IPython = (function (IPython) { } - SaveWidget.prototype.set_notebook_name = function (name) { - this.element.find('input#notebook_name').attr('value',name); + SaveWidget.prototype.set_notebook_name = function (nbname) { + this.element.find('input#notebook_name').attr('value',nbname); } + SaveWidget.prototype.get_notebook_id = function () { + return this.element.find('span#notebook_id').text() + }; + + + SaveWidget.prototype.update_url = function () { + var notebook_id = this.get_notebook_id(); + if (notebook_id !== '') { + window.history.replaceState({}, '', notebook_id); + }; + }; + + + SaveWidget.prototype.test_notebook_name = function () { + var nbname = this.get_notebook_name(); + if (this.notebook_name_re.test(nbname)) { + return true; + } else { + var bad_name = $('
'); + bad_name.html( + "The notebook name you entered (" + + nbname + + ") is not valid. Notebook names can contain any characters except / and \\" + ); + bad_name.dialog({title: 'Invalid name', modal: true}); + return false; + }; + }; + + + SaveWidget.prototype.status_save = function () { + this.element.find('span.ui-button-text').text('Save'); + this.element.find('button#save_notebook').button('enable'); + }; + + + SaveWidget.prototype.status_saving = function () { + this.element.find('span.ui-button-text').text('Saving'); + this.element.find('button#save_notebook').button('disable'); + }; + + + SaveWidget.prototype.status_loading = function () { + this.element.find('span.ui-button-text').text('Loading'); + this.element.find('button#save_notebook').button('disable'); + }; + + IPython.SaveWidget = SaveWidget; return IPython; diff --git a/IPython/frontend/html/notebook/static/js/textcell.js b/IPython/frontend/html/notebook/static/js/textcell.js index ff3c936..7141ded 100644 --- a/IPython/frontend/html/notebook/static/js/textcell.js +++ b/IPython/frontend/html/notebook/static/js/textcell.js @@ -129,17 +129,19 @@ var IPython = (function (IPython) { TextCell.prototype.fromJSON = function (data) { if (data.cell_type === 'text') { - this.set_text(data.text); - this.grow(this.element.find("textarea.text_cell_input")); + if (data.text !== undefined) { + this.set_text(data.text); + this.grow(this.element.find("textarea.text_cell_input")); + }; }; } TextCell.prototype.toJSON = function () { - return { - cell_type : 'text', - text : this.get_text(), - }; + var data = {} + data.cell_type = 'text'; + data.text = this.get_text(); + return data; }; IPython.TextCell = TextCell; diff --git a/IPython/frontend/html/notebook/templates/notebook.html b/IPython/frontend/html/notebook/templates/notebook.html index 2df57a0..024a7fa 100644 --- a/IPython/frontend/html/notebook/templates/notebook.html +++ b/IPython/frontend/html/notebook/templates/notebook.html @@ -33,6 +33,7 @@

IPython Notebook

+ Idle @@ -52,6 +53,18 @@ Actions
+
+ + + + + + +
diff --git a/IPython/nbformat/current.py b/IPython/nbformat/current.py index f857124..c587bac 100644 --- a/IPython/nbformat/current.py +++ b/IPython/nbformat/current.py @@ -5,6 +5,11 @@ import re from IPython.nbformat import v2 from IPython.nbformat import v1 +from IPython.nbformat.v2 import ( + NotebookNode, + new_code_cell, new_text_cell, new_notebook, new_output, new_worksheet +) + current_nbformat = 2 diff --git a/IPython/nbformat/v2/nbpy.py b/IPython/nbformat/v2/nbpy.py index 1c51cf0..aa775ff 100644 --- a/IPython/nbformat/v2/nbpy.py +++ b/IPython/nbformat/v2/nbpy.py @@ -36,10 +36,11 @@ class PyWriter(NotebookWriter): for ws in nb.worksheets: for cell in ws.cells: if cell.cell_type == 'code': - input = cell.input - lines.extend([u'# ',u'']) - lines.extend(input.splitlines()) - lines.extend([u'',u'# ']) + input = cell.get('input') + if input is not None: + lines.extend([u'# ',u'']) + lines.extend(input.splitlines()) + lines.extend([u'',u'# ']) lines.append('') return unicode('\n'.join(lines))