Show More
@@ -1,278 +1,310 b'' | |||||
1 | """Functions for signing notebooks""" |
|
1 | """Functions for signing notebooks""" | |
2 | #----------------------------------------------------------------------------- |
|
2 | #----------------------------------------------------------------------------- | |
3 | # Copyright (C) 2014, The IPython Development Team |
|
3 | # Copyright (C) 2014, The IPython Development Team | |
4 | # |
|
4 | # | |
5 | # Distributed under the terms of the BSD License. The full license is in |
|
5 | # Distributed under the terms of the BSD License. The full license is in | |
6 | # the file COPYING, distributed as part of this software. |
|
6 | # the file COPYING, distributed as part of this software. | |
7 | #----------------------------------------------------------------------------- |
|
7 | #----------------------------------------------------------------------------- | |
8 |
|
8 | |||
9 | #----------------------------------------------------------------------------- |
|
9 | #----------------------------------------------------------------------------- | |
10 | # Imports |
|
10 | # Imports | |
11 | #----------------------------------------------------------------------------- |
|
11 | #----------------------------------------------------------------------------- | |
12 |
|
12 | |||
13 | import base64 |
|
13 | import base64 | |
14 | from contextlib import contextmanager |
|
14 | from contextlib import contextmanager | |
15 | import hashlib |
|
15 | import hashlib | |
16 | from hmac import HMAC |
|
16 | from hmac import HMAC | |
17 | import io |
|
17 | import io | |
18 | import os |
|
18 | import os | |
19 |
|
19 | |||
20 | from IPython.utils.py3compat import string_types, unicode_type, cast_bytes |
|
20 | from IPython.utils.py3compat import string_types, unicode_type, cast_bytes | |
21 | from IPython.utils.traitlets import Instance, Bytes, Enum, Any, Unicode, Bool |
|
21 | from IPython.utils.traitlets import Instance, Bytes, Enum, Any, Unicode, Bool | |
22 | from IPython.config import LoggingConfigurable, MultipleInstanceError |
|
22 | from IPython.config import LoggingConfigurable, MultipleInstanceError | |
23 | from IPython.core.application import BaseIPythonApplication, base_flags |
|
23 | from IPython.core.application import BaseIPythonApplication, base_flags | |
24 |
|
24 | |||
25 | from .current import read, write |
|
25 | from .current import read, write | |
26 |
|
26 | |||
27 | #----------------------------------------------------------------------------- |
|
27 | #----------------------------------------------------------------------------- | |
28 | # Code |
|
28 | # Code | |
29 | #----------------------------------------------------------------------------- |
|
29 | #----------------------------------------------------------------------------- | |
30 | try: |
|
30 | try: | |
31 | # Python 3 |
|
31 | # Python 3 | |
32 | algorithms = hashlib.algorithms_guaranteed |
|
32 | algorithms = hashlib.algorithms_guaranteed | |
33 | except AttributeError: |
|
33 | except AttributeError: | |
34 | algorithms = hashlib.algorithms |
|
34 | algorithms = hashlib.algorithms | |
35 |
|
35 | |||
36 | def yield_everything(obj): |
|
36 | def yield_everything(obj): | |
37 | """Yield every item in a container as bytes |
|
37 | """Yield every item in a container as bytes | |
38 |
|
38 | |||
39 | Allows any JSONable object to be passed to an HMAC digester |
|
39 | Allows any JSONable object to be passed to an HMAC digester | |
40 | without having to serialize the whole thing. |
|
40 | without having to serialize the whole thing. | |
41 | """ |
|
41 | """ | |
42 | if isinstance(obj, dict): |
|
42 | if isinstance(obj, dict): | |
43 | for key in sorted(obj): |
|
43 | for key in sorted(obj): | |
44 | value = obj[key] |
|
44 | value = obj[key] | |
45 | yield cast_bytes(key) |
|
45 | yield cast_bytes(key) | |
46 | for b in yield_everything(value): |
|
46 | for b in yield_everything(value): | |
47 | yield b |
|
47 | yield b | |
48 | elif isinstance(obj, (list, tuple)): |
|
48 | elif isinstance(obj, (list, tuple)): | |
49 | for element in obj: |
|
49 | for element in obj: | |
50 | for b in yield_everything(element): |
|
50 | for b in yield_everything(element): | |
51 | yield b |
|
51 | yield b | |
52 | elif isinstance(obj, unicode_type): |
|
52 | elif isinstance(obj, unicode_type): | |
53 | yield obj.encode('utf8') |
|
53 | yield obj.encode('utf8') | |
54 | else: |
|
54 | else: | |
55 | yield unicode_type(obj).encode('utf8') |
|
55 | yield unicode_type(obj).encode('utf8') | |
56 |
|
56 | |||
57 |
|
57 | |||
58 | @contextmanager |
|
58 | @contextmanager | |
59 | def signature_removed(nb): |
|
59 | def signature_removed(nb): | |
60 | """Context manager for operating on a notebook with its signature removed |
|
60 | """Context manager for operating on a notebook with its signature removed | |
61 |
|
61 | |||
62 | Used for excluding the previous signature when computing a notebook's signature. |
|
62 | Used for excluding the previous signature when computing a notebook's signature. | |
63 | """ |
|
63 | """ | |
64 | save_signature = nb['metadata'].pop('signature', None) |
|
64 | save_signature = nb['metadata'].pop('signature', None) | |
65 | try: |
|
65 | try: | |
66 | yield |
|
66 | yield | |
67 | finally: |
|
67 | finally: | |
68 | if save_signature is not None: |
|
68 | if save_signature is not None: | |
69 | nb['metadata']['signature'] = save_signature |
|
69 | nb['metadata']['signature'] = save_signature | |
70 |
|
70 | |||
71 |
|
71 | |||
72 | class NotebookNotary(LoggingConfigurable): |
|
72 | class NotebookNotary(LoggingConfigurable): | |
73 | """A class for computing and verifying notebook signatures.""" |
|
73 | """A class for computing and verifying notebook signatures.""" | |
74 |
|
74 | |||
75 | profile_dir = Instance("IPython.core.profiledir.ProfileDir") |
|
75 | profile_dir = Instance("IPython.core.profiledir.ProfileDir") | |
76 | def _profile_dir_default(self): |
|
76 | def _profile_dir_default(self): | |
77 | from IPython.core.application import BaseIPythonApplication |
|
77 | from IPython.core.application import BaseIPythonApplication | |
78 | app = None |
|
78 | app = None | |
79 | try: |
|
79 | try: | |
80 | if BaseIPythonApplication.initialized(): |
|
80 | if BaseIPythonApplication.initialized(): | |
81 | app = BaseIPythonApplication.instance() |
|
81 | app = BaseIPythonApplication.instance() | |
82 | except MultipleInstanceError: |
|
82 | except MultipleInstanceError: | |
83 | pass |
|
83 | pass | |
84 | if app is None: |
|
84 | if app is None: | |
85 | # create an app, without the global instance |
|
85 | # create an app, without the global instance | |
86 | app = BaseIPythonApplication() |
|
86 | app = BaseIPythonApplication() | |
87 | app.initialize(argv=[]) |
|
87 | app.initialize(argv=[]) | |
88 | return app.profile_dir |
|
88 | return app.profile_dir | |
89 |
|
89 | |||
90 | algorithm = Enum(algorithms, default_value='sha256', config=True, |
|
90 | algorithm = Enum(algorithms, default_value='sha256', config=True, | |
91 | help="""The hashing algorithm used to sign notebooks.""" |
|
91 | help="""The hashing algorithm used to sign notebooks.""" | |
92 | ) |
|
92 | ) | |
93 | def _algorithm_changed(self, name, old, new): |
|
93 | def _algorithm_changed(self, name, old, new): | |
94 | self.digestmod = getattr(hashlib, self.algorithm) |
|
94 | self.digestmod = getattr(hashlib, self.algorithm) | |
95 |
|
95 | |||
96 | digestmod = Any() |
|
96 | digestmod = Any() | |
97 | def _digestmod_default(self): |
|
97 | def _digestmod_default(self): | |
98 | return getattr(hashlib, self.algorithm) |
|
98 | return getattr(hashlib, self.algorithm) | |
99 |
|
99 | |||
100 | secret_file = Unicode() |
|
100 | secret_file = Unicode() | |
101 | def _secret_file_default(self): |
|
101 | def _secret_file_default(self): | |
102 | if self.profile_dir is None: |
|
102 | if self.profile_dir is None: | |
103 | return '' |
|
103 | return '' | |
104 | return os.path.join(self.profile_dir.security_dir, 'notebook_secret') |
|
104 | return os.path.join(self.profile_dir.security_dir, 'notebook_secret') | |
105 |
|
105 | |||
106 | secret = Bytes(config=True, |
|
106 | secret = Bytes(config=True, | |
107 | help="""The secret key with which notebooks are signed.""" |
|
107 | help="""The secret key with which notebooks are signed.""" | |
108 | ) |
|
108 | ) | |
109 | def _secret_default(self): |
|
109 | def _secret_default(self): | |
110 | # note : this assumes an Application is running |
|
110 | # note : this assumes an Application is running | |
111 | if os.path.exists(self.secret_file): |
|
111 | if os.path.exists(self.secret_file): | |
112 | with io.open(self.secret_file, 'rb') as f: |
|
112 | with io.open(self.secret_file, 'rb') as f: | |
113 | return f.read() |
|
113 | return f.read() | |
114 | else: |
|
114 | else: | |
115 | secret = base64.encodestring(os.urandom(1024)) |
|
115 | secret = base64.encodestring(os.urandom(1024)) | |
116 | self._write_secret_file(secret) |
|
116 | self._write_secret_file(secret) | |
117 | return secret |
|
117 | return secret | |
118 |
|
118 | |||
119 | def _write_secret_file(self, secret): |
|
119 | def _write_secret_file(self, secret): | |
120 | """write my secret to my secret_file""" |
|
120 | """write my secret to my secret_file""" | |
121 | self.log.info("Writing notebook-signing key to %s", self.secret_file) |
|
121 | self.log.info("Writing notebook-signing key to %s", self.secret_file) | |
122 | with io.open(self.secret_file, 'wb') as f: |
|
122 | with io.open(self.secret_file, 'wb') as f: | |
123 | f.write(secret) |
|
123 | f.write(secret) | |
124 | try: |
|
124 | try: | |
125 | os.chmod(self.secret_file, 0o600) |
|
125 | os.chmod(self.secret_file, 0o600) | |
126 | except OSError: |
|
126 | except OSError: | |
127 | self.log.warn( |
|
127 | self.log.warn( | |
128 | "Could not set permissions on %s", |
|
128 | "Could not set permissions on %s", | |
129 | self.secret_file |
|
129 | self.secret_file | |
130 | ) |
|
130 | ) | |
131 | return secret |
|
131 | return secret | |
132 |
|
132 | |||
133 | def compute_signature(self, nb): |
|
133 | def compute_signature(self, nb): | |
134 | """Compute a notebook's signature |
|
134 | """Compute a notebook's signature | |
135 |
|
135 | |||
136 | by hashing the entire contents of the notebook via HMAC digest. |
|
136 | by hashing the entire contents of the notebook via HMAC digest. | |
137 | """ |
|
137 | """ | |
138 | hmac = HMAC(self.secret, digestmod=self.digestmod) |
|
138 | hmac = HMAC(self.secret, digestmod=self.digestmod) | |
139 | # don't include the previous hash in the content to hash |
|
139 | # don't include the previous hash in the content to hash | |
140 | with signature_removed(nb): |
|
140 | with signature_removed(nb): | |
141 | # sign the whole thing |
|
141 | # sign the whole thing | |
142 | for b in yield_everything(nb): |
|
142 | for b in yield_everything(nb): | |
143 | hmac.update(b) |
|
143 | hmac.update(b) | |
144 |
|
144 | |||
145 | return hmac.hexdigest() |
|
145 | return hmac.hexdigest() | |
146 |
|
146 | |||
147 | def check_signature(self, nb): |
|
147 | def check_signature(self, nb): | |
148 | """Check a notebook's stored signature |
|
148 | """Check a notebook's stored signature | |
149 |
|
149 | |||
150 | If a signature is stored in the notebook's metadata, |
|
150 | If a signature is stored in the notebook's metadata, | |
151 | a new signature is computed and compared with the stored value. |
|
151 | a new signature is computed and compared with the stored value. | |
152 |
|
152 | |||
153 | Returns True if the signature is found and matches, False otherwise. |
|
153 | Returns True if the signature is found and matches, False otherwise. | |
154 |
|
154 | |||
155 | The following conditions must all be met for a notebook to be trusted: |
|
155 | The following conditions must all be met for a notebook to be trusted: | |
156 | - a signature is stored in the form 'scheme:hexdigest' |
|
156 | - a signature is stored in the form 'scheme:hexdigest' | |
157 | - the stored scheme matches the requested scheme |
|
157 | - the stored scheme matches the requested scheme | |
158 | - the requested scheme is available from hashlib |
|
158 | - the requested scheme is available from hashlib | |
159 | - the computed hash from notebook_signature matches the stored hash |
|
159 | - the computed hash from notebook_signature matches the stored hash | |
160 | """ |
|
160 | """ | |
161 | stored_signature = nb['metadata'].get('signature', None) |
|
161 | stored_signature = nb['metadata'].get('signature', None) | |
162 | if not stored_signature \ |
|
162 | if not stored_signature \ | |
163 | or not isinstance(stored_signature, string_types) \ |
|
163 | or not isinstance(stored_signature, string_types) \ | |
164 | or ':' not in stored_signature: |
|
164 | or ':' not in stored_signature: | |
165 | return False |
|
165 | return False | |
166 | stored_algo, sig = stored_signature.split(':', 1) |
|
166 | stored_algo, sig = stored_signature.split(':', 1) | |
167 | if self.algorithm != stored_algo: |
|
167 | if self.algorithm != stored_algo: | |
168 | return False |
|
168 | return False | |
169 | my_signature = self.compute_signature(nb) |
|
169 | my_signature = self.compute_signature(nb) | |
170 | return my_signature == sig |
|
170 | return my_signature == sig | |
171 |
|
171 | |||
172 | def sign(self, nb): |
|
172 | def sign(self, nb): | |
173 | """Sign a notebook, indicating that its output is trusted |
|
173 | """Sign a notebook, indicating that its output is trusted | |
174 |
|
174 | |||
175 | stores 'algo:hmac-hexdigest' in notebook.metadata.signature |
|
175 | stores 'algo:hmac-hexdigest' in notebook.metadata.signature | |
176 |
|
176 | |||
177 | e.g. 'sha256:deadbeef123...' |
|
177 | e.g. 'sha256:deadbeef123...' | |
178 | """ |
|
178 | """ | |
179 | signature = self.compute_signature(nb) |
|
179 | signature = self.compute_signature(nb) | |
180 | nb['metadata']['signature'] = "%s:%s" % (self.algorithm, signature) |
|
180 | nb['metadata']['signature'] = "%s:%s" % (self.algorithm, signature) | |
181 |
|
181 | |||
182 | def mark_cells(self, nb, trusted): |
|
182 | def mark_cells(self, nb, trusted): | |
183 | """Mark cells as trusted if the notebook's signature can be verified |
|
183 | """Mark cells as trusted if the notebook's signature can be verified | |
184 |
|
184 | |||
185 | Sets ``cell.trusted = True | False`` on all code cells, |
|
185 | Sets ``cell.trusted = True | False`` on all code cells, | |
186 | depending on whether the stored signature can be verified. |
|
186 | depending on whether the stored signature can be verified. | |
187 |
|
187 | |||
188 | This function is the inverse of check_cells |
|
188 | This function is the inverse of check_cells | |
189 | """ |
|
189 | """ | |
190 | if not nb['worksheets']: |
|
190 | if not nb['worksheets']: | |
191 | # nothing to mark if there are no cells |
|
191 | # nothing to mark if there are no cells | |
192 | return |
|
192 | return | |
193 | for cell in nb['worksheets'][0]['cells']: |
|
193 | for cell in nb['worksheets'][0]['cells']: | |
194 | if cell['cell_type'] == 'code': |
|
194 | if cell['cell_type'] == 'code': | |
195 | cell['trusted'] = trusted |
|
195 | cell['trusted'] = trusted | |
196 |
|
196 | |||
|
197 | def _check_cell(self, cell): | |||
|
198 | """Do we trust an individual cell? | |||
|
199 | ||||
|
200 | Return True if: | |||
|
201 | ||||
|
202 | - cell is explicitly trusted | |||
|
203 | - cell has no potentially unsafe rich output | |||
|
204 | ||||
|
205 | If a cell has no output, or only simple print statements, | |||
|
206 | it will always be trusted. | |||
|
207 | """ | |||
|
208 | # explicitly trusted | |||
|
209 | if cell.pop("trusted", False): | |||
|
210 | return True | |||
|
211 | ||||
|
212 | # explicitly safe output | |||
|
213 | safe = { | |||
|
214 | 'text/plain', 'image/png', 'image/jpeg', | |||
|
215 | 'text', 'png', 'jpg', # v3-style short keys | |||
|
216 | } | |||
|
217 | ||||
|
218 | for output in cell['outputs']: | |||
|
219 | output_type = output['output_type'] | |||
|
220 | if output_type in ('pyout', 'display_data'): | |||
|
221 | # if there are any data keys not in the safe whitelist | |||
|
222 | output_keys = set(output).difference({"output_type", "prompt_number", "metadata"}) | |||
|
223 | if output_keys.difference(safe): | |||
|
224 | return False | |||
|
225 | ||||
|
226 | return True | |||
|
227 | ||||
197 | def check_cells(self, nb): |
|
228 | def check_cells(self, nb): | |
198 | """Return whether all code cells are trusted |
|
229 | """Return whether all code cells are trusted | |
199 |
|
230 | |||
200 | If there are no code cells, return True. |
|
231 | If there are no code cells, return True. | |
201 |
|
232 | |||
202 | This function is the inverse of mark_cells. |
|
233 | This function is the inverse of mark_cells. | |
203 | """ |
|
234 | """ | |
204 | if not nb['worksheets']: |
|
235 | if not nb['worksheets']: | |
205 | return True |
|
236 | return True | |
206 | trusted = True |
|
237 | trusted = True | |
207 | for cell in nb['worksheets'][0]['cells']: |
|
238 | for cell in nb['worksheets'][0]['cells']: | |
208 | if cell['cell_type'] != 'code': |
|
239 | if cell['cell_type'] != 'code': | |
209 | continue |
|
240 | continue | |
210 | if not cell.pop('trusted', False): |
|
241 | # only distrust a cell if it actually has some output to distrust | |
|
242 | if not self._check_cell(cell): | |||
211 | trusted = False |
|
243 | trusted = False | |
212 | return trusted |
|
244 | return trusted | |
213 |
|
245 | |||
214 |
|
246 | |||
215 | trust_flags = { |
|
247 | trust_flags = { | |
216 | 'reset' : ( |
|
248 | 'reset' : ( | |
217 | {'TrustNotebookApp' : { 'reset' : True}}, |
|
249 | {'TrustNotebookApp' : { 'reset' : True}}, | |
218 | """Generate a new key for notebook signature. |
|
250 | """Generate a new key for notebook signature. | |
219 | All previously signed notebooks will become untrusted. |
|
251 | All previously signed notebooks will become untrusted. | |
220 | """ |
|
252 | """ | |
221 | ), |
|
253 | ), | |
222 | } |
|
254 | } | |
223 | trust_flags.update(base_flags) |
|
255 | trust_flags.update(base_flags) | |
224 | trust_flags.pop('init') |
|
256 | trust_flags.pop('init') | |
225 |
|
257 | |||
226 |
|
258 | |||
227 | class TrustNotebookApp(BaseIPythonApplication): |
|
259 | class TrustNotebookApp(BaseIPythonApplication): | |
228 |
|
260 | |||
229 | description="""Sign one or more IPython notebooks with your key, |
|
261 | description="""Sign one or more IPython notebooks with your key, | |
230 | to trust their dynamic (HTML, Javascript) output. |
|
262 | to trust their dynamic (HTML, Javascript) output. | |
231 |
|
263 | |||
232 | Otherwise, you will have to re-execute the notebook to see output. |
|
264 | Otherwise, you will have to re-execute the notebook to see output. | |
233 | """ |
|
265 | """ | |
234 |
|
266 | |||
235 | examples = """ipython trust mynotebook.ipynb and_this_one.ipynb""" |
|
267 | examples = """ipython trust mynotebook.ipynb and_this_one.ipynb""" | |
236 |
|
268 | |||
237 | flags = trust_flags |
|
269 | flags = trust_flags | |
238 |
|
270 | |||
239 | reset = Bool(False, config=True, |
|
271 | reset = Bool(False, config=True, | |
240 | help="""If True, generate a new key for notebook signature. |
|
272 | help="""If True, generate a new key for notebook signature. | |
241 | After reset, all previously signed notebooks will become untrusted. |
|
273 | After reset, all previously signed notebooks will become untrusted. | |
242 | """ |
|
274 | """ | |
243 | ) |
|
275 | ) | |
244 |
|
276 | |||
245 | notary = Instance(NotebookNotary) |
|
277 | notary = Instance(NotebookNotary) | |
246 | def _notary_default(self): |
|
278 | def _notary_default(self): | |
247 | return NotebookNotary(parent=self, profile_dir=self.profile_dir) |
|
279 | return NotebookNotary(parent=self, profile_dir=self.profile_dir) | |
248 |
|
280 | |||
249 | def sign_notebook(self, notebook_path): |
|
281 | def sign_notebook(self, notebook_path): | |
250 | if not os.path.exists(notebook_path): |
|
282 | if not os.path.exists(notebook_path): | |
251 | self.log.error("Notebook missing: %s" % notebook_path) |
|
283 | self.log.error("Notebook missing: %s" % notebook_path) | |
252 | self.exit(1) |
|
284 | self.exit(1) | |
253 | with io.open(notebook_path, encoding='utf8') as f: |
|
285 | with io.open(notebook_path, encoding='utf8') as f: | |
254 | nb = read(f, 'json') |
|
286 | nb = read(f, 'json') | |
255 | if self.notary.check_signature(nb): |
|
287 | if self.notary.check_signature(nb): | |
256 | print("Notebook already signed: %s" % notebook_path) |
|
288 | print("Notebook already signed: %s" % notebook_path) | |
257 | else: |
|
289 | else: | |
258 | print("Signing notebook: %s" % notebook_path) |
|
290 | print("Signing notebook: %s" % notebook_path) | |
259 | self.notary.sign(nb) |
|
291 | self.notary.sign(nb) | |
260 | with io.open(notebook_path, 'w', encoding='utf8') as f: |
|
292 | with io.open(notebook_path, 'w', encoding='utf8') as f: | |
261 | write(nb, f, 'json') |
|
293 | write(nb, f, 'json') | |
262 |
|
294 | |||
263 | def generate_new_key(self): |
|
295 | def generate_new_key(self): | |
264 | """Generate a new notebook signature key""" |
|
296 | """Generate a new notebook signature key""" | |
265 | print("Generating new notebook key: %s" % self.notary.secret_file) |
|
297 | print("Generating new notebook key: %s" % self.notary.secret_file) | |
266 | self.notary._write_secret_file(os.urandom(1024)) |
|
298 | self.notary._write_secret_file(os.urandom(1024)) | |
267 |
|
299 | |||
268 | def start(self): |
|
300 | def start(self): | |
269 | if self.reset: |
|
301 | if self.reset: | |
270 | self.generate_new_key() |
|
302 | self.generate_new_key() | |
271 | return |
|
303 | return | |
272 | if not self.extra_args: |
|
304 | if not self.extra_args: | |
273 | self.log.critical("Specify at least one notebook to sign.") |
|
305 | self.log.critical("Specify at least one notebook to sign.") | |
274 | self.exit(1) |
|
306 | self.exit(1) | |
275 |
|
307 | |||
276 | for notebook_path in self.extra_args: |
|
308 | for notebook_path in self.extra_args: | |
277 | self.sign_notebook(notebook_path) |
|
309 | self.sign_notebook(notebook_path) | |
278 |
|
310 |
@@ -1,143 +1,156 b'' | |||||
1 | { |
|
1 | { | |
2 | "metadata": { |
|
2 | "metadata": { | |
3 | "cell_tags": [ |
|
3 | "cell_tags": [ | |
4 | [ |
|
4 | [ | |
5 | "<None>", |
|
5 | "<None>", | |
6 | null |
|
6 | null | |
7 | ] |
|
7 | ] | |
8 | ], |
|
8 | ], | |
9 | "name": "" |
|
9 | "name": "" | |
10 | }, |
|
10 | }, | |
11 | "nbformat": 3, |
|
11 | "nbformat": 3, | |
12 | "nbformat_minor": 0, |
|
12 | "nbformat_minor": 0, | |
13 | "worksheets": [ |
|
13 | "worksheets": [ | |
14 | { |
|
14 | { | |
15 | "cells": [ |
|
15 | "cells": [ | |
16 | { |
|
16 | { | |
17 | "cell_type": "heading", |
|
17 | "cell_type": "heading", | |
18 | "level": 1, |
|
18 | "level": 1, | |
19 | "metadata": {}, |
|
19 | "metadata": {}, | |
20 | "source": [ |
|
20 | "source": [ | |
21 | "nbconvert latex test" |
|
21 | "nbconvert latex test" | |
22 | ] |
|
22 | ] | |
23 | }, |
|
23 | }, | |
24 | { |
|
24 | { | |
25 | "cell_type": "markdown", |
|
25 | "cell_type": "markdown", | |
26 | "metadata": {}, |
|
26 | "metadata": {}, | |
27 | "source": [ |
|
27 | "source": [ | |
28 | "**Lorem ipsum** dolor sit amet, consectetur adipiscing elit. Nunc luctus bibendum felis dictum sodales. Ut suscipit, orci ut interdum imperdiet, purus ligula mollis *justo*, non malesuada nisl augue eget lorem. Donec bibendum, erat sit amet porttitor aliquam, urna lorem ornare libero, in vehicula diam diam ut ante. Nam non urna rhoncus, accumsan elit sit amet, mollis tellus. Vestibulum nec tellus metus. Vestibulum tempor, ligula et vehicula rhoncus, sapien turpis faucibus lorem, id dapibus turpis mauris ac orci. Sed volutpat vestibulum venenatis." |
|
28 | "**Lorem ipsum** dolor sit amet, consectetur adipiscing elit. Nunc luctus bibendum felis dictum sodales. Ut suscipit, orci ut interdum imperdiet, purus ligula mollis *justo*, non malesuada nisl augue eget lorem. Donec bibendum, erat sit amet porttitor aliquam, urna lorem ornare libero, in vehicula diam diam ut ante. Nam non urna rhoncus, accumsan elit sit amet, mollis tellus. Vestibulum nec tellus metus. Vestibulum tempor, ligula et vehicula rhoncus, sapien turpis faucibus lorem, id dapibus turpis mauris ac orci. Sed volutpat vestibulum venenatis." | |
29 | ] |
|
29 | ] | |
30 | }, |
|
30 | }, | |
31 | { |
|
31 | { | |
32 | "cell_type": "heading", |
|
32 | "cell_type": "heading", | |
33 | "level": 2, |
|
33 | "level": 2, | |
34 | "metadata": {}, |
|
34 | "metadata": {}, | |
35 | "source": [ |
|
35 | "source": [ | |
36 | "Printed Using Python" |
|
36 | "Printed Using Python" | |
37 | ] |
|
37 | ] | |
38 | }, |
|
38 | }, | |
39 | { |
|
39 | { | |
40 | "cell_type": "code", |
|
40 | "cell_type": "code", | |
41 | "collapsed": false, |
|
41 | "collapsed": false, | |
42 | "input": [ |
|
42 | "input": [ | |
43 | "next_paragraph = \"\"\"\n", |
|
43 | "print(\"hello\")" | |
44 | "Aenean vitae diam consectetur, tempus arcu quis, ultricies urna. Vivamus venenatis sem \n", |
|
|||
45 | "quis orci condimentum, sed feugiat dui porta.\n", |
|
|||
46 | "\"\"\"\n", |
|
|||
47 | "\n", |
|
|||
48 | "def nifty_print(text):\n", |
|
|||
49 | " \"\"\"Used to test syntax highlighting\"\"\"\n", |
|
|||
50 | " \n", |
|
|||
51 | " print(text * 2)\n", |
|
|||
52 | "\n", |
|
|||
53 | "nifty_print(next_paragraph)" |
|
|||
54 | ], |
|
44 | ], | |
55 | "language": "python", |
|
45 | "language": "python", | |
56 | "metadata": {}, |
|
46 | "metadata": {}, | |
57 | "outputs": [ |
|
47 | "outputs": [ | |
58 | { |
|
48 | { | |
59 | "output_type": "stream", |
|
49 | "output_type": "stream", | |
60 | "stream": "stdout", |
|
50 | "stream": "stdout", | |
61 | "text": [ |
|
51 | "text": [ | |
62 |
"\n" |
|
52 | "hello\n" | |
63 | "Aenean vitae diam consectetur, tempus arcu quis, ultricies urna. Vivamus venenatis sem \n", |
|
|||
64 | "quis orci condimentum, sed feugiat dui porta.\n", |
|
|||
65 | "\n", |
|
|||
66 | "Aenean vitae diam consectetur, tempus arcu quis, ultricies urna. Vivamus venenatis sem \n", |
|
|||
67 | "quis orci condimentum, sed feugiat dui porta.\n", |
|
|||
68 | "\n" |
|
|||
69 | ] |
|
53 | ] | |
70 | } |
|
54 | } | |
71 | ], |
|
55 | ], | |
72 |
"prompt_number": |
|
56 | "prompt_number": 1 | |
73 | }, |
|
57 | }, | |
74 | { |
|
58 | { | |
75 | "cell_type": "heading", |
|
59 | "cell_type": "heading", | |
76 | "level": 2, |
|
60 | "level": 2, | |
77 | "metadata": {}, |
|
61 | "metadata": {}, | |
78 | "source": [ |
|
62 | "source": [ | |
79 | "Pyout" |
|
63 | "Pyout" | |
80 | ] |
|
64 | ] | |
81 | }, |
|
65 | }, | |
82 | { |
|
66 | { | |
83 | "cell_type": "code", |
|
67 | "cell_type": "code", | |
84 | "collapsed": false, |
|
68 | "collapsed": false, | |
85 | "input": [ |
|
69 | "input": [ | |
86 | "Text = \"\"\"\n", |
|
70 | "from IPython.display import HTML\n", | |
87 | "Aliquam blandit aliquet enim, eget scelerisque eros adipiscing quis. Nunc sed metus \n", |
|
71 | "HTML(\"\"\"\n", | |
88 | "ut lorem condimentum condimentum nec id enim. Sed malesuada cursus hendrerit. Praesent \n", |
|
72 | "<script>\n", | |
89 | "et commodo justo. Interdum et malesuada fames ac ante ipsum primis in faucibus. \n", |
|
73 | "console.log(\"hello\");\n", | |
90 | "Curabitur et magna ante. Proin luctus tellus sit amet egestas laoreet. Sed dapibus \n", |
|
74 | "</script>\n", | |
91 | "neque ac nulla mollis cursus. Fusce mollis egestas libero mattis facilisis.\n", |
|
75 | "<b>HTML</b>\n", | |
92 |
"\"\"\" |
|
76 | "\"\"\")" | |
93 | "Text" |
|
|||
94 | ], |
|
77 | ], | |
95 | "language": "python", |
|
78 | "language": "python", | |
96 | "metadata": {}, |
|
79 | "metadata": {}, | |
97 | "outputs": [ |
|
80 | "outputs": [ | |
98 | { |
|
81 | { | |
|
82 | "html": [ | |||
|
83 | "\n", | |||
|
84 | "<script>\n", | |||
|
85 | "console.log(\"hello\");\n", | |||
|
86 | "</script>\n", | |||
|
87 | "<b>HTML</b>\n" | |||
|
88 | ], | |||
99 | "metadata": {}, |
|
89 | "metadata": {}, | |
100 | "output_type": "pyout", |
|
90 | "output_type": "pyout", | |
101 |
"prompt_number": |
|
91 | "prompt_number": 3, | |
|
92 | "text": [ | |||
|
93 | "<IPython.core.display.HTML at 0x1112757d0>" | |||
|
94 | ] | |||
|
95 | } | |||
|
96 | ], | |||
|
97 | "prompt_number": 3 | |||
|
98 | }, | |||
|
99 | { | |||
|
100 | "cell_type": "code", | |||
|
101 | "collapsed": false, | |||
|
102 | "input": [ | |||
|
103 | "%%javascript\n", | |||
|
104 | "console.log(\"hi\");" | |||
|
105 | ], | |||
|
106 | "language": "python", | |||
|
107 | "metadata": {}, | |||
|
108 | "outputs": [ | |||
|
109 | { | |||
|
110 | "javascript": [ | |||
|
111 | "console.log(\"hi\");" | |||
|
112 | ], | |||
|
113 | "metadata": {}, | |||
|
114 | "output_type": "display_data", | |||
102 | "text": [ |
|
115 | "text": [ | |
103 | "'\\nAliquam blandit aliquet enim, eget scelerisque eros adipiscing quis. Nunc sed metus \\nut lorem condimentum condimentum nec id enim. Sed malesuada cursus hendrerit. Praesent \\net commodo justo. Interdum et malesuada fames ac ante ipsum primis in faucibus. \\nCurabitur et magna ante. Proin luctus tellus sit amet egestas laoreet. Sed dapibus \\nneque ac nulla mollis cursus. Fusce mollis egestas libero mattis facilisis.\\n'" |
|
116 | "<IPython.core.display.Javascript at 0x1112b4b50>" | |
104 | ] |
|
117 | ] | |
105 | } |
|
118 | } | |
106 | ], |
|
119 | ], | |
107 |
"prompt_number": |
|
120 | "prompt_number": 7 | |
108 | }, |
|
121 | }, | |
109 | { |
|
122 | { | |
110 | "cell_type": "heading", |
|
123 | "cell_type": "heading", | |
111 | "level": 3, |
|
124 | "level": 3, | |
112 | "metadata": {}, |
|
125 | "metadata": {}, | |
113 | "source": [ |
|
126 | "source": [ | |
114 | "Image" |
|
127 | "Image" | |
115 | ] |
|
128 | ] | |
116 | }, |
|
129 | }, | |
117 | { |
|
130 | { | |
118 | "cell_type": "code", |
|
131 | "cell_type": "code", | |
119 | "collapsed": false, |
|
132 | "collapsed": false, | |
120 | "input": [ |
|
133 | "input": [ | |
121 |
"from IPython. |
|
134 | "from IPython.display import Image\n", | |
122 |
"Image( |
|
135 | "Image(\"http://ipython.org/_static/IPy_header.png\")" | |
123 | ], |
|
136 | ], | |
124 | "language": "python", |
|
137 | "language": "python", | |
125 | "metadata": {}, |
|
138 | "metadata": {}, | |
126 | "outputs": [ |
|
139 | "outputs": [ | |
127 | { |
|
140 | { | |
128 | "metadata": {}, |
|
141 | "metadata": {}, | |
129 | "output_type": "pyout", |
|
142 | "output_type": "pyout", | |
130 | "png": "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\n", |
|
143 | "png": "iVBORw0KGgoAAAANSUhEUgAAAggAAABDCAYAAAD5/P3lAAAABHNCSVQICAgIfAhkiAAAAAlwSFlz\nAAAH3AAAB9wBYvxo6AAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAACAASURB\nVHic7Z15uBxF1bjfugkJhCWBsCSAJGACNg4QCI3RT1lEAVE+UEBNOmwCDcjHT1wQgU+WD3dFxA1o\nCAikAZFFVlnCjizpsCUjHQjBIAkQlpCFJGS79fvjdGf69vTsc2fuza33eeaZmeqq6jM9vZw6dc4p\nBUwC+tE+fqW1fqmRDpRSHjCggS40sBxYDCxKvL8KzNBaL21EPoPB0DPIWVY/4NlE0ffzYfhgu+Qx\nGHoy/YFjaK+CcB3QkIIAHAWs3wRZsuhUSs0CXgQeBm7UWi/spn0Z+jA5yxpEfYruqnwYllRic5a1\nMaWv8U5gaT4M19Sx396IAnZLfB/SLkEMhp5O/3YL0AvoAHaKXl8HLlZK3QZcpbWe0lbJDOsaHuDU\n0e4u4JAy2wPk/C1JzrKWArOQ0fUtwH35MOysQxaDwbCO0NFuAXoh6wPjgQeUUvcqpUa0WyCDoQls\nCIwBjgfuAV7KWdY+7RWpmJxlXZezrEdylvXxdstiMKzrGAtCYxwI/EspdZbW+g/tFsbQ67kQuBHY\nFNgseh9FV6vCbUAeWBC9PgBeq2EfS6J2MQOBrRDTe5KdgAdzlvW1fBjeUUP/3UbOsoYBE6OvG7VT\nFoOhL9Af+BUwFLkZpV+DaY6V4UPkRpb1+ncT+m8nGwK/V0oN01qf025hDL2XfBi+DLycLMtZVo6u\nCsKfGnSq8/NheEpqHwOBEcDBwJnAsGhTP2ByzrJG5cPwnQb22Sy+0G4BDIa+RH+t9dmlNiqlFKIk\nJJWGi+jq5JPmq8BbJJQArfXqpkncczlbKbVQa/3rdgtiMNRCPgxXAK8Ar+Qs63LgXmDvaPPGwPeA\nH7VJvCRfbLcABkNfouwUg9ZaAwuj178BlFLvVejzgR4WFviM1npcuQpKqf6IyXIjxLS7GzAWuUnu\nXsO+fqWUellr3ZBJdq/jr9+BDn1uve07O9Rz0y6f8PtGZGgWe53oT6SBkZ/q1/nHZy47aloTRTKU\nIR+Gy3OWNR6Zxtg0Kv4KRkEwGPocxgcBiCwcsSI0F5iOhF+ilPok8C3gVGS+thK/VErdrbWuO2ys\ns/+aLZTuOKbe9krrIUCPUBB0B+PQ1P1bdKe6EzAKQgvJh+GbOct6gkJkxM45y+qXDIWMHBhjBWJe\nPgyDWvaRs6zPIVObAG/nw/DpEvUGAp8E9gGGJzbtl7Os7cvs4skqp0V0Yl8jgcOBjyMDhbmIZeWl\nfBg+UUVfReQsayhwELAnsAXi6/E28BxwTz4MP6iyn92RaSCA+/NhuCwqXx9R4MYhU0MfRTK/AjyW\nD8MFGd0ZDFVhFIQKaK3/BXxfKXUlklTq0xWafAI4Driyu2UzGLqRlygoCArYHJif2H4gcFb0+Z2c\nZW2bD8NV1XScs6yNgH8g/jsAPwCeTmzfFPgjYsnbiez71MUVdnMQcF8V4nyUs6whwB8QX4+0s2Ys\n0yPAt/NhGFbRZ/wbzgO+DaxXotqqnGX9GbigCkXhf5CBCsDngYdzljURGQhsWqLN+znL+iFwdT4M\ndYk6BkNJTJhjlWitQ2Bf4P4qqv848t8wGHor6Yd9+ruHJFkC2BI4rIa+D6egHKwmstYlGAxMQCwH\nrRjEPI5ER5S7ZvcFXsxZ1phKneUsawSi8HyH0soB0bbvAM9Ebaplt5xlnYkct1LKAYiFZhJwSQ19\nGwxrMRaEGtBar1RKfRX4JxIzXortou3PN1mE+YgJsSwaeoLHOQCqUy3QSr9eqZ6G/gq2aYVMhqrY\nOfF5FeJwvJZ8GM7JWdY/gC9HRS7wtyr7Pjrx+e6MqYC3KLbU7Qhck/h+FJIKvRRVjfSREXicU8EH\npgAvIIqLBZwGfC7avl5Uf29KkLOsTZCMq8npj9sQx89no37HIlaAODplNPBIzrJ2z4dhNVlaT0HC\nXwFmIkrAC4if2PaIz8/3KCgn385Z1pX5MJxeRd8Gw1qMglAjWutlSqnTgUcqVP0SzVYQtP5mcMXE\nSvvtUUy9YsK5QEWHy7EnTB6lOtSsFohkqEDOsgYAdqJoagkT9Z8pKAj75yzr4/kwnF2h748ho/GY\nq9J1oqiKLj4JOctKK8Yz8mH4Yrl9VcnHkXVYTsyHoZ8WJWdZNyPThbF5/3M5yzowH4alpi9+T0E5\nWA18Nx+Gf0zVeRG4KmdZ90R9bwCMRKwyX69C5h2j91uA4/JhuCSxbTYwJWdZtwNPIFbifsAFSISZ\nwVA1ZoqhDrTWjyIjjXIc3ApZDIZu4ELgY4nvt5Wody8wJ/qsgBOr6HsihfvOfCRrY7v5dYZyAECk\nGP0ISEZmZYZ55yxrB8SyEXNxhnKQ7Pt64H8TRUfmLGuXKmWeC4xPKQfJvp9CLCJlZTYYymEUhPq5\ntcL2XVsihcHQJHKWtU3Osi5GnAZj5iKWgiKitRouTxQdl7OscnPu0HV64dp8GLY7R8pyxEGxJPkw\nfBcZ9ceUSvN8IoV76upK/UZcgawcG3NKqYopfleFU+gDic/b5SzLWIwNNWFOmPqp5CG9sVJqPa11\nVZ7dBkOL2D1nWcmcBkOR8MFtgM/QdTXJZcCR+TBcXqa/SYj5egAFZ8VMX4ScZe2FRPnEXF2z9M3n\n3nwYVsrtAmK6/0z0uVR4ZXLtivvzYfhGpU7zYbgkZ1k3ACdHRQdWIQsUO3ZmkUzB3Q/xjaolLbeh\nj2MUhDrRWr+mlFpJ+eV5hyIxz4YWs98Fj/Rf8uZbozo0/ZYt7D8rf9ORK9stUw/hU9GrEnMAp1R+\ngph8GL4bzdNPiIpOorSzYtJ68FS1IYPdTLWp3hcnPm+Q3pizrA7E+TCmFn+aZN0dcpY1LB+G5e4b\ny6rM8bA49X39GmQyGMwUQ4NUGnkMrbDd0A3sdeLk4z6cN+89pTtDTWd+gyErF+7pTv5eu+XqJbyK\nTDHsmg/DJ6tsc2ni8+dzljUqXSGaevhmoqjIObFNVBzlV8kQug4W5tbQNl13WGatAv+poW+DoW6M\nBaExPgC2LrO9nHWhpSilDqI4NPMhrfXUJvS9M/DfqeJXtdY3N9p3rex50uQ9lFKT6BrTvoFCXbTX\nyZNfmnrZxHtbLVMP4xng74nvK5DzeD7wfIWRayb5MHwiZ1kzgF0oOCuemar2ZQoK8zLgr7Xup5t4\ns0n9DEl9b0RBSPeV5q0a+jYY6sYoCI1RacnZ91siRXUMAH6eKnsYicdulDOAY1NlpzWh35pRqG9R\nIuGN7uw4AfG878s8nw/DX3RDv5dScGY8NmdZP86HYXJaJzm9cHMp7/s2UHdK9BTpKaxBNbRN163k\nt9Rux05DH8FMMTTGZhW2v9sSKarjbopNk/sqpUY30qlSahCSGS/JCuD6RvqtF6UpMm/HaHTJbYaG\nmQzED/0umRVzlrUZhXwJ0HOmF5pJOlXyxzJrZbNt6rtZP8HQIzAKQp0opTZAlsItxTKtdTnv75YS\nLR7lpYqrjV0vx2EUH4fbtdZtucnpMqOrDjPy6jYii8DkRFHSYnAEhem22cBjrZKrVeTDcCldTf/p\nh345ksrEGprnF2EwNIRREOrnMxW2z2uJFLVxJcXmy2OVUo34ShydUda+EaIq7T2u0SZTY/eSdFY8\nMGdZm0efk86J6/LCQUnFp5pIkZjkcvQz8mH4YZPkMRgawigI9VNp7v7BlkhRA1rr+RQneNqC2hba\nWYtSajiS9z3JXLomaGktq/VllLIUdKqSWe0MjZMPwxlIel8Q/6Zv5CxrGIX8AJ10XU+hFtIRQ+UW\nKWoXyYyTu+Qsa79KDXKWNRpJyx5zZ9OlMhjqxCgIdaCU6g98o0K1npBCNotLM8rcOvuagCRgSXKN\n1rozq3IrCCZNfFkrfRjotWsCaJinUBODK51/tkuuPkTy/DoYOIDCfeb+fBjW4t2/lqhdcmRdbUri\nVnILXS2HZ1WRvfAcCk61K4A/dYdgBkM9GAWhPr5F6XSrIBf6Qy2SpSaidSReShV/XilV7veUIj29\noOkB2fGmXT7x7sCbOGpFf7VZx4A1m0/znG2nehMyc+0bms7NFJxzxwH7J7Y1OvWUPG9/mLOsLRvs\nr6lEaaOT0TtfBB5ITLWsJWdZg3KWdRNwTKL4wnwYzu9mMQ2GqjFhjjWilBqBpJYtx51a66UV6rST\nS+maJz52VvxRdvVilFK7UbzexGNa67Kr+bWS6X+ekPYs79HkLGt34JOI+Xyz6D2d1vfMnGUdini6\nL0C851/Oh2HD+SyaQT4MV+YsaxJyLm1Gwf9gAXBHg93/JNHHtsArOcuajCztPBDYCkkytBXg5sOw\n5QmF8mF4W86yLgK+HxXtC8zKWVaALMm8CslHsicS7RFzL8VhyAZDWzEKQg0opbYE7qd8prPVdF2h\nrSdyLfALYMNE2XFKqR/XsHbEURll62L4Wiv5PuBUqPPF6JXkLuCQbpGoPi4HfohYKGMHWD9axrlu\n8mF4Z7RuwfioaDBwaonqRemQW0U+DH+Qs6xFwHnIFNwQsv+3mMnA8dHiVwZDj8FMMVSJUuow4DkK\na7GX4gqt9cstEKlutNaL6boULMho5tBq2iul+lH8IFuCmJcNfZx8GM6hOCFVU5THfBhOQHxfylkH\n3gY+asb+6iUfhhcCewC3l5BlFbJk/P75MDwqlVTKYOgRKK1rizhSSk2h67ximo1abV5XSi2n9EIk\nz2itx5XYVqnfQcjI7DiqW2XtfeCTUbRA3ex50nWfUrqjeJEcrfcLrpj4SCN9xyilxgDPp4of0Fof\nUEXbg4B/pIqv1FrXnVNh7AmTR3V0qIwwRH1E4E28pd5+De0hZ1m/Bb4bfX0+H4Z7dMM+hgGjkDwC\nS5FpjFk9bR4/Z1mDkGmF4VHR20g4Y3oxJYOhR9EXphg6lFLlVjFbH0mZvDGwCTAayCFe0ntTOZ1y\nzDLgkEaVg1ahtX5BKfUU8OlE8ReUUjtorSstCduzch8YehSR5/6ERFG3nBvRuhE9frXUfBguA6pd\n+Mpg6DH0BQXBBro7o+Ea4Bta66e6eT/N5lK6KggKOAE4u1QDpdTGFOdNmNkLf7uh+zgYcRQEMa+3\nJe22wWBoDOOD0DhLgYla67vaLUgd3ETxglLHRXkeSnEExQ5gbQ9tNPQokis5TsqHoVlbwGDohRgF\noTECYHet9Y3tFqQetNYrKDb/DqN46eYk6emF1UhUhMFAzrImUEhDvgr4VRvFMRgMDWAUhPpYAvwf\n8Bmte31+/8uQBEdJMjMrKqW2o5A2N+YfWusePw9s6F5yltWRs6zxwKRE8RXtyEVgMBiaQ1/wQWgm\neWTe/jqtdU9Zz74htNavKaXuAw5KFB+glBqptZ6Tqj6RQlrYGDO90AfJWdY5wNeQFQwHIAmetk5U\neZFCsiCDwdALMQpCed5AphEC4NF12BHvUroqCAoJ7TwvVS+d++BdJEmPoe+xKRLnn0UeODwfhm3N\nRWAwGBqjLygIbwN/LbNdI1MGH6ReL/eWkMUmcDeSeGa7RNlRSqnzdZQoQym1C7Bzqt11NWReNKxb\nzEMU6GHAesBiYCaSLOviaF0Cg8HQi+kLCsLrWuvT2y1ET0ZrvUYp5SG57mO2Bz4LPB59/2ZRQ5P7\noM+SD8OLgYvbLYfBYOg+jJOiIeZKxOs8STJiIb28daC1/lf3imQwGAyGdmEUBAMA0XTKraniI5VS\nA6O0zOnloI31wGAwGNZhjIJgSHJp6vtgJBNlehW65cANLZHIYDAYDG3BKAiGtWitHwVeShV/muLF\nuW7VWi9qjVQGg8FgaAd9wUnRUBuXAn9IfN8f+FyqTo/OfbDnSX8brDpXnqEUe2ropzQvdtDx66ev\nGN9XolIMPQDb9T8LrBd4zsPtlsXQe7Bd/0BgQeA5QbtlMQqCIc21wC+ADaPv6WWu5wAPtVKgWtjt\n6Os2XG/9jhdQjIzTQ2rFF9bQecy4E2/I9UQlwXb9LYDDK1R7K/Cc21shj6FxbNcfDjwGKNv1Rwae\n83q7ZWo2tusPBb6ELGW9BbAICX99Gngs8Jx0hlZDBWzXHwvcC6ywXX9o4DlL2ymPURAMXdBaL1ZK\n+ZRItwz8Jc6N0BMZMFB9GxiZsWnzTjrPAH7QWomqYgTF/h9pngC6RUGwXf+XwC2B50ztjv57M7br\nXwJMCjxneo1NP0SWgAfJq7LOYLv+esAFwOkUL9wWM912/d0Dz+lsnWQ9A9v1BwEXAT8PPKfWVOML\nkPVt3kNWQm0rxgfBkEWph5UG/tJCOWqnQ40ttUkrvWcrRamWwHOmAZsguSfGAi9Hmy5AUhgPAz7f\nHfu2XX8k8ENgx+7ovzdju/4uwP9D/peaCDxnCbANsF3gOYubLVu7sF1/AHAHcBaiHDwI/C+ywNsE\n4KfA68BdfVE5iNgbOBmxqtRE4Dn/BoYDnwg8Z02zBasVY0EwFKG1fkEp9RTioJjkIa11zzaVarYq\nvVFt2TpBaiN6oCwB5tiu/2FUPCvwnLTTaLM5oJv77800dGwCz1kXHXkvRNKydwI/Cjzn1+kKtuuf\ni2TX7Ks0et681yxBGsUoCIZSBBQrCL0h98EbdW7rddiuPwoYFJu/bdffFNgL2BZ4DZgWKR5ZbRWS\n2+KIqGiE7fpjUtXmlrtZRdaHscBAYDowM/CckimWbdffFfgw8JzXou/9kfUccojV5MXAcz4s0XYw\nsCsymu8PzAVmBJ7zVqn9pdoPRVKF7wSsAN4EgqzRve36HcAoZDEqgO0zjs3rged8kGo3gOJ05ADT\ns0bTkan+k9HXGaVGjNFxykVf81nH2Hb9Ich/MRJJeT291H9fL7brj6CwANfPspQDgOi3rijRx/rI\nb8kB7wPPBZ4zL6Ne/JvfCDzn/WhufhvgvsBzVkR1dgN2AR4JPGduom38P7wXeM7c6FzfCfgU4iMR\nlFLebNfPIefXzMBzikz8tusPQyx676bljmTeCfhyVLST7frp//TV9Dluu/6GwOhUvTWB58zIkjFq\nsykyNfmfwHMW2K7fLzoWeyDTFPnAc14t1T7qYwNgT+Rc/wi5ZyT/N20UBEMRSqn+wNdTxQspTqTU\n41BaP6yVOipzGzzSYnG6m6uBz0YPv7OQm3dytc35tuuflHZutF3/BuArwEaJ4p/QNdU2wGnAH9M7\njRSTG5CbS5LQdv2joymTLKYBzwHjbNc/DomW2TCxfbXt+sMCz3k/sa8RwM+Qh/X6qf5W2q4/CTit\nzMN1OPB7CopQktW2658YeM5fEvXvRKZzBiXqZaWUPha4JlW2NfB8Rt0hiANfmjWIuf5jiLPfvVm/\nAfmvbgNmB54zKrkheuD+Bjg11Wap7fpnBJ5TybelFk4E+iE+Fb+ptbHt+scg//nGqfJbgeMDz1mY\nKN4UOZYX2q7fSWHhuNdt198ZOBc4MypbbLv+5wPPeTb6PiJqe5ft+ichx3WXRN8rbdc/OfCcrGis\nR4ChiHKSlSn2f4BzkOvitMRvCKJ9DEzU9TPafwGZlkkyBvExSrKUrtdnmoOBycA5tus/iCyat3li\nu7Zd/0rk2ihS1mzXPwT4E3LulaLTKAiGLL6EaMlJbtBat91pphIjFw289t9DVh4N7Jva9EKnWnpJ\nG0RqBXcjCa08YCqy/PJE4L8A33b9HQPPeTNR/0bgvujzGchoywPSq5U+nd6R7fp7IDfRjYDrEE99\nDeyHrPb5lO364xI36zTb2q4/AUnt/SSyLHQHMvJZklQOIhYChyCLid2FWBoGIQrDfwGnAP8Gskzd\nVvSbBgPvIMdpJjLHuxdikXgg1ewa4Jbo84+BHRAFI/3gT9/QQZa+/iIy9zwccVQrSeA5nbbrX4s8\ncI6htIIQK7xdFJLIAvEEYjmYBlyP/E4LeXj92Xb94YHnnFtOjhrYJ3q/vtbpE9v1fwqcjYxUL0GO\n51bI//g1YIzt+mNTSgJIivfNEIXgBOThfx0ySv8Nct7vgzgfj0+1HQf8E5iPKM/vI+vLHA9cZbs+\nJZSEevgDBZ++3yIKzgVI1FeSrCnD6ci0zebAJxCfjmoZjxzXPPBL5By0gW8jCt3sqHwtkYL1N0RB\n/R2ymOG2yHE5CLFAHAu8ahQEQxbfyijrDdML3HTTkWvUBRfsb88bPb6TzjEK+oHKL184YHL+Jmdl\nu+XrJsYBhwaec0dcYLu+hzw0dkcu/AvjbUmLgu36DqIgPB54zuQq9nURMgI8LjnyBibZrj8z2s/l\ntuvvVcJJbWvkXDoi8JzbKu0s8JxFtut/IqXgAPzOdv0/IiPnb5KhICAjpMGIEjAhPV1iu35HWsbA\nc25ObD8ZURAeqibENBqpTYnark8FBSHiakRBOMx2/cHpB29kSv4KooSlLRYnIcrBHcBXk7/Fdv0b\ngReAM23Xvz7wnJlVyFIJK3qfXUsj2/U/jiiiq4B9ktEytuv/Fhlpfx2xEnw31XxHYLfAc6bbrv8k\ncny/Bnwz8Jy/2q6/DTLd9F8Zu94ceXAeEHhOvM7MNbbrT0UU4vNs15+c2FY3gedcm/hNP0EUhDvL\nKMrJtkuIFPboWNWiIOSAO4HDE7/Dj67FSxEn21+m2pyOWDpuCDxn7fG2Xf8e4F1EIVsceE5oohgM\nXVBKjURuSEke11qXMhv3OPR553VO9Sb407yJZwTexO8FnnNV/qYj11XlAOCfSeUA1s4D/y36mp7f\nrAvb9fdGLDMzU8pBzMXIg2wsMhLKQiFhgxWVg5gM5SDm+uh9VHqD7fr7IlaNFcAJWb4UPcHLPvCc\n2YgVZn3gyIwq30AsQg8lQ+aiefUfR1/PzlB08sD9Udusfmsi2t+Q6GutjspnIE6L16dDaSN/irMR\np8dTbddPOxK/nwgxTZr8747e30SsEkNL7PvXGQrAVYgvwggK/gK9mXMyfuON0fvWkY9Dkp2i97uT\nhYHnLKNgURsDxknRUMz5FJ8XP22DHIbqSc9pxsSOW8ObtJ89ovdXbNcvpQC8j4zcdiTbnAoy4q2b\n6Ia3CYV5/Y0zqsXOf4/WEYveaq5GQuOOQaZekhydqJNkW2BLZF2UzhL/R+xE2XAIa+A52nb9lUho\nY63hd7GD5d1ZGwPPmW27/iuIUrkLXc/n9xP13rZd/yNgVezoF8n1NjAyyyKETGGl97fGdv1/IlaL\n3h7e+06WM2PgOQtt11+GTMcNo6vVJ1aWsyK+4nvFQjAKgiGBUmoshfnOmGe11vdl1Tf0GOaUKI9v\nlqrE9lqJb6b/Hb3KsU2Zba/VslPb9bdDfA0ORLz0N62iWWxVqMkc3iZuRuawP2u7/g6JKI9RSCTR\nYoodhOP/YgNKK2Ix2zZJzjnINMN2NbaL/4uiaIUE/0EUhB3pqiCkMwl2IscjXZZFJ/B2iW1xRtWR\nZWTqDcwps63U9f8Q0TSN7fp/iK0PtuvviPjmrCHyR1qrICilNkTmHjZDLsDke/JzOtwnzY1KqXcR\nR4cFiBab9XlRT87I19dQSo1GNPz0tJOxHvR8mhrOVobB0XuAOBiWo1zmwaqdXW3X3x+4BzGVv4SM\npN9AnPEg21McxMIArTs2dRN4zoe26/8NOA6xGJwfbYqV9b8GnrM81Sz+Lz5A0qOXo2y4Ww3MoT4F\nIY4+KTfNF58TaXN4VthstVNDitLKcdxvOjKmEj0tv0M953fs87E3Eul0B2JliBflOzfwnFcA+iul\n5iEmwQFNEBaK569L0amUWggcqrXO8gg2FKHG2CdW4Uem9XvBlUflu7RUaiByU3lPa92ZKN8cSav8\nfUQBTHKr1rrqueIsxp18/eg1azrLjSYB6NfRsY3G6Is9nDjDYxh4zundvbMotvtm5N50duA5P09t\nT0faJIkfirU+zNrF1YiC4FBQECZE73/JqB//F+u14r+ImIVEOB1iu/6ZNfhwzEamp7YuU2e7RN1m\noZBnW5YVIfZ1qNWfotw51yuIph++hET0bAkcikwpTAEuCjxnSly3PzIP0a8NcnYgD6SBlSoaIhQX\nV2UtVup24LBU6S7IyG+NUuodZP52awojrTSvIjeshlij9XdQKh2jXYRRDtpGfOCruQfEpmzbdn0V\ndP9iPLsgjnEryI67Lzd/PCt6/5Tt+v3LJXAqQ/z7ut2ZO/Ccx23XfxUYZbt+7D8xCngl8Jwsa80s\nZBS8ke36O7cg4ybA5UgegJ0QE/XN5auvZRaiIMQRF12wXX8TCv9ls6eERpOtIMR+EXNS5YsRh8dS\nTo/V+CzUck21i6uR5++4wHNeKFXJRDH0PfoR5fqmtHKwDDhCa73O5JA3lCSeF04v6Z3FPRTMzBO7\nS6AE8Q12PbomgYn5Xpm29yMPhu2RUK96iKMn9q6zfa38JXo/NHoly7oQeM5K4Iro60+jKINuJVJC\nYu/439uuX805A4VkWyfbrp+V/MdFnOmeCmpfFKsSRYMc2/U/DeyG3OfSjpOx5WmfVHmcuXFcFfus\n5ZpqObbrb45EtswqpxyAcVI0FDMbOFxrXeT9a+heopvnEArzolvashT0wmbEapdgGpIU5XDb9R9F\nYqrXQyyL8wPPeTeuGHjOMtv1T0VuqldH6W//jigNmyHOcAcBgwPPcZog20xkRLcJ8DPb9S9CRqM7\nI7kDvoDE1hfdxwLPWWy7/plI7oCLbNffHXm4zUQeRtsjGRP/EXhOKSfcABkpj49i5+9G/putgHmB\n5yxIN4iSF21C14V6Rtiu/yYSW15uHv4a4P8oKAedlPcvOAv4KmItfCTKKfAS8v8NR1ILHwnsl5GA\nqF7ORdYaGA48HGWyfBqYgViDRwCfQR72PkDgOU9E2TvHI4m0TgeeRczb30DyH2iKcyA0ymrgWNv1\nFyDK1NvIQ3tStN3LCH+9HUl29UPb9echFo8BUbtLEKfJtJ9EmgA59ifbrj8bCR3cGDlvZqdTLcPa\n9NCbUMhs2GFLKvPFSAKxZl7/CxEL8pgoA+QMxD+kE3HenAHcHnjOGmNB6Dt8iGjHWSFKK4HHkcQr\nOxvloLXYrr+77fqrEIejNyiE6P0WccZbabv+lFLtG+Ry5AY/BHkYfRDtR9M79QAAA3FJREFUcwYS\nNdCFwHPuQR6a7wHfAR5GMhk+i9xcT6G6KIOKBJ6zFBn9r0GUmBlIWN9ziHf/5yjO/phsfy2yqt4i\nxOJxF3INTI9k/Q7ZoV4xv0PC5LZCci4sQm6g08kYHdquvxy5lt4DwsSmF5EENCts1//Idv3M9LbR\negJTkEx4NvBA1joFifqLIjkeR6wcfwdeQfIFTEEcjHNU79RXkShvw95Ixs5+yOj/KuSh+ATiAHcq\nxb4fxwOXRfJMQc6zlxGF6B3g4MBznmmWnBFzEUfP0xDFcCGiAG+JHKushESXIdanjRBF4l3EInAj\n8vuOqWK/5yNRGaOQFNkfIhkOX6CQgwAA2/W3jkI3V0T7ejjatAFyXb2PXP/LbVnroWGi6bbzo697\nIlaWk5Br93wkk+jztusP7o94Lna7eaoMZU0cVXIAped7eqGZfP2ZqmPFl+ptrVf3n19UpvVMYLRS\nagBywxuEjLwWAe9qrTMXV2mUzs7OP/Xrp+6qt33Hmn5Zue3XNeZTOVoky5nqKiQkrNT883Qk3WvJ\nsMLAc1bbrv9Z5AH6KWRkOB+5wRWlWo7a3Ga7/mOIomAho/GFyI30YeDREru7ELlOq07TG3jONbbr\nT0Nu9KOQm+i/gFsDz3nTdv2fI2FbpdpfHnlpH4LcnHdAlIz5yLErqXgFnvOR7fo28lDYE7lu3kKO\nTdZ9K52xrhTl7knnUVB6SqVeTsr4apQU6lDEbG4hCsFbROsRBE1ebjrwnNB2/XGIGf5gRBkYhPyv\n7yDpjR9MtVkOnGK7/vWIgrFrVPcF4O8ZKbaXIuduWkH6KfL/JbkEsWClfWK2CDzHt10/jzhXjkGO\nyzNIZEiRD00ga3ocaLv+kUh2xo8hSuVURKmIUyiXVGYCWVzKQlJD7xrJNg85b9LX8RLgF6X6SpFU\n9Cpe28gaJgORqEEAbNffDLlvHIQoAndR8NEYilwjExD/nwuUiTQ0GAwGw7qC7fqjEUvKqsBzmhWd\nt05gu/5pyNoifw48J9N5PForxQeeNFMMBoPBYDD0DWL/llvK1In9jt4zCoLBYDAYDH2DePo5MwrJ\ndv0hFPwTnjBRDAaDwWAw9A3+hPgOHRPl25iK+FhsiuR4OARx0Lwf+J1REAwGg8Fg6AMEnvNklL78\nHMRRca/E5hVINNIVwI2B56z6/3ExLRI31pXNAAAAAElFTkSuQmCC\n", | |
131 | "prompt_number": 6, |
|
144 | "prompt_number": 6, | |
132 | "text": [ |
|
145 | "text": [ | |
133 |
"<IPython.core.display.Image at 0x |
|
146 | "<IPython.core.display.Image at 0x111275490>" | |
134 | ] |
|
147 | ] | |
135 | } |
|
148 | } | |
136 | ], |
|
149 | ], | |
137 | "prompt_number": 6 |
|
150 | "prompt_number": 6 | |
138 | } |
|
151 | } | |
139 | ], |
|
152 | ], | |
140 | "metadata": {} |
|
153 | "metadata": {} | |
141 | } |
|
154 | } | |
142 | ] |
|
155 | ] | |
143 | } No newline at end of file |
|
156 | } |
@@ -1,112 +1,120 b'' | |||||
1 | """Test Notebook signing""" |
|
1 | """Test Notebook signing""" | |
2 | #----------------------------------------------------------------------------- |
|
2 | #----------------------------------------------------------------------------- | |
3 | # Copyright (C) 2014, The IPython Development Team |
|
3 | # Copyright (C) 2014, The IPython Development Team | |
4 | # |
|
4 | # | |
5 | # Distributed under the terms of the BSD License. The full license is in |
|
5 | # Distributed under the terms of the BSD License. The full license is in | |
6 | # the file COPYING, distributed as part of this software. |
|
6 | # the file COPYING, distributed as part of this software. | |
7 | #----------------------------------------------------------------------------- |
|
7 | #----------------------------------------------------------------------------- | |
8 |
|
8 | |||
9 | #----------------------------------------------------------------------------- |
|
9 | #----------------------------------------------------------------------------- | |
10 | # Imports |
|
10 | # Imports | |
11 | #----------------------------------------------------------------------------- |
|
11 | #----------------------------------------------------------------------------- | |
12 |
|
12 | |||
13 | from .. import sign |
|
13 | from .. import sign | |
14 | from .base import TestsBase |
|
14 | from .base import TestsBase | |
15 |
|
15 | |||
16 | from ..current import read |
|
16 | from ..current import read | |
17 | from IPython.core.getipython import get_ipython |
|
17 | from IPython.core.getipython import get_ipython | |
18 |
|
18 | |||
19 | #----------------------------------------------------------------------------- |
|
19 | #----------------------------------------------------------------------------- | |
20 | # Classes and functions |
|
20 | # Classes and functions | |
21 | #----------------------------------------------------------------------------- |
|
21 | #----------------------------------------------------------------------------- | |
22 |
|
22 | |||
23 | class TestNotary(TestsBase): |
|
23 | class TestNotary(TestsBase): | |
24 |
|
24 | |||
25 | def setUp(self): |
|
25 | def setUp(self): | |
26 | self.notary = sign.NotebookNotary( |
|
26 | self.notary = sign.NotebookNotary( | |
27 | secret=b'secret', |
|
27 | secret=b'secret', | |
28 | profile_dir=get_ipython().profile_dir |
|
28 | profile_dir=get_ipython().profile_dir | |
29 | ) |
|
29 | ) | |
30 | with self.fopen(u'test3.ipynb', u'r') as f: |
|
30 | with self.fopen(u'test3.ipynb', u'r') as f: | |
31 | self.nb = read(f, u'json') |
|
31 | self.nb = read(f, u'json') | |
32 |
|
32 | |||
33 | def test_algorithms(self): |
|
33 | def test_algorithms(self): | |
34 | last_sig = '' |
|
34 | last_sig = '' | |
35 | for algo in sign.algorithms: |
|
35 | for algo in sign.algorithms: | |
36 | self.notary.algorithm = algo |
|
36 | self.notary.algorithm = algo | |
37 | self.notary.sign(self.nb) |
|
37 | self.notary.sign(self.nb) | |
38 | sig = self.nb.metadata.signature |
|
38 | sig = self.nb.metadata.signature | |
39 | print(sig) |
|
39 | print(sig) | |
40 | self.assertEqual(sig[:len(self.notary.algorithm)+1], '%s:' % self.notary.algorithm) |
|
40 | self.assertEqual(sig[:len(self.notary.algorithm)+1], '%s:' % self.notary.algorithm) | |
41 | self.assertNotEqual(last_sig, sig) |
|
41 | self.assertNotEqual(last_sig, sig) | |
42 | last_sig = sig |
|
42 | last_sig = sig | |
43 |
|
43 | |||
44 | def test_sign_same(self): |
|
44 | def test_sign_same(self): | |
45 | """Multiple signatures of the same notebook are the same""" |
|
45 | """Multiple signatures of the same notebook are the same""" | |
46 | sig1 = self.notary.compute_signature(self.nb) |
|
46 | sig1 = self.notary.compute_signature(self.nb) | |
47 | sig2 = self.notary.compute_signature(self.nb) |
|
47 | sig2 = self.notary.compute_signature(self.nb) | |
48 | self.assertEqual(sig1, sig2) |
|
48 | self.assertEqual(sig1, sig2) | |
49 |
|
49 | |||
50 | def test_change_secret(self): |
|
50 | def test_change_secret(self): | |
51 | """Changing the secret changes the signature""" |
|
51 | """Changing the secret changes the signature""" | |
52 | sig1 = self.notary.compute_signature(self.nb) |
|
52 | sig1 = self.notary.compute_signature(self.nb) | |
53 | self.notary.secret = b'different' |
|
53 | self.notary.secret = b'different' | |
54 | sig2 = self.notary.compute_signature(self.nb) |
|
54 | sig2 = self.notary.compute_signature(self.nb) | |
55 | self.assertNotEqual(sig1, sig2) |
|
55 | self.assertNotEqual(sig1, sig2) | |
56 |
|
56 | |||
57 | def test_sign(self): |
|
57 | def test_sign(self): | |
58 | self.notary.sign(self.nb) |
|
58 | self.notary.sign(self.nb) | |
59 | sig = self.nb.metadata.signature |
|
59 | sig = self.nb.metadata.signature | |
60 | self.assertEqual(sig[:len(self.notary.algorithm)+1], '%s:' % self.notary.algorithm) |
|
60 | self.assertEqual(sig[:len(self.notary.algorithm)+1], '%s:' % self.notary.algorithm) | |
61 |
|
61 | |||
62 | def test_check_signature(self): |
|
62 | def test_check_signature(self): | |
63 | nb = self.nb |
|
63 | nb = self.nb | |
64 | md = nb.metadata |
|
64 | md = nb.metadata | |
65 | notary = self.notary |
|
65 | notary = self.notary | |
66 | check_signature = notary.check_signature |
|
66 | check_signature = notary.check_signature | |
67 | # no signature: |
|
67 | # no signature: | |
68 | md.pop('signature', None) |
|
68 | md.pop('signature', None) | |
69 | self.assertFalse(check_signature(nb)) |
|
69 | self.assertFalse(check_signature(nb)) | |
70 | # hash only, no algo |
|
70 | # hash only, no algo | |
71 | md.signature = notary.compute_signature(nb) |
|
71 | md.signature = notary.compute_signature(nb) | |
72 | self.assertFalse(check_signature(nb)) |
|
72 | self.assertFalse(check_signature(nb)) | |
73 | # proper signature, algo mismatch |
|
73 | # proper signature, algo mismatch | |
74 | notary.algorithm = 'sha224' |
|
74 | notary.algorithm = 'sha224' | |
75 | notary.sign(nb) |
|
75 | notary.sign(nb) | |
76 | notary.algorithm = 'sha256' |
|
76 | notary.algorithm = 'sha256' | |
77 | self.assertFalse(check_signature(nb)) |
|
77 | self.assertFalse(check_signature(nb)) | |
78 | # check correctly signed notebook |
|
78 | # check correctly signed notebook | |
79 | notary.sign(nb) |
|
79 | notary.sign(nb) | |
80 | self.assertTrue(check_signature(nb)) |
|
80 | self.assertTrue(check_signature(nb)) | |
81 |
|
81 | |||
82 | def test_mark_cells_untrusted(self): |
|
82 | def test_mark_cells_untrusted(self): | |
83 | cells = self.nb.worksheets[0].cells |
|
83 | cells = self.nb.worksheets[0].cells | |
84 | self.notary.mark_cells(self.nb, False) |
|
84 | self.notary.mark_cells(self.nb, False) | |
85 | for cell in cells: |
|
85 | for cell in cells: | |
86 | if cell.cell_type == 'code': |
|
86 | if cell.cell_type == 'code': | |
87 | self.assertIn('trusted', cell) |
|
87 | self.assertIn('trusted', cell) | |
88 | self.assertFalse(cell.trusted) |
|
88 | self.assertFalse(cell.trusted) | |
89 | else: |
|
89 | else: | |
90 | self.assertNotIn('trusted', cell) |
|
90 | self.assertNotIn('trusted', cell) | |
91 |
|
91 | |||
92 | def test_mark_cells_trusted(self): |
|
92 | def test_mark_cells_trusted(self): | |
93 | cells = self.nb.worksheets[0].cells |
|
93 | cells = self.nb.worksheets[0].cells | |
94 | self.notary.mark_cells(self.nb, True) |
|
94 | self.notary.mark_cells(self.nb, True) | |
95 | for cell in cells: |
|
95 | for cell in cells: | |
96 | if cell.cell_type == 'code': |
|
96 | if cell.cell_type == 'code': | |
97 | self.assertIn('trusted', cell) |
|
97 | self.assertIn('trusted', cell) | |
98 | self.assertTrue(cell.trusted) |
|
98 | self.assertTrue(cell.trusted) | |
99 | else: |
|
99 | else: | |
100 | self.assertNotIn('trusted', cell) |
|
100 | self.assertNotIn('trusted', cell) | |
101 |
|
101 | |||
102 | def test_check_cells(self): |
|
102 | def test_check_cells(self): | |
103 | nb = self.nb |
|
103 | nb = self.nb | |
104 | self.notary.mark_cells(nb, True) |
|
104 | self.notary.mark_cells(nb, True) | |
105 | self.assertTrue(self.notary.check_cells(nb)) |
|
105 | self.assertTrue(self.notary.check_cells(nb)) | |
106 | for cell in nb.worksheets[0].cells: |
|
106 | for cell in nb.worksheets[0].cells: | |
107 | self.assertNotIn('trusted', cell) |
|
107 | self.assertNotIn('trusted', cell) | |
108 | self.notary.mark_cells(nb, False) |
|
108 | self.notary.mark_cells(nb, False) | |
109 | self.assertFalse(self.notary.check_cells(nb)) |
|
109 | self.assertFalse(self.notary.check_cells(nb)) | |
110 | for cell in nb.worksheets[0].cells: |
|
110 | for cell in nb.worksheets[0].cells: | |
111 | self.assertNotIn('trusted', cell) |
|
111 | self.assertNotIn('trusted', cell) | |
|
112 | ||||
|
113 | def test_trust_no_output(self): | |||
|
114 | nb = self.nb | |||
|
115 | self.notary.mark_cells(nb, False) | |||
|
116 | for cell in nb.worksheets[0].cells: | |||
|
117 | if cell.cell_type == 'code': | |||
|
118 | cell.outputs = [] | |||
|
119 | self.assertTrue(self.notary.check_cells(nb)) | |||
112 |
|
120 |
General Comments 0
You need to be logged in to leave comments.
Login now