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 . |
|
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 |
|
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 . |
|
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 . |
|
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 . |
|
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, |
|
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 |
|
288 | with atomic_writing(notebook_path) as f: | |
288 |
write(nb, |
|
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 . |
|
11 | from IPython.nbformat import read, current_nbformat, writes | |
16 |
|
12 | |||
17 |
|
13 | |||
18 |
class Test |
|
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', |
|
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, |
|
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 .. |
|
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 . |
|
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, |
|
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 . |
|
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, |
|
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, |
|
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, |
|
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, |
|
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 |
|
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 .. |
|
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 |
|
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 . |
|
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