##// END OF EJS Templates
don't store signatures in notebooks...
Min RK -
Show More
@@ -1,331 +1,411 b''
1 """Functions for signing notebooks"""
1 """Utilities for signing notebooks"""
2
2
3 # Copyright (c) IPython Development Team.
3 # Copyright (c) IPython Development Team.
4 # Distributed under the terms of the Modified BSD License.
4 # Distributed under the terms of the Modified BSD License.
5
5
6 import base64
6 import base64
7 from contextlib import contextmanager
7 from contextlib import contextmanager
8 from datetime import datetime
8 import hashlib
9 import hashlib
9 from hmac import HMAC
10 from hmac import HMAC
10 import io
11 import io
11 import os
12 import os
12
13
14 try:
15 import sqlite3
16 except ImportError:
17 try:
18 from pysqlite2 import dbapi2 as sqlite3
19 except ImportError:
20 sqlite3 = None
21
13 from IPython.utils.io import atomic_writing
22 from IPython.utils.io import atomic_writing
14 from IPython.utils.py3compat import string_types, unicode_type, cast_bytes
23 from IPython.utils.py3compat import unicode_type, cast_bytes
15 from IPython.utils.traitlets import Instance, Bytes, Enum, Any, Unicode, Bool
24 from IPython.utils.traitlets import Instance, Bytes, Enum, Any, Unicode, Bool, Integer
16 from IPython.config import LoggingConfigurable, MultipleInstanceError
25 from IPython.config import LoggingConfigurable, MultipleInstanceError
17 from IPython.core.application import BaseIPythonApplication, base_flags
26 from IPython.core.application import BaseIPythonApplication, base_flags
18
27
19 from . import read, write, NO_CONVERT
28 from . import read, write, NO_CONVERT
20
29
21 try:
30 try:
22 # Python 3
31 # Python 3
23 algorithms = hashlib.algorithms_guaranteed
32 algorithms = hashlib.algorithms_guaranteed
24 except AttributeError:
33 except AttributeError:
25 algorithms = hashlib.algorithms
34 algorithms = hashlib.algorithms
26
35
27
36
28 def yield_everything(obj):
37 def yield_everything(obj):
29 """Yield every item in a container as bytes
38 """Yield every item in a container as bytes
30
39
31 Allows any JSONable object to be passed to an HMAC digester
40 Allows any JSONable object to be passed to an HMAC digester
32 without having to serialize the whole thing.
41 without having to serialize the whole thing.
33 """
42 """
34 if isinstance(obj, dict):
43 if isinstance(obj, dict):
35 for key in sorted(obj):
44 for key in sorted(obj):
36 value = obj[key]
45 value = obj[key]
37 yield cast_bytes(key)
46 yield cast_bytes(key)
38 for b in yield_everything(value):
47 for b in yield_everything(value):
39 yield b
48 yield b
40 elif isinstance(obj, (list, tuple)):
49 elif isinstance(obj, (list, tuple)):
41 for element in obj:
50 for element in obj:
42 for b in yield_everything(element):
51 for b in yield_everything(element):
43 yield b
52 yield b
44 elif isinstance(obj, unicode_type):
53 elif isinstance(obj, unicode_type):
45 yield obj.encode('utf8')
54 yield obj.encode('utf8')
46 else:
55 else:
47 yield unicode_type(obj).encode('utf8')
56 yield unicode_type(obj).encode('utf8')
48
57
49 def yield_code_cells(nb):
58 def yield_code_cells(nb):
50 """Iterator that yields all cells in a notebook
59 """Iterator that yields all cells in a notebook
51
60
52 nbformat version independent
61 nbformat version independent
53 """
62 """
54 if nb.nbformat >= 4:
63 if nb.nbformat >= 4:
55 for cell in nb['cells']:
64 for cell in nb['cells']:
56 if cell['cell_type'] == 'code':
65 if cell['cell_type'] == 'code':
57 yield cell
66 yield cell
58 elif nb.nbformat == 3:
67 elif nb.nbformat == 3:
59 for ws in nb['worksheets']:
68 for ws in nb['worksheets']:
60 for cell in ws['cells']:
69 for cell in ws['cells']:
61 if cell['cell_type'] == 'code':
70 if cell['cell_type'] == 'code':
62 yield cell
71 yield cell
63
72
64 @contextmanager
73 @contextmanager
65 def signature_removed(nb):
74 def signature_removed(nb):
66 """Context manager for operating on a notebook with its signature removed
75 """Context manager for operating on a notebook with its signature removed
67
76
68 Used for excluding the previous signature when computing a notebook's signature.
77 Used for excluding the previous signature when computing a notebook's signature.
69 """
78 """
70 save_signature = nb['metadata'].pop('signature', None)
79 save_signature = nb['metadata'].pop('signature', None)
71 try:
80 try:
72 yield
81 yield
73 finally:
82 finally:
74 if save_signature is not None:
83 if save_signature is not None:
75 nb['metadata']['signature'] = save_signature
84 nb['metadata']['signature'] = save_signature
76
85
77
86
78 class NotebookNotary(LoggingConfigurable):
87 class NotebookNotary(LoggingConfigurable):
79 """A class for computing and verifying notebook signatures."""
88 """A class for computing and verifying notebook signatures."""
80
89
81 profile_dir = Instance("IPython.core.profiledir.ProfileDir")
90 profile_dir = Instance("IPython.core.profiledir.ProfileDir")
82 def _profile_dir_default(self):
91 def _profile_dir_default(self):
83 from IPython.core.application import BaseIPythonApplication
92 from IPython.core.application import BaseIPythonApplication
84 app = None
93 app = None
85 try:
94 try:
86 if BaseIPythonApplication.initialized():
95 if BaseIPythonApplication.initialized():
87 app = BaseIPythonApplication.instance()
96 app = BaseIPythonApplication.instance()
88 except MultipleInstanceError:
97 except MultipleInstanceError:
89 pass
98 pass
90 if app is None:
99 if app is None:
91 # create an app, without the global instance
100 # create an app, without the global instance
92 app = BaseIPythonApplication()
101 app = BaseIPythonApplication()
93 app.initialize(argv=[])
102 app.initialize(argv=[])
94 return app.profile_dir
103 return app.profile_dir
95
104
105 db_file = Unicode(config=True)
106 def _db_file_default(self):
107 if self.profile_dir is None:
108 return ':memory:'
109 return os.path.join(self.profile_dir.security_dir, u'nbsignatures.db')
110
111 # 64k entries ~ 12MB
112 db_size_limit = Integer(65535, config=True)
113 db = Any()
114 def _db_default(self):
115 if sqlite3 is None:
116 self.log.warn("Missing SQLite3, all notebooks will be untrusted!")
117 return
118 kwargs = dict(detect_types=sqlite3.PARSE_DECLTYPES|sqlite3.PARSE_COLNAMES)
119 db = sqlite3.connect(self.db_file, **kwargs)
120 self.init_db(db)
121 return db
122
123 def init_db(self, db):
124 db.execute("""
125 CREATE TABLE IF NOT EXISTS nbsignatures
126 (
127 id integer PRIMARY KEY AUTOINCREMENT,
128 algorithm text,
129 signature text,
130 path text,
131 last_seen timestamp
132 )""")
133 db.execute("""
134 CREATE INDEX IF NOT EXISTS algosig ON nbsignatures(algorithm, signature)
135 """)
136 db.commit()
137
96 algorithm = Enum(algorithms, default_value='sha256', config=True,
138 algorithm = Enum(algorithms, default_value='sha256', config=True,
97 help="""The hashing algorithm used to sign notebooks."""
139 help="""The hashing algorithm used to sign notebooks."""
98 )
140 )
99 def _algorithm_changed(self, name, old, new):
141 def _algorithm_changed(self, name, old, new):
100 self.digestmod = getattr(hashlib, self.algorithm)
142 self.digestmod = getattr(hashlib, self.algorithm)
101
143
102 digestmod = Any()
144 digestmod = Any()
103 def _digestmod_default(self):
145 def _digestmod_default(self):
104 return getattr(hashlib, self.algorithm)
146 return getattr(hashlib, self.algorithm)
105
147
106 secret_file = Unicode(config=True,
148 secret_file = Unicode(config=True,
107 help="""The file where the secret key is stored."""
149 help="""The file where the secret key is stored."""
108 )
150 )
109 def _secret_file_default(self):
151 def _secret_file_default(self):
110 if self.profile_dir is None:
152 if self.profile_dir is None:
111 return ''
153 return ''
112 return os.path.join(self.profile_dir.security_dir, 'notebook_secret')
154 return os.path.join(self.profile_dir.security_dir, 'notebook_secret')
113
155
114 secret = Bytes(config=True,
156 secret = Bytes(config=True,
115 help="""The secret key with which notebooks are signed."""
157 help="""The secret key with which notebooks are signed."""
116 )
158 )
117 def _secret_default(self):
159 def _secret_default(self):
118 # note : this assumes an Application is running
160 # note : this assumes an Application is running
119 if os.path.exists(self.secret_file):
161 if os.path.exists(self.secret_file):
120 with io.open(self.secret_file, 'rb') as f:
162 with io.open(self.secret_file, 'rb') as f:
121 return f.read()
163 return f.read()
122 else:
164 else:
123 secret = base64.encodestring(os.urandom(1024))
165 secret = base64.encodestring(os.urandom(1024))
124 self._write_secret_file(secret)
166 self._write_secret_file(secret)
125 return secret
167 return secret
126
168
127 def _write_secret_file(self, secret):
169 def _write_secret_file(self, secret):
128 """write my secret to my secret_file"""
170 """write my secret to my secret_file"""
129 self.log.info("Writing notebook-signing key to %s", self.secret_file)
171 self.log.info("Writing notebook-signing key to %s", self.secret_file)
130 with io.open(self.secret_file, 'wb') as f:
172 with io.open(self.secret_file, 'wb') as f:
131 f.write(secret)
173 f.write(secret)
132 try:
174 try:
133 os.chmod(self.secret_file, 0o600)
175 os.chmod(self.secret_file, 0o600)
134 except OSError:
176 except OSError:
135 self.log.warn(
177 self.log.warn(
136 "Could not set permissions on %s",
178 "Could not set permissions on %s",
137 self.secret_file
179 self.secret_file
138 )
180 )
139 return secret
181 return secret
140
182
141 def compute_signature(self, nb):
183 def compute_signature(self, nb):
142 """Compute a notebook's signature
184 """Compute a notebook's signature
143
185
144 by hashing the entire contents of the notebook via HMAC digest.
186 by hashing the entire contents of the notebook via HMAC digest.
145 """
187 """
146 hmac = HMAC(self.secret, digestmod=self.digestmod)
188 hmac = HMAC(self.secret, digestmod=self.digestmod)
147 # don't include the previous hash in the content to hash
189 # don't include the previous hash in the content to hash
148 with signature_removed(nb):
190 with signature_removed(nb):
149 # sign the whole thing
191 # sign the whole thing
150 for b in yield_everything(nb):
192 for b in yield_everything(nb):
151 hmac.update(b)
193 hmac.update(b)
152
194
153 return hmac.hexdigest()
195 return hmac.hexdigest()
154
196
155 def check_signature(self, nb):
197 def check_signature(self, nb):
156 """Check a notebook's stored signature
198 """Check a notebook's stored signature
157
199
158 If a signature is stored in the notebook's metadata,
200 If a signature is stored in the notebook's metadata,
159 a new signature is computed and compared with the stored value.
201 a new signature is computed and compared with the stored value.
160
202
161 Returns True if the signature is found and matches, False otherwise.
203 Returns True if the signature is found and matches, False otherwise.
162
204
163 The following conditions must all be met for a notebook to be trusted:
205 The following conditions must all be met for a notebook to be trusted:
164 - a signature is stored in the form 'scheme:hexdigest'
206 - a signature is stored in the form 'scheme:hexdigest'
165 - the stored scheme matches the requested scheme
207 - the stored scheme matches the requested scheme
166 - the requested scheme is available from hashlib
208 - the requested scheme is available from hashlib
167 - the computed hash from notebook_signature matches the stored hash
209 - the computed hash from notebook_signature matches the stored hash
168 """
210 """
169 if nb.nbformat < 3:
211 if nb.nbformat < 3:
170 return False
212 return False
171 stored_signature = nb['metadata'].get('signature', None)
213 if self.db is None:
172 if not stored_signature \
173 or not isinstance(stored_signature, string_types) \
174 or ':' not in stored_signature:
175 return False
214 return False
176 stored_algo, sig = stored_signature.split(':', 1)
215 signature = self.compute_signature(nb)
177 if self.algorithm != stored_algo:
216 r = self.db.execute("""SELECT id FROM nbsignatures WHERE
217 algorithm = ? AND
218 signature = ?;
219 """, (self.algorithm, signature)).fetchone()
220 if r is None:
178 return False
221 return False
179 my_signature = self.compute_signature(nb)
222 self.db.execute("""UPDATE nbsignatures SET last_seen = ? WHERE
180 return my_signature == sig
223 algorithm = ? AND
224 signature = ?;
225 """,
226 (datetime.utcnow(), self.algorithm, signature),
227 )
228 self.db.commit()
229 return True
181
230
182 def sign(self, nb):
231 def sign(self, nb):
183 """Sign a notebook, indicating that its output is trusted
232 """Sign a notebook, indicating that its output is trusted on this machine
184
185 stores 'algo:hmac-hexdigest' in notebook.metadata.signature
186
233
187 e.g. 'sha256:deadbeef123...'
234 Stores hash algorithm and hmac digest in a local database of trusted notebooks.
188 """
235 """
189 if nb.nbformat < 3:
236 if nb.nbformat < 3:
190 return
237 return
191 signature = self.compute_signature(nb)
238 signature = self.compute_signature(nb)
192 nb['metadata']['signature'] = "%s:%s" % (self.algorithm, signature)
239 self.store_signature(signature, nb)
240
241 def store_signature(self, signature, nb):
242 if self.db is None:
243 return
244 self.db.execute("""INSERT OR IGNORE INTO nbsignatures
245 (algorithm, signature, last_seen) VALUES (?, ?, ?)""",
246 (self.algorithm, signature, datetime.utcnow())
247 )
248 self.db.execute("""UPDATE nbsignatures SET last_seen = ? WHERE
249 algorithm = ? AND
250 signature = ?;
251 """,
252 (datetime.utcnow(), self.algorithm, signature),
253 )
254 self.db.commit()
255
256 def unsign(self, nb):
257 """Ensure that a notebook is untrusted
258
259 by removing its signature from the trusted database, if present.
260 """
261 signature = self.compute_signature(nb)
262 self.db.execute("""DELETE FROM nbsignatures WHERE
263 algorithm = ? AND
264 signature = ?;
265 """,
266 (self.algorithm, signature)
267 )
268 self.db.commit()
269
270 def cull_db(self):
271 self.db.execute("""DELETE FROM nbsignatures WHERE id IN (
272 SELECT id FROM nbsignatures ORDER BY last_seen DESC LIMIT -1 OFFSET ?
273 );
274 """, (self.db_size_limit,))
193
275
194 def mark_cells(self, nb, trusted):
276 def mark_cells(self, nb, trusted):
195 """Mark cells as trusted if the notebook's signature can be verified
277 """Mark cells as trusted if the notebook's signature can be verified
196
278
197 Sets ``cell.metadata.trusted = True | False`` on all code cells,
279 Sets ``cell.metadata.trusted = True | False`` on all code cells,
198 depending on whether the stored signature can be verified.
280 depending on whether the stored signature can be verified.
199
281
200 This function is the inverse of check_cells
282 This function is the inverse of check_cells
201 """
283 """
202 if nb.nbformat < 3:
284 if nb.nbformat < 3:
203 return
285 return
204
286
205 for cell in yield_code_cells(nb):
287 for cell in yield_code_cells(nb):
206 cell['metadata']['trusted'] = trusted
288 cell['metadata']['trusted'] = trusted
207
289
208 def _check_cell(self, cell, nbformat_version):
290 def _check_cell(self, cell, nbformat_version):
209 """Do we trust an individual cell?
291 """Do we trust an individual cell?
210
292
211 Return True if:
293 Return True if:
212
294
213 - cell is explicitly trusted
295 - cell is explicitly trusted
214 - cell has no potentially unsafe rich output
296 - cell has no potentially unsafe rich output
215
297
216 If a cell has no output, or only simple print statements,
298 If a cell has no output, or only simple print statements,
217 it will always be trusted.
299 it will always be trusted.
218 """
300 """
219 # explicitly trusted
301 # explicitly trusted
220 if cell['metadata'].pop("trusted", False):
302 if cell['metadata'].pop("trusted", False):
221 return True
303 return True
222
304
223 # explicitly safe output
305 # explicitly safe output
224 if nbformat_version >= 4:
306 if nbformat_version >= 4:
225 safe = {'text/plain', 'image/png', 'image/jpeg'}
226 unsafe_output_types = ['execute_result', 'display_data']
307 unsafe_output_types = ['execute_result', 'display_data']
227 safe_keys = {"output_type", "execution_count", "metadata"}
308 safe_keys = {"output_type", "execution_count", "metadata"}
228 else: # v3
309 else: # v3
229 safe = {'text', 'png', 'jpeg'}
230 unsafe_output_types = ['pyout', 'display_data']
310 unsafe_output_types = ['pyout', 'display_data']
231 safe_keys = {"output_type", "prompt_number", "metadata"}
311 safe_keys = {"output_type", "prompt_number", "metadata"}
232
312
233 for output in cell['outputs']:
313 for output in cell['outputs']:
234 output_type = output['output_type']
314 output_type = output['output_type']
235 if output_type in unsafe_output_types:
315 if output_type in unsafe_output_types:
236 # if there are any data keys not in the safe whitelist
316 # if there are any data keys not in the safe whitelist
237 output_keys = set(output)
317 output_keys = set(output)
238 if output_keys.difference(safe_keys):
318 if output_keys.difference(safe_keys):
239 return False
319 return False
240
320
241 return True
321 return True
242
322
243 def check_cells(self, nb):
323 def check_cells(self, nb):
244 """Return whether all code cells are trusted
324 """Return whether all code cells are trusted
245
325
246 If there are no code cells, return True.
326 If there are no code cells, return True.
247
327
248 This function is the inverse of mark_cells.
328 This function is the inverse of mark_cells.
249 """
329 """
250 if nb.nbformat < 3:
330 if nb.nbformat < 3:
251 return False
331 return False
252 trusted = True
332 trusted = True
253 for cell in yield_code_cells(nb):
333 for cell in yield_code_cells(nb):
254 # only distrust a cell if it actually has some output to distrust
334 # only distrust a cell if it actually has some output to distrust
255 if not self._check_cell(cell, nb.nbformat):
335 if not self._check_cell(cell, nb.nbformat):
256 trusted = False
336 trusted = False
257
337
258 return trusted
338 return trusted
259
339
260
340
261 trust_flags = {
341 trust_flags = {
262 'reset' : (
342 'reset' : (
263 {'TrustNotebookApp' : { 'reset' : True}},
343 {'TrustNotebookApp' : { 'reset' : True}},
264 """Generate a new key for notebook signature.
344 """Generate a new key for notebook signature.
265 All previously signed notebooks will become untrusted.
345 All previously signed notebooks will become untrusted.
266 """
346 """
267 ),
347 ),
268 }
348 }
269 trust_flags.update(base_flags)
349 trust_flags.update(base_flags)
270 trust_flags.pop('init')
350 trust_flags.pop('init')
271
351
272
352
273 class TrustNotebookApp(BaseIPythonApplication):
353 class TrustNotebookApp(BaseIPythonApplication):
274
354
275 description="""Sign one or more IPython notebooks with your key,
355 description="""Sign one or more IPython notebooks with your key,
276 to trust their dynamic (HTML, Javascript) output.
356 to trust their dynamic (HTML, Javascript) output.
277
357
278 Trusting a notebook only applies to the current IPython profile.
358 Trusting a notebook only applies to the current IPython profile.
279 To trust a notebook for use with a profile other than default,
359 To trust a notebook for use with a profile other than default,
280 add `--profile [profile name]`.
360 add `--profile [profile name]`.
281
361
282 Otherwise, you will have to re-execute the notebook to see output.
362 Otherwise, you will have to re-execute the notebook to see output.
283 """
363 """
284
364
285 examples = """
365 examples = """
286 ipython trust mynotebook.ipynb and_this_one.ipynb
366 ipython trust mynotebook.ipynb and_this_one.ipynb
287 ipython trust --profile myprofile mynotebook.ipynb
367 ipython trust --profile myprofile mynotebook.ipynb
288 """
368 """
289
369
290 flags = trust_flags
370 flags = trust_flags
291
371
292 reset = Bool(False, config=True,
372 reset = Bool(False, config=True,
293 help="""If True, generate a new key for notebook signature.
373 help="""If True, generate a new key for notebook signature.
294 After reset, all previously signed notebooks will become untrusted.
374 After reset, all previously signed notebooks will become untrusted.
295 """
375 """
296 )
376 )
297
377
298 notary = Instance(NotebookNotary)
378 notary = Instance(NotebookNotary)
299 def _notary_default(self):
379 def _notary_default(self):
300 return NotebookNotary(parent=self, profile_dir=self.profile_dir)
380 return NotebookNotary(parent=self, profile_dir=self.profile_dir)
301
381
302 def sign_notebook(self, notebook_path):
382 def sign_notebook(self, notebook_path):
303 if not os.path.exists(notebook_path):
383 if not os.path.exists(notebook_path):
304 self.log.error("Notebook missing: %s" % notebook_path)
384 self.log.error("Notebook missing: %s" % notebook_path)
305 self.exit(1)
385 self.exit(1)
306 with io.open(notebook_path, encoding='utf8') as f:
386 with io.open(notebook_path, encoding='utf8') as f:
307 nb = read(f, NO_CONVERT)
387 nb = read(f, NO_CONVERT)
308 if self.notary.check_signature(nb):
388 if self.notary.check_signature(nb):
309 print("Notebook already signed: %s" % notebook_path)
389 print("Notebook already signed: %s" % notebook_path)
310 else:
390 else:
311 print("Signing notebook: %s" % notebook_path)
391 print("Signing notebook: %s" % notebook_path)
312 self.notary.sign(nb)
392 self.notary.sign(nb)
313 with atomic_writing(notebook_path) as f:
393 with atomic_writing(notebook_path) as f:
314 write(nb, f, NO_CONVERT)
394 write(nb, f, NO_CONVERT)
315
395
316 def generate_new_key(self):
396 def generate_new_key(self):
317 """Generate a new notebook signature key"""
397 """Generate a new notebook signature key"""
318 print("Generating new notebook key: %s" % self.notary.secret_file)
398 print("Generating new notebook key: %s" % self.notary.secret_file)
319 self.notary._write_secret_file(os.urandom(1024))
399 self.notary._write_secret_file(os.urandom(1024))
320
400
321 def start(self):
401 def start(self):
322 if self.reset:
402 if self.reset:
323 self.generate_new_key()
403 self.generate_new_key()
324 return
404 return
325 if not self.extra_args:
405 if not self.extra_args:
326 self.log.critical("Specify at least one notebook to sign.")
406 self.log.critical("Specify at least one notebook to sign.")
327 self.exit(1)
407 self.exit(1)
328
408
329 for notebook_path in self.extra_args:
409 for notebook_path in self.extra_args:
330 self.sign_notebook(notebook_path)
410 self.sign_notebook(notebook_path)
331
411
@@ -1,150 +1,200 b''
1 """Test Notebook signing"""
1 """Test Notebook signing"""
2
2
3 # Copyright (c) IPython Development Team.
3 # Copyright (c) IPython Development Team.
4 # Distributed under the terms of the Modified BSD License.
4 # Distributed under the terms of the Modified BSD License.
5
5
6 import copy
7 import time
8
6 from .base import TestsBase
9 from .base import TestsBase
7
10
8 from IPython.nbformat import read, sign
11 from IPython.nbformat import read, sign
9 from IPython.core.getipython import get_ipython
12 from IPython.core.getipython import get_ipython
10
13
11
14
12 class TestNotary(TestsBase):
15 class TestNotary(TestsBase):
13
16
14 def setUp(self):
17 def setUp(self):
15 self.notary = sign.NotebookNotary(
18 self.notary = sign.NotebookNotary(
16 secret=b'secret',
19 secret=b'secret',
17 profile_dir=get_ipython().profile_dir
20 profile_dir=get_ipython().profile_dir,
21 db_url=':memory:'
18 )
22 )
19 with self.fopen(u'test3.ipynb', u'r') as f:
23 with self.fopen(u'test3.ipynb', u'r') as f:
20 self.nb = read(f, as_version=4)
24 self.nb = read(f, as_version=4)
21 with self.fopen(u'test3.ipynb', u'r') as f:
25 with self.fopen(u'test3.ipynb', u'r') as f:
22 self.nb3 = read(f, as_version=3)
26 self.nb3 = read(f, as_version=3)
23
27
24 def test_algorithms(self):
28 def test_algorithms(self):
25 last_sig = ''
29 last_sig = ''
26 for algo in sign.algorithms:
30 for algo in sign.algorithms:
27 self.notary.algorithm = algo
31 self.notary.algorithm = algo
28 self.notary.sign(self.nb)
32 sig = self.notary.compute_signature(self.nb)
29 sig = self.nb.metadata.signature
30 print(sig)
31 self.assertEqual(sig[:len(self.notary.algorithm)+1], '%s:' % self.notary.algorithm)
32 self.assertNotEqual(last_sig, sig)
33 self.assertNotEqual(last_sig, sig)
33 last_sig = sig
34 last_sig = sig
34
35
35 def test_sign_same(self):
36 def test_sign_same(self):
36 """Multiple signatures of the same notebook are the same"""
37 """Multiple signatures of the same notebook are the same"""
37 sig1 = self.notary.compute_signature(self.nb)
38 sig1 = self.notary.compute_signature(self.nb)
38 sig2 = self.notary.compute_signature(self.nb)
39 sig2 = self.notary.compute_signature(self.nb)
39 self.assertEqual(sig1, sig2)
40 self.assertEqual(sig1, sig2)
40
41
41 def test_change_secret(self):
42 def test_change_secret(self):
42 """Changing the secret changes the signature"""
43 """Changing the secret changes the signature"""
43 sig1 = self.notary.compute_signature(self.nb)
44 sig1 = self.notary.compute_signature(self.nb)
44 self.notary.secret = b'different'
45 self.notary.secret = b'different'
45 sig2 = self.notary.compute_signature(self.nb)
46 sig2 = self.notary.compute_signature(self.nb)
46 self.assertNotEqual(sig1, sig2)
47 self.assertNotEqual(sig1, sig2)
47
48
48 def test_sign(self):
49 def test_sign(self):
50 self.assertFalse(self.notary.check_signature(self.nb))
51 self.notary.sign(self.nb)
52 self.assertTrue(self.notary.check_signature(self.nb))
53
54 def test_unsign(self):
49 self.notary.sign(self.nb)
55 self.notary.sign(self.nb)
50 sig = self.nb.metadata.signature
56 self.assertTrue(self.notary.check_signature(self.nb))
51 self.assertEqual(sig[:len(self.notary.algorithm)+1], '%s:' % self.notary.algorithm)
57 self.notary.unsign(self.nb)
58 self.assertFalse(self.notary.check_signature(self.nb))
59 self.notary.unsign(self.nb)
60 self.assertFalse(self.notary.check_signature(self.nb))
61
62 def test_cull_db(self):
63 # this test has various sleeps of 2ms
64 # to ensure low resolution timestamps compare as expected
65 dt = 2e-3
66 nbs = [
67 copy.deepcopy(self.nb) for i in range(5)
68 ]
69 for i, nb in enumerate(nbs):
70 nb.metadata.dirty = i
71 self.notary.sign(nb)
72
73 for i, nb in enumerate(nbs):
74 time.sleep(dt)
75 self.assertTrue(self.notary.check_signature(nb), 'nb %i is trusted' % i)
76
77 self.notary.db_size_limit = 2
78 self.notary.cull_db()
79
80 # expect all but last two signatures to be culled
81 self.assertEqual(
82 [self.notary.check_signature(nb) for nb in nbs],
83 [False] * (len(nbs) - 2) + [True] * 2
84 )
85
86 # sign them all again
87 for nb in nbs:
88 time.sleep(dt)
89 self.notary.sign(nb)
90
91 # checking front two marks them as newest for next cull instead of oldest
92 time.sleep(dt)
93 self.notary.check_signature(nbs[0])
94 self.notary.check_signature(nbs[1])
95 self.notary.cull_db()
96
97 self.assertEqual(
98 [self.notary.check_signature(nb) for nb in nbs],
99 [True] * 2 + [False] * (len(nbs) - 2)
100 )
101
52
102
53 def test_check_signature(self):
103 def test_check_signature(self):
54 nb = self.nb
104 nb = self.nb
55 md = nb.metadata
105 md = nb.metadata
56 notary = self.notary
106 notary = self.notary
57 check_signature = notary.check_signature
107 check_signature = notary.check_signature
58 # no signature:
108 # no signature:
59 md.pop('signature', None)
109 md.pop('signature', None)
60 self.assertFalse(check_signature(nb))
110 self.assertFalse(check_signature(nb))
61 # hash only, no algo
111 # hash only, no algo
62 md.signature = notary.compute_signature(nb)
112 md.signature = notary.compute_signature(nb)
63 self.assertFalse(check_signature(nb))
113 self.assertFalse(check_signature(nb))
64 # proper signature, algo mismatch
114 # proper signature, algo mismatch
65 notary.algorithm = 'sha224'
115 notary.algorithm = 'sha224'
66 notary.sign(nb)
116 notary.sign(nb)
67 notary.algorithm = 'sha256'
117 notary.algorithm = 'sha256'
68 self.assertFalse(check_signature(nb))
118 self.assertFalse(check_signature(nb))
69 # check correctly signed notebook
119 # check correctly signed notebook
70 notary.sign(nb)
120 notary.sign(nb)
71 self.assertTrue(check_signature(nb))
121 self.assertTrue(check_signature(nb))
72
122
73 def test_mark_cells_untrusted(self):
123 def test_mark_cells_untrusted(self):
74 cells = self.nb.cells
124 cells = self.nb.cells
75 self.notary.mark_cells(self.nb, False)
125 self.notary.mark_cells(self.nb, False)
76 for cell in cells:
126 for cell in cells:
77 self.assertNotIn('trusted', cell)
127 self.assertNotIn('trusted', cell)
78 if cell.cell_type == 'code':
128 if cell.cell_type == 'code':
79 self.assertIn('trusted', cell.metadata)
129 self.assertIn('trusted', cell.metadata)
80 self.assertFalse(cell.metadata.trusted)
130 self.assertFalse(cell.metadata.trusted)
81 else:
131 else:
82 self.assertNotIn('trusted', cell.metadata)
132 self.assertNotIn('trusted', cell.metadata)
83
133
84 def test_mark_cells_trusted(self):
134 def test_mark_cells_trusted(self):
85 cells = self.nb.cells
135 cells = self.nb.cells
86 self.notary.mark_cells(self.nb, True)
136 self.notary.mark_cells(self.nb, True)
87 for cell in cells:
137 for cell in cells:
88 self.assertNotIn('trusted', cell)
138 self.assertNotIn('trusted', cell)
89 if cell.cell_type == 'code':
139 if cell.cell_type == 'code':
90 self.assertIn('trusted', cell.metadata)
140 self.assertIn('trusted', cell.metadata)
91 self.assertTrue(cell.metadata.trusted)
141 self.assertTrue(cell.metadata.trusted)
92 else:
142 else:
93 self.assertNotIn('trusted', cell.metadata)
143 self.assertNotIn('trusted', cell.metadata)
94
144
95 def test_check_cells(self):
145 def test_check_cells(self):
96 nb = self.nb
146 nb = self.nb
97 self.notary.mark_cells(nb, True)
147 self.notary.mark_cells(nb, True)
98 self.assertTrue(self.notary.check_cells(nb))
148 self.assertTrue(self.notary.check_cells(nb))
99 for cell in nb.cells:
149 for cell in nb.cells:
100 self.assertNotIn('trusted', cell)
150 self.assertNotIn('trusted', cell)
101 self.notary.mark_cells(nb, False)
151 self.notary.mark_cells(nb, False)
102 self.assertFalse(self.notary.check_cells(nb))
152 self.assertFalse(self.notary.check_cells(nb))
103 for cell in nb.cells:
153 for cell in nb.cells:
104 self.assertNotIn('trusted', cell)
154 self.assertNotIn('trusted', cell)
105
155
106 def test_trust_no_output(self):
156 def test_trust_no_output(self):
107 nb = self.nb
157 nb = self.nb
108 self.notary.mark_cells(nb, False)
158 self.notary.mark_cells(nb, False)
109 for cell in nb.cells:
159 for cell in nb.cells:
110 if cell.cell_type == 'code':
160 if cell.cell_type == 'code':
111 cell.outputs = []
161 cell.outputs = []
112 self.assertTrue(self.notary.check_cells(nb))
162 self.assertTrue(self.notary.check_cells(nb))
113
163
114 def test_mark_cells_untrusted_v3(self):
164 def test_mark_cells_untrusted_v3(self):
115 nb = self.nb3
165 nb = self.nb3
116 cells = nb.worksheets[0].cells
166 cells = nb.worksheets[0].cells
117 self.notary.mark_cells(nb, False)
167 self.notary.mark_cells(nb, False)
118 for cell in cells:
168 for cell in cells:
119 self.assertNotIn('trusted', cell)
169 self.assertNotIn('trusted', cell)
120 if cell.cell_type == 'code':
170 if cell.cell_type == 'code':
121 self.assertIn('trusted', cell.metadata)
171 self.assertIn('trusted', cell.metadata)
122 self.assertFalse(cell.metadata.trusted)
172 self.assertFalse(cell.metadata.trusted)
123 else:
173 else:
124 self.assertNotIn('trusted', cell.metadata)
174 self.assertNotIn('trusted', cell.metadata)
125
175
126 def test_mark_cells_trusted_v3(self):
176 def test_mark_cells_trusted_v3(self):
127 nb = self.nb3
177 nb = self.nb3
128 cells = nb.worksheets[0].cells
178 cells = nb.worksheets[0].cells
129 self.notary.mark_cells(nb, True)
179 self.notary.mark_cells(nb, True)
130 for cell in cells:
180 for cell in cells:
131 self.assertNotIn('trusted', cell)
181 self.assertNotIn('trusted', cell)
132 if cell.cell_type == 'code':
182 if cell.cell_type == 'code':
133 self.assertIn('trusted', cell.metadata)
183 self.assertIn('trusted', cell.metadata)
134 self.assertTrue(cell.metadata.trusted)
184 self.assertTrue(cell.metadata.trusted)
135 else:
185 else:
136 self.assertNotIn('trusted', cell.metadata)
186 self.assertNotIn('trusted', cell.metadata)
137
187
138 def test_check_cells_v3(self):
188 def test_check_cells_v3(self):
139 nb = self.nb3
189 nb = self.nb3
140 cells = nb.worksheets[0].cells
190 cells = nb.worksheets[0].cells
141 self.notary.mark_cells(nb, True)
191 self.notary.mark_cells(nb, True)
142 self.assertTrue(self.notary.check_cells(nb))
192 self.assertTrue(self.notary.check_cells(nb))
143 for cell in cells:
193 for cell in cells:
144 self.assertNotIn('trusted', cell)
194 self.assertNotIn('trusted', cell)
145 self.notary.mark_cells(nb, False)
195 self.notary.mark_cells(nb, False)
146 self.assertFalse(self.notary.check_cells(nb))
196 self.assertFalse(self.notary.check_cells(nb))
147 for cell in cells:
197 for cell in cells:
148 self.assertNotIn('trusted', cell)
198 self.assertNotIn('trusted', cell)
149
199
150
200
@@ -1,252 +1,253 b''
1 """Code for converting notebooks to and from v3."""
1 """Code for converting notebooks to and from v3."""
2
2
3 # Copyright (c) IPython Development Team.
3 # Copyright (c) IPython Development Team.
4 # Distributed under the terms of the Modified BSD License.
4 # Distributed under the terms of the Modified BSD License.
5
5
6 import json
6 import json
7 import re
7 import re
8
8
9 from .nbbase import (
9 from .nbbase import (
10 nbformat, nbformat_minor,
10 nbformat, nbformat_minor,
11 NotebookNode,
11 NotebookNode,
12 )
12 )
13
13
14 from IPython.nbformat import v3
14 from IPython.nbformat import v3
15 from IPython.utils.log import get_logger
15 from IPython.utils.log import get_logger
16
16
17 def _warn_if_invalid(nb, version):
17 def _warn_if_invalid(nb, version):
18 """Log validation errors, if there are any."""
18 """Log validation errors, if there are any."""
19 from IPython.nbformat import validate, ValidationError
19 from IPython.nbformat import validate, ValidationError
20 try:
20 try:
21 validate(nb, version=version)
21 validate(nb, version=version)
22 except ValidationError as e:
22 except ValidationError as e:
23 get_logger().error("Notebook JSON is not valid v%i: %s", version, e)
23 get_logger().error("Notebook JSON is not valid v%i: %s", version, e)
24
24
25 def upgrade(nb, from_version=3, from_minor=0):
25 def upgrade(nb, from_version=3, from_minor=0):
26 """Convert a notebook to v4.
26 """Convert a notebook to v4.
27
27
28 Parameters
28 Parameters
29 ----------
29 ----------
30 nb : NotebookNode
30 nb : NotebookNode
31 The Python representation of the notebook to convert.
31 The Python representation of the notebook to convert.
32 from_version : int
32 from_version : int
33 The original version of the notebook to convert.
33 The original version of the notebook to convert.
34 from_minor : int
34 from_minor : int
35 The original minor version of the notebook to convert (only relevant for v >= 3).
35 The original minor version of the notebook to convert (only relevant for v >= 3).
36 """
36 """
37 if from_version == 3:
37 if from_version == 3:
38 # Validate the notebook before conversion
38 # Validate the notebook before conversion
39 _warn_if_invalid(nb, from_version)
39 _warn_if_invalid(nb, from_version)
40
40
41 # Mark the original nbformat so consumers know it has been converted
41 # Mark the original nbformat so consumers know it has been converted
42 orig_nbformat = nb.pop('orig_nbformat', None)
42 orig_nbformat = nb.pop('orig_nbformat', None)
43 nb.metadata.orig_nbformat = orig_nbformat or 3
43 nb.metadata.orig_nbformat = orig_nbformat or 3
44
44
45 # Mark the new format
45 # Mark the new format
46 nb.nbformat = nbformat
46 nb.nbformat = nbformat
47 nb.nbformat_minor = nbformat_minor
47 nb.nbformat_minor = nbformat_minor
48
48
49 # remove worksheet(s)
49 # remove worksheet(s)
50 nb['cells'] = cells = []
50 nb['cells'] = cells = []
51 # In the unlikely event of multiple worksheets,
51 # In the unlikely event of multiple worksheets,
52 # they will be flattened
52 # they will be flattened
53 for ws in nb.pop('worksheets', []):
53 for ws in nb.pop('worksheets', []):
54 # upgrade each cell
54 # upgrade each cell
55 for cell in ws['cells']:
55 for cell in ws['cells']:
56 cells.append(upgrade_cell(cell))
56 cells.append(upgrade_cell(cell))
57 # upgrade metadata
57 # upgrade metadata
58 nb.metadata.pop('name', '')
58 nb.metadata.pop('name', '')
59 nb.metadata.pop('signature', '')
59 # Validate the converted notebook before returning it
60 # Validate the converted notebook before returning it
60 _warn_if_invalid(nb, nbformat)
61 _warn_if_invalid(nb, nbformat)
61 return nb
62 return nb
62 elif from_version == 4:
63 elif from_version == 4:
63 # nothing to do
64 # nothing to do
64 if from_minor != nbformat_minor:
65 if from_minor != nbformat_minor:
65 nb.metadata.orig_nbformat_minor = from_minor
66 nb.metadata.orig_nbformat_minor = from_minor
66 nb.nbformat_minor = nbformat_minor
67 nb.nbformat_minor = nbformat_minor
67
68
68 return nb
69 return nb
69 else:
70 else:
70 raise ValueError('Cannot convert a notebook directly from v%s to v4. ' \
71 raise ValueError('Cannot convert a notebook directly from v%s to v4. ' \
71 'Try using the IPython.nbformat.convert module.' % from_version)
72 'Try using the IPython.nbformat.convert module.' % from_version)
72
73
73 def upgrade_cell(cell):
74 def upgrade_cell(cell):
74 """upgrade a cell from v3 to v4
75 """upgrade a cell from v3 to v4
75
76
76 heading cell:
77 heading cell:
77 - -> markdown heading
78 - -> markdown heading
78 code cell:
79 code cell:
79 - remove language metadata
80 - remove language metadata
80 - cell.input -> cell.source
81 - cell.input -> cell.source
81 - cell.prompt_number -> cell.execution_count
82 - cell.prompt_number -> cell.execution_count
82 - update outputs
83 - update outputs
83 """
84 """
84 cell.setdefault('metadata', NotebookNode())
85 cell.setdefault('metadata', NotebookNode())
85 if cell.cell_type == 'code':
86 if cell.cell_type == 'code':
86 cell.pop('language', '')
87 cell.pop('language', '')
87 if 'collapsed' in cell:
88 if 'collapsed' in cell:
88 cell.metadata['collapsed'] = cell.pop('collapsed')
89 cell.metadata['collapsed'] = cell.pop('collapsed')
89 cell.source = cell.pop('input', '')
90 cell.source = cell.pop('input', '')
90 cell.execution_count = cell.pop('prompt_number', None)
91 cell.execution_count = cell.pop('prompt_number', None)
91 cell.outputs = upgrade_outputs(cell.outputs)
92 cell.outputs = upgrade_outputs(cell.outputs)
92 elif cell.cell_type == 'heading':
93 elif cell.cell_type == 'heading':
93 cell.cell_type = 'markdown'
94 cell.cell_type = 'markdown'
94 level = cell.pop('level', 1)
95 level = cell.pop('level', 1)
95 cell.source = u'{hashes} {single_line}'.format(
96 cell.source = u'{hashes} {single_line}'.format(
96 hashes='#' * level,
97 hashes='#' * level,
97 single_line = ' '.join(cell.get('source', '').splitlines()),
98 single_line = ' '.join(cell.get('source', '').splitlines()),
98 )
99 )
99 elif cell.cell_type == 'html':
100 elif cell.cell_type == 'html':
100 # Technically, this exists. It will never happen in practice.
101 # Technically, this exists. It will never happen in practice.
101 cell.cell_type = 'markdown'
102 cell.cell_type = 'markdown'
102 return cell
103 return cell
103
104
104 def downgrade_cell(cell):
105 def downgrade_cell(cell):
105 """downgrade a cell from v4 to v3
106 """downgrade a cell from v4 to v3
106
107
107 code cell:
108 code cell:
108 - set cell.language
109 - set cell.language
109 - cell.input <- cell.source
110 - cell.input <- cell.source
110 - cell.prompt_number <- cell.execution_count
111 - cell.prompt_number <- cell.execution_count
111 - update outputs
112 - update outputs
112 markdown cell:
113 markdown cell:
113 - single-line heading -> heading cell
114 - single-line heading -> heading cell
114 """
115 """
115 if cell.cell_type == 'code':
116 if cell.cell_type == 'code':
116 cell.language = 'python'
117 cell.language = 'python'
117 cell.input = cell.pop('source', '')
118 cell.input = cell.pop('source', '')
118 cell.prompt_number = cell.pop('execution_count', None)
119 cell.prompt_number = cell.pop('execution_count', None)
119 cell.collapsed = cell.metadata.pop('collapsed', False)
120 cell.collapsed = cell.metadata.pop('collapsed', False)
120 cell.outputs = downgrade_outputs(cell.outputs)
121 cell.outputs = downgrade_outputs(cell.outputs)
121 elif cell.cell_type == 'markdown':
122 elif cell.cell_type == 'markdown':
122 source = cell.get('source', '')
123 source = cell.get('source', '')
123 if '\n' not in source and source.startswith('#'):
124 if '\n' not in source and source.startswith('#'):
124 prefix, text = re.match(r'(#+)\s*(.*)', source).groups()
125 prefix, text = re.match(r'(#+)\s*(.*)', source).groups()
125 cell.cell_type = 'heading'
126 cell.cell_type = 'heading'
126 cell.source = text
127 cell.source = text
127 cell.level = len(prefix)
128 cell.level = len(prefix)
128 return cell
129 return cell
129
130
130 _mime_map = {
131 _mime_map = {
131 "text" : "text/plain",
132 "text" : "text/plain",
132 "html" : "text/html",
133 "html" : "text/html",
133 "svg" : "image/svg+xml",
134 "svg" : "image/svg+xml",
134 "png" : "image/png",
135 "png" : "image/png",
135 "jpeg" : "image/jpeg",
136 "jpeg" : "image/jpeg",
136 "latex" : "text/latex",
137 "latex" : "text/latex",
137 "json" : "application/json",
138 "json" : "application/json",
138 "javascript" : "application/javascript",
139 "javascript" : "application/javascript",
139 };
140 };
140
141
141 def to_mime_key(d):
142 def to_mime_key(d):
142 """convert dict with v3 aliases to plain mime-type keys"""
143 """convert dict with v3 aliases to plain mime-type keys"""
143 for alias, mime in _mime_map.items():
144 for alias, mime in _mime_map.items():
144 if alias in d:
145 if alias in d:
145 d[mime] = d.pop(alias)
146 d[mime] = d.pop(alias)
146 return d
147 return d
147
148
148 def from_mime_key(d):
149 def from_mime_key(d):
149 """convert dict with mime-type keys to v3 aliases"""
150 """convert dict with mime-type keys to v3 aliases"""
150 for alias, mime in _mime_map.items():
151 for alias, mime in _mime_map.items():
151 if mime in d:
152 if mime in d:
152 d[alias] = d.pop(mime)
153 d[alias] = d.pop(mime)
153 return d
154 return d
154
155
155 def upgrade_output(output):
156 def upgrade_output(output):
156 """upgrade a single code cell output from v3 to v4
157 """upgrade a single code cell output from v3 to v4
157
158
158 - pyout -> execute_result
159 - pyout -> execute_result
159 - pyerr -> error
160 - pyerr -> error
160 - output.type -> output.data.mime/type
161 - output.type -> output.data.mime/type
161 - mime-type keys
162 - mime-type keys
162 - stream.stream -> stream.name
163 - stream.stream -> stream.name
163 """
164 """
164 if output['output_type'] in {'pyout', 'display_data'}:
165 if output['output_type'] in {'pyout', 'display_data'}:
165 output.setdefault('metadata', NotebookNode())
166 output.setdefault('metadata', NotebookNode())
166 if output['output_type'] == 'pyout':
167 if output['output_type'] == 'pyout':
167 output['output_type'] = 'execute_result'
168 output['output_type'] = 'execute_result'
168 output['execution_count'] = output.pop('prompt_number', None)
169 output['execution_count'] = output.pop('prompt_number', None)
169
170
170 # move output data into data sub-dict
171 # move output data into data sub-dict
171 data = {}
172 data = {}
172 for key in list(output):
173 for key in list(output):
173 if key in {'output_type', 'execution_count', 'metadata'}:
174 if key in {'output_type', 'execution_count', 'metadata'}:
174 continue
175 continue
175 data[key] = output.pop(key)
176 data[key] = output.pop(key)
176 to_mime_key(data)
177 to_mime_key(data)
177 output['data'] = data
178 output['data'] = data
178 to_mime_key(output.metadata)
179 to_mime_key(output.metadata)
179 if 'application/json' in data:
180 if 'application/json' in data:
180 data['application/json'] = json.loads(data['application/json'])
181 data['application/json'] = json.loads(data['application/json'])
181 # promote ascii bytes (from v2) to unicode
182 # promote ascii bytes (from v2) to unicode
182 for key in ('image/png', 'image/jpeg'):
183 for key in ('image/png', 'image/jpeg'):
183 if key in data and isinstance(data[key], bytes):
184 if key in data and isinstance(data[key], bytes):
184 data[key] = data[key].decode('ascii')
185 data[key] = data[key].decode('ascii')
185 elif output['output_type'] == 'pyerr':
186 elif output['output_type'] == 'pyerr':
186 output['output_type'] = 'error'
187 output['output_type'] = 'error'
187 elif output['output_type'] == 'stream':
188 elif output['output_type'] == 'stream':
188 output['name'] = output.pop('stream')
189 output['name'] = output.pop('stream')
189 return output
190 return output
190
191
191 def downgrade_output(output):
192 def downgrade_output(output):
192 """downgrade a single code cell output to v3 from v4
193 """downgrade a single code cell output to v3 from v4
193
194
194 - pyout <- execute_result
195 - pyout <- execute_result
195 - pyerr <- error
196 - pyerr <- error
196 - output.data.mime/type -> output.type
197 - output.data.mime/type -> output.type
197 - un-mime-type keys
198 - un-mime-type keys
198 - stream.stream <- stream.name
199 - stream.stream <- stream.name
199 """
200 """
200 if output['output_type'] in {'execute_result', 'display_data'}:
201 if output['output_type'] in {'execute_result', 'display_data'}:
201 if output['output_type'] == 'execute_result':
202 if output['output_type'] == 'execute_result':
202 output['output_type'] = 'pyout'
203 output['output_type'] = 'pyout'
203 output['prompt_number'] = output.pop('execution_count', None)
204 output['prompt_number'] = output.pop('execution_count', None)
204
205
205 # promote data dict to top-level output namespace
206 # promote data dict to top-level output namespace
206 data = output.pop('data', {})
207 data = output.pop('data', {})
207 if 'application/json' in data:
208 if 'application/json' in data:
208 data['application/json'] = json.dumps(data['application/json'])
209 data['application/json'] = json.dumps(data['application/json'])
209 from_mime_key(data)
210 from_mime_key(data)
210 output.update(data)
211 output.update(data)
211 from_mime_key(output.get('metadata', {}))
212 from_mime_key(output.get('metadata', {}))
212 elif output['output_type'] == 'error':
213 elif output['output_type'] == 'error':
213 output['output_type'] = 'pyerr'
214 output['output_type'] = 'pyerr'
214 elif output['output_type'] == 'stream':
215 elif output['output_type'] == 'stream':
215 output['stream'] = output.pop('name')
216 output['stream'] = output.pop('name')
216 return output
217 return output
217
218
218 def upgrade_outputs(outputs):
219 def upgrade_outputs(outputs):
219 """upgrade outputs of a code cell from v3 to v4"""
220 """upgrade outputs of a code cell from v3 to v4"""
220 return [upgrade_output(op) for op in outputs]
221 return [upgrade_output(op) for op in outputs]
221
222
222 def downgrade_outputs(outputs):
223 def downgrade_outputs(outputs):
223 """downgrade outputs of a code cell to v3 from v4"""
224 """downgrade outputs of a code cell to v3 from v4"""
224 return [downgrade_output(op) for op in outputs]
225 return [downgrade_output(op) for op in outputs]
225
226
226 def downgrade(nb):
227 def downgrade(nb):
227 """Convert a v4 notebook to v3.
228 """Convert a v4 notebook to v3.
228
229
229 Parameters
230 Parameters
230 ----------
231 ----------
231 nb : NotebookNode
232 nb : NotebookNode
232 The Python representation of the notebook to convert.
233 The Python representation of the notebook to convert.
233 """
234 """
234 if nb.nbformat != nbformat:
235 if nb.nbformat != nbformat:
235 return nb
236 return nb
236
237
237 # Validate the notebook before conversion
238 # Validate the notebook before conversion
238 _warn_if_invalid(nb, nbformat)
239 _warn_if_invalid(nb, nbformat)
239
240
240 nb.nbformat = v3.nbformat
241 nb.nbformat = v3.nbformat
241 nb.nbformat_minor = v3.nbformat_minor
242 nb.nbformat_minor = v3.nbformat_minor
242 cells = [ downgrade_cell(cell) for cell in nb.pop('cells') ]
243 cells = [ downgrade_cell(cell) for cell in nb.pop('cells') ]
243 nb.worksheets = [v3.new_worksheet(cells=cells)]
244 nb.worksheets = [v3.new_worksheet(cells=cells)]
244 nb.metadata.setdefault('name', '')
245 nb.metadata.setdefault('name', '')
245
246
246 # Validate the converted notebook before returning it
247 # Validate the converted notebook before returning it
247 _warn_if_invalid(nb, v3.nbformat)
248 _warn_if_invalid(nb, v3.nbformat)
248
249
249 nb.orig_nbformat = nb.metadata.pop('orig_nbformat', nbformat)
250 nb.orig_nbformat = nb.metadata.pop('orig_nbformat', nbformat)
250 nb.orig_nbformat_minor = nb.metadata.pop('orig_nbformat_minor', nbformat_minor)
251 nb.orig_nbformat_minor = nb.metadata.pop('orig_nbformat_minor', nbformat_minor)
251
252
252 return nb
253 return nb
@@ -1,375 +1,371 b''
1 {
1 {
2 "$schema": "http://json-schema.org/draft-04/schema#",
2 "$schema": "http://json-schema.org/draft-04/schema#",
3 "description": "IPython Notebook v4.0 JSON schema.",
3 "description": "IPython Notebook v4.0 JSON schema.",
4 "type": "object",
4 "type": "object",
5 "additionalProperties": false,
5 "additionalProperties": false,
6 "required": ["metadata", "nbformat_minor", "nbformat", "cells"],
6 "required": ["metadata", "nbformat_minor", "nbformat", "cells"],
7 "properties": {
7 "properties": {
8 "metadata": {
8 "metadata": {
9 "description": "Notebook root-level metadata.",
9 "description": "Notebook root-level metadata.",
10 "type": "object",
10 "type": "object",
11 "additionalProperties": true,
11 "additionalProperties": true,
12 "properties": {
12 "properties": {
13 "kernelspec": {
13 "kernelspec": {
14 "description": "Kernel information.",
14 "description": "Kernel information.",
15 "type": "object",
15 "type": "object",
16 "required": ["name", "display_name"],
16 "required": ["name", "display_name"],
17 "properties": {
17 "properties": {
18 "name": {
18 "name": {
19 "description": "Name of the kernel specification.",
19 "description": "Name of the kernel specification.",
20 "type": "string"
20 "type": "string"
21 },
21 },
22 "display_name": {
22 "display_name": {
23 "description": "Name to display in UI.",
23 "description": "Name to display in UI.",
24 "type": "string"
24 "type": "string"
25 }
25 }
26 }
26 }
27 },
27 },
28 "language_info": {
28 "language_info": {
29 "description": "Kernel information.",
29 "description": "Kernel information.",
30 "type": "object",
30 "type": "object",
31 "required": ["name"],
31 "required": ["name"],
32 "properties": {
32 "properties": {
33 "name": {
33 "name": {
34 "description": "The programming language which this kernel runs.",
34 "description": "The programming language which this kernel runs.",
35 "type": "string"
35 "type": "string"
36 },
36 },
37 "codemirror_mode": {
37 "codemirror_mode": {
38 "description": "The codemirror mode to use for code in this language.",
38 "description": "The codemirror mode to use for code in this language.",
39 "oneOf": [
39 "oneOf": [
40 {"type": "string"},
40 {"type": "string"},
41 {"type": "object"}
41 {"type": "object"}
42 ]
42 ]
43 },
43 },
44 "file_extension": {
44 "file_extension": {
45 "description": "The file extension for files in this language.",
45 "description": "The file extension for files in this language.",
46 "type": "string"
46 "type": "string"
47 },
47 },
48 "mimetype": {
48 "mimetype": {
49 "description": "The mimetype corresponding to files in this language.",
49 "description": "The mimetype corresponding to files in this language.",
50 "type": "string"
50 "type": "string"
51 },
51 },
52 "pygments_lexer": {
52 "pygments_lexer": {
53 "description": "The pygments lexer to use for code in this language.",
53 "description": "The pygments lexer to use for code in this language.",
54 "type": "string"
54 "type": "string"
55 }
55 }
56 }
56 }
57 },
57 },
58 "signature": {
59 "description": "Hash of the notebook.",
60 "type": "string"
61 },
62 "orig_nbformat": {
58 "orig_nbformat": {
63 "description": "Original notebook format (major number) before converting the notebook between versions. This should never be written to a file.",
59 "description": "Original notebook format (major number) before converting the notebook between versions. This should never be written to a file.",
64 "type": "integer",
60 "type": "integer",
65 "minimum": 1
61 "minimum": 1
66 }
62 }
67 }
63 }
68 },
64 },
69 "nbformat_minor": {
65 "nbformat_minor": {
70 "description": "Notebook format (minor number). Incremented for backward compatible changes to the notebook format.",
66 "description": "Notebook format (minor number). Incremented for backward compatible changes to the notebook format.",
71 "type": "integer",
67 "type": "integer",
72 "minimum": 0
68 "minimum": 0
73 },
69 },
74 "nbformat": {
70 "nbformat": {
75 "description": "Notebook format (major number). Incremented between backwards incompatible changes to the notebook format.",
71 "description": "Notebook format (major number). Incremented between backwards incompatible changes to the notebook format.",
76 "type": "integer",
72 "type": "integer",
77 "minimum": 4,
73 "minimum": 4,
78 "maximum": 4
74 "maximum": 4
79 },
75 },
80 "cells": {
76 "cells": {
81 "description": "Array of cells of the current notebook.",
77 "description": "Array of cells of the current notebook.",
82 "type": "array",
78 "type": "array",
83 "items": {"$ref": "#/definitions/cell"}
79 "items": {"$ref": "#/definitions/cell"}
84 }
80 }
85 },
81 },
86
82
87 "definitions": {
83 "definitions": {
88 "cell": {
84 "cell": {
89 "type": "object",
85 "type": "object",
90 "oneOf": [
86 "oneOf": [
91 {"$ref": "#/definitions/raw_cell"},
87 {"$ref": "#/definitions/raw_cell"},
92 {"$ref": "#/definitions/markdown_cell"},
88 {"$ref": "#/definitions/markdown_cell"},
93 {"$ref": "#/definitions/code_cell"}
89 {"$ref": "#/definitions/code_cell"}
94 ]
90 ]
95 },
91 },
96
92
97 "raw_cell": {
93 "raw_cell": {
98 "description": "Notebook raw nbconvert cell.",
94 "description": "Notebook raw nbconvert cell.",
99 "type": "object",
95 "type": "object",
100 "additionalProperties": false,
96 "additionalProperties": false,
101 "required": ["cell_type", "metadata", "source"],
97 "required": ["cell_type", "metadata", "source"],
102 "properties": {
98 "properties": {
103 "cell_type": {
99 "cell_type": {
104 "description": "String identifying the type of cell.",
100 "description": "String identifying the type of cell.",
105 "enum": ["raw"]
101 "enum": ["raw"]
106 },
102 },
107 "metadata": {
103 "metadata": {
108 "description": "Cell-level metadata.",
104 "description": "Cell-level metadata.",
109 "type": "object",
105 "type": "object",
110 "additionalProperties": true,
106 "additionalProperties": true,
111 "properties": {
107 "properties": {
112 "format": {
108 "format": {
113 "description": "Raw cell metadata format for nbconvert.",
109 "description": "Raw cell metadata format for nbconvert.",
114 "type": "string"
110 "type": "string"
115 },
111 },
116 "name": {"$ref": "#/definitions/misc/metadata_name"},
112 "name": {"$ref": "#/definitions/misc/metadata_name"},
117 "tags": {"$ref": "#/definitions/misc/metadata_tags"}
113 "tags": {"$ref": "#/definitions/misc/metadata_tags"}
118 }
114 }
119 },
115 },
120 "source": {"$ref": "#/definitions/misc/source"}
116 "source": {"$ref": "#/definitions/misc/source"}
121 }
117 }
122 },
118 },
123
119
124 "markdown_cell": {
120 "markdown_cell": {
125 "description": "Notebook markdown cell.",
121 "description": "Notebook markdown cell.",
126 "type": "object",
122 "type": "object",
127 "additionalProperties": false,
123 "additionalProperties": false,
128 "required": ["cell_type", "metadata", "source"],
124 "required": ["cell_type", "metadata", "source"],
129 "properties": {
125 "properties": {
130 "cell_type": {
126 "cell_type": {
131 "description": "String identifying the type of cell.",
127 "description": "String identifying the type of cell.",
132 "enum": ["markdown"]
128 "enum": ["markdown"]
133 },
129 },
134 "metadata": {
130 "metadata": {
135 "description": "Cell-level metadata.",
131 "description": "Cell-level metadata.",
136 "type": "object",
132 "type": "object",
137 "properties": {
133 "properties": {
138 "name": {"$ref": "#/definitions/misc/metadata_name"},
134 "name": {"$ref": "#/definitions/misc/metadata_name"},
139 "tags": {"$ref": "#/definitions/misc/metadata_tags"}
135 "tags": {"$ref": "#/definitions/misc/metadata_tags"}
140 },
136 },
141 "additionalProperties": true
137 "additionalProperties": true
142 },
138 },
143 "source": {"$ref": "#/definitions/misc/source"}
139 "source": {"$ref": "#/definitions/misc/source"}
144 }
140 }
145 },
141 },
146
142
147 "code_cell": {
143 "code_cell": {
148 "description": "Notebook code cell.",
144 "description": "Notebook code cell.",
149 "type": "object",
145 "type": "object",
150 "additionalProperties": false,
146 "additionalProperties": false,
151 "required": ["cell_type", "metadata", "source", "outputs", "execution_count"],
147 "required": ["cell_type", "metadata", "source", "outputs", "execution_count"],
152 "properties": {
148 "properties": {
153 "cell_type": {
149 "cell_type": {
154 "description": "String identifying the type of cell.",
150 "description": "String identifying the type of cell.",
155 "enum": ["code"]
151 "enum": ["code"]
156 },
152 },
157 "metadata": {
153 "metadata": {
158 "description": "Cell-level metadata.",
154 "description": "Cell-level metadata.",
159 "type": "object",
155 "type": "object",
160 "additionalProperties": true,
156 "additionalProperties": true,
161 "properties": {
157 "properties": {
162 "collapsed": {
158 "collapsed": {
163 "description": "Whether the cell is collapsed/expanded.",
159 "description": "Whether the cell is collapsed/expanded.",
164 "type": "boolean"
160 "type": "boolean"
165 },
161 },
166 "autoscroll": {
162 "autoscroll": {
167 "description": "Whether the cell's output is scrolled, unscrolled, or autoscrolled.",
163 "description": "Whether the cell's output is scrolled, unscrolled, or autoscrolled.",
168 "enum": [true, false, "auto"]
164 "enum": [true, false, "auto"]
169 },
165 },
170 "name": {"$ref": "#/definitions/misc/metadata_name"},
166 "name": {"$ref": "#/definitions/misc/metadata_name"},
171 "tags": {"$ref": "#/definitions/misc/metadata_tags"}
167 "tags": {"$ref": "#/definitions/misc/metadata_tags"}
172 }
168 }
173 },
169 },
174 "source": {"$ref": "#/definitions/misc/source"},
170 "source": {"$ref": "#/definitions/misc/source"},
175 "outputs": {
171 "outputs": {
176 "description": "Execution, display, or stream outputs.",
172 "description": "Execution, display, or stream outputs.",
177 "type": "array",
173 "type": "array",
178 "items": {"$ref": "#/definitions/output"}
174 "items": {"$ref": "#/definitions/output"}
179 },
175 },
180 "execution_count": {
176 "execution_count": {
181 "description": "The code cell's prompt number. Will be null if the cell has not been run.",
177 "description": "The code cell's prompt number. Will be null if the cell has not been run.",
182 "type": ["integer", "null"],
178 "type": ["integer", "null"],
183 "minimum": 0
179 "minimum": 0
184 }
180 }
185 }
181 }
186 },
182 },
187
183
188 "unrecognized_cell": {
184 "unrecognized_cell": {
189 "description": "Unrecognized cell from a future minor-revision to the notebook format.",
185 "description": "Unrecognized cell from a future minor-revision to the notebook format.",
190 "type": "object",
186 "type": "object",
191 "additionalProperties": true,
187 "additionalProperties": true,
192 "required": ["cell_type", "metadata"],
188 "required": ["cell_type", "metadata"],
193 "properties": {
189 "properties": {
194 "cell_type": {
190 "cell_type": {
195 "description": "String identifying the type of cell.",
191 "description": "String identifying the type of cell.",
196 "not" : {
192 "not" : {
197 "enum": ["markdown", "code", "raw"]
193 "enum": ["markdown", "code", "raw"]
198 }
194 }
199 },
195 },
200 "metadata": {
196 "metadata": {
201 "description": "Cell-level metadata.",
197 "description": "Cell-level metadata.",
202 "type": "object",
198 "type": "object",
203 "properties": {
199 "properties": {
204 "name": {"$ref": "#/definitions/misc/metadata_name"},
200 "name": {"$ref": "#/definitions/misc/metadata_name"},
205 "tags": {"$ref": "#/definitions/misc/metadata_tags"}
201 "tags": {"$ref": "#/definitions/misc/metadata_tags"}
206 },
202 },
207 "additionalProperties": true
203 "additionalProperties": true
208 }
204 }
209 }
205 }
210 },
206 },
211
207
212 "output": {
208 "output": {
213 "type": "object",
209 "type": "object",
214 "oneOf": [
210 "oneOf": [
215 {"$ref": "#/definitions/execute_result"},
211 {"$ref": "#/definitions/execute_result"},
216 {"$ref": "#/definitions/display_data"},
212 {"$ref": "#/definitions/display_data"},
217 {"$ref": "#/definitions/stream"},
213 {"$ref": "#/definitions/stream"},
218 {"$ref": "#/definitions/error"}
214 {"$ref": "#/definitions/error"}
219 ]
215 ]
220 },
216 },
221
217
222 "execute_result": {
218 "execute_result": {
223 "description": "Result of executing a code cell.",
219 "description": "Result of executing a code cell.",
224 "type": "object",
220 "type": "object",
225 "additionalProperties": false,
221 "additionalProperties": false,
226 "required": ["output_type", "data", "metadata", "execution_count"],
222 "required": ["output_type", "data", "metadata", "execution_count"],
227 "properties": {
223 "properties": {
228 "output_type": {
224 "output_type": {
229 "description": "Type of cell output.",
225 "description": "Type of cell output.",
230 "enum": ["execute_result"]
226 "enum": ["execute_result"]
231 },
227 },
232 "execution_count": {
228 "execution_count": {
233 "description": "A result's prompt number.",
229 "description": "A result's prompt number.",
234 "type": ["integer", "null"],
230 "type": ["integer", "null"],
235 "minimum": 0
231 "minimum": 0
236 },
232 },
237 "data": {"$ref": "#/definitions/misc/mimebundle"},
233 "data": {"$ref": "#/definitions/misc/mimebundle"},
238 "metadata": {"$ref": "#/definitions/misc/output_metadata"}
234 "metadata": {"$ref": "#/definitions/misc/output_metadata"}
239 }
235 }
240 },
236 },
241
237
242 "display_data": {
238 "display_data": {
243 "description": "Data displayed as a result of code cell execution.",
239 "description": "Data displayed as a result of code cell execution.",
244 "type": "object",
240 "type": "object",
245 "additionalProperties": false,
241 "additionalProperties": false,
246 "required": ["output_type", "data", "metadata"],
242 "required": ["output_type", "data", "metadata"],
247 "properties": {
243 "properties": {
248 "output_type": {
244 "output_type": {
249 "description": "Type of cell output.",
245 "description": "Type of cell output.",
250 "enum": ["display_data"]
246 "enum": ["display_data"]
251 },
247 },
252 "data": {"$ref": "#/definitions/misc/mimebundle"},
248 "data": {"$ref": "#/definitions/misc/mimebundle"},
253 "metadata": {"$ref": "#/definitions/misc/output_metadata"}
249 "metadata": {"$ref": "#/definitions/misc/output_metadata"}
254 }
250 }
255 },
251 },
256
252
257 "stream": {
253 "stream": {
258 "description": "Stream output from a code cell.",
254 "description": "Stream output from a code cell.",
259 "type": "object",
255 "type": "object",
260 "additionalProperties": false,
256 "additionalProperties": false,
261 "required": ["output_type", "name", "text"],
257 "required": ["output_type", "name", "text"],
262 "properties": {
258 "properties": {
263 "output_type": {
259 "output_type": {
264 "description": "Type of cell output.",
260 "description": "Type of cell output.",
265 "enum": ["stream"]
261 "enum": ["stream"]
266 },
262 },
267 "name": {
263 "name": {
268 "description": "The name of the stream (stdout, stderr).",
264 "description": "The name of the stream (stdout, stderr).",
269 "type": "string"
265 "type": "string"
270 },
266 },
271 "text": {
267 "text": {
272 "description": "The stream's text output, represented as an array of strings.",
268 "description": "The stream's text output, represented as an array of strings.",
273 "$ref": "#/definitions/misc/multiline_string"
269 "$ref": "#/definitions/misc/multiline_string"
274 }
270 }
275 }
271 }
276 },
272 },
277
273
278 "error": {
274 "error": {
279 "description": "Output of an error that occurred during code cell execution.",
275 "description": "Output of an error that occurred during code cell execution.",
280 "type": "object",
276 "type": "object",
281 "additionalProperties": false,
277 "additionalProperties": false,
282 "required": ["output_type", "ename", "evalue", "traceback"],
278 "required": ["output_type", "ename", "evalue", "traceback"],
283 "properties": {
279 "properties": {
284 "output_type": {
280 "output_type": {
285 "description": "Type of cell output.",
281 "description": "Type of cell output.",
286 "enum": ["error"]
282 "enum": ["error"]
287 },
283 },
288 "ename": {
284 "ename": {
289 "description": "The name of the error.",
285 "description": "The name of the error.",
290 "type": "string"
286 "type": "string"
291 },
287 },
292 "evalue": {
288 "evalue": {
293 "description": "The value, or message, of the error.",
289 "description": "The value, or message, of the error.",
294 "type": "string"
290 "type": "string"
295 },
291 },
296 "traceback": {
292 "traceback": {
297 "description": "The error's traceback, represented as an array of strings.",
293 "description": "The error's traceback, represented as an array of strings.",
298 "type": "array",
294 "type": "array",
299 "items": {"type": "string"}
295 "items": {"type": "string"}
300 }
296 }
301 }
297 }
302 },
298 },
303
299
304 "unrecognized_output": {
300 "unrecognized_output": {
305 "description": "Unrecognized output from a future minor-revision to the notebook format.",
301 "description": "Unrecognized output from a future minor-revision to the notebook format.",
306 "type": "object",
302 "type": "object",
307 "additionalProperties": true,
303 "additionalProperties": true,
308 "required": ["output_type"],
304 "required": ["output_type"],
309 "properties": {
305 "properties": {
310 "output_type": {
306 "output_type": {
311 "description": "Type of cell output.",
307 "description": "Type of cell output.",
312 "not": {
308 "not": {
313 "enum": ["execute_result", "display_data", "stream", "error"]
309 "enum": ["execute_result", "display_data", "stream", "error"]
314 }
310 }
315 }
311 }
316 }
312 }
317 },
313 },
318
314
319 "misc": {
315 "misc": {
320 "metadata_name": {
316 "metadata_name": {
321 "description": "The cell's name. If present, must be a non-empty string.",
317 "description": "The cell's name. If present, must be a non-empty string.",
322 "type": "string",
318 "type": "string",
323 "pattern": "^.+$"
319 "pattern": "^.+$"
324 },
320 },
325 "metadata_tags": {
321 "metadata_tags": {
326 "description": "The cell's tags. Tags must be unique, and must not contain commas.",
322 "description": "The cell's tags. Tags must be unique, and must not contain commas.",
327 "type": "array",
323 "type": "array",
328 "uniqueItems": true,
324 "uniqueItems": true,
329 "items": {
325 "items": {
330 "type": "string",
326 "type": "string",
331 "pattern": "^[^,]+$"
327 "pattern": "^[^,]+$"
332 }
328 }
333 },
329 },
334 "source": {
330 "source": {
335 "description": "Contents of the cell, represented as an array of lines.",
331 "description": "Contents of the cell, represented as an array of lines.",
336 "$ref": "#/definitions/misc/multiline_string"
332 "$ref": "#/definitions/misc/multiline_string"
337 },
333 },
338 "execution_count": {
334 "execution_count": {
339 "description": "The code cell's prompt number. Will be null if the cell has not been run.",
335 "description": "The code cell's prompt number. Will be null if the cell has not been run.",
340 "type": ["integer", "null"],
336 "type": ["integer", "null"],
341 "minimum": 0
337 "minimum": 0
342 },
338 },
343 "mimebundle": {
339 "mimebundle": {
344 "description": "A mime-type keyed dictionary of data",
340 "description": "A mime-type keyed dictionary of data",
345 "type": "object",
341 "type": "object",
346 "additionalProperties": false,
342 "additionalProperties": false,
347 "properties": {
343 "properties": {
348 "application/json": {
344 "application/json": {
349 "type": "object"
345 "type": "object"
350 }
346 }
351 },
347 },
352 "patternProperties": {
348 "patternProperties": {
353 "^(?!application/json$)[a-zA-Z0-9]+/[a-zA-Z0-9\\-\\+\\.]+$": {
349 "^(?!application/json$)[a-zA-Z0-9]+/[a-zA-Z0-9\\-\\+\\.]+$": {
354 "description": "mimetype output (e.g. text/plain), represented as either an array of strings or a string.",
350 "description": "mimetype output (e.g. text/plain), represented as either an array of strings or a string.",
355 "$ref": "#/definitions/misc/multiline_string"
351 "$ref": "#/definitions/misc/multiline_string"
356 }
352 }
357 }
353 }
358 },
354 },
359 "output_metadata": {
355 "output_metadata": {
360 "description": "Cell output metadata.",
356 "description": "Cell output metadata.",
361 "type": "object",
357 "type": "object",
362 "additionalProperties": true
358 "additionalProperties": true
363 },
359 },
364 "multiline_string": {
360 "multiline_string": {
365 "oneOf" : [
361 "oneOf" : [
366 {"type": "string"},
362 {"type": "string"},
367 {
363 {
368 "type": "array",
364 "type": "array",
369 "items": {"type": "string"}
365 "items": {"type": "string"}
370 }
366 }
371 ]
367 ]
372 }
368 }
373 }
369 }
374 }
370 }
375 }
371 }
@@ -1,95 +1,96 b''
1 """Base classes and utilities for readers and writers."""
1 """Base classes and utilities for readers and writers."""
2
2
3 # Copyright (c) IPython Development Team.
3 # Copyright (c) IPython Development Team.
4 # Distributed under the terms of the Modified BSD License.
4 # Distributed under the terms of the Modified BSD License.
5
5
6 from IPython.utils.py3compat import string_types, cast_unicode_py2
6 from IPython.utils.py3compat import string_types, cast_unicode_py2
7
7
8
8
9 def rejoin_lines(nb):
9 def rejoin_lines(nb):
10 """rejoin multiline text into strings
10 """rejoin multiline text into strings
11
11
12 For reversing effects of ``split_lines(nb)``.
12 For reversing effects of ``split_lines(nb)``.
13
13
14 This only rejoins lines that have been split, so if text objects were not split
14 This only rejoins lines that have been split, so if text objects were not split
15 they will pass through unchanged.
15 they will pass through unchanged.
16
16
17 Used when reading JSON files that may have been passed through split_lines.
17 Used when reading JSON files that may have been passed through split_lines.
18 """
18 """
19 for cell in nb.cells:
19 for cell in nb.cells:
20 if 'source' in cell and isinstance(cell.source, list):
20 if 'source' in cell and isinstance(cell.source, list):
21 cell.source = ''.join(cell.source)
21 cell.source = ''.join(cell.source)
22 if cell.get('cell_type', None) == 'code':
22 if cell.get('cell_type', None) == 'code':
23 for output in cell.get('outputs', []):
23 for output in cell.get('outputs', []):
24 output_type = output.get('output_type', '')
24 output_type = output.get('output_type', '')
25 if output_type in {'execute_result', 'display_data'}:
25 if output_type in {'execute_result', 'display_data'}:
26 for key, value in output.get('data', {}).items():
26 for key, value in output.get('data', {}).items():
27 if key != 'application/json' and isinstance(value, list):
27 if key != 'application/json' and isinstance(value, list):
28 output.data[key] = ''.join(value)
28 output.data[key] = ''.join(value)
29 elif output_type:
29 elif output_type:
30 if isinstance(output.get('text', ''), list):
30 if isinstance(output.get('text', ''), list):
31 output.text = ''.join(output.text)
31 output.text = ''.join(output.text)
32 return nb
32 return nb
33
33
34
34
35 def split_lines(nb):
35 def split_lines(nb):
36 """split likely multiline text into lists of strings
36 """split likely multiline text into lists of strings
37
37
38 For file output more friendly to line-based VCS. ``rejoin_lines(nb)`` will
38 For file output more friendly to line-based VCS. ``rejoin_lines(nb)`` will
39 reverse the effects of ``split_lines(nb)``.
39 reverse the effects of ``split_lines(nb)``.
40
40
41 Used when writing JSON files.
41 Used when writing JSON files.
42 """
42 """
43 for cell in nb.cells:
43 for cell in nb.cells:
44 source = cell.get('source', None)
44 source = cell.get('source', None)
45 if isinstance(source, string_types):
45 if isinstance(source, string_types):
46 cell['source'] = source.splitlines(True)
46 cell['source'] = source.splitlines(True)
47
47
48 if cell.cell_type == 'code':
48 if cell.cell_type == 'code':
49 for output in cell.outputs:
49 for output in cell.outputs:
50 if output.output_type in {'execute_result', 'display_data'}:
50 if output.output_type in {'execute_result', 'display_data'}:
51 for key, value in output.data.items():
51 for key, value in output.data.items():
52 if key != 'application/json' and isinstance(value, string_types):
52 if key != 'application/json' and isinstance(value, string_types):
53 output.data[key] = value.splitlines(True)
53 output.data[key] = value.splitlines(True)
54 elif output.output_type == 'stream':
54 elif output.output_type == 'stream':
55 if isinstance(output.text, string_types):
55 if isinstance(output.text, string_types):
56 output.text = output.text.splitlines(True)
56 output.text = output.text.splitlines(True)
57 return nb
57 return nb
58
58
59
59
60 def strip_transient(nb):
60 def strip_transient(nb):
61 """Strip transient values that shouldn't be stored in files.
61 """Strip transient values that shouldn't be stored in files.
62
62
63 This should be called in *both* read and write.
63 This should be called in *both* read and write.
64 """
64 """
65 nb.metadata.pop('orig_nbformat', None)
65 nb.metadata.pop('orig_nbformat', None)
66 nb.metadata.pop('orig_nbformat_minor', None)
66 nb.metadata.pop('orig_nbformat_minor', None)
67 nb.metadata.pop('signature', None)
67 for cell in nb.cells:
68 for cell in nb.cells:
68 cell.metadata.pop('trusted', None)
69 cell.metadata.pop('trusted', None)
69 return nb
70 return nb
70
71
71
72
72 class NotebookReader(object):
73 class NotebookReader(object):
73 """A class for reading notebooks."""
74 """A class for reading notebooks."""
74
75
75 def reads(self, s, **kwargs):
76 def reads(self, s, **kwargs):
76 """Read a notebook from a string."""
77 """Read a notebook from a string."""
77 raise NotImplementedError("loads must be implemented in a subclass")
78 raise NotImplementedError("loads must be implemented in a subclass")
78
79
79 def read(self, fp, **kwargs):
80 def read(self, fp, **kwargs):
80 """Read a notebook from a file like object"""
81 """Read a notebook from a file like object"""
81 nbs = cast_unicode_py2(fp.read())
82 nbs = cast_unicode_py2(fp.read())
82 return self.reads(nbs, **kwargs)
83 return self.reads(nbs, **kwargs)
83
84
84
85
85 class NotebookWriter(object):
86 class NotebookWriter(object):
86 """A class for writing notebooks."""
87 """A class for writing notebooks."""
87
88
88 def writes(self, nb, **kwargs):
89 def writes(self, nb, **kwargs):
89 """Write a notebook to a string."""
90 """Write a notebook to a string."""
90 raise NotImplementedError("loads must be implemented in a subclass")
91 raise NotImplementedError("loads must be implemented in a subclass")
91
92
92 def write(self, nb, fp, **kwargs):
93 def write(self, nb, fp, **kwargs):
93 """Write a notebook to a file like object"""
94 """Write a notebook to a file like object"""
94 nbs = cast_unicode_py2(self.writes(nb, **kwargs))
95 nbs = cast_unicode_py2(self.writes(nb, **kwargs))
95 return fp.write(nbs)
96 return fp.write(nbs)
General Comments 0
You need to be logged in to leave comments. Login now