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