##// END OF EJS Templates
Add top-level IPython.nbformat API...
MinRK -
Show More
@@ -0,0 +1,143 b''
1 """The IPython notebook format
2
3 Use this module to read or write notebook files as particular nbformat versions.
4 """
5
6 # Copyright (c) IPython Development Team.
7 # Distributed under the terms of the Modified BSD License.
8
9 from IPython.utils.log import get_logger
10
11 from . import v1
12 from . import v2
13 from . import v3
14 from . import v4
15
16 versions = {
17 1: v1,
18 2: v2,
19 3: v3,
20 4: v4,
21 }
22
23 from .validator import validate, ValidationError
24 from .converter import convert
25 from . import reader
26
27 from .v4 import (
28 nbformat as current_nbformat,
29 nbformat_minor as current_nbformat_minor,
30 )
31
32 class NBFormatError(ValueError):
33 pass
34
35 # no-conversion singleton
36 NO_CONVERT = object()
37
38 def reads(s, as_version, **kwargs):
39 """Read a notebook from a string and return the NotebookNode object as the given version.
40
41 The string can contain a notebook of any version.
42 The notebook will be returned `as_version`, converting, if necessary.
43
44 Notebook format errors will be logged.
45
46 Parameters
47 ----------
48 s : unicode
49 The raw unicode string to read the notebook from.
50 as_version : int
51 The version of the notebook format to return.
52 The notebook will be converted, if necessary.
53 Pass nbformat.NO_CONVERT to prevent conversion.
54
55 Returns
56 -------
57 nb : NotebookNode
58 The notebook that was read.
59 """
60 nb = reader.reads(s, **kwargs)
61 if as_version is not NO_CONVERT:
62 nb = convert(nb, as_version)
63 try:
64 validate(nb)
65 except ValidationError as e:
66 get_logger().error("Notebook JSON is invalid: %s", e)
67 return nb
68
69
70 def writes(nb, version, **kwargs):
71 """Write a notebook to a string in a given format in the given nbformat version.
72
73 Any notebook format errors will be logged.
74
75 Parameters
76 ----------
77 nb : NotebookNode
78 The notebook to write.
79 version : int
80 The nbformat version to write.
81 If nb is not this version, it will be converted.
82 Pass nbformat.NO_CONVERT to prevent conversion.
83
84 Returns
85 -------
86 s : unicode
87 The notebook as a JSON string.
88 """
89 if version is not NO_CONVERT:
90 nb = convert(nb, version)
91 else:
92 version, _ = reader.get_version(nb)
93 try:
94 validate(nb)
95 except ValidationError as e:
96 get_logger().error("Notebook JSON is invalid: %s", e)
97 return versions[version].writes_json(nb, **kwargs)
98
99
100 def read(fp, as_version, **kwargs):
101 """Read a notebook from a file as a NotebookNode of the given version.
102
103 The string can contain a notebook of any version.
104 The notebook will be returned `as_version`, converting, if necessary.
105
106 Notebook format errors will be logged.
107
108 Parameters
109 ----------
110 fp : file
111 Any file-like object with a read method.
112 as_version: int
113 The version of the notebook format to return.
114 The notebook will be converted, if necessary.
115 Pass nbformat.NO_CONVERT to prevent conversion.
116
117 Returns
118 -------
119 nb : NotebookNode
120 The notebook that was read.
121 """
122 return reads(fp.read(), as_version, **kwargs)
123
124
125 def write(fp, nb, version, **kwargs):
126 """Write a notebook to a file in a given nbformat version.
127
128 The file-like object must accept unicode input.
129
130 Parameters
131 ----------
132 fp : file
133 Any file-like object with a write method that accepts unicode.
134 nb : NotebookNode
135 The notebook to write.
136 version : int
137 The nbformat version to write.
138 If nb is not this version, it will be converted.
139 """
140 s = writes(nb, version, **kwargs)
141 if isinstance(s, bytes):
142 s = s.decode('utf8')
143 return fp.write(s)
@@ -1,53 +1,54 b''
1 1 """API for converting notebooks between versions."""
2 2
3 3 # Copyright (c) IPython Development Team.
4 4 # Distributed under the terms of the Modified BSD License.
5 5
6 from .reader import get_version, versions
6 from . import versions
7 from .reader import get_version
7 8
8 9
9 10 def convert(nb, to_version):
10 11 """Convert a notebook node object to a specific version. Assumes that
11 12 all the versions starting from 1 to the latest major X are implemented.
12 13 In other words, there should never be a case where v1 v2 v3 v5 exist without
13 14 a v4. Also assumes that all conversions can be made in one step increments
14 15 between major versions and ignores minor revisions.
15 16
16 17 Parameters
17 18 ----------
18 19 nb : NotebookNode
19 20 to_version : int
20 21 Major revision to convert the notebook to. Can either be an upgrade or
21 22 a downgrade.
22 23 """
23 24
24 25 # Get input notebook version.
25 26 (version, version_minor) = get_version(nb)
26 27
27 # Check if destination is current version, if so return contents
28 # Check if destination is target version, if so return contents
28 29 if version == to_version:
29 30 return nb
30 31
31 32 # If the version exist, try to convert to it one step at a time.
32 33 elif to_version in versions:
33 34
34 35 # Get the the version that this recursion will convert to as a step
35 36 # closer to the final revision. Make sure the newer of the conversion
36 37 # functions is used to perform the conversion.
37 38 if to_version > version:
38 39 step_version = version + 1
39 40 convert_function = versions[step_version].upgrade
40 41 else:
41 42 step_version = version - 1
42 43 convert_function = versions[version].downgrade
43 44
44 45 # Convert and make sure version changed during conversion.
45 46 converted = convert_function(nb)
46 47 if converted.get('nbformat', 1) == version:
47 48 raise ValueError("Failed to convert notebook from v%d to v%d." % (version, step_version))
48 49
49 50 # Recursively convert until target version is reached.
50 51 return convert(converted, to_version)
51 52 else:
52 53 raise ValueError("Cannot convert notebook to v%d because that " \
53 54 "version doesn't exist" % (to_version))
@@ -1,182 +1,182 b''
1 1 """The official API for working with notebooks in the current format version."""
2 2
3 3 # Copyright (c) IPython Development Team.
4 4 # Distributed under the terms of the Modified BSD License.
5 5
6 6 from __future__ import print_function
7 7
8 8 import re
9 9 import warnings
10 10
11 11 from IPython.nbformat.v3 import (
12 12 NotebookNode,
13 13 new_code_cell, new_text_cell, new_notebook, new_output, new_worksheet,
14 14 parse_filename, new_metadata, new_author, new_heading_cell, nbformat,
15 15 nbformat_minor, nbformat_schema, to_notebook_json,
16 16 )
17 17 from IPython.nbformat import v3 as _v_latest
18 18
19 19 from .reader import reads as reader_reads
20 from .reader import versions
21 from .convert import convert
20 from . import versions
21 from .converter import convert
22 22 from .validator import validate, ValidationError
23 23
24 24 from IPython.utils.log import get_logger
25 25
26 26 __all__ = ['NotebookNode', 'new_code_cell', 'new_text_cell', 'new_notebook',
27 27 'new_output', 'new_worksheet', 'parse_filename', 'new_metadata', 'new_author',
28 28 'new_heading_cell', 'nbformat', 'nbformat_minor', 'nbformat_schema',
29 29 'to_notebook_json', 'convert', 'validate', 'NBFormatError', 'parse_py',
30 30 'reads_json', 'writes_json', 'reads_py', 'writes_py', 'reads', 'writes', 'read',
31 31 'write']
32 32
33 33 current_nbformat = nbformat
34 34 current_nbformat_minor = nbformat_minor
35 35 current_nbformat_module = _v_latest.__name__
36 36
37 37
38 38 class NBFormatError(ValueError):
39 39 pass
40 40
41 41
42 42 def _warn_format():
43 43 warnings.warn("""Non-JSON file support in nbformat is deprecated.
44 44 Use nbconvert to create files of other formats.""")
45 45
46 46
47 47 def parse_py(s, **kwargs):
48 48 """Parse a string into a (nbformat, string) tuple."""
49 49 nbf = current_nbformat
50 50 nbm = current_nbformat_minor
51 51
52 52 pattern = r'# <nbformat>(?P<nbformat>\d+[\.\d+]*)</nbformat>'
53 53 m = re.search(pattern,s)
54 54 if m is not None:
55 55 digits = m.group('nbformat').split('.')
56 56 nbf = int(digits[0])
57 57 if len(digits) > 1:
58 58 nbm = int(digits[1])
59 59
60 60 return nbf, nbm, s
61 61
62 62
63 63 def reads_json(nbjson, **kwargs):
64 64 """DEPRECATED, use reads"""
65 65 warnings.warn("reads_json is deprecated, use reads")
66 66 return reads(nbjson)
67 67
68 68 def writes_json(nb, **kwargs):
69 69 """DEPRECATED, use writes"""
70 70 warnings.warn("writes_json is deprecated, use writes")
71 71 return writes(nb, **kwargs)
72 72
73 73 def reads_py(s, **kwargs):
74 74 """DEPRECATED: use nbconvert"""
75 75 _warn_format()
76 76 nbf, nbm, s = parse_py(s, **kwargs)
77 77 if nbf in (2, 3):
78 78 nb = versions[nbf].to_notebook_py(s, **kwargs)
79 79 else:
80 80 raise NBFormatError('Unsupported PY nbformat version: %i' % nbf)
81 81 return nb
82 82
83 83 def writes_py(nb, **kwargs):
84 84 """DEPRECATED: use nbconvert"""
85 85 _warn_format()
86 86 return versions[3].writes_py(nb, **kwargs)
87 87
88 88
89 89 # High level API
90 90
91 91
92 92 def reads(s, format='DEPRECATED', version=current_nbformat, **kwargs):
93 93 """Read a notebook from a string and return the NotebookNode object.
94 94
95 95 This function properly handles notebooks of any version. The notebook
96 96 returned will always be in the current version's format.
97 97
98 98 Parameters
99 99 ----------
100 100 s : unicode
101 101 The raw unicode string to read the notebook from.
102 102
103 103 Returns
104 104 -------
105 105 nb : NotebookNode
106 106 The notebook that was read.
107 107 """
108 108 if format not in {'DEPRECATED', 'json'}:
109 109 _warn_format()
110 110 nb = reader_reads(s, **kwargs)
111 111 nb = convert(nb, version)
112 112 try:
113 113 validate(nb)
114 114 except ValidationError as e:
115 115 get_logger().error("Notebook JSON is invalid: %s", e)
116 116 return nb
117 117
118 118
119 119 def writes(nb, format='DEPRECATED', version=current_nbformat, **kwargs):
120 120 """Write a notebook to a string in a given format in the current nbformat version.
121 121
122 122 This function always writes the notebook in the current nbformat version.
123 123
124 124 Parameters
125 125 ----------
126 126 nb : NotebookNode
127 127 The notebook to write.
128 128 version : int
129 129 The nbformat version to write.
130 130 Used for downgrading notebooks.
131 131
132 132 Returns
133 133 -------
134 134 s : unicode
135 135 The notebook string.
136 136 """
137 137 if format not in {'DEPRECATED', 'json'}:
138 138 _warn_format()
139 139 nb = convert(nb, version)
140 140 try:
141 141 validate(nb)
142 142 except ValidationError as e:
143 143 get_logger().error("Notebook JSON is invalid: %s", e)
144 144 return versions[version].writes_json(nb, **kwargs)
145 145
146 146
147 147 def read(fp, format='DEPRECATED', **kwargs):
148 148 """Read a notebook from a file and return the NotebookNode object.
149 149
150 150 This function properly handles notebooks of any version. The notebook
151 151 returned will always be in the current version's format.
152 152
153 153 Parameters
154 154 ----------
155 155 fp : file
156 156 Any file-like object with a read method.
157 157
158 158 Returns
159 159 -------
160 160 nb : NotebookNode
161 161 The notebook that was read.
162 162 """
163 163 return reads(fp.read(), **kwargs)
164 164
165 165
166 166 def write(nb, fp, format='DEPRECATED', **kwargs):
167 167 """Write a notebook to a file in a given format in the current nbformat version.
168 168
169 169 This function always writes the notebook in the current nbformat version.
170 170
171 171 Parameters
172 172 ----------
173 173 nb : NotebookNode
174 174 The notebook to write.
175 175 fp : file
176 176 Any file-like object with a write method.
177 177 """
178 178 s = writes(nb, **kwargs)
179 179 if isinstance(s, bytes):
180 180 s = s.decode('utf8')
181 181 return fp.write(s)
182 182
@@ -1,95 +1,82 b''
1 1 """API for reading notebooks of different versions"""
2 2
3 3 # Copyright (c) IPython Development Team.
4 4 # Distributed under the terms of the Modified BSD License.
5 5
6 6 import json
7 7
8 from . import v1
9 from . import v2
10 from . import v3
11 from . import v4
12
13 versions = {
14 1: v1,
15 2: v2,
16 3: v3,
17 4: v4,
18 }
19
20
21 8 class NotJSONError(ValueError):
22 9 pass
23 10
24 11 def parse_json(s, **kwargs):
25 12 """Parse a JSON string into a dict."""
26 13 try:
27 14 nb_dict = json.loads(s, **kwargs)
28 15 except ValueError:
29 16 # Limit the error message to 80 characters. Display whatever JSON will fit.
30 17 raise NotJSONError(("Notebook does not appear to be JSON: %r" % s)[:77] + "...")
31 18 return nb_dict
32 19
33 20 # High level API
34 21
35 22 def get_version(nb):
36 23 """Get the version of a notebook.
37 24
38 25 Parameters
39 26 ----------
40 27 nb : dict
41 28 NotebookNode or dict containing notebook data.
42 29
43 30 Returns
44 31 -------
45 32 Tuple containing major (int) and minor (int) version numbers
46 33 """
47 34 major = nb.get('nbformat', 1)
48 35 minor = nb.get('nbformat_minor', 0)
49 36 return (major, minor)
50 37
51 38
52 39 def reads(s, **kwargs):
53 40 """Read a notebook from a json string and return the
54 41 NotebookNode object.
55 42
56 43 This function properly reads notebooks of any version. No version
57 44 conversion is performed.
58 45
59 46 Parameters
60 47 ----------
61 48 s : unicode
62 49 The raw unicode string to read the notebook from.
63 50
64 51 Returns
65 52 -------
66 53 nb : NotebookNode
67 54 The notebook that was read.
68 55 """
69 from .current import NBFormatError
56 from . import versions, NBFormatError
70 57
71 58 nb_dict = parse_json(s, **kwargs)
72 59 (major, minor) = get_version(nb_dict)
73 60 if major in versions:
74 61 return versions[major].to_notebook_json(nb_dict, minor=minor)
75 62 else:
76 63 raise NBFormatError('Unsupported nbformat version %s' % major)
77 64
78 65
79 66 def read(fp, **kwargs):
80 67 """Read a notebook from a file and return the NotebookNode object.
81 68
82 69 This function properly reads notebooks of any version. No version
83 70 conversion is performed.
84 71
85 72 Parameters
86 73 ----------
87 74 fp : file
88 75 Any file-like object with a read method.
89 76
90 77 Returns
91 78 -------
92 79 nb : NotebookNode
93 80 The notebook that was read.
94 81 """
95 82 return reads(fp.read(), **kwargs)
@@ -1,305 +1,306 b''
1 1 """Functions for signing notebooks"""
2 2
3 3 # Copyright (c) IPython Development Team.
4 4 # Distributed under the terms of the Modified BSD License.
5 5
6 6 import base64
7 7 from contextlib import contextmanager
8 8 import hashlib
9 9 from hmac import HMAC
10 10 import io
11 11 import os
12 12
13 from IPython.utils.io import atomic_writing
13 14 from IPython.utils.py3compat import string_types, unicode_type, cast_bytes
14 15 from IPython.utils.traitlets import Instance, Bytes, Enum, Any, Unicode, Bool
15 16 from IPython.config import LoggingConfigurable, MultipleInstanceError
16 17 from IPython.core.application import BaseIPythonApplication, base_flags
17 18
18 from .current import read, write
19 from . import read, write, NO_CONVERT
19 20
20 21 try:
21 22 # Python 3
22 23 algorithms = hashlib.algorithms_guaranteed
23 24 except AttributeError:
24 25 algorithms = hashlib.algorithms
25 26
26 27
27 28 def yield_everything(obj):
28 29 """Yield every item in a container as bytes
29 30
30 31 Allows any JSONable object to be passed to an HMAC digester
31 32 without having to serialize the whole thing.
32 33 """
33 34 if isinstance(obj, dict):
34 35 for key in sorted(obj):
35 36 value = obj[key]
36 37 yield cast_bytes(key)
37 38 for b in yield_everything(value):
38 39 yield b
39 40 elif isinstance(obj, (list, tuple)):
40 41 for element in obj:
41 42 for b in yield_everything(element):
42 43 yield b
43 44 elif isinstance(obj, unicode_type):
44 45 yield obj.encode('utf8')
45 46 else:
46 47 yield unicode_type(obj).encode('utf8')
47 48
48 49
49 50 @contextmanager
50 51 def signature_removed(nb):
51 52 """Context manager for operating on a notebook with its signature removed
52 53
53 54 Used for excluding the previous signature when computing a notebook's signature.
54 55 """
55 56 save_signature = nb['metadata'].pop('signature', None)
56 57 try:
57 58 yield
58 59 finally:
59 60 if save_signature is not None:
60 61 nb['metadata']['signature'] = save_signature
61 62
62 63
63 64 class NotebookNotary(LoggingConfigurable):
64 65 """A class for computing and verifying notebook signatures."""
65 66
66 67 profile_dir = Instance("IPython.core.profiledir.ProfileDir")
67 68 def _profile_dir_default(self):
68 69 from IPython.core.application import BaseIPythonApplication
69 70 app = None
70 71 try:
71 72 if BaseIPythonApplication.initialized():
72 73 app = BaseIPythonApplication.instance()
73 74 except MultipleInstanceError:
74 75 pass
75 76 if app is None:
76 77 # create an app, without the global instance
77 78 app = BaseIPythonApplication()
78 79 app.initialize(argv=[])
79 80 return app.profile_dir
80 81
81 82 algorithm = Enum(algorithms, default_value='sha256', config=True,
82 83 help="""The hashing algorithm used to sign notebooks."""
83 84 )
84 85 def _algorithm_changed(self, name, old, new):
85 86 self.digestmod = getattr(hashlib, self.algorithm)
86 87
87 88 digestmod = Any()
88 89 def _digestmod_default(self):
89 90 return getattr(hashlib, self.algorithm)
90 91
91 92 secret_file = Unicode(config=True,
92 93 help="""The file where the secret key is stored."""
93 94 )
94 95 def _secret_file_default(self):
95 96 if self.profile_dir is None:
96 97 return ''
97 98 return os.path.join(self.profile_dir.security_dir, 'notebook_secret')
98 99
99 100 secret = Bytes(config=True,
100 101 help="""The secret key with which notebooks are signed."""
101 102 )
102 103 def _secret_default(self):
103 104 # note : this assumes an Application is running
104 105 if os.path.exists(self.secret_file):
105 106 with io.open(self.secret_file, 'rb') as f:
106 107 return f.read()
107 108 else:
108 109 secret = base64.encodestring(os.urandom(1024))
109 110 self._write_secret_file(secret)
110 111 return secret
111 112
112 113 def _write_secret_file(self, secret):
113 114 """write my secret to my secret_file"""
114 115 self.log.info("Writing notebook-signing key to %s", self.secret_file)
115 116 with io.open(self.secret_file, 'wb') as f:
116 117 f.write(secret)
117 118 try:
118 119 os.chmod(self.secret_file, 0o600)
119 120 except OSError:
120 121 self.log.warn(
121 122 "Could not set permissions on %s",
122 123 self.secret_file
123 124 )
124 125 return secret
125 126
126 127 def compute_signature(self, nb):
127 128 """Compute a notebook's signature
128 129
129 130 by hashing the entire contents of the notebook via HMAC digest.
130 131 """
131 132 hmac = HMAC(self.secret, digestmod=self.digestmod)
132 133 # don't include the previous hash in the content to hash
133 134 with signature_removed(nb):
134 135 # sign the whole thing
135 136 for b in yield_everything(nb):
136 137 hmac.update(b)
137 138
138 139 return hmac.hexdigest()
139 140
140 141 def check_signature(self, nb):
141 142 """Check a notebook's stored signature
142 143
143 144 If a signature is stored in the notebook's metadata,
144 145 a new signature is computed and compared with the stored value.
145 146
146 147 Returns True if the signature is found and matches, False otherwise.
147 148
148 149 The following conditions must all be met for a notebook to be trusted:
149 150 - a signature is stored in the form 'scheme:hexdigest'
150 151 - the stored scheme matches the requested scheme
151 152 - the requested scheme is available from hashlib
152 153 - the computed hash from notebook_signature matches the stored hash
153 154 """
154 155 stored_signature = nb['metadata'].get('signature', None)
155 156 if not stored_signature \
156 157 or not isinstance(stored_signature, string_types) \
157 158 or ':' not in stored_signature:
158 159 return False
159 160 stored_algo, sig = stored_signature.split(':', 1)
160 161 if self.algorithm != stored_algo:
161 162 return False
162 163 my_signature = self.compute_signature(nb)
163 164 return my_signature == sig
164 165
165 166 def sign(self, nb):
166 167 """Sign a notebook, indicating that its output is trusted
167 168
168 169 stores 'algo:hmac-hexdigest' in notebook.metadata.signature
169 170
170 171 e.g. 'sha256:deadbeef123...'
171 172 """
172 173 signature = self.compute_signature(nb)
173 174 nb['metadata']['signature'] = "%s:%s" % (self.algorithm, signature)
174 175
175 176 def mark_cells(self, nb, trusted):
176 177 """Mark cells as trusted if the notebook's signature can be verified
177 178
178 179 Sets ``cell.metadata.trusted = True | False`` on all code cells,
179 180 depending on whether the stored signature can be verified.
180 181
181 182 This function is the inverse of check_cells
182 183 """
183 184 for cell in nb['cells']:
184 185 if cell['cell_type'] == 'code':
185 186 cell['metadata']['trusted'] = trusted
186 187
187 188 def _check_cell(self, cell):
188 189 """Do we trust an individual cell?
189 190
190 191 Return True if:
191 192
192 193 - cell is explicitly trusted
193 194 - cell has no potentially unsafe rich output
194 195
195 196 If a cell has no output, or only simple print statements,
196 197 it will always be trusted.
197 198 """
198 199 # explicitly trusted
199 200 if cell['metadata'].pop("trusted", False):
200 201 return True
201 202
202 203 # explicitly safe output
203 204 safe = {
204 205 'text/plain', 'image/png', 'image/jpeg',
205 206 }
206 207
207 208 for output in cell['outputs']:
208 209 output_type = output['output_type']
209 210 if output_type in {'execute_result', 'display_data'}:
210 211 # if there are any data keys not in the safe whitelist
211 212 output_keys = set(output).difference({"output_type", "execution_count", "metadata"})
212 213 if output_keys.difference(safe):
213 214 return False
214 215
215 216 return True
216 217
217 218 def check_cells(self, nb):
218 219 """Return whether all code cells are trusted
219 220
220 221 If there are no code cells, return True.
221 222
222 223 This function is the inverse of mark_cells.
223 224 """
224 225 trusted = True
225 226 for cell in nb['cells']:
226 227 if cell['cell_type'] != 'code':
227 228 continue
228 229 # only distrust a cell if it actually has some output to distrust
229 230 if not self._check_cell(cell):
230 231 trusted = False
231 232
232 233 return trusted
233 234
234 235
235 236 trust_flags = {
236 237 'reset' : (
237 238 {'TrustNotebookApp' : { 'reset' : True}},
238 239 """Generate a new key for notebook signature.
239 240 All previously signed notebooks will become untrusted.
240 241 """
241 242 ),
242 243 }
243 244 trust_flags.update(base_flags)
244 245 trust_flags.pop('init')
245 246
246 247
247 248 class TrustNotebookApp(BaseIPythonApplication):
248 249
249 250 description="""Sign one or more IPython notebooks with your key,
250 251 to trust their dynamic (HTML, Javascript) output.
251 252
252 253 Trusting a notebook only applies to the current IPython profile.
253 254 To trust a notebook for use with a profile other than default,
254 255 add `--profile [profile name]`.
255 256
256 257 Otherwise, you will have to re-execute the notebook to see output.
257 258 """
258 259
259 260 examples = """
260 261 ipython trust mynotebook.ipynb and_this_one.ipynb
261 262 ipython trust --profile myprofile mynotebook.ipynb
262 263 """
263 264
264 265 flags = trust_flags
265 266
266 267 reset = Bool(False, config=True,
267 268 help="""If True, generate a new key for notebook signature.
268 269 After reset, all previously signed notebooks will become untrusted.
269 270 """
270 271 )
271 272
272 273 notary = Instance(NotebookNotary)
273 274 def _notary_default(self):
274 275 return NotebookNotary(parent=self, profile_dir=self.profile_dir)
275 276
276 277 def sign_notebook(self, notebook_path):
277 278 if not os.path.exists(notebook_path):
278 279 self.log.error("Notebook missing: %s" % notebook_path)
279 280 self.exit(1)
280 281 with io.open(notebook_path, encoding='utf8') as f:
281 nb = read(f, 'json')
282 nb = read(f, NO_CONVERT)
282 283 if self.notary.check_signature(nb):
283 284 print("Notebook already signed: %s" % notebook_path)
284 285 else:
285 286 print("Signing notebook: %s" % notebook_path)
286 287 self.notary.sign(nb)
287 with io.open(notebook_path, 'w', encoding='utf8') as f:
288 write(nb, f, 'json')
288 with atomic_writing(notebook_path) as f:
289 write(f, nb, NO_CONVERT)
289 290
290 291 def generate_new_key(self):
291 292 """Generate a new notebook signature key"""
292 293 print("Generating new notebook key: %s" % self.notary.secret_file)
293 294 self.notary._write_secret_file(os.urandom(1024))
294 295
295 296 def start(self):
296 297 if self.reset:
297 298 self.generate_new_key()
298 299 return
299 300 if not self.extra_args:
300 301 self.log.critical("Specify at least one notebook to sign.")
301 302 self.exit(1)
302 303
303 304 for notebook_path in self.extra_args:
304 305 self.sign_notebook(notebook_path)
305 306
@@ -1,41 +1,37 b''
1 """
2 Contains tests class for current.py
3 """
1 """Test the APIs at the top-level of nbformat"""
4 2
5 3 # Copyright (c) IPython Development Team.
6 4 # Distributed under the terms of the Modified BSD License.
7 5
8 import io
9 6 import json
10 import tempfile
11 7
12 8 from .base import TestsBase
13 9
14 10 from ..reader import get_version
15 from ..current import read, current_nbformat, validate, writes
11 from IPython.nbformat import read, current_nbformat, writes
16 12
17 13
18 class TestCurrent(TestsBase):
14 class TestAPI(TestsBase):
19 15
20 16 def test_read(self):
21 17 """Can older notebooks be opened and automatically converted to the current
22 18 nbformat?"""
23 19
24 20 # Open a version 2 notebook.
25 with self.fopen(u'test2.ipynb', u'r') as f:
26 nb = read(f)
21 with self.fopen(u'test2.ipynb', 'r') as f:
22 nb = read(f, as_version=current_nbformat)
27 23
28 24 # Check that the notebook was upgraded to the latest version automatically.
29 25 (major, minor) = get_version(nb)
30 26 self.assertEqual(major, current_nbformat)
31 27
32 28 def test_write_downgrade_2(self):
33 29 """dowgrade a v3 notebook to v2"""
34 30 # Open a version 3 notebook.
35 31 with self.fopen(u'test3.ipynb', 'r') as f:
36 nb = read(f, u'json')
32 nb = read(f, as_version=3)
37 33
38 34 jsons = writes(nb, version=2)
39 35 nb2 = json.loads(jsons)
40 36 (major, minor) = get_version(nb2)
41 37 self.assertEqual(major, 2)
@@ -1,57 +1,57 b''
1 1 """Tests for nbformat.convert"""
2 2
3 3 # Copyright (c) IPython Development Team.
4 4 # Distributed under the terms of the Modified BSD License.
5 5
6 6 from .base import TestsBase
7 7
8 from ..convert import convert
8 from ..converter import convert
9 9 from ..reader import read, get_version
10 from ..current import current_nbformat
10 from .. import current_nbformat
11 11
12 12
13 13 class TestConvert(TestsBase):
14 14
15 15 def test_downgrade_3_2(self):
16 16 """Do notebook downgrades work?"""
17 17
18 18 # Open a version 3 notebook and attempt to downgrade it to version 2.
19 19 with self.fopen(u'test3.ipynb', u'r') as f:
20 20 nb = read(f)
21 21 nb = convert(nb, 2)
22 22
23 23 # Check if downgrade was successful.
24 24 (major, minor) = get_version(nb)
25 25 self.assertEqual(major, 2)
26 26
27 27
28 28 def test_upgrade_2_3(self):
29 29 """Do notebook upgrades work?"""
30 30
31 31 # Open a version 2 notebook and attempt to upgrade it to version 3.
32 32 with self.fopen(u'test2.ipynb', u'r') as f:
33 33 nb = read(f)
34 34 nb = convert(nb, 3)
35 35
36 36 # Check if upgrade was successful.
37 37 (major, minor) = get_version(nb)
38 38 self.assertEqual(major, 3)
39 39
40 40
41 41 def test_open_current(self):
42 42 """Can an old notebook be opened and converted to the current version
43 43 while remembering the original version of the notebook?"""
44 44
45 45 # Open a version 2 notebook and attempt to upgrade it to the current version
46 46 # while remembering it's version information.
47 47 with self.fopen(u'test2.ipynb', u'r') as f:
48 48 nb = read(f)
49 49 (original_major, original_minor) = get_version(nb)
50 50 nb = convert(nb, current_nbformat)
51 51
52 52 # Check if upgrade was successful.
53 53 (major, minor) = get_version(nb)
54 54 self.assertEqual(major, current_nbformat)
55 55
56 56 # Check if the original major revision was remembered.
57 57 self.assertEqual(original_major, 2)
@@ -1,112 +1,111 b''
1 1 """Test Notebook signing"""
2 2
3 3 # Copyright (c) IPython Development Team.
4 4 # Distributed under the terms of the Modified BSD License.
5 5
6 from .. import sign
7 6 from .base import TestsBase
8 7
9 from ..current import read
8 from IPython.nbformat import read, sign
10 9 from IPython.core.getipython import get_ipython
11 10
12 11
13 12 class TestNotary(TestsBase):
14 13
15 14 def setUp(self):
16 15 self.notary = sign.NotebookNotary(
17 16 secret=b'secret',
18 17 profile_dir=get_ipython().profile_dir
19 18 )
20 19 with self.fopen(u'test3.ipynb', u'r') as f:
21 self.nb = read(f, u'json')
20 self.nb = read(f, as_version=4)
22 21
23 22 def test_algorithms(self):
24 23 last_sig = ''
25 24 for algo in sign.algorithms:
26 25 self.notary.algorithm = algo
27 26 self.notary.sign(self.nb)
28 27 sig = self.nb.metadata.signature
29 28 print(sig)
30 29 self.assertEqual(sig[:len(self.notary.algorithm)+1], '%s:' % self.notary.algorithm)
31 30 self.assertNotEqual(last_sig, sig)
32 31 last_sig = sig
33 32
34 33 def test_sign_same(self):
35 34 """Multiple signatures of the same notebook are the same"""
36 35 sig1 = self.notary.compute_signature(self.nb)
37 36 sig2 = self.notary.compute_signature(self.nb)
38 37 self.assertEqual(sig1, sig2)
39 38
40 39 def test_change_secret(self):
41 40 """Changing the secret changes the signature"""
42 41 sig1 = self.notary.compute_signature(self.nb)
43 42 self.notary.secret = b'different'
44 43 sig2 = self.notary.compute_signature(self.nb)
45 44 self.assertNotEqual(sig1, sig2)
46 45
47 46 def test_sign(self):
48 47 self.notary.sign(self.nb)
49 48 sig = self.nb.metadata.signature
50 49 self.assertEqual(sig[:len(self.notary.algorithm)+1], '%s:' % self.notary.algorithm)
51 50
52 51 def test_check_signature(self):
53 52 nb = self.nb
54 53 md = nb.metadata
55 54 notary = self.notary
56 55 check_signature = notary.check_signature
57 56 # no signature:
58 57 md.pop('signature', None)
59 58 self.assertFalse(check_signature(nb))
60 59 # hash only, no algo
61 60 md.signature = notary.compute_signature(nb)
62 61 self.assertFalse(check_signature(nb))
63 62 # proper signature, algo mismatch
64 63 notary.algorithm = 'sha224'
65 64 notary.sign(nb)
66 65 notary.algorithm = 'sha256'
67 66 self.assertFalse(check_signature(nb))
68 67 # check correctly signed notebook
69 68 notary.sign(nb)
70 69 self.assertTrue(check_signature(nb))
71 70
72 71 def test_mark_cells_untrusted(self):
73 72 cells = self.nb.cells
74 73 self.notary.mark_cells(self.nb, False)
75 74 for cell in cells:
76 75 self.assertNotIn('trusted', cell)
77 76 if cell.cell_type == 'code':
78 77 self.assertIn('trusted', cell.metadata)
79 78 self.assertFalse(cell.metadata.trusted)
80 79 else:
81 80 self.assertNotIn('trusted', cell.metadata)
82 81
83 82 def test_mark_cells_trusted(self):
84 83 cells = self.nb.cells
85 84 self.notary.mark_cells(self.nb, True)
86 85 for cell in cells:
87 86 self.assertNotIn('trusted', cell)
88 87 if cell.cell_type == 'code':
89 88 self.assertIn('trusted', cell.metadata)
90 89 self.assertTrue(cell.metadata.trusted)
91 90 else:
92 91 self.assertNotIn('trusted', cell.metadata)
93 92
94 93 def test_check_cells(self):
95 94 nb = self.nb
96 95 self.notary.mark_cells(nb, True)
97 96 self.assertTrue(self.notary.check_cells(nb))
98 97 for cell in nb.cells:
99 98 self.assertNotIn('trusted', cell)
100 99 self.notary.mark_cells(nb, False)
101 100 self.assertFalse(self.notary.check_cells(nb))
102 101 for cell in nb.cells:
103 102 self.assertNotIn('trusted', cell)
104 103
105 104 def test_trust_no_output(self):
106 105 nb = self.nb
107 106 self.notary.mark_cells(nb, False)
108 107 for cell in nb.cells:
109 108 if cell.cell_type == 'code':
110 109 cell.outputs = []
111 110 self.assertTrue(self.notary.check_cells(nb))
112 111
@@ -1,58 +1,58 b''
1 1 """Test nbformat.validator"""
2 2
3 3 # Copyright (c) IPython Development Team.
4 4 # Distributed under the terms of the Modified BSD License.
5 5
6 6 import os
7 7
8 8 from .base import TestsBase
9 9 from jsonschema import ValidationError
10 from ..current import read
10 from IPython.nbformat import read
11 11 from ..validator import isvalid, validate
12 12
13 13
14 14 class TestValidator(TestsBase):
15 15
16 16 def test_nb2(self):
17 17 """Test that a v2 notebook converted to current passes validation"""
18 18 with self.fopen(u'test2.ipynb', u'r') as f:
19 nb = read(f, u'json')
19 nb = read(f, as_version=4)
20 20 validate(nb)
21 21 self.assertEqual(isvalid(nb), True)
22 22
23 23 def test_nb3(self):
24 24 """Test that a v3 notebook passes validation"""
25 25 with self.fopen(u'test3.ipynb', u'r') as f:
26 nb = read(f, u'json')
26 nb = read(f, as_version=4)
27 27 validate(nb)
28 28 self.assertEqual(isvalid(nb), True)
29 29
30 30 def test_nb4(self):
31 31 """Test that a v4 notebook passes validation"""
32 32 with self.fopen(u'test4.ipynb', u'r') as f:
33 nb = read(f, u'json')
33 nb = read(f, as_version=4)
34 34 validate(nb)
35 35 self.assertEqual(isvalid(nb), True)
36 36
37 37 def test_invalid(self):
38 38 """Test than an invalid notebook does not pass validation"""
39 39 # this notebook has a few different errors:
40 40 # - one cell is missing its source
41 41 # - invalid cell type
42 42 # - invalid output_type
43 43 with self.fopen(u'invalid.ipynb', u'r') as f:
44 nb = read(f, u'json')
44 nb = read(f, as_version=4)
45 45 with self.assertRaises(ValidationError):
46 46 validate(nb)
47 47 self.assertEqual(isvalid(nb), False)
48 48
49 49 def test_future(self):
50 50 """Test than a notebook from the future with extra keys passes validation"""
51 51 with self.fopen(u'test4plus.ipynb', u'r') as f:
52 nb = read(f)
52 nb = read(f, as_version=4)
53 53 with self.assertRaises(ValidationError):
54 54 validate(nb, version=4)
55 55
56 56 self.assertEqual(isvalid(nb, version=4), False)
57 57 self.assertEqual(isvalid(nb), True)
58 58
@@ -1,249 +1,249 b''
1 1 """Code for converting notebooks to and from v3."""
2 2
3 3 # Copyright (c) IPython Development Team.
4 4 # Distributed under the terms of the Modified BSD License.
5 5
6 6 import json
7 7 import re
8 8
9 9 from .nbbase import (
10 10 nbformat, nbformat_minor,
11 11 NotebookNode,
12 12 )
13 13
14 14 from IPython.nbformat import v3
15 15 from IPython.utils.log import get_logger
16 16
17 17 def _warn_if_invalid(nb, version):
18 18 """Log validation errors, if there are any."""
19 from IPython.nbformat.current import validate, ValidationError
19 from IPython.nbformat import validate, ValidationError
20 20 try:
21 21 validate(nb, version=version)
22 22 except ValidationError as e:
23 23 get_logger().error("Notebook JSON is not valid v%i: %s", version, e)
24 24
25 25 def upgrade(nb, from_version=3, from_minor=0):
26 26 """Convert a notebook to v4.
27 27
28 28 Parameters
29 29 ----------
30 30 nb : NotebookNode
31 31 The Python representation of the notebook to convert.
32 32 from_version : int
33 33 The original version of the notebook to convert.
34 34 from_minor : int
35 35 The original minor version of the notebook to convert (only relevant for v >= 3).
36 36 """
37 37 if from_version == 3:
38 38 # Validate the notebook before conversion
39 39 _warn_if_invalid(nb, from_version)
40 40
41 41 # Mark the original nbformat so consumers know it has been converted
42 42 orig_nbformat = nb.pop('orig_nbformat', None)
43 43 nb.metadata.orig_nbformat = orig_nbformat or 3
44 44
45 45 # Mark the new format
46 46 nb.nbformat = nbformat
47 47 nb.nbformat_minor = nbformat_minor
48 48
49 49 # remove worksheet(s)
50 50 nb['cells'] = cells = []
51 51 # In the unlikely event of multiple worksheets,
52 52 # they will be flattened
53 53 for ws in nb.pop('worksheets', []):
54 54 # upgrade each cell
55 55 for cell in ws['cells']:
56 56 cells.append(upgrade_cell(cell))
57 57 # upgrade metadata
58 58 nb.metadata.pop('name', '')
59 59 # Validate the converted notebook before returning it
60 60 _warn_if_invalid(nb, nbformat)
61 61 return nb
62 62 elif from_version == 4:
63 63 # nothing to do
64 64 if from_minor != nbformat_minor:
65 65 nb.metadata.orig_nbformat_minor = from_minor
66 66 nb.nbformat_minor = nbformat_minor
67 67
68 68 return nb
69 69 else:
70 70 raise ValueError('Cannot convert a notebook directly from v%s to v4. ' \
71 71 'Try using the IPython.nbformat.convert module.' % from_version)
72 72
73 73 def upgrade_cell(cell):
74 74 """upgrade a cell from v3 to v4
75 75
76 76 heading cell -> markdown heading
77 77 code cell:
78 78 - remove language metadata
79 79 - cell.input -> cell.source
80 80 - cell.prompt_number -> cell.execution_count
81 81 - update outputs
82 82 """
83 83 cell.setdefault('metadata', NotebookNode())
84 84 if cell.cell_type == 'code':
85 85 cell.pop('language', '')
86 86 if 'collapsed' in cell:
87 87 cell.metadata['collapsed'] = cell.pop('collapsed')
88 88 cell.source = cell.pop('input', '')
89 89 cell.execution_count = cell.pop('prompt_number', None)
90 90 cell.outputs = upgrade_outputs(cell.outputs)
91 91 elif cell.cell_type == 'heading':
92 92 cell.cell_type = 'markdown'
93 93 level = cell.pop('level', 1)
94 94 cell.source = '{hashes} {single_line}'.format(
95 95 hashes='#' * level,
96 96 single_line = ' '.join(cell.get('source', '').splitlines()),
97 97 )
98 98 elif cell.cell_type == 'html':
99 99 # Technically, this exists. It will never happen in practice.
100 100 cell.cell_type = 'markdown'
101 101 return cell
102 102
103 103 def downgrade_cell(cell):
104 104 """downgrade a cell from v4 to v3
105 105
106 106 code cell:
107 107 - set cell.language
108 108 - cell.input <- cell.source
109 109 - cell.prompt_number <- cell.execution_count
110 110 - update outputs
111 111 markdown cell:
112 112 - single-line heading -> heading cell
113 113 """
114 114 if cell.cell_type == 'code':
115 115 cell.language = 'python'
116 116 cell.input = cell.pop('source', '')
117 117 cell.prompt_number = cell.pop('execution_count', None)
118 118 cell.collapsed = cell.metadata.pop('collapsed', False)
119 119 cell.outputs = downgrade_outputs(cell.outputs)
120 120 elif cell.cell_type == 'markdown':
121 121 source = cell.get('source', '')
122 122 if '\n' not in source and source.startswith('#'):
123 123 prefix, text = re.match(r'(#+)\s*(.*)', source).groups()
124 124 cell.cell_type = 'heading'
125 125 cell.source = text
126 126 cell.level = len(prefix)
127 127 return cell
128 128
129 129 _mime_map = {
130 130 "text" : "text/plain",
131 131 "html" : "text/html",
132 132 "svg" : "image/svg+xml",
133 133 "png" : "image/png",
134 134 "jpeg" : "image/jpeg",
135 135 "latex" : "text/latex",
136 136 "json" : "application/json",
137 137 "javascript" : "application/javascript",
138 138 };
139 139
140 140 def to_mime_key(d):
141 141 """convert dict with v3 aliases to plain mime-type keys"""
142 142 for alias, mime in _mime_map.items():
143 143 if alias in d:
144 144 d[mime] = d.pop(alias)
145 145 return d
146 146
147 147 def from_mime_key(d):
148 148 """convert dict with mime-type keys to v3 aliases"""
149 149 for alias, mime in _mime_map.items():
150 150 if mime in d:
151 151 d[alias] = d.pop(mime)
152 152 return d
153 153
154 154 def upgrade_output(output):
155 155 """upgrade a single code cell output from v3 to v4
156 156
157 157 - pyout -> execute_result
158 158 - pyerr -> error
159 159 - output.type -> output.data.mime/type
160 160 - mime-type keys
161 161 - stream.stream -> stream.name
162 162 """
163 163 if output['output_type'] in {'pyout', 'display_data'}:
164 164 output.setdefault('metadata', NotebookNode())
165 165 if output['output_type'] == 'pyout':
166 166 output['output_type'] = 'execute_result'
167 167 output['execution_count'] = output.pop('prompt_number', None)
168 168
169 169 # move output data into data sub-dict
170 170 data = {}
171 171 for key in list(output):
172 172 if key in {'output_type', 'execution_count', 'metadata'}:
173 173 continue
174 174 data[key] = output.pop(key)
175 175 to_mime_key(data)
176 176 output['data'] = data
177 177 to_mime_key(output.metadata)
178 178 if 'application/json' in data:
179 179 data['application/json'] = json.loads(data['application/json'])
180 180 # promote ascii bytes (from v2) to unicode
181 181 for key in ('image/png', 'image/jpeg'):
182 182 if key in data and isinstance(data[key], bytes):
183 183 data[key] = data[key].decode('ascii')
184 184 elif output['output_type'] == 'pyerr':
185 185 output['output_type'] = 'error'
186 186 elif output['output_type'] == 'stream':
187 187 output['name'] = output.pop('stream')
188 188 return output
189 189
190 190 def downgrade_output(output):
191 191 """downgrade a single code cell output to v3 from v4
192 192
193 193 - pyout <- execute_result
194 194 - pyerr <- error
195 195 - output.data.mime/type -> output.type
196 196 - un-mime-type keys
197 197 - stream.stream <- stream.name
198 198 """
199 199 if output['output_type'] in {'execute_result', 'display_data'}:
200 200 if output['output_type'] == 'execute_result':
201 201 output['output_type'] = 'pyout'
202 202 output['prompt_number'] = output.pop('execution_count', None)
203 203
204 204 # promote data dict to top-level output namespace
205 205 data = output.pop('data', {})
206 206 if 'application/json' in data:
207 207 data['application/json'] = json.dumps(data['application/json'])
208 208 from_mime_key(data)
209 209 output.update(data)
210 210 from_mime_key(output.get('metadata', {}))
211 211 elif output['output_type'] == 'error':
212 212 output['output_type'] = 'pyerr'
213 213 elif output['output_type'] == 'stream':
214 214 output['stream'] = output.pop('name')
215 215 return output
216 216
217 217 def upgrade_outputs(outputs):
218 218 """upgrade outputs of a code cell from v3 to v4"""
219 219 return [upgrade_output(op) for op in outputs]
220 220
221 221 def downgrade_outputs(outputs):
222 222 """downgrade outputs of a code cell to v3 from v4"""
223 223 return [downgrade_output(op) for op in outputs]
224 224
225 225 def downgrade(nb):
226 226 """Convert a v4 notebook to v3.
227 227
228 228 Parameters
229 229 ----------
230 230 nb : NotebookNode
231 231 The Python representation of the notebook to convert.
232 232 """
233 233 if nb.nbformat != nbformat:
234 234 return nb
235 235
236 236 # Validate the notebook before conversion
237 237 _warn_if_invalid(nb, nbformat)
238 238
239 239 nb.nbformat = v3.nbformat
240 240 nb.nbformat_minor = v3.nbformat_minor
241 241 cells = [ downgrade_cell(cell) for cell in nb.pop('cells') ]
242 242 nb.worksheets = [v3.new_worksheet(cells=cells)]
243 243 nb.metadata.setdefault('name', '')
244 244 nb.metadata.pop('orig_nbformat', None)
245 245 nb.metadata.pop('orig_nbformat_minor', None)
246 246
247 247 # Validate the converted notebook before returning it
248 248 _warn_if_invalid(nb, v3.nbformat)
249 249 return nb
@@ -1,149 +1,149 b''
1 1 """Python API for composing notebook elements
2 2
3 3 The Python representation of a notebook is a nested structure of
4 4 dictionary subclasses that support attribute access
5 5 (IPython.utils.ipstruct.Struct). The functions in this module are merely
6 6 helpers to build the structs in the right form.
7 7 """
8 8
9 9 # Copyright (c) IPython Development Team.
10 10 # Distributed under the terms of the Modified BSD License.
11 11
12 12 from IPython.utils.ipstruct import Struct
13 13
14 14 # Change this when incrementing the nbformat version
15 15 nbformat = 4
16 16 nbformat_minor = 0
17 17 nbformat_schema = 'nbformat.v4.schema.json'
18 18
19 19
20 20 def validate(node, ref=None):
21 21 """validate a v4 node"""
22 from ..current import validate
22 from .. import validate
23 23 return validate(node, ref=ref, version=nbformat)
24 24
25 25
26 26 class NotebookNode(Struct):
27 27 pass
28 28
29 29 def from_dict(d):
30 30 if isinstance(d, dict):
31 31 return NotebookNode({k:from_dict(v) for k,v in d.items()})
32 32 elif isinstance(d, (tuple, list)):
33 33 return [from_dict(i) for i in d]
34 34 else:
35 35 return d
36 36
37 37
38 38 def new_output(output_type, data=None, **kwargs):
39 39 """Create a new output, to go in the ``cell.outputs`` list of a code cell."""
40 40 output = NotebookNode(output_type=output_type)
41 41
42 42 # populate defaults:
43 43 if output_type == 'stream':
44 44 output.name = u'stdout'
45 45 output.text = u''
46 46 elif output_type in {'execute_result', 'display_data'}:
47 47 output.metadata = NotebookNode()
48 48 output.data = NotebookNode()
49 49 # load from args:
50 50 output.update(from_dict(kwargs))
51 51 if data is not None:
52 52 output.data = from_dict(data)
53 53 # validate
54 54 validate(output, output_type)
55 55 return output
56 56
57 57
58 58 def output_from_msg(msg):
59 59 """Create a NotebookNode for an output from a kernel's IOPub message.
60 60
61 61 Returns
62 62 -------
63 63
64 64 NotebookNode: the output as a notebook node.
65 65
66 66 Raises
67 67 ------
68 68
69 69 ValueError: if the message is not an output message.
70 70
71 71 """
72 72 msg_type = msg['header']['msg_type']
73 73 content = msg['content']
74 74
75 75 if msg_type == 'execute_result':
76 76 return new_output(output_type=msg_type,
77 77 metadata=content['metadata'],
78 78 data=content['data'],
79 79 execution_count=content['execution_count'],
80 80 )
81 81 elif msg_type == 'stream':
82 82 return new_output(output_type=msg_type,
83 83 name=content['name'],
84 84 text=content['text'],
85 85 )
86 86 elif msg_type == 'display_data':
87 87 return new_output(output_type=msg_type,
88 88 metadata=content['metadata'],
89 89 data=content['data'],
90 90 )
91 91 elif msg_type == 'error':
92 92 return new_output(output_type=msg_type,
93 93 ename=content['ename'],
94 94 evalue=content['evalue'],
95 95 traceback=content['traceback'],
96 96 )
97 97 else:
98 98 raise ValueError("Unrecognized output msg type: %r" % msg_type)
99 99
100 100
101 101 def new_code_cell(source='', **kwargs):
102 102 """Create a new code cell"""
103 103 cell = NotebookNode(
104 104 cell_type='code',
105 105 metadata=NotebookNode(),
106 106 execution_count=None,
107 107 source=source,
108 108 outputs=[],
109 109 )
110 110 cell.update(from_dict(kwargs))
111 111
112 112 validate(cell, 'code_cell')
113 113 return cell
114 114
115 115 def new_markdown_cell(source='', **kwargs):
116 116 """Create a new markdown cell"""
117 117 cell = NotebookNode(
118 118 cell_type='markdown',
119 119 source=source,
120 120 metadata=NotebookNode(),
121 121 )
122 122 cell.update(from_dict(kwargs))
123 123
124 124 validate(cell, 'markdown_cell')
125 125 return cell
126 126
127 127 def new_raw_cell(source='', **kwargs):
128 128 """Create a new raw cell"""
129 129 cell = NotebookNode(
130 130 cell_type='raw',
131 131 source=source,
132 132 metadata=NotebookNode(),
133 133 )
134 134 cell.update(from_dict(kwargs))
135 135
136 136 validate(cell, 'raw_cell')
137 137 return cell
138 138
139 139 def new_notebook(**kwargs):
140 140 """Create a new notebook"""
141 141 nb = NotebookNode(
142 142 nbformat=nbformat,
143 143 nbformat_minor=nbformat_minor,
144 144 metadata=NotebookNode(),
145 145 cells=[],
146 146 )
147 147 nb.update(from_dict(kwargs))
148 148 validate(nb)
149 149 return nb
@@ -1,67 +1,67 b''
1 1 import copy
2 2
3 3 import nose.tools as nt
4 4
5 from IPython.nbformat.current import validate
5 from IPython.nbformat import validate
6 6 from .. import convert
7 7
8 8 from . import nbexamples
9 9 from IPython.nbformat.v3.tests import nbexamples as v3examples
10 10 from IPython.nbformat import v3, v4
11 11
12 12 def test_upgrade_notebook():
13 13 nb03 = copy.deepcopy(v3examples.nb0)
14 14 validate(nb03)
15 15 nb04 = convert.upgrade(nb03)
16 16 validate(nb04)
17 17
18 18 def test_downgrade_notebook():
19 19 nb04 = copy.deepcopy(nbexamples.nb0)
20 20 validate(nb04)
21 21 nb03 = convert.downgrade(nb04)
22 22 validate(nb03)
23 23
24 24 def test_upgrade_heading():
25 25 v3h = v3.new_heading_cell
26 26 v4m = v4.new_markdown_cell
27 27 for v3cell, expected in [
28 28 (
29 29 v3h(source='foo', level=1),
30 30 v4m(source='# foo'),
31 31 ),
32 32 (
33 33 v3h(source='foo\nbar\nmulti-line\n', level=4),
34 34 v4m(source='#### foo bar multi-line'),
35 35 ),
36 36 ]:
37 37 upgraded = convert.upgrade_cell(v3cell)
38 38 nt.assert_equal(upgraded, expected)
39 39
40 40 def test_downgrade_heading():
41 41 v3h = v3.new_heading_cell
42 42 v4m = v4.new_markdown_cell
43 43 v3m = lambda source: v3.new_text_cell('markdown', source)
44 44 for v4cell, expected in [
45 45 (
46 46 v4m(source='# foo'),
47 47 v3h(source='foo', level=1),
48 48 ),
49 49 (
50 50 v4m(source='#foo'),
51 51 v3h(source='foo', level=1),
52 52 ),
53 53 (
54 54 v4m(source='#\tfoo'),
55 55 v3h(source='foo', level=1),
56 56 ),
57 57 (
58 58 v4m(source='# \t foo'),
59 59 v3h(source='foo', level=1),
60 60 ),
61 61 (
62 62 v4m(source='# foo\nbar'),
63 63 v3m(source='# foo\nbar'),
64 64 ),
65 65 ]:
66 66 downgraded = convert.downgrade_cell(v4cell)
67 67 nt.assert_equal(downgraded, expected)
@@ -1,146 +1,147 b''
1 1 # Copyright (c) IPython Development Team.
2 2 # Distributed under the terms of the Modified BSD License.
3 3
4 4 from __future__ import print_function
5 5 import json
6 6 import os
7 7 import warnings
8 8
9 9 try:
10 10 from jsonschema import ValidationError
11 11 from jsonschema import Draft4Validator as Validator
12 12 except ImportError as e:
13 13 verbose_msg = """
14 14
15 15 IPython notebook format depends on the jsonschema package:
16 16
17 17 https://pypi.python.org/pypi/jsonschema
18 18
19 19 Please install it first.
20 20 """
21 21 raise ImportError(str(e) + verbose_msg)
22 22
23 23 from IPython.utils.importstring import import_item
24 24
25 25
26 26 validators = {}
27 27
28 28 def _relax_additional_properties(obj):
29 29 """relax any `additionalProperties`"""
30 30 if isinstance(obj, dict):
31 31 for key, value in obj.items():
32 32 if key == 'additionalProperties':
33 33 print(obj)
34 34 value = True
35 35 else:
36 36 value = _relax_additional_properties(value)
37 37 obj[key] = value
38 38 elif isinstance(obj, list):
39 39 for i, value in enumerate(obj):
40 40 obj[i] = _relax_additional_properties(value)
41 41 return obj
42 42
43 43 def get_validator(version=None, version_minor=None):
44 44 """Load the JSON schema into a Validator"""
45 45 if version is None:
46 from .current import nbformat as version
46 from .. import current_nbformat
47 version = current_nbformat
47 48
48 49 v = import_item("IPython.nbformat.v%s" % version)
49 50 current_minor = v.nbformat_minor
50 51 if version_minor is None:
51 52 version_minor = current_minor
52 53
53 54 version_tuple = (version, version_minor)
54 55
55 56 if version_tuple not in validators:
56 57 try:
57 58 v.nbformat_schema
58 59 except AttributeError:
59 60 # no validator
60 61 return None
61 62 schema_path = os.path.join(os.path.dirname(v.__file__), v.nbformat_schema)
62 63 with open(schema_path) as f:
63 64 schema_json = json.load(f)
64 65
65 66 if current_minor < version_minor:
66 67 # notebook from the future, relax all `additionalProperties: False` requirements
67 68 schema_json = _relax_additional_properties(schema_json)
68 69
69 70 validators[version_tuple] = Validator(schema_json)
70 71 return validators[version_tuple]
71 72
72 73 def isvalid(nbjson, ref=None, version=None, version_minor=None):
73 74 """Checks whether the given notebook JSON conforms to the current
74 75 notebook format schema. Returns True if the JSON is valid, and
75 76 False otherwise.
76 77
77 78 To see the individual errors that were encountered, please use the
78 79 `validate` function instead.
79 80 """
80 81 try:
81 82 validate(nbjson, ref, version, version_minor)
82 83 except ValidationError:
83 84 return False
84 85 else:
85 86 return True
86 87
87 88
88 89 def better_validation_error(error, version, version_minor):
89 90 """Get better ValidationError on oneOf failures
90 91
91 92 oneOf errors aren't informative.
92 93 if it's a cell type or output_type error,
93 94 try validating directly based on the type for a better error message
94 95 """
95 96 key = error.schema_path[-1]
96 97 if key.endswith('Of'):
97 98
98 99 ref = None
99 100 if isinstance(error.instance, dict):
100 101 if 'cell_type' in error.instance:
101 102 ref = error.instance['cell_type'] + "_cell"
102 103 elif 'output_type' in error.instance:
103 104 ref = error.instance['output_type']
104 105
105 106 if ref:
106 107 try:
107 108 validate(error.instance,
108 109 ref,
109 110 version=version,
110 111 version_minor=version_minor,
111 112 )
112 113 except ValidationError as e:
113 114 return better_validation_error(e, version, version_minor)
114 115 except:
115 116 # if it fails for some reason,
116 117 # let the original error through
117 118 pass
118 119
119 120 return error
120 121
121 122
122 123 def validate(nbjson, ref=None, version=None, version_minor=None):
123 124 """Checks whether the given notebook JSON conforms to the current
124 125 notebook format schema.
125 126
126 127 Raises ValidationError if not valid.
127 128 """
128 129 if version is None:
129 130 from .reader import get_version
130 131 (version, version_minor) = get_version(nbjson)
131 132
132 133 validator = get_validator(version, version_minor)
133 134
134 135 if validator is None:
135 136 # no validator
136 137 warnings.warn("No schema for validating v%s notebooks" % version, UserWarning)
137 138 return
138 139
139 140 try:
140 141 if ref:
141 142 return validator.validate(nbjson, {'$ref' : '#/definitions/%s' % ref})
142 143 else:
143 144 return validator.validate(nbjson)
144 145 except ValidationError as e:
145 146 raise better_validation_error(e, version, version_minor)
146 147
General Comments 0
You need to be logged in to leave comments. Login now