##// END OF EJS Templates
Merge pull request #6915 from minrk/contents-no-0...
Thomas Kluyver -
r18835:b7c85f43 merge
parent child Browse files
Show More
@@ -1,373 +1,380
1 1 """A base class for contents managers."""
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 fnmatch import fnmatch
7 7 import itertools
8 8 import json
9 9 import os
10 import re
10 11
11 12 from tornado.web import HTTPError
12 13
13 14 from IPython.config.configurable import LoggingConfigurable
14 15 from IPython.nbformat import sign, validate, ValidationError
15 16 from IPython.nbformat.v4 import new_notebook
16 17 from IPython.utils.traitlets import Instance, Unicode, List
17 18
19 copy_pat = re.compile(r'\-Copy\d*\.')
18 20
19 21 class ContentsManager(LoggingConfigurable):
20 22 """Base class for serving files and directories.
21 23
22 24 This serves any text or binary file,
23 25 as well as directories,
24 26 with special handling for JSON notebook documents.
25 27
26 28 Most APIs take a path argument,
27 29 which is always an API-style unicode path,
28 30 and always refers to a directory.
29 31
30 32 - unicode, not url-escaped
31 33 - '/'-separated
32 34 - leading and trailing '/' will be stripped
33 35 - if unspecified, path defaults to '',
34 36 indicating the root path.
35 37
36 38 """
37 39
38 40 notary = Instance(sign.NotebookNotary)
39 41 def _notary_default(self):
40 42 return sign.NotebookNotary(parent=self)
41 43
42 44 hide_globs = List(Unicode, [
43 45 u'__pycache__', '*.pyc', '*.pyo',
44 46 '.DS_Store', '*.so', '*.dylib', '*~',
45 47 ], config=True, help="""
46 48 Glob patterns to hide in file and directory listings.
47 49 """)
48 50
49 51 untitled_notebook = Unicode("Untitled", config=True,
50 52 help="The base name used when creating untitled notebooks."
51 53 )
52 54
53 55 untitled_file = Unicode("untitled", config=True,
54 56 help="The base name used when creating untitled files."
55 57 )
56 58
57 59 untitled_directory = Unicode("Untitled Folder", config=True,
58 60 help="The base name used when creating untitled directories."
59 61 )
60 62
61 63 # ContentsManager API part 1: methods that must be
62 64 # implemented in subclasses.
63 65
64 66 def dir_exists(self, path):
65 67 """Does the API-style path (directory) actually exist?
66 68
67 69 Like os.path.isdir
68 70
69 71 Override this method in subclasses.
70 72
71 73 Parameters
72 74 ----------
73 75 path : string
74 76 The path to check
75 77
76 78 Returns
77 79 -------
78 80 exists : bool
79 81 Whether the path does indeed exist.
80 82 """
81 83 raise NotImplementedError
82 84
83 85 def is_hidden(self, path):
84 86 """Does the API style path correspond to a hidden directory or file?
85 87
86 88 Parameters
87 89 ----------
88 90 path : string
89 91 The path to check. This is an API path (`/` separated,
90 92 relative to root dir).
91 93
92 94 Returns
93 95 -------
94 96 hidden : bool
95 97 Whether the path is hidden.
96 98
97 99 """
98 100 raise NotImplementedError
99 101
100 102 def file_exists(self, path=''):
101 103 """Does a file exist at the given path?
102 104
103 105 Like os.path.isfile
104 106
105 107 Override this method in subclasses.
106 108
107 109 Parameters
108 110 ----------
109 111 name : string
110 112 The name of the file you are checking.
111 113 path : string
112 114 The relative path to the file's directory (with '/' as separator)
113 115
114 116 Returns
115 117 -------
116 118 exists : bool
117 119 Whether the file exists.
118 120 """
119 121 raise NotImplementedError('must be implemented in a subclass')
120 122
121 123 def exists(self, path):
122 124 """Does a file or directory exist at the given path?
123 125
124 126 Like os.path.exists
125 127
126 128 Parameters
127 129 ----------
128 130 path : string
129 131 The relative path to the file's directory (with '/' as separator)
130 132
131 133 Returns
132 134 -------
133 135 exists : bool
134 136 Whether the target exists.
135 137 """
136 138 return self.file_exists(path) or self.dir_exists(path)
137 139
138 140 def get(self, path, content=True, type_=None, format=None):
139 141 """Get the model of a file or directory with or without content."""
140 142 raise NotImplementedError('must be implemented in a subclass')
141 143
142 144 def save(self, model, path):
143 145 """Save the file or directory and return the model with no content."""
144 146 raise NotImplementedError('must be implemented in a subclass')
145 147
146 148 def update(self, model, path):
147 149 """Update the file or directory and return the model with no content.
148 150
149 151 For use in PATCH requests, to enable renaming a file without
150 152 re-uploading its contents. Only used for renaming at the moment.
151 153 """
152 154 raise NotImplementedError('must be implemented in a subclass')
153 155
154 156 def delete(self, path):
155 157 """Delete file or directory by path."""
156 158 raise NotImplementedError('must be implemented in a subclass')
157 159
158 160 def create_checkpoint(self, path):
159 161 """Create a checkpoint of the current state of a file
160 162
161 163 Returns a checkpoint_id for the new checkpoint.
162 164 """
163 165 raise NotImplementedError("must be implemented in a subclass")
164 166
165 167 def list_checkpoints(self, path):
166 168 """Return a list of checkpoints for a given file"""
167 169 return []
168 170
169 171 def restore_checkpoint(self, checkpoint_id, path):
170 172 """Restore a file from one of its checkpoints"""
171 173 raise NotImplementedError("must be implemented in a subclass")
172 174
173 175 def delete_checkpoint(self, checkpoint_id, path):
174 176 """delete a checkpoint for a file"""
175 177 raise NotImplementedError("must be implemented in a subclass")
176 178
177 179 # ContentsManager API part 2: methods that have useable default
178 180 # implementations, but can be overridden in subclasses.
179 181
180 182 def info_string(self):
181 183 return "Serving contents"
182 184
183 185 def get_kernel_path(self, path, model=None):
184 186 """Return the API path for the kernel
185 187
186 188 KernelManagers can turn this value into a filesystem path,
187 189 or ignore it altogether.
188 190 """
189 191 return path
190 192
191 def increment_filename(self, filename, path=''):
193 def increment_filename(self, filename, path='', insert=''):
192 194 """Increment a filename until it is unique.
193 195
194 196 Parameters
195 197 ----------
196 198 filename : unicode
197 199 The name of a file, including extension
198 200 path : unicode
199 201 The API path of the target's directory
200 202
201 203 Returns
202 204 -------
203 205 name : unicode
204 206 A filename that is unique, based on the input filename.
205 207 """
206 208 path = path.strip('/')
207 209 basename, ext = os.path.splitext(filename)
208 210 for i in itertools.count():
209 name = u'{basename}{i}{ext}'.format(basename=basename, i=i,
210 ext=ext)
211 if i:
212 insert_i = '{}{}'.format(insert, i)
213 else:
214 insert_i = ''
215 name = u'{basename}{insert}{ext}'.format(basename=basename,
216 insert=insert_i, ext=ext)
211 217 if not self.exists(u'{}/{}'.format(path, name)):
212 218 break
213 219 return name
214 220
215 221 def validate_notebook_model(self, model):
216 222 """Add failed-validation message to model"""
217 223 try:
218 224 validate(model['content'])
219 225 except ValidationError as e:
220 226 model['message'] = u'Notebook Validation failed: {}:\n{}'.format(
221 227 e.message, json.dumps(e.instance, indent=1, default=lambda obj: '<UNKNOWN>'),
222 228 )
223 229 return model
224 230
225 231 def new_untitled(self, path='', type='', ext=''):
226 232 """Create a new untitled file or directory in path
227 233
228 234 path must be a directory
229 235
230 236 File extension can be specified.
231 237
232 238 Use `new` to create files with a fully specified path (including filename).
233 239 """
234 240 path = path.strip('/')
235 241 if not self.dir_exists(path):
236 242 raise HTTPError(404, 'No such directory: %s' % path)
237 243
238 244 model = {}
239 245 if type:
240 246 model['type'] = type
241 247
242 248 if ext == '.ipynb':
243 249 model.setdefault('type', 'notebook')
244 250 else:
245 251 model.setdefault('type', 'file')
246 252
253 insert = ''
247 254 if model['type'] == 'directory':
248 255 untitled = self.untitled_directory
256 insert = ' '
249 257 elif model['type'] == 'notebook':
250 258 untitled = self.untitled_notebook
251 259 ext = '.ipynb'
252 260 elif model['type'] == 'file':
253 261 untitled = self.untitled_file
254 262 else:
255 263 raise HTTPError(400, "Unexpected model type: %r" % model['type'])
256 264
257 name = self.increment_filename(untitled + ext, path)
265 name = self.increment_filename(untitled + ext, path, insert=insert)
258 266 path = u'{0}/{1}'.format(path, name)
259 267 return self.new(model, path)
260 268
261 269 def new(self, model=None, path=''):
262 270 """Create a new file or directory and return its model with no content.
263 271
264 272 To create a new untitled entity in a directory, use `new_untitled`.
265 273 """
266 274 path = path.strip('/')
267 275 if model is None:
268 276 model = {}
269 277
270 278 if path.endswith('.ipynb'):
271 279 model.setdefault('type', 'notebook')
272 280 else:
273 281 model.setdefault('type', 'file')
274 282
275 283 # no content, not a directory, so fill out new-file model
276 284 if 'content' not in model and model['type'] != 'directory':
277 285 if model['type'] == 'notebook':
278 286 model['content'] = new_notebook()
279 287 model['format'] = 'json'
280 288 else:
281 289 model['content'] = ''
282 290 model['type'] = 'file'
283 291 model['format'] = 'text'
284 292
285 293 model = self.save(model, path)
286 294 return model
287 295
288 296 def copy(self, from_path, to_path=None):
289 297 """Copy an existing file and return its new model.
290 298
291 299 If to_path not specified, it will be the parent directory of from_path.
292 300 If to_path is a directory, filename will increment `from_path-Copy#.ext`.
293 301
294 302 from_path must be a full path to a file.
295 303 """
296 304 path = from_path.strip('/')
297 305 if '/' in path:
298 306 from_dir, from_name = path.rsplit('/', 1)
299 307 else:
300 308 from_dir = ''
301 309 from_name = path
302 310
303 311 model = self.get(path)
304 312 model.pop('path', None)
305 313 model.pop('name', None)
306 314 if model['type'] == 'directory':
307 315 raise HTTPError(400, "Can't copy directories")
308 316
309 317 if not to_path:
310 318 to_path = from_dir
311 319 if self.dir_exists(to_path):
312 base, ext = os.path.splitext(from_name)
313 copy_name = u'{0}-Copy{1}'.format(base, ext)
314 to_name = self.increment_filename(copy_name, to_path)
320 name = copy_pat.sub(u'.', from_name)
321 to_name = self.increment_filename(name, to_path, insert='-Copy')
315 322 to_path = u'{0}/{1}'.format(to_path, to_name)
316 323
317 324 model = self.save(model, to_path)
318 325 return model
319 326
320 327 def log_info(self):
321 328 self.log.info(self.info_string())
322 329
323 330 def trust_notebook(self, path):
324 331 """Explicitly trust a notebook
325 332
326 333 Parameters
327 334 ----------
328 335 path : string
329 336 The path of a notebook
330 337 """
331 338 model = self.get(path)
332 339 nb = model['content']
333 340 self.log.warn("Trusting notebook %s", path)
334 341 self.notary.mark_cells(nb, True)
335 342 self.save(model, path)
336 343
337 344 def check_and_sign(self, nb, path=''):
338 345 """Check for trusted cells, and sign the notebook.
339 346
340 347 Called as a part of saving notebooks.
341 348
342 349 Parameters
343 350 ----------
344 351 nb : dict
345 352 The notebook dict
346 353 path : string
347 354 The notebook's path (for logging)
348 355 """
349 356 if self.notary.check_cells(nb):
350 357 self.notary.sign(nb)
351 358 else:
352 359 self.log.warn("Saving untrusted notebook %s", path)
353 360
354 361 def mark_trusted_cells(self, nb, path=''):
355 362 """Mark cells as trusted if the notebook signature matches.
356 363
357 364 Called as a part of loading notebooks.
358 365
359 366 Parameters
360 367 ----------
361 368 nb : dict
362 369 The notebook object (in current nbformat)
363 370 path : string
364 371 The notebook's path (for logging)
365 372 """
366 373 trusted = self.notary.check_signature(nb)
367 374 if not trusted:
368 375 self.log.warn("Notebook %s is not trusted", path)
369 376 self.notary.mark_cells(nb, trusted)
370 377
371 378 def should_list(self, name):
372 379 """Should this file/directory name be displayed in a listing?"""
373 380 return not any(fnmatch(name, glob) for glob in self.hide_globs)
@@ -1,511 +1,521
1 1 # coding: utf-8
2 2 """Test the contents webservice API."""
3 3
4 4 import base64
5 5 import io
6 6 import json
7 7 import os
8 8 import shutil
9 9 from unicodedata import normalize
10 10
11 11 pjoin = os.path.join
12 12
13 13 import requests
14 14
15 15 from IPython.html.utils import url_path_join, url_escape
16 16 from IPython.html.tests.launchnotebook import NotebookTestBase, assert_http_error
17 17 from IPython.nbformat import read, write, from_dict
18 18 from IPython.nbformat.v4 import (
19 19 new_notebook, new_markdown_cell,
20 20 )
21 21 from IPython.nbformat import v2
22 22 from IPython.utils import py3compat
23 23 from IPython.utils.data import uniq_stable
24 24
25 25
26 26 def notebooks_only(dir_model):
27 27 return [nb for nb in dir_model['content'] if nb['type']=='notebook']
28 28
29 29 def dirs_only(dir_model):
30 30 return [x for x in dir_model['content'] if x['type']=='directory']
31 31
32 32
33 33 class API(object):
34 34 """Wrapper for contents API calls."""
35 35 def __init__(self, base_url):
36 36 self.base_url = base_url
37 37
38 38 def _req(self, verb, path, body=None, params=None):
39 39 response = requests.request(verb,
40 40 url_path_join(self.base_url, 'api/contents', path),
41 41 data=body, params=params,
42 42 )
43 43 response.raise_for_status()
44 44 return response
45 45
46 46 def list(self, path='/'):
47 47 return self._req('GET', path)
48 48
49 49 def read(self, path, type_=None, format=None):
50 50 params = {}
51 51 if type_ is not None:
52 52 params['type'] = type_
53 53 if format is not None:
54 54 params['format'] = format
55 55 return self._req('GET', path, params=params)
56 56
57 57 def create_untitled(self, path='/', ext='.ipynb'):
58 58 body = None
59 59 if ext:
60 60 body = json.dumps({'ext': ext})
61 61 return self._req('POST', path, body)
62 62
63 63 def mkdir_untitled(self, path='/'):
64 64 return self._req('POST', path, json.dumps({'type': 'directory'}))
65 65
66 66 def copy(self, copy_from, path='/'):
67 67 body = json.dumps({'copy_from':copy_from})
68 68 return self._req('POST', path, body)
69 69
70 70 def create(self, path='/'):
71 71 return self._req('PUT', path)
72 72
73 73 def upload(self, path, body):
74 74 return self._req('PUT', path, body)
75 75
76 76 def mkdir_untitled(self, path='/'):
77 77 return self._req('POST', path, json.dumps({'type': 'directory'}))
78 78
79 79 def mkdir(self, path='/'):
80 80 return self._req('PUT', path, json.dumps({'type': 'directory'}))
81 81
82 82 def copy_put(self, copy_from, path='/'):
83 83 body = json.dumps({'copy_from':copy_from})
84 84 return self._req('PUT', path, body)
85 85
86 86 def save(self, path, body):
87 87 return self._req('PUT', path, body)
88 88
89 89 def delete(self, path='/'):
90 90 return self._req('DELETE', path)
91 91
92 92 def rename(self, path, new_path):
93 93 body = json.dumps({'path': new_path})
94 94 return self._req('PATCH', path, body)
95 95
96 96 def get_checkpoints(self, path):
97 97 return self._req('GET', url_path_join(path, 'checkpoints'))
98 98
99 99 def new_checkpoint(self, path):
100 100 return self._req('POST', url_path_join(path, 'checkpoints'))
101 101
102 102 def restore_checkpoint(self, path, checkpoint_id):
103 103 return self._req('POST', url_path_join(path, 'checkpoints', checkpoint_id))
104 104
105 105 def delete_checkpoint(self, path, checkpoint_id):
106 106 return self._req('DELETE', url_path_join(path, 'checkpoints', checkpoint_id))
107 107
108 108 class APITest(NotebookTestBase):
109 109 """Test the kernels web service API"""
110 110 dirs_nbs = [('', 'inroot'),
111 111 ('Directory with spaces in', 'inspace'),
112 112 (u'unicodé', 'innonascii'),
113 113 ('foo', 'a'),
114 114 ('foo', 'b'),
115 115 ('foo', 'name with spaces'),
116 116 ('foo', u'unicodé'),
117 117 ('foo/bar', 'baz'),
118 118 ('ordering', 'A'),
119 119 ('ordering', 'b'),
120 120 ('ordering', 'C'),
121 121 (u'å b', u'ç d'),
122 122 ]
123 123 hidden_dirs = ['.hidden', '__pycache__']
124 124
125 125 dirs = uniq_stable([py3compat.cast_unicode(d) for (d,n) in dirs_nbs])
126 126 del dirs[0] # remove ''
127 127 top_level_dirs = {normalize('NFC', d.split('/')[0]) for d in dirs}
128 128
129 129 @staticmethod
130 130 def _blob_for_name(name):
131 131 return name.encode('utf-8') + b'\xFF'
132 132
133 133 @staticmethod
134 134 def _txt_for_name(name):
135 135 return u'%s text file' % name
136 136
137 137 def setUp(self):
138 138 nbdir = self.notebook_dir.name
139 139 self.blob = os.urandom(100)
140 140 self.b64_blob = base64.encodestring(self.blob).decode('ascii')
141 141
142 142 for d in (self.dirs + self.hidden_dirs):
143 143 d.replace('/', os.sep)
144 144 if not os.path.isdir(pjoin(nbdir, d)):
145 145 os.mkdir(pjoin(nbdir, d))
146 146
147 147 for d, name in self.dirs_nbs:
148 148 d = d.replace('/', os.sep)
149 149 # create a notebook
150 150 with io.open(pjoin(nbdir, d, '%s.ipynb' % name), 'w',
151 151 encoding='utf-8') as f:
152 152 nb = new_notebook()
153 153 write(nb, f, version=4)
154 154
155 155 # create a text file
156 156 with io.open(pjoin(nbdir, d, '%s.txt' % name), 'w',
157 157 encoding='utf-8') as f:
158 158 f.write(self._txt_for_name(name))
159 159
160 160 # create a binary file
161 161 with io.open(pjoin(nbdir, d, '%s.blob' % name), 'wb') as f:
162 162 f.write(self._blob_for_name(name))
163 163
164 164 self.api = API(self.base_url())
165 165
166 166 def tearDown(self):
167 167 nbdir = self.notebook_dir.name
168 168
169 169 for dname in (list(self.top_level_dirs) + self.hidden_dirs):
170 170 shutil.rmtree(pjoin(nbdir, dname), ignore_errors=True)
171 171
172 172 if os.path.isfile(pjoin(nbdir, 'inroot.ipynb')):
173 173 os.unlink(pjoin(nbdir, 'inroot.ipynb'))
174 174
175 175 def test_list_notebooks(self):
176 176 nbs = notebooks_only(self.api.list().json())
177 177 self.assertEqual(len(nbs), 1)
178 178 self.assertEqual(nbs[0]['name'], 'inroot.ipynb')
179 179
180 180 nbs = notebooks_only(self.api.list('/Directory with spaces in/').json())
181 181 self.assertEqual(len(nbs), 1)
182 182 self.assertEqual(nbs[0]['name'], 'inspace.ipynb')
183 183
184 184 nbs = notebooks_only(self.api.list(u'/unicodé/').json())
185 185 self.assertEqual(len(nbs), 1)
186 186 self.assertEqual(nbs[0]['name'], 'innonascii.ipynb')
187 187 self.assertEqual(nbs[0]['path'], u'unicodé/innonascii.ipynb')
188 188
189 189 nbs = notebooks_only(self.api.list('/foo/bar/').json())
190 190 self.assertEqual(len(nbs), 1)
191 191 self.assertEqual(nbs[0]['name'], 'baz.ipynb')
192 192 self.assertEqual(nbs[0]['path'], 'foo/bar/baz.ipynb')
193 193
194 194 nbs = notebooks_only(self.api.list('foo').json())
195 195 self.assertEqual(len(nbs), 4)
196 196 nbnames = { normalize('NFC', n['name']) for n in nbs }
197 197 expected = [ u'a.ipynb', u'b.ipynb', u'name with spaces.ipynb', u'unicodé.ipynb']
198 198 expected = { normalize('NFC', name) for name in expected }
199 199 self.assertEqual(nbnames, expected)
200 200
201 201 nbs = notebooks_only(self.api.list('ordering').json())
202 202 nbnames = [n['name'] for n in nbs]
203 203 expected = ['A.ipynb', 'b.ipynb', 'C.ipynb']
204 204 self.assertEqual(nbnames, expected)
205 205
206 206 def test_list_dirs(self):
207 207 print(self.api.list().json())
208 208 dirs = dirs_only(self.api.list().json())
209 209 dir_names = {normalize('NFC', d['name']) for d in dirs}
210 210 print(dir_names)
211 211 print(self.top_level_dirs)
212 212 self.assertEqual(dir_names, self.top_level_dirs) # Excluding hidden dirs
213 213
214 214 def test_list_nonexistant_dir(self):
215 215 with assert_http_error(404):
216 216 self.api.list('nonexistant')
217 217
218 218 def test_get_nb_contents(self):
219 219 for d, name in self.dirs_nbs:
220 220 path = url_path_join(d, name + '.ipynb')
221 221 nb = self.api.read(path).json()
222 222 self.assertEqual(nb['name'], u'%s.ipynb' % name)
223 223 self.assertEqual(nb['path'], path)
224 224 self.assertEqual(nb['type'], 'notebook')
225 225 self.assertIn('content', nb)
226 226 self.assertEqual(nb['format'], 'json')
227 227 self.assertIn('content', nb)
228 228 self.assertIn('metadata', nb['content'])
229 229 self.assertIsInstance(nb['content']['metadata'], dict)
230 230
231 231 def test_get_contents_no_such_file(self):
232 232 # Name that doesn't exist - should be a 404
233 233 with assert_http_error(404):
234 234 self.api.read('foo/q.ipynb')
235 235
236 236 def test_get_text_file_contents(self):
237 237 for d, name in self.dirs_nbs:
238 238 path = url_path_join(d, name + '.txt')
239 239 model = self.api.read(path).json()
240 240 self.assertEqual(model['name'], u'%s.txt' % name)
241 241 self.assertEqual(model['path'], path)
242 242 self.assertIn('content', model)
243 243 self.assertEqual(model['format'], 'text')
244 244 self.assertEqual(model['type'], 'file')
245 245 self.assertEqual(model['content'], self._txt_for_name(name))
246 246
247 247 # Name that doesn't exist - should be a 404
248 248 with assert_http_error(404):
249 249 self.api.read('foo/q.txt')
250 250
251 251 # Specifying format=text should fail on a non-UTF-8 file
252 252 with assert_http_error(400):
253 253 self.api.read('foo/bar/baz.blob', type_='file', format='text')
254 254
255 255 def test_get_binary_file_contents(self):
256 256 for d, name in self.dirs_nbs:
257 257 path = url_path_join(d, name + '.blob')
258 258 model = self.api.read(path).json()
259 259 self.assertEqual(model['name'], u'%s.blob' % name)
260 260 self.assertEqual(model['path'], path)
261 261 self.assertIn('content', model)
262 262 self.assertEqual(model['format'], 'base64')
263 263 self.assertEqual(model['type'], 'file')
264 264 b64_data = base64.encodestring(self._blob_for_name(name)).decode('ascii')
265 265 self.assertEqual(model['content'], b64_data)
266 266
267 267 # Name that doesn't exist - should be a 404
268 268 with assert_http_error(404):
269 269 self.api.read('foo/q.txt')
270 270
271 271 def test_get_bad_type(self):
272 272 with assert_http_error(400):
273 273 self.api.read(u'unicodé', type_='file') # this is a directory
274 274
275 275 with assert_http_error(400):
276 276 self.api.read(u'unicodé/innonascii.ipynb', type_='directory')
277 277
278 278 def _check_created(self, resp, path, type='notebook'):
279 279 self.assertEqual(resp.status_code, 201)
280 280 location_header = py3compat.str_to_unicode(resp.headers['Location'])
281 281 self.assertEqual(location_header, url_escape(url_path_join(u'/api/contents', path)))
282 282 rjson = resp.json()
283 283 self.assertEqual(rjson['name'], path.rsplit('/', 1)[-1])
284 284 self.assertEqual(rjson['path'], path)
285 285 self.assertEqual(rjson['type'], type)
286 286 isright = os.path.isdir if type == 'directory' else os.path.isfile
287 287 assert isright(pjoin(
288 288 self.notebook_dir.name,
289 289 path.replace('/', os.sep),
290 290 ))
291 291
292 292 def test_create_untitled(self):
293 293 resp = self.api.create_untitled(path=u'å b')
294 self._check_created(resp, u'å b/Untitled0.ipynb')
294 self._check_created(resp, u'å b/Untitled.ipynb')
295 295
296 296 # Second time
297 297 resp = self.api.create_untitled(path=u'å b')
298 298 self._check_created(resp, u'å b/Untitled1.ipynb')
299 299
300 300 # And two directories down
301 301 resp = self.api.create_untitled(path='foo/bar')
302 self._check_created(resp, 'foo/bar/Untitled0.ipynb')
302 self._check_created(resp, 'foo/bar/Untitled.ipynb')
303 303
304 304 def test_create_untitled_txt(self):
305 305 resp = self.api.create_untitled(path='foo/bar', ext='.txt')
306 self._check_created(resp, 'foo/bar/untitled0.txt', type='file')
306 self._check_created(resp, 'foo/bar/untitled.txt', type='file')
307 307
308 resp = self.api.read(path='foo/bar/untitled0.txt')
308 resp = self.api.read(path='foo/bar/untitled.txt')
309 309 model = resp.json()
310 310 self.assertEqual(model['type'], 'file')
311 311 self.assertEqual(model['format'], 'text')
312 312 self.assertEqual(model['content'], '')
313 313
314 314 def test_upload(self):
315 315 nb = new_notebook()
316 316 nbmodel = {'content': nb, 'type': 'notebook'}
317 317 path = u'å b/Upload tést.ipynb'
318 318 resp = self.api.upload(path, body=json.dumps(nbmodel))
319 319 self._check_created(resp, path)
320 320
321 321 def test_mkdir_untitled(self):
322 322 resp = self.api.mkdir_untitled(path=u'å b')
323 self._check_created(resp, u'å b/Untitled Folder0', type='directory')
323 self._check_created(resp, u'å b/Untitled Folder', type='directory')
324 324
325 325 # Second time
326 326 resp = self.api.mkdir_untitled(path=u'å b')
327 self._check_created(resp, u'å b/Untitled Folder1', type='directory')
327 self._check_created(resp, u'å b/Untitled Folder 1', type='directory')
328 328
329 329 # And two directories down
330 330 resp = self.api.mkdir_untitled(path='foo/bar')
331 self._check_created(resp, 'foo/bar/Untitled Folder0', type='directory')
331 self._check_created(resp, 'foo/bar/Untitled Folder', type='directory')
332 332
333 333 def test_mkdir(self):
334 334 path = u'å b/New ∂ir'
335 335 resp = self.api.mkdir(path)
336 336 self._check_created(resp, path, type='directory')
337 337
338 338 def test_mkdir_hidden_400(self):
339 339 with assert_http_error(400):
340 340 resp = self.api.mkdir(u'å b/.hidden')
341 341
342 342 def test_upload_txt(self):
343 343 body = u'ünicode téxt'
344 344 model = {
345 345 'content' : body,
346 346 'format' : 'text',
347 347 'type' : 'file',
348 348 }
349 349 path = u'å b/Upload tést.txt'
350 350 resp = self.api.upload(path, body=json.dumps(model))
351 351
352 352 # check roundtrip
353 353 resp = self.api.read(path)
354 354 model = resp.json()
355 355 self.assertEqual(model['type'], 'file')
356 356 self.assertEqual(model['format'], 'text')
357 357 self.assertEqual(model['content'], body)
358 358
359 359 def test_upload_b64(self):
360 360 body = b'\xFFblob'
361 361 b64body = base64.encodestring(body).decode('ascii')
362 362 model = {
363 363 'content' : b64body,
364 364 'format' : 'base64',
365 365 'type' : 'file',
366 366 }
367 367 path = u'å b/Upload tést.blob'
368 368 resp = self.api.upload(path, body=json.dumps(model))
369 369
370 370 # check roundtrip
371 371 resp = self.api.read(path)
372 372 model = resp.json()
373 373 self.assertEqual(model['type'], 'file')
374 374 self.assertEqual(model['path'], path)
375 375 self.assertEqual(model['format'], 'base64')
376 376 decoded = base64.decodestring(model['content'].encode('ascii'))
377 377 self.assertEqual(decoded, body)
378 378
379 379 def test_upload_v2(self):
380 380 nb = v2.new_notebook()
381 381 ws = v2.new_worksheet()
382 382 nb.worksheets.append(ws)
383 383 ws.cells.append(v2.new_code_cell(input='print("hi")'))
384 384 nbmodel = {'content': nb, 'type': 'notebook'}
385 385 path = u'å b/Upload tést.ipynb'
386 386 resp = self.api.upload(path, body=json.dumps(nbmodel))
387 387 self._check_created(resp, path)
388 388 resp = self.api.read(path)
389 389 data = resp.json()
390 390 self.assertEqual(data['content']['nbformat'], 4)
391 391
392 392 def test_copy(self):
393 resp = self.api.copy(u'å b/ç d.ipynb', u'unicodé')
394 self._check_created(resp, u'unicodé/ç d-Copy0.ipynb')
393 resp = self.api.copy(u'å b/ç d.ipynb', u'å b')
394 self._check_created(resp, u'å b/ç d-Copy1.ipynb')
395 395
396 396 resp = self.api.copy(u'å b/ç d.ipynb', u'å b')
397 self._check_created(resp, u'å b/ç d-Copy0.ipynb')
398
397 self._check_created(resp, u'å b/ç d-Copy2.ipynb')
398
399 def test_copy_copy(self):
400 resp = self.api.copy(u'å b/ç d.ipynb', u'å b')
401 self._check_created(resp, u'å b/ç d-Copy1.ipynb')
402
403 resp = self.api.copy(u'å b/ç d-Copy1.ipynb', u'å b')
404 self._check_created(resp, u'å b/ç d-Copy2.ipynb')
405
399 406 def test_copy_path(self):
400 407 resp = self.api.copy(u'foo/a.ipynb', u'å b')
401 self._check_created(resp, u'å b/a-Copy0.ipynb')
408 self._check_created(resp, u'å b/a.ipynb')
409
410 resp = self.api.copy(u'foo/a.ipynb', u'å b')
411 self._check_created(resp, u'å b/a-Copy1.ipynb')
402 412
403 413 def test_copy_put_400(self):
404 414 with assert_http_error(400):
405 415 resp = self.api.copy_put(u'å b/ç d.ipynb', u'å b/cøpy.ipynb')
406 416
407 417 def test_copy_dir_400(self):
408 418 # can't copy directories
409 419 with assert_http_error(400):
410 420 resp = self.api.copy(u'å b', u'foo')
411 421
412 422 def test_delete(self):
413 423 for d, name in self.dirs_nbs:
414 424 print('%r, %r' % (d, name))
415 425 resp = self.api.delete(url_path_join(d, name + '.ipynb'))
416 426 self.assertEqual(resp.status_code, 204)
417 427
418 428 for d in self.dirs + ['/']:
419 429 nbs = notebooks_only(self.api.list(d).json())
420 430 print('------')
421 431 print(d)
422 432 print(nbs)
423 433 self.assertEqual(nbs, [])
424 434
425 435 def test_delete_dirs(self):
426 436 # depth-first delete everything, so we don't try to delete empty directories
427 437 for name in sorted(self.dirs + ['/'], key=len, reverse=True):
428 438 listing = self.api.list(name).json()['content']
429 439 for model in listing:
430 440 self.api.delete(model['path'])
431 441 listing = self.api.list('/').json()['content']
432 442 self.assertEqual(listing, [])
433 443
434 444 def test_delete_non_empty_dir(self):
435 445 """delete non-empty dir raises 400"""
436 446 with assert_http_error(400):
437 447 self.api.delete(u'å b')
438 448
439 449 def test_rename(self):
440 450 resp = self.api.rename('foo/a.ipynb', 'foo/z.ipynb')
441 451 self.assertEqual(resp.headers['Location'].split('/')[-1], 'z.ipynb')
442 452 self.assertEqual(resp.json()['name'], 'z.ipynb')
443 453 self.assertEqual(resp.json()['path'], 'foo/z.ipynb')
444 454 assert os.path.isfile(pjoin(self.notebook_dir.name, 'foo', 'z.ipynb'))
445 455
446 456 nbs = notebooks_only(self.api.list('foo').json())
447 457 nbnames = set(n['name'] for n in nbs)
448 458 self.assertIn('z.ipynb', nbnames)
449 459 self.assertNotIn('a.ipynb', nbnames)
450 460
451 461 def test_rename_existing(self):
452 462 with assert_http_error(409):
453 463 self.api.rename('foo/a.ipynb', 'foo/b.ipynb')
454 464
455 465 def test_save(self):
456 466 resp = self.api.read('foo/a.ipynb')
457 467 nbcontent = json.loads(resp.text)['content']
458 468 nb = from_dict(nbcontent)
459 469 nb.cells.append(new_markdown_cell(u'Created by test ³'))
460 470
461 471 nbmodel= {'content': nb, 'type': 'notebook'}
462 472 resp = self.api.save('foo/a.ipynb', body=json.dumps(nbmodel))
463 473
464 474 nbfile = pjoin(self.notebook_dir.name, 'foo', 'a.ipynb')
465 475 with io.open(nbfile, 'r', encoding='utf-8') as f:
466 476 newnb = read(f, as_version=4)
467 477 self.assertEqual(newnb.cells[0].source,
468 478 u'Created by test ³')
469 479 nbcontent = self.api.read('foo/a.ipynb').json()['content']
470 480 newnb = from_dict(nbcontent)
471 481 self.assertEqual(newnb.cells[0].source,
472 482 u'Created by test ³')
473 483
474 484
475 485 def test_checkpoints(self):
476 486 resp = self.api.read('foo/a.ipynb')
477 487 r = self.api.new_checkpoint('foo/a.ipynb')
478 488 self.assertEqual(r.status_code, 201)
479 489 cp1 = r.json()
480 490 self.assertEqual(set(cp1), {'id', 'last_modified'})
481 491 self.assertEqual(r.headers['Location'].split('/')[-1], cp1['id'])
482 492
483 493 # Modify it
484 494 nbcontent = json.loads(resp.text)['content']
485 495 nb = from_dict(nbcontent)
486 496 hcell = new_markdown_cell('Created by test')
487 497 nb.cells.append(hcell)
488 498 # Save
489 499 nbmodel= {'content': nb, 'type': 'notebook'}
490 500 resp = self.api.save('foo/a.ipynb', body=json.dumps(nbmodel))
491 501
492 502 # List checkpoints
493 503 cps = self.api.get_checkpoints('foo/a.ipynb').json()
494 504 self.assertEqual(cps, [cp1])
495 505
496 506 nbcontent = self.api.read('foo/a.ipynb').json()['content']
497 507 nb = from_dict(nbcontent)
498 508 self.assertEqual(nb.cells[0].source, 'Created by test')
499 509
500 510 # Restore cp1
501 511 r = self.api.restore_checkpoint('foo/a.ipynb', cp1['id'])
502 512 self.assertEqual(r.status_code, 204)
503 513 nbcontent = self.api.read('foo/a.ipynb').json()['content']
504 514 nb = from_dict(nbcontent)
505 515 self.assertEqual(nb.cells, [])
506 516
507 517 # Delete cp1
508 518 r = self.api.delete_checkpoint('foo/a.ipynb', cp1['id'])
509 519 self.assertEqual(r.status_code, 204)
510 520 cps = self.api.get_checkpoints('foo/a.ipynb').json()
511 521 self.assertEqual(cps, [])
@@ -1,365 +1,369
1 1 # coding: utf-8
2 2 """Tests for the notebook manager."""
3 3 from __future__ import print_function
4 4
5 5 import logging
6 6 import os
7 7
8 8 from tornado.web import HTTPError
9 9 from unittest import TestCase
10 10 from tempfile import NamedTemporaryFile
11 11
12 12 from IPython.nbformat import v4 as nbformat
13 13
14 14 from IPython.utils.tempdir import TemporaryDirectory
15 15 from IPython.utils.traitlets import TraitError
16 16 from IPython.html.utils import url_path_join
17 17 from IPython.testing import decorators as dec
18 18
19 19 from ..filemanager import FileContentsManager
20 20 from ..manager import ContentsManager
21 21
22 22
23 23 class TestFileContentsManager(TestCase):
24 24
25 25 def test_root_dir(self):
26 26 with TemporaryDirectory() as td:
27 27 fm = FileContentsManager(root_dir=td)
28 28 self.assertEqual(fm.root_dir, td)
29 29
30 30 def test_missing_root_dir(self):
31 31 with TemporaryDirectory() as td:
32 32 root = os.path.join(td, 'notebook', 'dir', 'is', 'missing')
33 33 self.assertRaises(TraitError, FileContentsManager, root_dir=root)
34 34
35 35 def test_invalid_root_dir(self):
36 36 with NamedTemporaryFile() as tf:
37 37 self.assertRaises(TraitError, FileContentsManager, root_dir=tf.name)
38 38
39 39 def test_get_os_path(self):
40 40 # full filesystem path should be returned with correct operating system
41 41 # separators.
42 42 with TemporaryDirectory() as td:
43 43 root = td
44 44 fm = FileContentsManager(root_dir=root)
45 45 path = fm._get_os_path('/path/to/notebook/test.ipynb')
46 46 rel_path_list = '/path/to/notebook/test.ipynb'.split('/')
47 47 fs_path = os.path.join(fm.root_dir, *rel_path_list)
48 48 self.assertEqual(path, fs_path)
49 49
50 50 fm = FileContentsManager(root_dir=root)
51 51 path = fm._get_os_path('test.ipynb')
52 52 fs_path = os.path.join(fm.root_dir, 'test.ipynb')
53 53 self.assertEqual(path, fs_path)
54 54
55 55 fm = FileContentsManager(root_dir=root)
56 56 path = fm._get_os_path('////test.ipynb')
57 57 fs_path = os.path.join(fm.root_dir, 'test.ipynb')
58 58 self.assertEqual(path, fs_path)
59 59
60 60 def test_checkpoint_subdir(self):
61 61 subd = u'sub ∂ir'
62 62 cp_name = 'test-cp.ipynb'
63 63 with TemporaryDirectory() as td:
64 64 root = td
65 65 os.mkdir(os.path.join(td, subd))
66 66 fm = FileContentsManager(root_dir=root)
67 67 cp_dir = fm.get_checkpoint_path('cp', 'test.ipynb')
68 68 cp_subdir = fm.get_checkpoint_path('cp', '/%s/test.ipynb' % subd)
69 69 self.assertNotEqual(cp_dir, cp_subdir)
70 70 self.assertEqual(cp_dir, os.path.join(root, fm.checkpoint_dir, cp_name))
71 71 self.assertEqual(cp_subdir, os.path.join(root, subd, fm.checkpoint_dir, cp_name))
72 72
73 73
74 74 class TestContentsManager(TestCase):
75 75
76 76 def setUp(self):
77 77 self._temp_dir = TemporaryDirectory()
78 78 self.td = self._temp_dir.name
79 79 self.contents_manager = FileContentsManager(
80 80 root_dir=self.td,
81 81 log=logging.getLogger()
82 82 )
83 83
84 84 def tearDown(self):
85 85 self._temp_dir.cleanup()
86 86
87 87 def make_dir(self, abs_path, rel_path):
88 88 """make subdirectory, rel_path is the relative path
89 89 to that directory from the location where the server started"""
90 90 os_path = os.path.join(abs_path, rel_path)
91 91 try:
92 92 os.makedirs(os_path)
93 93 except OSError:
94 94 print("Directory already exists: %r" % os_path)
95 95 return os_path
96 96
97 97 def add_code_cell(self, nb):
98 98 output = nbformat.new_output("display_data", {'application/javascript': "alert('hi');"})
99 99 cell = nbformat.new_code_cell("print('hi')", outputs=[output])
100 100 nb.cells.append(cell)
101 101
102 102 def new_notebook(self):
103 103 cm = self.contents_manager
104 104 model = cm.new_untitled(type='notebook')
105 105 name = model['name']
106 106 path = model['path']
107 107
108 108 full_model = cm.get(path)
109 109 nb = full_model['content']
110 110 self.add_code_cell(nb)
111 111
112 112 cm.save(full_model, path)
113 113 return nb, name, path
114 114
115 115 def test_new_untitled(self):
116 116 cm = self.contents_manager
117 117 # Test in root directory
118 118 model = cm.new_untitled(type='notebook')
119 119 assert isinstance(model, dict)
120 120 self.assertIn('name', model)
121 121 self.assertIn('path', model)
122 122 self.assertIn('type', model)
123 123 self.assertEqual(model['type'], 'notebook')
124 self.assertEqual(model['name'], 'Untitled0.ipynb')
125 self.assertEqual(model['path'], 'Untitled0.ipynb')
124 self.assertEqual(model['name'], 'Untitled.ipynb')
125 self.assertEqual(model['path'], 'Untitled.ipynb')
126 126
127 127 # Test in sub-directory
128 128 model = cm.new_untitled(type='directory')
129 129 assert isinstance(model, dict)
130 130 self.assertIn('name', model)
131 131 self.assertIn('path', model)
132 132 self.assertIn('type', model)
133 133 self.assertEqual(model['type'], 'directory')
134 self.assertEqual(model['name'], 'Untitled Folder0')
135 self.assertEqual(model['path'], 'Untitled Folder0')
134 self.assertEqual(model['name'], 'Untitled Folder')
135 self.assertEqual(model['path'], 'Untitled Folder')
136 136 sub_dir = model['path']
137 137
138 138 model = cm.new_untitled(path=sub_dir)
139 139 assert isinstance(model, dict)
140 140 self.assertIn('name', model)
141 141 self.assertIn('path', model)
142 142 self.assertIn('type', model)
143 143 self.assertEqual(model['type'], 'file')
144 self.assertEqual(model['name'], 'untitled0')
145 self.assertEqual(model['path'], '%s/untitled0' % sub_dir)
144 self.assertEqual(model['name'], 'untitled')
145 self.assertEqual(model['path'], '%s/untitled' % sub_dir)
146 146
147 147 def test_get(self):
148 148 cm = self.contents_manager
149 149 # Create a notebook
150 150 model = cm.new_untitled(type='notebook')
151 151 name = model['name']
152 152 path = model['path']
153 153
154 154 # Check that we 'get' on the notebook we just created
155 155 model2 = cm.get(path)
156 156 assert isinstance(model2, dict)
157 157 self.assertIn('name', model2)
158 158 self.assertIn('path', model2)
159 159 self.assertEqual(model['name'], name)
160 160 self.assertEqual(model['path'], path)
161 161
162 162 nb_as_file = cm.get(path, content=True, type_='file')
163 163 self.assertEqual(nb_as_file['path'], path)
164 164 self.assertEqual(nb_as_file['type'], 'file')
165 165 self.assertEqual(nb_as_file['format'], 'text')
166 166 self.assertNotIsInstance(nb_as_file['content'], dict)
167 167
168 168 nb_as_bin_file = cm.get(path, content=True, type_='file', format='base64')
169 169 self.assertEqual(nb_as_bin_file['format'], 'base64')
170 170
171 171 # Test in sub-directory
172 172 sub_dir = '/foo/'
173 173 self.make_dir(cm.root_dir, 'foo')
174 174 model = cm.new_untitled(path=sub_dir, ext='.ipynb')
175 175 model2 = cm.get(sub_dir + name)
176 176 assert isinstance(model2, dict)
177 177 self.assertIn('name', model2)
178 178 self.assertIn('path', model2)
179 179 self.assertIn('content', model2)
180 self.assertEqual(model2['name'], 'Untitled0.ipynb')
180 self.assertEqual(model2['name'], 'Untitled.ipynb')
181 181 self.assertEqual(model2['path'], '{0}/{1}'.format(sub_dir.strip('/'), name))
182 182
183 183 # Test getting directory model
184 184 dirmodel = cm.get('foo')
185 185 self.assertEqual(dirmodel['type'], 'directory')
186 186
187 187 with self.assertRaises(HTTPError):
188 188 cm.get('foo', type_='file')
189 189
190 190
191 191 @dec.skip_win32
192 192 def test_bad_symlink(self):
193 193 cm = self.contents_manager
194 194 path = 'test bad symlink'
195 195 os_path = self.make_dir(cm.root_dir, path)
196 196
197 197 file_model = cm.new_untitled(path=path, ext='.txt')
198 198
199 199 # create a broken symlink
200 200 os.symlink("target", os.path.join(os_path, "bad symlink"))
201 201 model = cm.get(path)
202 202 self.assertEqual(model['content'], [file_model])
203 203
204 204 @dec.skip_win32
205 205 def test_good_symlink(self):
206 206 cm = self.contents_manager
207 207 parent = 'test good symlink'
208 208 name = 'good symlink'
209 209 path = '{0}/{1}'.format(parent, name)
210 210 os_path = self.make_dir(cm.root_dir, parent)
211 211
212 212 file_model = cm.new(path=parent + '/zfoo.txt')
213 213
214 214 # create a good symlink
215 215 os.symlink(file_model['name'], os.path.join(os_path, name))
216 216 symlink_model = cm.get(path, content=False)
217 217 dir_model = cm.get(parent)
218 218 self.assertEqual(
219 219 sorted(dir_model['content'], key=lambda x: x['name']),
220 220 [symlink_model, file_model],
221 221 )
222 222
223 223 def test_update(self):
224 224 cm = self.contents_manager
225 225 # Create a notebook
226 226 model = cm.new_untitled(type='notebook')
227 227 name = model['name']
228 228 path = model['path']
229 229
230 230 # Change the name in the model for rename
231 231 model['path'] = 'test.ipynb'
232 232 model = cm.update(model, path)
233 233 assert isinstance(model, dict)
234 234 self.assertIn('name', model)
235 235 self.assertIn('path', model)
236 236 self.assertEqual(model['name'], 'test.ipynb')
237 237
238 238 # Make sure the old name is gone
239 239 self.assertRaises(HTTPError, cm.get, path)
240 240
241 241 # Test in sub-directory
242 242 # Create a directory and notebook in that directory
243 243 sub_dir = '/foo/'
244 244 self.make_dir(cm.root_dir, 'foo')
245 245 model = cm.new_untitled(path=sub_dir, type='notebook')
246 246 name = model['name']
247 247 path = model['path']
248 248
249 249 # Change the name in the model for rename
250 250 d = path.rsplit('/', 1)[0]
251 251 new_path = model['path'] = d + '/test_in_sub.ipynb'
252 252 model = cm.update(model, path)
253 253 assert isinstance(model, dict)
254 254 self.assertIn('name', model)
255 255 self.assertIn('path', model)
256 256 self.assertEqual(model['name'], 'test_in_sub.ipynb')
257 257 self.assertEqual(model['path'], new_path)
258 258
259 259 # Make sure the old name is gone
260 260 self.assertRaises(HTTPError, cm.get, path)
261 261
262 262 def test_save(self):
263 263 cm = self.contents_manager
264 264 # Create a notebook
265 265 model = cm.new_untitled(type='notebook')
266 266 name = model['name']
267 267 path = model['path']
268 268
269 269 # Get the model with 'content'
270 270 full_model = cm.get(path)
271 271
272 272 # Save the notebook
273 273 model = cm.save(full_model, path)
274 274 assert isinstance(model, dict)
275 275 self.assertIn('name', model)
276 276 self.assertIn('path', model)
277 277 self.assertEqual(model['name'], name)
278 278 self.assertEqual(model['path'], path)
279 279
280 280 # Test in sub-directory
281 281 # Create a directory and notebook in that directory
282 282 sub_dir = '/foo/'
283 283 self.make_dir(cm.root_dir, 'foo')
284 284 model = cm.new_untitled(path=sub_dir, type='notebook')
285 285 name = model['name']
286 286 path = model['path']
287 287 model = cm.get(path)
288 288
289 289 # Change the name in the model for rename
290 290 model = cm.save(model, path)
291 291 assert isinstance(model, dict)
292 292 self.assertIn('name', model)
293 293 self.assertIn('path', model)
294 self.assertEqual(model['name'], 'Untitled0.ipynb')
295 self.assertEqual(model['path'], 'foo/Untitled0.ipynb')
294 self.assertEqual(model['name'], 'Untitled.ipynb')
295 self.assertEqual(model['path'], 'foo/Untitled.ipynb')
296 296
297 297 def test_delete(self):
298 298 cm = self.contents_manager
299 299 # Create a notebook
300 300 nb, name, path = self.new_notebook()
301 301
302 302 # Delete the notebook
303 303 cm.delete(path)
304 304
305 305 # Check that a 'get' on the deleted notebook raises and error
306 306 self.assertRaises(HTTPError, cm.get, path)
307 307
308 308 def test_copy(self):
309 309 cm = self.contents_manager
310 310 parent = u'å b'
311 311 name = u'nb √.ipynb'
312 312 path = u'{0}/{1}'.format(parent, name)
313 313 os.mkdir(os.path.join(cm.root_dir, parent))
314 314 orig = cm.new(path=path)
315 315
316 316 # copy with unspecified name
317 317 copy = cm.copy(path)
318 self.assertEqual(copy['name'], orig['name'].replace('.ipynb', '-Copy0.ipynb'))
318 self.assertEqual(copy['name'], orig['name'].replace('.ipynb', '-Copy1.ipynb'))
319 319
320 320 # copy with specified name
321 321 copy2 = cm.copy(path, u'å b/copy 2.ipynb')
322 322 self.assertEqual(copy2['name'], u'copy 2.ipynb')
323 323 self.assertEqual(copy2['path'], u'å b/copy 2.ipynb')
324 # copy with specified path
325 copy2 = cm.copy(path, u'/')
326 self.assertEqual(copy2['name'], name)
327 self.assertEqual(copy2['path'], name)
324 328
325 329 def test_trust_notebook(self):
326 330 cm = self.contents_manager
327 331 nb, name, path = self.new_notebook()
328 332
329 333 untrusted = cm.get(path)['content']
330 334 assert not cm.notary.check_cells(untrusted)
331 335
332 336 # print(untrusted)
333 337 cm.trust_notebook(path)
334 338 trusted = cm.get(path)['content']
335 339 # print(trusted)
336 340 assert cm.notary.check_cells(trusted)
337 341
338 342 def test_mark_trusted_cells(self):
339 343 cm = self.contents_manager
340 344 nb, name, path = self.new_notebook()
341 345
342 346 cm.mark_trusted_cells(nb, path)
343 347 for cell in nb.cells:
344 348 if cell.cell_type == 'code':
345 349 assert not cell.metadata.trusted
346 350
347 351 cm.trust_notebook(path)
348 352 nb = cm.get(path)['content']
349 353 for cell in nb.cells:
350 354 if cell.cell_type == 'code':
351 355 assert cell.metadata.trusted
352 356
353 357 def test_check_and_sign(self):
354 358 cm = self.contents_manager
355 359 nb, name, path = self.new_notebook()
356 360
357 361 cm.mark_trusted_cells(nb, path)
358 362 cm.check_and_sign(nb, path)
359 363 assert not cm.notary.check_signature(nb)
360 364
361 365 cm.trust_notebook(path)
362 366 nb = cm.get(path)['content']
363 367 cm.mark_trusted_cells(nb, path)
364 368 cm.check_and_sign(nb, path)
365 369 assert cm.notary.check_signature(nb)
General Comments 0
You need to be logged in to leave comments. Login now