##// END OF EJS Templates
Add test for and fix REST save with rename
Thomas Kluyver -
Show More
@@ -1,412 +1,412 b''
1 1 """A notebook manager that uses the local file system for storage.
2 2
3 3 Authors:
4 4
5 5 * Brian Granger
6 6 * Zach Sailer
7 7 """
8 8
9 9 #-----------------------------------------------------------------------------
10 10 # Copyright (C) 2011 The IPython Development Team
11 11 #
12 12 # Distributed under the terms of the BSD License. The full license is in
13 13 # the file COPYING, distributed as part of this software.
14 14 #-----------------------------------------------------------------------------
15 15
16 16 #-----------------------------------------------------------------------------
17 17 # Imports
18 18 #-----------------------------------------------------------------------------
19 19
20 20 import datetime
21 21 import io
22 22 import os
23 23 import glob
24 24 import shutil
25 25
26 26 from unicodedata import normalize
27 27
28 28 from tornado import web
29 29
30 30 from .nbmanager import NotebookManager
31 31 from IPython.nbformat import current
32 32 from IPython.utils.traitlets import Unicode, Dict, Bool, TraitError
33 33 from IPython.utils import tz
34 34
35 35 #-----------------------------------------------------------------------------
36 36 # Classes
37 37 #-----------------------------------------------------------------------------
38 38
39 39 class FileNotebookManager(NotebookManager):
40 40
41 41 save_script = Bool(False, config=True,
42 42 help="""Automatically create a Python script when saving the notebook.
43 43
44 44 For easier use of import, %run and %load across notebooks, a
45 45 <notebook-name>.py script will be created next to any
46 46 <notebook-name>.ipynb on each save. This can also be set with the
47 47 short `--script` flag.
48 48 """
49 49 )
50 50
51 51 checkpoint_dir = Unicode(config=True,
52 52 help="""The location in which to keep notebook checkpoints
53 53
54 54 By default, it is notebook-dir/.ipynb_checkpoints
55 55 """
56 56 )
57 57 def _checkpoint_dir_default(self):
58 58 return os.path.join(self.notebook_dir, '.ipynb_checkpoints')
59 59
60 60 def _checkpoint_dir_changed(self, name, old, new):
61 61 """do a bit of validation of the checkpoint dir"""
62 62 if not os.path.isabs(new):
63 63 # If we receive a non-absolute path, make it absolute.
64 64 abs_new = os.path.abspath(new)
65 65 self.checkpoint_dir = abs_new
66 66 return
67 67 if os.path.exists(new) and not os.path.isdir(new):
68 68 raise TraitError("checkpoint dir %r is not a directory" % new)
69 69 if not os.path.exists(new):
70 70 self.log.info("Creating checkpoint dir %s", new)
71 71 try:
72 72 os.mkdir(new)
73 73 except:
74 74 raise TraitError("Couldn't create checkpoint dir %r" % new)
75 75
76 76 def get_notebook_names(self, path=''):
77 77 """List all notebook names in the notebook dir and path."""
78 78 path = path.strip('/')
79 79 names = glob.glob(self.get_os_path('*'+self.filename_ext, path))
80 80 names = [os.path.basename(name)
81 81 for name in names]
82 82 return names
83 83
84 84 def increment_filename(self, basename, path=''):
85 85 """Return a non-used filename of the form basename<int>."""
86 86 path = path.strip('/')
87 87 i = 0
88 88 while True:
89 89 name = u'%s%i.ipynb' % (basename,i)
90 90 os_path = self.get_os_path(name, path)
91 91 if not os.path.isfile(os_path):
92 92 break
93 93 else:
94 94 i = i+1
95 95 return name
96 96
97 97 def path_exists(self, path):
98 98 """Does the API-style path (directory) actually exist?
99 99
100 100 Parameters
101 101 ----------
102 102 path : string
103 103 The path to check. This is an API path (`/` separated,
104 104 relative to base notebook-dir).
105 105
106 106 Returns
107 107 -------
108 108 exists : bool
109 109 Whether the path is indeed a directory.
110 110 """
111 111 path = path.strip('/')
112 112 os_path = self.get_os_path(path=path)
113 113 return os.path.isdir(os_path)
114 114
115 115 def get_os_path(self, name=None, path=''):
116 116 """Given a notebook name and a URL path, return its file system
117 117 path.
118 118
119 119 Parameters
120 120 ----------
121 121 name : string
122 122 The name of a notebook file with the .ipynb extension
123 123 path : string
124 124 The relative URL path (with '/' as separator) to the named
125 125 notebook.
126 126
127 127 Returns
128 128 -------
129 129 path : string
130 130 A file system path that combines notebook_dir (location where
131 131 server started), the relative path, and the filename with the
132 132 current operating system's url.
133 133 """
134 134 parts = path.strip('/').split('/')
135 135 parts = [p for p in parts if p != ''] # remove duplicate splits
136 136 if name is not None:
137 137 parts.append(name)
138 138 path = os.path.join(self.notebook_dir, *parts)
139 139 return path
140 140
141 141 def notebook_exists(self, name, path=''):
142 142 """Returns a True if the notebook exists. Else, returns False.
143 143
144 144 Parameters
145 145 ----------
146 146 name : string
147 147 The name of the notebook you are checking.
148 148 path : string
149 149 The relative path to the notebook (with '/' as separator)
150 150
151 151 Returns
152 152 -------
153 153 bool
154 154 """
155 155 path = path.strip('/')
156 156 nbpath = self.get_os_path(name, path=path)
157 157 return os.path.isfile(nbpath)
158 158
159 159 def list_notebooks(self, path):
160 160 """Returns a list of dictionaries that are the standard model
161 161 for all notebooks in the relative 'path'.
162 162
163 163 Parameters
164 164 ----------
165 165 path : str
166 166 the URL path that describes the relative path for the
167 167 listed notebooks
168 168
169 169 Returns
170 170 -------
171 171 notebooks : list of dicts
172 172 a list of the notebook models without 'content'
173 173 """
174 174 path = path.strip('/')
175 175 notebook_names = self.get_notebook_names(path)
176 176 notebooks = []
177 177 for name in notebook_names:
178 178 model = self.get_notebook_model(name, path, content=False)
179 179 notebooks.append(model)
180 180 notebooks = sorted(notebooks, key=lambda item: item['name'])
181 181 return notebooks
182 182
183 183 def get_notebook_model(self, name, path='', content=True):
184 184 """ Takes a path and name for a notebook and returns it's model
185 185
186 186 Parameters
187 187 ----------
188 188 name : str
189 189 the name of the notebook
190 190 path : str
191 191 the URL path that describes the relative path for
192 192 the notebook
193 193
194 194 Returns
195 195 -------
196 196 model : dict
197 197 the notebook model. If contents=True, returns the 'contents'
198 198 dict in the model as well.
199 199 """
200 200 path = path.strip('/')
201 201 if not self.notebook_exists(name=name, path=path):
202 202 raise web.HTTPError(404, u'Notebook does not exist: %s' % name)
203 203 os_path = self.get_os_path(name, path)
204 204 info = os.stat(os_path)
205 205 last_modified = tz.utcfromtimestamp(info.st_mtime)
206 206 created = tz.utcfromtimestamp(info.st_ctime)
207 207 # Create the notebook model.
208 208 model ={}
209 209 model['name'] = name
210 210 model['path'] = path
211 211 model['last_modified'] = last_modified
212 212 model['created'] = last_modified
213 213 if content is True:
214 214 with open(os_path, 'r') as f:
215 215 try:
216 216 nb = current.read(f, u'json')
217 217 except Exception as e:
218 218 raise web.HTTPError(400, u"Unreadable Notebook: %s %s" % (os_path, e))
219 219 model['content'] = nb
220 220 return model
221 221
222 222 def save_notebook_model(self, model, name='', path=''):
223 223 """Save the notebook model and return the model with no content."""
224 224 path = path.strip('/')
225 225
226 226 if 'content' not in model:
227 227 raise web.HTTPError(400, u'No notebook JSON data provided')
228 228
229 229 new_path = model.get('path', path).strip('/')
230 230 new_name = model.get('name', name)
231 231
232 232 if path != new_path or name != new_name:
233 233 self.rename_notebook(name, path, new_name, new_path)
234 234
235 235 # Save the notebook file
236 236 os_path = self.get_os_path(new_name, new_path)
237 237 nb = current.to_notebook_json(model['content'])
238 238 if 'name' in nb['metadata']:
239 239 nb['metadata']['name'] = u''
240 240 try:
241 241 self.log.debug("Autosaving notebook %s", os_path)
242 242 with open(os_path, 'w') as f:
243 243 current.write(nb, f, u'json')
244 244 except Exception as e:
245 245 raise web.HTTPError(400, u'Unexpected error while autosaving notebook: %s %s' % (os_path, e))
246 246
247 247 # Save .py script as well
248 248 if self.save_script:
249 249 py_path = os.path.splitext(os_path)[0] + '.py'
250 250 self.log.debug("Writing script %s", py_path)
251 251 try:
252 252 with io.open(py_path, 'w', encoding='utf-8') as f:
253 253 current.write(model, f, u'py')
254 254 except Exception as e:
255 255 raise web.HTTPError(400, u'Unexpected error while saving notebook as script: %s %s' % (py_path, e))
256 256
257 model = self.get_notebook_model(name, path, content=False)
257 model = self.get_notebook_model(new_name, new_path, content=False)
258 258 return model
259 259
260 260 def update_notebook_model(self, model, name, path=''):
261 261 """Update the notebook's path and/or name"""
262 262 path = path.strip('/')
263 263 new_name = model.get('name', name)
264 264 new_path = model.get('path', path).strip('/')
265 265 if path != new_path or name != new_name:
266 266 self.rename_notebook(name, path, new_name, new_path)
267 267 model = self.get_notebook_model(new_name, new_path, content=False)
268 268 return model
269 269
270 270 def delete_notebook_model(self, name, path=''):
271 271 """Delete notebook by name and path."""
272 272 path = path.strip('/')
273 273 os_path = self.get_os_path(name, path)
274 274 if not os.path.isfile(os_path):
275 275 raise web.HTTPError(404, u'Notebook does not exist: %s' % os_path)
276 276
277 277 # clear checkpoints
278 278 for checkpoint in self.list_checkpoints(name, path):
279 279 checkpoint_id = checkpoint['checkpoint_id']
280 280 cp_path = self.get_checkpoint_path(checkpoint_id, name, path)
281 281 if os.path.isfile(cp_path):
282 282 self.log.debug("Unlinking checkpoint %s", cp_path)
283 283 os.unlink(cp_path)
284 284
285 285 self.log.debug("Unlinking notebook %s", os_path)
286 286 os.unlink(os_path)
287 287
288 288 def rename_notebook(self, old_name, old_path, new_name, new_path):
289 289 """Rename a notebook."""
290 290 old_path = old_path.strip('/')
291 291 new_path = new_path.strip('/')
292 292 if new_name == old_name and new_path == old_path:
293 293 return
294 294
295 295 new_os_path = self.get_os_path(new_name, new_path)
296 296 old_os_path = self.get_os_path(old_name, old_path)
297 297
298 298 # Should we proceed with the move?
299 299 if os.path.isfile(new_os_path):
300 300 raise web.HTTPError(409, u'Notebook with name already exists: %s' % new_os_path)
301 301 if self.save_script:
302 302 old_py_path = os.path.splitext(old_os_path)[0] + '.py'
303 303 new_py_path = os.path.splitext(new_os_path)[0] + '.py'
304 304 if os.path.isfile(new_py_path):
305 305 raise web.HTTPError(409, u'Python script with name already exists: %s' % new_py_path)
306 306
307 307 # Move the notebook file
308 308 try:
309 309 os.rename(old_os_path, new_os_path)
310 310 except Exception as e:
311 311 raise web.HTTPError(500, u'Unknown error renaming notebook: %s %s' % (old_os_path, e))
312 312
313 313 # Move the checkpoints
314 314 old_checkpoints = self.list_checkpoints(old_name, old_path)
315 315 for cp in old_checkpoints:
316 316 checkpoint_id = cp['checkpoint_id']
317 317 old_cp_path = self.get_checkpoint_path(checkpoint_id, old_name, old_path)
318 318 new_cp_path = self.get_checkpoint_path(checkpoint_id, new_name, new_path)
319 319 if os.path.isfile(old_cp_path):
320 320 self.log.debug("Renaming checkpoint %s -> %s", old_cp_path, new_cp_path)
321 321 os.rename(old_cp_path, new_cp_path)
322 322
323 323 # Move the .py script
324 324 if self.save_script:
325 325 os.rename(old_py_path, new_py_path)
326 326
327 327 # Checkpoint-related utilities
328 328
329 329 def get_checkpoint_path(self, checkpoint_id, name, path=''):
330 330 """find the path to a checkpoint"""
331 331 path = path.strip('/')
332 332 filename = u"{name}-{checkpoint_id}{ext}".format(
333 333 name=name,
334 334 checkpoint_id=checkpoint_id,
335 335 ext=self.filename_ext,
336 336 )
337 337 cp_path = os.path.join(path, self.checkpoint_dir, filename)
338 338 return cp_path
339 339
340 340 def get_checkpoint_model(self, checkpoint_id, name, path=''):
341 341 """construct the info dict for a given checkpoint"""
342 342 path = path.strip('/')
343 343 cp_path = self.get_checkpoint_path(checkpoint_id, name, path)
344 344 stats = os.stat(cp_path)
345 345 last_modified = tz.utcfromtimestamp(stats.st_mtime)
346 346 info = dict(
347 347 checkpoint_id = checkpoint_id,
348 348 last_modified = last_modified,
349 349 )
350 350 return info
351 351
352 352 # public checkpoint API
353 353
354 354 def create_checkpoint(self, name, path=''):
355 355 """Create a checkpoint from the current state of a notebook"""
356 356 path = path.strip('/')
357 357 nb_path = self.get_os_path(name, path)
358 358 # only the one checkpoint ID:
359 359 checkpoint_id = u"checkpoint"
360 360 cp_path = self.get_checkpoint_path(checkpoint_id, name, path)
361 361 self.log.debug("creating checkpoint for notebook %s", name)
362 362 if not os.path.exists(self.checkpoint_dir):
363 363 os.mkdir(self.checkpoint_dir)
364 364 shutil.copy2(nb_path, cp_path)
365 365
366 366 # return the checkpoint info
367 367 return self.get_checkpoint_model(checkpoint_id, name, path)
368 368
369 369 def list_checkpoints(self, name, path=''):
370 370 """list the checkpoints for a given notebook
371 371
372 372 This notebook manager currently only supports one checkpoint per notebook.
373 373 """
374 374 path = path.strip('/')
375 375 checkpoint_id = "checkpoint"
376 376 path = self.get_checkpoint_path(checkpoint_id, name, path)
377 377 if not os.path.exists(path):
378 378 return []
379 379 else:
380 380 return [self.get_checkpoint_model(checkpoint_id, name, path)]
381 381
382 382
383 383 def restore_checkpoint(self, checkpoint_id, name, path=''):
384 384 """restore a notebook to a checkpointed state"""
385 385 path = path.strip('/')
386 386 self.log.info("restoring Notebook %s from checkpoint %s", name, checkpoint_id)
387 387 nb_path = self.get_os_path(name, path)
388 388 cp_path = self.get_checkpoint_path(checkpoint_id, name, path)
389 389 if not os.path.isfile(cp_path):
390 390 self.log.debug("checkpoint file does not exist: %s", cp_path)
391 391 raise web.HTTPError(404,
392 392 u'Notebook checkpoint does not exist: %s-%s' % (name, checkpoint_id)
393 393 )
394 394 # ensure notebook is readable (never restore from an unreadable notebook)
395 395 with file(cp_path, 'r') as f:
396 396 nb = current.read(f, u'json')
397 397 shutil.copy2(cp_path, nb_path)
398 398 self.log.debug("copying %s -> %s", cp_path, nb_path)
399 399
400 400 def delete_checkpoint(self, checkpoint_id, name, path=''):
401 401 """delete a notebook's checkpoint"""
402 402 path = path.strip('/')
403 403 cp_path = self.get_checkpoint_path(checkpoint_id, name, path)
404 404 if not os.path.isfile(cp_path):
405 405 raise web.HTTPError(404,
406 406 u'Notebook checkpoint does not exist: %s%s-%s' % (path, name, checkpoint_id)
407 407 )
408 408 self.log.debug("unlinking %s", cp_path)
409 409 os.unlink(cp_path)
410 410
411 411 def info_string(self):
412 412 return "Serving notebooks from local directory: %s" % self.notebook_dir
@@ -1,200 +1,213 b''
1 1 """Test the notebooks webservice API."""
2 2
3 3 import io
4 4 import os
5 5 import shutil
6 6 from zmq.utils import jsonapi
7 7
8 8 pjoin = os.path.join
9 9
10 10 import requests
11 11
12 12 from IPython.html.utils import url_path_join
13 13 from IPython.html.tests.launchnotebook import NotebookTestBase
14 14 from IPython.nbformat.current import (new_notebook, write, read, new_worksheet,
15 15 new_heading_cell, to_notebook_json)
16 16 from IPython.utils.data import uniq_stable
17 17
18 18 class NBAPI(object):
19 19 """Wrapper for notebook API calls."""
20 20 def __init__(self, base_url):
21 21 self.base_url = base_url
22 22
23 23 @property
24 24 def nb_url(self):
25 25 return url_path_join(self.base_url, 'api/notebooks')
26 26
27 27 def _req(self, verb, path, body=None):
28 28 response = requests.request(verb,
29 29 url_path_join(self.base_url, 'api/notebooks', path), data=body)
30 30 response.raise_for_status()
31 31 return response
32 32
33 33 def list(self, path='/'):
34 34 return self._req('GET', path)
35 35
36 36 def read(self, name, path='/'):
37 37 return self._req('GET', url_path_join(path, name))
38 38
39 39 def create_untitled(self, path='/'):
40 40 return self._req('POST', path)
41 41
42 42 def upload(self, name, body, path='/'):
43 43 return self._req('POST', url_path_join(path, name), body)
44 44
45 45 def copy(self, name, path='/'):
46 46 return self._req('POST', url_path_join(path, name, 'copy'))
47 47
48 48 def save(self, name, body, path='/'):
49 49 return self._req('PUT', url_path_join(path, name), body)
50 50
51 51 def delete(self, name, path='/'):
52 52 return self._req('DELETE', url_path_join(path, name))
53 53
54 54 def rename(self, name, path, new_name):
55 55 body = jsonapi.dumps({'name': new_name})
56 56 return self._req('PATCH', url_path_join(path, name), body)
57 57
58 58 class APITest(NotebookTestBase):
59 59 """Test the kernels web service API"""
60 60 dirs_nbs = [('', 'inroot'),
61 61 ('Directory with spaces in', 'inspace'),
62 62 (u'unicodΓ©', 'innonascii'),
63 63 ('foo', 'a'),
64 64 ('foo', 'b'),
65 65 ('foo', 'name with spaces'),
66 66 ('foo', u'unicodΓ©'),
67 67 ('foo/bar', 'baz'),
68 68 ]
69 69
70 70 dirs = uniq_stable([d for (d,n) in dirs_nbs])
71 71 del dirs[0] # remove ''
72 72
73 73 def setUp(self):
74 74 nbdir = self.notebook_dir.name
75 75
76 76 for d in self.dirs:
77 77 os.mkdir(pjoin(nbdir, d))
78 78
79 79 for d, name in self.dirs_nbs:
80 80 with io.open(pjoin(nbdir, d, '%s.ipynb' % name), 'w') as f:
81 81 nb = new_notebook(name=name)
82 82 write(nb, f, format='ipynb')
83 83
84 84 self.nb_api = NBAPI(self.base_url())
85 85
86 86 def tearDown(self):
87 87 nbdir = self.notebook_dir.name
88 88
89 89 for dname in ['foo', 'Directory with spaces in', u'unicodΓ©']:
90 90 shutil.rmtree(pjoin(nbdir, dname), ignore_errors=True)
91 91
92 92 if os.path.isfile(pjoin(nbdir, 'inroot.ipynb')):
93 93 os.unlink(pjoin(nbdir, 'inroot.ipynb'))
94 94
95 95 def test_list_notebooks(self):
96 96 nbs = self.nb_api.list().json()
97 97 self.assertEqual(len(nbs), 1)
98 98 self.assertEqual(nbs[0]['name'], 'inroot.ipynb')
99 99
100 100 nbs = self.nb_api.list('/Directory with spaces in/').json()
101 101 self.assertEqual(len(nbs), 1)
102 102 self.assertEqual(nbs[0]['name'], 'inspace.ipynb')
103 103
104 104 nbs = self.nb_api.list(u'/unicodΓ©/').json()
105 105 self.assertEqual(len(nbs), 1)
106 106 self.assertEqual(nbs[0]['name'], 'innonascii.ipynb')
107 107
108 108 nbs = self.nb_api.list('/foo/bar/').json()
109 109 self.assertEqual(len(nbs), 1)
110 110 self.assertEqual(nbs[0]['name'], 'baz.ipynb')
111 111
112 112 nbs = self.nb_api.list('foo').json()
113 113 self.assertEqual(len(nbs), 4)
114 114 nbnames = set(n['name'] for n in nbs)
115 115 self.assertEqual(nbnames, {'a.ipynb', 'b.ipynb',
116 116 'name with spaces.ipynb', u'unicodΓ©.ipynb'})
117 117
118 def assert_404(self, name, path):
119 try:
120 self.nb_api.read(name, path)
121 except requests.HTTPError as e:
122 self.assertEqual(e.response.status_code, 404)
123 else:
124 assert False, "Reading a non-existent notebook should fail"
125
118 126 def test_get_contents(self):
119 127 for d, name in self.dirs_nbs:
120 128 nb = self.nb_api.read('%s.ipynb' % name, d+'/').json()
121 129 self.assertEqual(nb['name'], '%s.ipynb' % name)
122 130 self.assertIn('content', nb)
123 131 self.assertIn('metadata', nb['content'])
124 132 self.assertIsInstance(nb['content']['metadata'], dict)
125 133
126 134 # Name that doesn't exist - should be a 404
127 try:
128 self.nb_api.read('q.ipynb', 'foo')
129 except requests.HTTPError as e:
130 self.assertEqual(e.response.status_code, 404)
131 else:
132 assert False, "Reading a non-existent notebook should fail"
135 self.assert_404('q.ipynb', 'foo')
133 136
134 137 def _check_nb_created(self, resp, name, path):
135 138 self.assertEqual(resp.status_code, 201)
136 139 self.assertEqual(resp.headers['Location'].split('/')[-1], name)
137 140 self.assertEqual(resp.json()['name'], name)
138 141 assert os.path.isfile(pjoin(self.notebook_dir.name, path, name))
139 142
140 143 def test_create_untitled(self):
141 144 resp = self.nb_api.create_untitled(path='foo')
142 145 self._check_nb_created(resp, 'Untitled0.ipynb', 'foo')
143 146
144 147 # Second time
145 148 resp = self.nb_api.create_untitled(path='foo')
146 149 self._check_nb_created(resp, 'Untitled1.ipynb', 'foo')
147 150
148 151 # And two directories down
149 152 resp = self.nb_api.create_untitled(path='foo/bar')
150 153 self._check_nb_created(resp, 'Untitled0.ipynb', pjoin('foo', 'bar'))
151 154
152 155 def test_upload(self):
153 156 nb = new_notebook(name='Upload test')
154 157 nbmodel = {'content': nb}
155 158 resp = self.nb_api.upload('Upload test.ipynb', path='foo',
156 159 body=jsonapi.dumps(nbmodel))
157 160 self._check_nb_created(resp, 'Upload test.ipynb', 'foo')
158 161
159 162 def test_copy(self):
160 163 resp = self.nb_api.copy('a.ipynb', path='foo')
161 164 self._check_nb_created(resp, 'a-Copy0.ipynb', 'foo')
162 165
163 166 def test_delete(self):
164 167 for d, name in self.dirs_nbs:
165 168 resp = self.nb_api.delete('%s.ipynb' % name, d)
166 169 self.assertEqual(resp.status_code, 204)
167 170
168 171 for d in self.dirs + ['/']:
169 172 nbs = self.nb_api.list(d).json()
170 173 self.assertEqual(len(nbs), 0)
171 174
172 175 def test_rename(self):
173 176 resp = self.nb_api.rename('a.ipynb', 'foo', 'z.ipynb')
174 177 if False:
175 178 # XXX: Spec says this should be set, but it isn't
176 179 self.assertEqual(resp.headers['Location'].split('/')[-1], 'z.ipynb')
177 180 self.assertEqual(resp.json()['name'], 'z.ipynb')
178 181 assert os.path.isfile(pjoin(self.notebook_dir.name, 'foo', 'z.ipynb'))
179 182
180 183 nbs = self.nb_api.list('foo').json()
181 184 nbnames = set(n['name'] for n in nbs)
182 185 self.assertIn('z.ipynb', nbnames)
183 186 self.assertNotIn('a.ipynb', nbnames)
184 187
185 188 def test_save(self):
186 189 resp = self.nb_api.read('a.ipynb', 'foo')
187 190 nbcontent = jsonapi.loads(resp.text)['content']
188 191 nb = to_notebook_json(nbcontent)
189 192 ws = new_worksheet()
190 193 nb.worksheets = [ws]
191 194 ws.cells.append(new_heading_cell('Created by test'))
192 195
193 196 nbmodel= {'name': 'a.ipynb', 'path':'foo', 'content': nb}
194 197 resp = self.nb_api.save('a.ipynb', path='foo', body=jsonapi.dumps(nbmodel))
195 198
196 199 nbfile = pjoin(self.notebook_dir.name, 'foo', 'a.ipynb')
197 200 with open(nbfile, 'r') as f:
198 201 newnb = read(f, format='ipynb')
199 202 self.assertEqual(newnb.worksheets[0].cells[0].source,
200 203 'Created by test')
204
205 # Save and rename
206 nbmodel= {'name': 'a2.ipynb', 'path':'foo/bar', 'content': nb}
207 resp = self.nb_api.save('a.ipynb', path='foo', body=jsonapi.dumps(nbmodel))
208 saved = resp.json()
209 self.assertEqual(saved['name'], 'a2.ipynb')
210 self.assertEqual(saved['path'], 'foo/bar')
211 assert os.path.isfile(pjoin(self.notebook_dir.name,'foo','bar','a2.ipynb'))
212 assert not os.path.isfile(pjoin(self.notebook_dir.name, 'foo', 'a.ipynb'))
213 self.assert_404('a.ipynb', 'foo') No newline at end of file
General Comments 0
You need to be logged in to leave comments. Login now