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