From 8ee671a6ed8b3d28a0db296a1bd2975e17cceffb 2014-11-01 23:41:00
From: MinRK <benjaminrk@gmail.com>
Date: 2014-11-01 23:41:00
Subject: [PATCH] test and update conversion between v3 <-> v4

---

diff --git a/IPython/nbformat/v4/convert.py b/IPython/nbformat/v4/convert.py
index a7ec9b5..3368cae 100644
--- a/IPython/nbformat/v4/convert.py
+++ b/IPython/nbformat/v4/convert.py
@@ -3,15 +3,15 @@
 # Copyright (c) IPython Development Team.
 # Distributed under the terms of the Modified BSD License.
 
+import json
+
 from .nbbase import (
     nbformat, nbformat_minor,
+    NotebookNode,
 )
 
 from IPython.nbformat import v3
 
-#-----------------------------------------------------------------------------
-# Code
-#-----------------------------------------------------------------------------
 
 def upgrade(nb, from_version=3, from_minor=0):
     """Convert a notebook to v4.
@@ -25,12 +25,17 @@ def upgrade(nb, from_version=3, from_minor=0):
     from_minor : int
         The original minor version of the notebook to convert (only relevant for v >= 3).
     """
+    from IPython.nbformat.current import validate
     if from_version == 3:
+        # Validate the notebook before conversion
+        validate(nb, version=from_version)
+
         # Mark the original nbformat so consumers know it has been converted.
+        nb.metadata.orig_nbformat = 3
+        # Mark the new format
         nb.nbformat = nbformat
         nb.nbformat_minor = nbformat_minor
 
-        nb.orig_nbformat = 3
         # remove worksheet(s)
         nb['cells'] = cells = []
         # In the unlikely event of multiple worksheets,
@@ -39,14 +44,17 @@ def upgrade(nb, from_version=3, from_minor=0):
             # upgrade each cell
             for cell in ws['cells']:
                 cells.append(upgrade_cell(cell))
-        # upgrade metadata?
+        # upgrade metadata
         nb.metadata.pop('name', '')
+        # Validate the converted notebook before returning it
+        validate(nb, version=nbformat)
         return nb
     elif from_version == 4:
         # nothing to do
         if from_minor != nbformat_minor:
-            nb.orig_nbformat_minor = from_minor
+            nb.metadata.orig_nbformat_minor = from_minor
         nb.nbformat_minor = nbformat_minor
+
         return nb
     else:
         raise ValueError('Cannot convert a notebook directly from v%s to v4.  ' \
@@ -60,16 +68,24 @@ def upgrade_cell(cell):
         - cell.input -> cell.source
         - update outputs
     """
+    cell.setdefault('metadata', NotebookNode())
     if cell.cell_type == 'code':
-        cell.metadata.pop('language', '')
+        cell.pop('language', '')
+        cell.metadata.collapsed = cell.pop('collapsed')
         cell.source = cell.pop('input')
-        cell.outputs = upgrade_outputs(cell)
+        cell.setdefault('prompt_number', None)
+        cell.outputs = upgrade_outputs(cell.outputs)
+    elif cell.cell_type == 'html':
+        # Technically, this exists. It will never happen in practice.
+        cell.cell_type = 'markdown'
     return cell
 
 def downgrade_cell(cell):
     if cell.cell_type == 'code':
-        cell.input = cell.pop('source')
-        cell.outputs = downgrade_outputs(cell)
+        cell.language = 'python'
+        cell.input = cell.pop('source', '')
+        cell.collapsed = cell.metadata.pop('collapsed', False)
+        cell.outputs = downgrade_outputs(cell.outputs)
     return cell
 
 _mime_map = {
@@ -103,16 +119,24 @@ def upgrade_output(output):
     - pyout -> execute_result
     - pyerr -> error
     - mime-type keys
+    - stream.stream -> stream.name
     """
-    if output['output_type'] == 'pyout':
-        output['output_type'] = 'execute_result'
+    output.setdefault('metadata', NotebookNode())
+    if output['output_type'] in {'pyout', 'display_data'}:
+        if output['output_type'] == 'pyout':
+            output['output_type'] = 'execute_result'
         to_mime_key(output)
-        to_mime_key(output.get('metadata', {}))
+        to_mime_key(output.metadata)
+        if 'application/json' in output:
+            output['application/json'] = json.loads(output['application/json'])
+        # promote ascii bytes (from v2) to unicode
+        for key in ('image/png', 'image/jpeg'):
+            if key in output and isinstance(output[key], bytes):
+                output[key] = output[key].decode('ascii')
     elif output['output_type'] == 'pyerr':
         output['output_type'] = 'error'
-    elif output['output_type'] == 'display_data':
-        to_mime_key(output)
-        to_mime_key(output.get('metadata', {}))
+    elif output['output_type'] == 'stream':
+        output['name'] = output.pop('stream')
     return output
 
 def downgrade_output(output):
@@ -121,16 +145,24 @@ def downgrade_output(output):
     - pyout <- execute_result
     - pyerr <- error
     - un-mime-type keys
+    - stream.stream <- stream.name
     """
     if output['output_type'] == 'execute_result':
         output['output_type'] = 'pyout'
+        if 'application/json' in output:
+            output['application/json'] = json.dumps(output['application/json'])
         from_mime_key(output)
         from_mime_key(output.get('metadata', {}))
     elif output['output_type'] == 'error':
         output['output_type'] = 'pyerr'
     elif output['output_type'] == 'display_data':
+        if 'application/json' in output:
+            output['application/json'] = json.dumps(output['application/json'])
         from_mime_key(output)
         from_mime_key(output.get('metadata', {}))
+    elif output['output_type'] == 'stream':
+        output['stream'] = output.pop('name')
+        output.pop('metadata')
     return output
 
 def upgrade_outputs(outputs):
@@ -149,11 +181,20 @@ def downgrade(nb):
     nb : NotebookNode
         The Python representation of the notebook to convert.
     """
+    from IPython.nbformat.current import validate
+    # Validate the notebook before conversion
+    validate(nb, version=nbformat)
+
     if nb.nbformat != 4:
         return nb
     nb.nbformat = v3.nbformat
     nb.nbformat_minor = v3.nbformat_minor
-    cells = [ downgrade_cell(cell) for cell in nb.cells ]
+    cells = [ downgrade_cell(cell) for cell in nb.pop('cells') ]
     nb.worksheets = [v3.new_worksheet(cells=cells)]
     nb.metadata.setdefault('name', '')
-    return nb
\ No newline at end of file
+    nb.metadata.pop('orig_nbformat', None)
+    nb.metadata.pop('orig_nbformat_minor', None)
+
+    # Validate the converted notebook before returning it
+    validate(nb, version=v3.nbformat)
+    return nb
diff --git a/IPython/nbformat/v4/tests/test_convert.py b/IPython/nbformat/v4/tests/test_convert.py
new file mode 100644
index 0000000..06f59e1
--- /dev/null
+++ b/IPython/nbformat/v4/tests/test_convert.py
@@ -0,0 +1,19 @@
+import copy
+
+from IPython.nbformat.current import validate
+from .. import convert
+
+from . import nbexamples
+from IPython.nbformat.v3.tests import nbexamples as v3examples
+
+def test_upgrade_notebook():
+    nb03 = copy.deepcopy(v3examples.nb0)
+    validate(nb03)
+    nb04 = convert.upgrade(nb03)
+    validate(nb04)
+
+def test_downgrade_notebook():
+    nb04 = copy.deepcopy(nbexamples.nb0)
+    validate(nb04)
+    nb03 = convert.downgrade(nb04)
+    validate(nb03)