[Python-modules-commits] [nbformat] 01/07: Import nbformat_4.2.0.orig.tar.gz

Gordon Ball chronitis-guest at moszumanska.debian.org
Sun Dec 4 12:29:55 UTC 2016


This is an automated email from the git hooks/post-receive script.

chronitis-guest pushed a commit to branch master
in repository nbformat.

commit abea36f71cf6aa39a0cf653f6476c033c72ec7f8
Author: Gordon Ball <gordon at chronitis.net>
Date:   Sun Dec 4 12:55:47 2016 +0100

    Import nbformat_4.2.0.orig.tar.gz
---
 .gitignore                          |   8 +-
 .travis.yml                         |   7 +-
 docs/changelog.rst                  |  18 +++
 docs/format_description.rst         |  23 ++-
 nbformat/_version.py                |   2 +-
 nbformat/sign.py                    |  74 ++++++---
 nbformat/tests/test4custom.ipynb    |  52 ++++++
 nbformat/tests/test4docinfo.ipynb   | 311 ++++++++++++++++++++++++++++++++++++
 nbformat/tests/test_sign.py         |  22 ++-
 nbformat/tests/test_validator.py    |  18 ++-
 nbformat/v4/convert.py              |   8 +-
 nbformat/v4/nbbase.py               |   2 +-
 nbformat/v4/nbformat.v4.schema.json |  38 +++--
 nbformat/v4/rwbase.py               |  41 ++++-
 nbformat/v4/tests/nbexamples.py     |  33 +++-
 nbformat/v4/tests/test_convert.py   |   6 +-
 nbformat/v4/tests/test_json.py      |  31 ++++
 nbformat/v4/tests/test_nbbase.py    |  60 ++++---
 nbformat/v4/tests/test_validate.py  |  26 +--
 setup.py                            |   6 +-
 20 files changed, 672 insertions(+), 114 deletions(-)

diff --git a/.gitignore b/.gitignore
index 86345c8..82ec99b 100644
--- a/.gitignore
+++ b/.gitignore
@@ -3,12 +3,7 @@ build
 dist
 _build
 docs/man/*.gz
-docs/source/api/generated
-docs/source/config/options
-docs/source/interactive/magics-generated.txt
-docs/gh-pages
-IPython/html/notebook/static/mathjax
-IPython/html/static/style/*.map
+.cache
 *.py[co]
 __pycache__
 *.egg-info
@@ -20,3 +15,4 @@ __pycache__
 \#*#
 .#*
 .coverage
+.cache
diff --git a/.travis.yml b/.travis.yml
index ef3c53f..be4ef33 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -1,5 +1,6 @@
 language: python
 python:
+    - nightly
     - 3.5
     - 3.4
     - 3.3
@@ -10,6 +11,10 @@ install:
     - pip install . codecov
     - pip install nbformat[test]
 script:
-    - nosetests --with-coverage --cover-package=nbformat nbformat
+    - py.test -v --cov nbformat nbformat
 after_success:
     - codecov
+matrix:
+  allow_failures:
+    - python: "nightly"
+
diff --git a/docs/changelog.rst b/docs/changelog.rst
index 0de1f87..e90f968 100644
--- a/docs/changelog.rst
+++ b/docs/changelog.rst
@@ -4,9 +4,27 @@
 Changes in nbformat
 =========================
 
+4.2
+===
+
+
+4.2.0
+-----
+
+`4.2 on GitHub <https://github.com/jupyter/nbformat/milestones/4.2>`__
+
+- Update nbformat spec version to 4.2, allowing JSON outputs to have any JSONable type,  not just ``object``,
+  and mime-types of the form ``application/anything+json``.
+- Define basics of ``authors`` in notebook metadata.
+  ``nb.metadata.authors`` shall be a list of objects with the property ``name``, a string of each author's full name.
+- Update use of traitlets API to require traitlets 4.1.
+- Support trusting notebooks on stdin with ``cat notebook | jupyter trust``
+
+
 4.1
 ===
 
+
 4.1.0
 -----
 
diff --git a/docs/format_description.rst b/docs/format_description.rst
index 4576e73..ed0a67a 100644
--- a/docs/format_description.rst
+++ b/docs/format_description.rst
@@ -312,12 +312,12 @@ keyed by filename that represents the files attached to the cell.
     {
       "cell_type" : "markdown",
       "metadata" : {},
-      "source" : ["Here is an *inline* image ![inline image](attachment:test.png)"],
+      "source" : ["Here is an *inline* image ![inline image](attachment://test.png)"],
       "attachments" : {
         "test.png": {
-            "image/png" : ["base64-encoded-png-data"],
-        },
-      },
+            "image/png" : "base64-encoded-png-data"
+        }
+      }
     }
 
 
@@ -356,8 +356,23 @@ The following metadata keys are defined at the notebook level:
 Key         Value           Interpretation
 =========== =============== ==============
 kernelspec  dict            A :ref:`kernel specification <kernelspecs>`
+authors     list of dicts   A list of authors of the document
 =========== =============== ==============
 
+A notebook's authors is a list of dictionaries containing information about each author of the notebook.
+Currently, only the name is required.
+Additional fields may be added.
+
+.. sourcecode:: python
+
+    nb.metadata.authors = [
+        {
+            'name': 'Fernando Perez',
+        },
+        {
+            'name': 'Brian Granger',
+        },
+    ]
 
 Cell metadata
 -------------
diff --git a/nbformat/_version.py b/nbformat/_version.py
index cbea018..4723687 100644
--- a/nbformat/_version.py
+++ b/nbformat/_version.py
@@ -1,2 +1,2 @@
-version_info = (4, 1, 0)
+version_info = (4, 2, 0)
 __version__ = '.'.join(map(str, version_info))
diff --git a/nbformat/sign.py b/nbformat/sign.py
index 901c29c..ffd3204 100644
--- a/nbformat/sign.py
+++ b/nbformat/sign.py
@@ -10,6 +10,7 @@ import hashlib
 from hmac import HMAC
 import io
 import os
+import sys
 
 try:
     import sqlite3
@@ -19,16 +20,22 @@ except ImportError:
     except ImportError:
         sqlite3 = None
 
-from ipython_genutils.py3compat import unicode_type, cast_bytes
-from traitlets import Instance, Bytes, Enum, Any, Unicode, Bool, Integer
+from ipython_genutils.py3compat import unicode_type, cast_bytes, cast_unicode
+from traitlets import (
+    Instance, Bytes, Enum, Any, Unicode, Bool, Integer,
+    default, observe,
+)
 from traitlets.config import LoggingConfigurable, MultipleInstanceError
 from jupyter_core.application import JupyterApp, base_flags
 
-from . import read, NO_CONVERT, __version__
+from . import read, reads, NO_CONVERT, __version__
 
 try:
     # Python 3
     algorithms = hashlib.algorithms_guaranteed
+    # shake algorithms in py36 are not compatible with hmac
+    # due to required length argument in digests
+    algorithms = [ a for a in algorithms if not a.startswith('shake_') ]
 except AttributeError:
     algorithms = hashlib.algorithms
 
@@ -87,6 +94,7 @@ class NotebookNotary(LoggingConfigurable):
     """A class for computing and verifying notebook signatures."""
     
     data_dir = Unicode()
+    @default('data_dir')
     def _data_dir_default(self):
         app = None
         try:
@@ -100,24 +108,27 @@ class NotebookNotary(LoggingConfigurable):
             app.initialize(argv=[])
         return app.data_dir
     
-    db_file = Unicode(config=True,
+    db_file = Unicode(
         help="""The sqlite file in which to store notebook signatures.
         By default, this will be in your Jupyter data directory.
         You can set it to ':memory:' to disable sqlite writing to the filesystem.
-        """)
+        """).tag(config=True)
+
+    @default('db_file')
     def _db_file_default(self):
         if not self.data_dir:
             return ':memory:'
         return os.path.join(self.data_dir, u'nbsignatures.db')
     
     # 64k entries ~ 12MB
-    cache_size = Integer(65535, config=True,
+    cache_size = Integer(65535,
         help="""The number of notebook signatures to cache.
         When the number of signatures exceeds this value,
         the oldest 25% of signatures will be culled.
         """
-    )
+    ).tag(config=True)
     db = Any()
+    @default('db')
     def _db_default(self):
         if sqlite3 is None:
             self.log.warn("Missing SQLite3, all notebooks will be untrusted!")
@@ -159,27 +170,31 @@ class NotebookNotary(LoggingConfigurable):
         """)
         db.commit()
     
-    algorithm = Enum(algorithms, default_value='sha256', config=True,
+    algorithm = Enum(algorithms, default_value='sha256',
         help="""The hashing algorithm used to sign notebooks."""
-    )
-    def _algorithm_changed(self, name, old, new):
-        self.digestmod = getattr(hashlib, self.algorithm)
+    ).tag(config=True)
+    @observe('algorithm')
+    def _algorithm_changed(self, change):
+        self.digestmod = getattr(hashlib, change.new)
     
     digestmod = Any()
+    @default('digestmod')
     def _digestmod_default(self):
         return getattr(hashlib, self.algorithm)
     
-    secret_file = Unicode(config=True,
+    secret_file = Unicode(
         help="""The file where the secret key is stored."""
-    )
+    ).tag(config=True)
+    @default('secret_file')
     def _secret_file_default(self):
         if not self.data_dir:
             return ''
         return os.path.join(self.data_dir, 'notebook_secret')
     
-    secret = Bytes(config=True,
+    secret = Bytes(
         help="""The secret key with which notebooks are signed."""
-    )
+    ).tag(config=True)
+    @default('secret')
     def _secret_default(self):
         # note : this assumes an Application is running
         if os.path.exists(self.secret_file):
@@ -391,22 +406,28 @@ class TrustNotebookApp(JupyterApp):
     
     flags = trust_flags
     
-    reset = Bool(False, config=True,
+    reset = Bool(False,
         help="""If True, delete the trusted signature cache.
         After reset, all previously signed notebooks will become untrusted.
         """
-    )
+    ).tag(config=True)
     
     notary = Instance(NotebookNotary)
+    @default('notary')
     def _notary_default(self):
         return NotebookNotary(parent=self, data_dir=self.data_dir)
     
-    def sign_notebook(self, notebook_path):
+    def sign_notebook_file(self, notebook_path):
+        """Sign a notebook from the filesystem"""
         if not os.path.exists(notebook_path):
             self.log.error("Notebook missing: %s" % notebook_path)
             self.exit(1)
         with io.open(notebook_path, encoding='utf8') as f:
             nb = read(f, NO_CONVERT)
+        self.sign_notebook(nb, notebook_path)
+    
+    def sign_notebook(self, nb, notebook_path='<stdin>'):
+        """Sign a notebook that's been loaded"""
         if self.notary.check_signature(nb):
             print("Notebook already signed: %s" % notebook_path)
         else:
@@ -426,9 +447,16 @@ class TrustNotebookApp(JupyterApp):
             self.generate_new_key()
             return
         if not self.extra_args:
-            self.log.critical("Specify at least one notebook to sign.")
-            self.exit(1)
-        
-        for notebook_path in self.extra_args:
-            self.sign_notebook(notebook_path)
+            self.log.debug("Reading notebook from stdin")
+            nb_s = cast_unicode(sys.stdin.read())
+            nb = reads(nb_s, NO_CONVERT)
+            self.sign_notebook(nb, '<stdin>')
+        else:
+            for notebook_path in self.extra_args:
+                self.sign_notebook_file(notebook_path)
+
+
+main = TrustNotebookApp.launch_instance
 
+if __name__ == '__main__':
+    main()
diff --git a/nbformat/tests/test4custom.ipynb b/nbformat/tests/test4custom.ipynb
new file mode 100644
index 0000000..79e5689
--- /dev/null
+++ b/nbformat/tests/test4custom.ipynb
@@ -0,0 +1,52 @@
+{
+ "cells": [
+  {
+   "cell_type": "code",
+   "execution_count": 2,
+   "metadata": {
+    "collapsed": false
+   },
+   "outputs": [
+    {
+     "data": {
+      "application/vnd.raw.v1+json": {
+       "apples": [
+        "🍎",
+        "🍏"
+       ],
+       "bananas": 2,
+       "oranges": "apples"
+      }
+     },
+     "metadata": {},
+     "output_type": "display_data"
+    }
+   ],
+   "source": [
+    "import IPython\n",
+    "\n",
+    "bundle = {}\n",
+    "bundle['application/vnd.raw.v1+json'] = {\n",
+    "    'apples': ['🍎', '🍏'],\n",
+    "    'bananas': 2,\n",
+    "    'oranges': 'apples'\n",
+    "}\n",
+    "\n",
+    "IPython.display.display(bundle, raw=True)"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {
+    "collapsed": true
+   },
+   "outputs": [],
+   "source": []
+  }
+ ],
+ "metadata": {
+ },
+ "nbformat": 4,
+ "nbformat_minor": 0
+}
diff --git a/nbformat/tests/test4docinfo.ipynb b/nbformat/tests/test4docinfo.ipynb
new file mode 100644
index 0000000..7c30892
--- /dev/null
+++ b/nbformat/tests/test4docinfo.ipynb
@@ -0,0 +1,311 @@
+{
+ "cells": [
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "# nbconvert latex test"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "**Lorem ipsum** dolor sit amet, consectetur adipiscing elit. Nunc luctus bibendum felis dictum sodales. Ut suscipit, orci ut interdum imperdiet, purus ligula mollis *justo*, non malesuada nisl augue eget lorem. Donec bibendum, erat sit amet porttitor aliquam, urna lorem ornare libero, in vehicula diam diam ut ante. Nam non urna rhoncus, accumsan elit sit amet, mollis tellus. Vestibulum nec tellus metus. Vestibulum tempor, ligula et vehicula rhoncus, sapien turpis faucibus lorem, id  [...]
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "## Printed Using Python"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 1,
+   "metadata": {
+    "collapsed": false
+   },
+   "outputs": [
+    {
+     "name": "stdout",
+     "output_type": "stream",
+     "text": [
+      "hello\n"
+     ]
+    }
+   ],
+   "source": [
+    "print(\"hello\")"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "## Pyout"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 3,
+   "metadata": {
+    "collapsed": false
+   },
+   "outputs": [
+    {
+     "data": {
+      "text/html": [
+       "\n",
+       "<script>\n",
+       "console.log(\"hello\");\n",
+       "</script>\n",
+       "<b>HTML</b>\n"
+      ],
+      "text/plain": [
+       "<IPython.core.display.HTML at 0x1112757d0>"
+      ]
+     },
+     "execution_count": 3,
+     "metadata": {},
+     "output_type": "execute_result"
+    }
+   ],
+   "source": [
+    "from IPython.display import HTML\n",
+    "HTML(\"\"\"\n",
+    "<script>\n",
+    "console.log(\"hello\");\n",
+    "</script>\n",
+    "<b>HTML</b>\n",
+    "\"\"\")"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 7,
+   "metadata": {
+    "collapsed": false
+   },
+   "outputs": [
+    {
+     "data": {
+      "application/javascript": [
+       "console.log(\"hi\");"
+      ],
+      "text/plain": [
+       "<IPython.core.display.Javascript at 0x1112b4b50>"
+      ]
+     },
+     "metadata": {},
+     "output_type": "display_data"
+    }
+   ],
+   "source": [
+    "%%javascript\n",
+    "console.log(\"hi\");"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "### Image"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 6,
+   "metadata": {
+    "collapsed": false
+   },
+   "outputs": [
+    {
+     "data": {
+      "image/png": [
+       "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\n"
+      ],
+      "text/plain": [
+       "<IPython.core.display.Image at 0x111275490>"
+      ]
+     },
+     "execution_count": 6,
+     "metadata": {},
+     "output_type": "execute_result"
+    }
+   ],
+   "source": [
+    "from IPython.display import Image\n",
+    "Image(\"http://ipython.org/_static/IPy_header.png\")"
+   ]
+  }
+ ],
+ "metadata": {
+   "title": "Test Notebook",
+   "authors": [{"name": "Jean Tester"}]
+ },
+ "nbformat": 4,
+ "nbformat_minor": 0
+}
diff --git a/nbformat/tests/test_sign.py b/nbformat/tests/test_sign.py
index b4473ae..707d922 100644
--- a/nbformat/tests/test_sign.py
+++ b/nbformat/tests/test_sign.py
@@ -3,16 +3,19 @@
 # Copyright (c) IPython Development Team.
 # Distributed under the terms of the Modified BSD License.
 
+import codecs
 import copy
 import os
 import shutil
+from subprocess import Popen, PIPE
+import sys
 import time
 import tempfile
 import testpath
 
 from .base import TestsBase
 
-from nbformat import read, sign
+from nbformat import read, sign, write
 
 class TestNotary(TestsBase):
     
@@ -207,4 +210,21 @@ class TestNotary(TestsBase):
         self.assertFalse(self.notary.check_cells(nb))
         for cell in cells:
             self.assertNotIn('trusted', cell)
+    
+    def test_sign_stdin(self):
+        def sign_stdin(nb):
+            env = os.environ.copy()
+            env["JUPYTER_DATA_DIR"] = self.data_dir
+            p = Popen([sys.executable, '-m', 'nbformat.sign', '--log-level=0'], stdin=PIPE, stdout=PIPE,
+                env=env,
+            )
+            write(nb, codecs.getwriter("utf8")(p.stdin))
+            p.stdin.close()
+            p.wait()
+            self.assertEqual(p.returncode, 0)
+            return p.stdout.read().decode('utf8', 'replace')
 
+        out = sign_stdin(self.nb3)
+        self.assertIn('Signing notebook: <stdin>', out)
+        out = sign_stdin(self.nb3)
+        self.assertIn('already signed: <stdin>', out)
diff --git a/nbformat/tests/test_validator.py b/nbformat/tests/test_validator.py
index 2059c2b..8bd28e9 100644
--- a/nbformat/tests/test_validator.py
+++ b/nbformat/tests/test_validator.py
@@ -34,6 +34,20 @@ class TestValidator(TestsBase):
         validate(nb)
         self.assertEqual(isvalid(nb), True)
 
+    def test_nb4_document_info(self):
+        """Test that a notebook with document_info passes validation"""
+        with self.fopen(u'test4docinfo.ipynb', u'r') as f:
+            nb = read(f, as_version=4)
+        validate(nb)
+        self.assertEqual(isvalid(nb), True)
+
+    def test_nb4custom(self):
+        """Test that a notebook with a custom JSON mimetype passes validation"""
+        with self.fopen(u'test4custom.ipynb', u'r') as f:
+            nb = read(f, as_version=4)
+        validate(nb)
+        self.assertEqual(isvalid(nb), True)
+
     def test_invalid(self):
         """Test than an invalid notebook does not pass validation"""
         # this notebook has a few different errors:
@@ -52,10 +66,10 @@ class TestValidator(TestsBase):
             nb = read(f, as_version=4)
         with self.assertRaises(ValidationError):
             validate(nb, version=4)
-        
+
         self.assertEqual(isvalid(nb, version=4), False)
         self.assertEqual(isvalid(nb), True)
-    
+
     def test_validation_error(self):
         with self.fopen(u'invalid.ipynb', u'r') as f:
             nb = read(f, as_version=4)
diff --git a/nbformat/v4/convert.py b/nbformat/v4/convert.py
index 557ca7a..b727e19 100644
--- a/nbformat/v4/convert.py
+++ b/nbformat/v4/convert.py
@@ -128,6 +128,7 @@ def downgrade_cell(cell):
             cell.cell_type = 'heading'
             cell.source = text
             cell.level = len(prefix)
+    cell.pop('attachments', None)
     return cell
 
 _mime_map = {
@@ -150,10 +151,11 @@ def to_mime_key(d):
 
 def from_mime_key(d):
     """convert dict with mime-type keys to v3 aliases"""
+    d2 = {}
     for alias, mime in _mime_map.items():
         if mime in d:
-            d[alias] = d.pop(mime)
-    return d
+            d2[alias] = d[mime]
+    return d2
 
 def upgrade_output(output):
     """upgrade a single code cell output from v3 to v4
@@ -209,7 +211,7 @@ def downgrade_output(output):
         data = output.pop('data', {})
         if 'application/json' in data:
             data['application/json'] = json.dumps(data['application/json'])
-        from_mime_key(data)
+        data = from_mime_key(data)
         output.update(data)
         from_mime_key(output.get('metadata', {}))
     elif output['output_type'] == 'error':
diff --git a/nbformat/v4/nbbase.py b/nbformat/v4/nbbase.py
index 4253971..f0294ab 100644
--- a/nbformat/v4/nbbase.py
+++ b/nbformat/v4/nbbase.py
@@ -13,7 +13,7 @@ from ..notebooknode import from_dict, NotebookNode
 
 # Change this when incrementing the nbformat version
 nbformat = 4
-nbformat_minor = 1
+nbformat_minor = 2
 nbformat_schema = 'nbformat.v4.schema.json'
 
 
diff --git a/nbformat/v4/nbformat.v4.schema.json b/nbformat/v4/nbformat.v4.schema.json
index 65174b6..8e34431 100644
--- a/nbformat/v4/nbformat.v4.schema.json
+++ b/nbformat/v4/nbformat.v4.schema.json
@@ -1,6 +1,6 @@
 {
     "$schema": "http://json-schema.org/draft-04/schema#",
-    "description": "IPython Notebook v4.0 JSON schema.",
+    "description": "Jupyter Notebook v4.2 JSON schema.",
     "type": "object",
     "additionalProperties": false,
     "required": ["metadata", "nbformat_minor", "nbformat", "cells"],
@@ -59,6 +59,23 @@
                     "description": "Original notebook format (major number) before converting the notebook between versions. This should never be written to a file.",
                     "type": "integer",
                     "minimum": 1
+                },
+                "title": {
+                    "description": "The title of the notebook document",
+                    "type": "string"
+                },
+                "authors": {
+                    "description": "The author(s) of the notebook document",
+                    "type": "array",
+                    "item": {
+                      "type": "object",
+                      "properties": {
+                        "name": {
+                          "type": "string"
+                        }
+                      },
+                      "additionalProperties": true
+                    }
                 }
             }
         },
@@ -182,7 +199,7 @@
                 }
             }
         },
-        
+
         "unrecognized_cell": {
             "description": "Unrecognized cell from a future minor-revision to the notebook format.",
             "type": "object",
@@ -206,7 +223,7 @@
                 }
             }
         },
-        
+
         "output": {
             "type": "object",
             "oneOf": [
@@ -316,7 +333,7 @@
 
         "misc": {
             "metadata_name": {
-                "description": "The cell's name. If present, must be a non-empty string.",
+                "description": "The cell's name. If present, must be a non-empty string. Must be unique across all the cells of a given notebook.",
                 "type": "string",
                 "pattern": "^.+$"
             },
@@ -351,16 +368,13 @@
             "mimebundle": {
                 "description": "A mime-type keyed dictionary of data",
                 "type": "object",
-                "additionalProperties": false,
-                "properties": {
-                    "application/json": {
-                        "type": "object"
-                    }
+                "additionalProperties": {
+                  "description": "mimetype output (e.g. text/plain), represented as either an array of strings or a string.",
+                  "$ref": "#/definitions/misc/multiline_string"
                 },
                 "patternProperties": {
-                    "^(?!application/json$)[a-zA-Z0-9]+/[a-zA-Z0-9\\-\\+\\.]+$": {
-                        "description": "mimetype output (e.g. text/plain), represented as either an array of strings or a string.",
-                        "$ref": "#/definitions/misc/multiline_string"
+                    "^application/(.*\\+)?json$": {
+                        "description": "Mimetypes with JSON output, can be any type"
                     }
                 }
             },
diff --git a/nbformat/v4/rwbase.py b/nbformat/v4/rwbase.py
index dd4e226..42dfac8 100644
--- a/nbformat/v4/rwbase.py
+++ b/nbformat/v4/rwbase.py
@@ -5,6 +5,19 @@
 
 from ipython_genutils.py3compat import string_types, cast_unicode_py2
 
+def _is_json_mime(mime):
+    """Is a key a JSON mime-type that should be left alone?"""
+    return mime == 'application/json' or \
+        (mime.startswith('application/') and mime.endswith('+json'))
+
+def _rejoin_mimebundle(data):
+    """Rejoin the multi-line string fields in a mimebundle (in-place)"""
+    for key, value in list(data.items()):
+        if not _is_json_mime(key) \
+        and isinstance(value, list) \
+        and all(isinstance(line, string_types) for line in value):
+            data[key] = ''.join(value)
+    return data
 
 def rejoin_lines(nb):
     """rejoin multiline text into strings
@@ -19,13 +32,16 @@ def rejoin_lines(nb):
     for cell in nb.cells:
         if 'source' in cell and isinstance(cell.source, list):
             cell.source = ''.join(cell.source)
+
+        attachments = cell.get('attachments', {})
+        for key, attachment in attachments.items():
+            _rejoin_mimebundle(attachment)
+
         if cell.get('cell_type', None) == 'code':
             for output in cell.get('outputs', []):
                 output_type = output.get('output_type', '')
                 if output_type in {'execute_result', 'display_data'}:
-                    for key, value in output.get('data', {}).items():
-                        if key != 'application/json' and isinstance(value, list):
-                            output.data[key] = ''.join(value)
+                    _rejoin_mimebundle(output.get('data', {}))
                 elif output_type:
                     if isinstance(output.get('text', ''), list):
                         output.text = ''.join(output.text)
@@ -36,6 +52,15 @@ _non_text_split_mimes = {
     'image/svg+xml',
 }
 
+def _split_mimebundle(data):
+    """Split multi-line string fields in a mimebundle (in-place)"""
+    for key, value in list(data.items()):
+        if isinstance(value, string_types) and (
+            key.startswith('text/') or key in _non_text_split_mimes
+        ):
+            data[key] = value.splitlines(True)
+    return data
+
 def split_lines(nb):
     """split likely multiline text into lists of strings
 
@@ -49,14 +74,14 @@ def split_lines(nb):
         if isinstance(source, string_types):
             cell['source'] = source.splitlines(True)
 
+        attachments = cell.get('attachments', {})
+        for key, attachment in attachments.items():
+            _split_mimebundle(attachment)
+
         if cell.cell_type == 'code':
             for output in cell.outputs:
                 if output.output_type in {'execute_result', 'display_data'}:
-                    for key, value in output.data.items():
-                        if isinstance(value, string_types) and (
-                            key.startswith('text/') or key in _non_text_split_mimes
-                        ):
-                            output.data[key] = value.splitlines(True)
... 378 lines suppressed ...

-- 
Alioth's /usr/local/bin/git-commit-notice on /srv/git.debian.org/git/python-modules/packages/nbformat.git



More information about the Python-modules-commits mailing list