mirror of
https://github.com/aykhans/AzSuicideDataVisualization.git
synced 2025-07-03 06:31:28 +00:00
first commit
This commit is contained in:
@ -0,0 +1,734 @@
|
||||
"""Test the contents webservice API."""
|
||||
|
||||
from contextlib import contextmanager
|
||||
from functools import partial
|
||||
import json
|
||||
import os
|
||||
import shutil
|
||||
import sys
|
||||
from unicodedata import normalize
|
||||
|
||||
pjoin = os.path.join
|
||||
|
||||
import requests
|
||||
from send2trash import send2trash
|
||||
from send2trash.exceptions import TrashPermissionError
|
||||
|
||||
from ..filecheckpoints import GenericFileCheckpoints
|
||||
|
||||
from traitlets.config import Config
|
||||
from notebook.utils import url_path_join, url_escape, to_os_path
|
||||
from notebook.tests.launchnotebook import NotebookTestBase, assert_http_error
|
||||
from nbformat import write, from_dict
|
||||
from nbformat.v4 import (
|
||||
new_notebook, new_markdown_cell,
|
||||
)
|
||||
from nbformat import v2
|
||||
from ipython_genutils import py3compat
|
||||
from ipython_genutils.tempdir import TemporaryDirectory
|
||||
|
||||
from base64 import encodebytes, decodebytes
|
||||
|
||||
|
||||
def uniq_stable(elems):
|
||||
"""uniq_stable(elems) -> list
|
||||
|
||||
Return from an iterable, a list of all the unique elements in the input,
|
||||
maintaining the order in which they first appear.
|
||||
"""
|
||||
seen = set()
|
||||
return [x for x in elems if x not in seen and not seen.add(x)]
|
||||
|
||||
def notebooks_only(dir_model):
|
||||
return [nb for nb in dir_model['content'] if nb['type']=='notebook']
|
||||
|
||||
def dirs_only(dir_model):
|
||||
return [x for x in dir_model['content'] if x['type']=='directory']
|
||||
|
||||
|
||||
class API:
|
||||
"""Wrapper for contents API calls."""
|
||||
def __init__(self, request):
|
||||
self.request = request
|
||||
|
||||
def _req(self, verb, path, body=None, params=None):
|
||||
response = self.request(verb,
|
||||
url_path_join('api/contents', path),
|
||||
data=body, params=params,
|
||||
)
|
||||
response.raise_for_status()
|
||||
return response
|
||||
|
||||
def list(self, path='/'):
|
||||
return self._req('GET', path)
|
||||
|
||||
def read(self, path, type=None, format=None, content=None):
|
||||
params = {}
|
||||
if type is not None:
|
||||
params['type'] = type
|
||||
if format is not None:
|
||||
params['format'] = format
|
||||
if content == False:
|
||||
params['content'] = '0'
|
||||
return self._req('GET', path, params=params)
|
||||
|
||||
def create_untitled(self, path='/', ext='.ipynb'):
|
||||
body = None
|
||||
if ext:
|
||||
body = json.dumps({'ext': ext})
|
||||
return self._req('POST', path, body)
|
||||
|
||||
def mkdir_untitled(self, path='/'):
|
||||
return self._req('POST', path, json.dumps({'type': 'directory'}))
|
||||
|
||||
def copy(self, copy_from, path='/'):
|
||||
body = json.dumps({'copy_from':copy_from})
|
||||
return self._req('POST', path, body)
|
||||
|
||||
def create(self, path='/'):
|
||||
return self._req('PUT', path)
|
||||
|
||||
def upload(self, path, body):
|
||||
return self._req('PUT', path, body)
|
||||
|
||||
def mkdir(self, path='/'):
|
||||
return self._req('PUT', path, json.dumps({'type': 'directory'}))
|
||||
|
||||
def copy_put(self, copy_from, path='/'):
|
||||
body = json.dumps({'copy_from':copy_from})
|
||||
return self._req('PUT', path, body)
|
||||
|
||||
def save(self, path, body):
|
||||
return self._req('PUT', path, body)
|
||||
|
||||
def delete(self, path='/'):
|
||||
return self._req('DELETE', path)
|
||||
|
||||
def rename(self, path, new_path):
|
||||
body = json.dumps({'path': new_path})
|
||||
return self._req('PATCH', path, body)
|
||||
|
||||
def get_checkpoints(self, path):
|
||||
return self._req('GET', url_path_join(path, 'checkpoints'))
|
||||
|
||||
def new_checkpoint(self, path):
|
||||
return self._req('POST', url_path_join(path, 'checkpoints'))
|
||||
|
||||
def restore_checkpoint(self, path, checkpoint_id):
|
||||
return self._req('POST', url_path_join(path, 'checkpoints', checkpoint_id))
|
||||
|
||||
def delete_checkpoint(self, path, checkpoint_id):
|
||||
return self._req('DELETE', url_path_join(path, 'checkpoints', checkpoint_id))
|
||||
|
||||
class APITest(NotebookTestBase):
|
||||
"""Test the kernels web service API"""
|
||||
dirs_nbs = [('', 'inroot'),
|
||||
('Directory with spaces in', 'inspace'),
|
||||
('unicodé', 'innonascii'),
|
||||
('foo', 'a'),
|
||||
('foo', 'b'),
|
||||
('foo', 'name with spaces'),
|
||||
('foo', 'unicodé'),
|
||||
('foo/bar', 'baz'),
|
||||
('ordering', 'A'),
|
||||
('ordering', 'b'),
|
||||
('ordering', 'C'),
|
||||
('å b', 'ç d'),
|
||||
]
|
||||
hidden_dirs = ['.hidden', '__pycache__']
|
||||
|
||||
# Don't include root dir.
|
||||
dirs = uniq_stable([py3compat.cast_unicode(d) for (d,n) in dirs_nbs[1:]])
|
||||
top_level_dirs = {normalize('NFC', d.split('/')[0]) for d in dirs}
|
||||
|
||||
@staticmethod
|
||||
def _blob_for_name(name):
|
||||
return name.encode('utf-8') + b'\xFF'
|
||||
|
||||
@staticmethod
|
||||
def _txt_for_name(name):
|
||||
return f'{name} text file'
|
||||
|
||||
def to_os_path(self, api_path):
|
||||
return to_os_path(api_path, root=self.notebook_dir)
|
||||
|
||||
def make_dir(self, api_path):
|
||||
"""Create a directory at api_path"""
|
||||
os_path = self.to_os_path(api_path)
|
||||
try:
|
||||
os.makedirs(os_path)
|
||||
except OSError:
|
||||
print(f"Directory already exists: {os_path!r}")
|
||||
|
||||
def make_txt(self, api_path, txt):
|
||||
"""Make a text file at a given api_path"""
|
||||
os_path = self.to_os_path(api_path)
|
||||
with open(os_path, 'w', encoding='utf-8') as f:
|
||||
f.write(txt)
|
||||
|
||||
def make_blob(self, api_path, blob):
|
||||
"""Make a binary file at a given api_path"""
|
||||
os_path = self.to_os_path(api_path)
|
||||
with open(os_path, 'wb') as f:
|
||||
f.write(blob)
|
||||
|
||||
def make_nb(self, api_path, nb):
|
||||
"""Make a notebook file at a given api_path"""
|
||||
os_path = self.to_os_path(api_path)
|
||||
|
||||
with open(os_path, 'w', encoding='utf-8') as f:
|
||||
write(nb, f, version=4)
|
||||
|
||||
def delete_dir(self, api_path):
|
||||
"""Delete a directory at api_path, removing any contents."""
|
||||
os_path = self.to_os_path(api_path)
|
||||
shutil.rmtree(os_path, ignore_errors=True)
|
||||
|
||||
def delete_file(self, api_path):
|
||||
"""Delete a file at the given path if it exists."""
|
||||
if self.isfile(api_path):
|
||||
os.unlink(self.to_os_path(api_path))
|
||||
|
||||
def isfile(self, api_path):
|
||||
return os.path.isfile(self.to_os_path(api_path))
|
||||
|
||||
def isdir(self, api_path):
|
||||
return os.path.isdir(self.to_os_path(api_path))
|
||||
|
||||
def can_send2trash(self, api_path):
|
||||
"""Send a path to trash, if possible. Return success."""
|
||||
try:
|
||||
send2trash(self.to_os_path(api_path))
|
||||
return True
|
||||
except TrashPermissionError as e:
|
||||
return False
|
||||
|
||||
def setUp(self):
|
||||
for d in (self.dirs + self.hidden_dirs):
|
||||
self.make_dir(d)
|
||||
self.addCleanup(partial(self.delete_dir, d))
|
||||
|
||||
for d, name in self.dirs_nbs:
|
||||
# create a notebook
|
||||
nb = new_notebook()
|
||||
nbname = f'{d}/{name}.ipynb'
|
||||
self.make_nb(nbname, nb)
|
||||
self.addCleanup(partial(self.delete_file, nbname))
|
||||
|
||||
# create a text file
|
||||
txt = self._txt_for_name(name)
|
||||
txtname = f'{d}/{name}.txt'
|
||||
self.make_txt(txtname, txt)
|
||||
self.addCleanup(partial(self.delete_file, txtname))
|
||||
|
||||
blob = self._blob_for_name(name)
|
||||
blobname = f'{d}/{name}.blob'
|
||||
self.make_blob(blobname, blob)
|
||||
self.addCleanup(partial(self.delete_file, blobname))
|
||||
|
||||
self.api = API(self.request)
|
||||
|
||||
def test_list_notebooks(self):
|
||||
nbs = notebooks_only(self.api.list().json())
|
||||
self.assertEqual(len(nbs), 1)
|
||||
self.assertEqual(nbs[0]['name'], 'inroot.ipynb')
|
||||
|
||||
nbs = notebooks_only(self.api.list('/Directory with spaces in/').json())
|
||||
self.assertEqual(len(nbs), 1)
|
||||
self.assertEqual(nbs[0]['name'], 'inspace.ipynb')
|
||||
|
||||
nbs = notebooks_only(self.api.list('/unicodé/').json())
|
||||
self.assertEqual(len(nbs), 1)
|
||||
self.assertEqual(nbs[0]['name'], 'innonascii.ipynb')
|
||||
self.assertEqual(nbs[0]['path'], 'unicodé/innonascii.ipynb')
|
||||
|
||||
nbs = notebooks_only(self.api.list('/foo/bar/').json())
|
||||
self.assertEqual(len(nbs), 1)
|
||||
self.assertEqual(nbs[0]['name'], 'baz.ipynb')
|
||||
self.assertEqual(nbs[0]['path'], 'foo/bar/baz.ipynb')
|
||||
|
||||
nbs = notebooks_only(self.api.list('foo').json())
|
||||
self.assertEqual(len(nbs), 4)
|
||||
nbnames = { normalize('NFC', n['name']) for n in nbs }
|
||||
expected = [ 'a.ipynb', 'b.ipynb', 'name with spaces.ipynb', 'unicodé.ipynb']
|
||||
expected = { normalize('NFC', name) for name in expected }
|
||||
self.assertEqual(nbnames, expected)
|
||||
|
||||
nbs = notebooks_only(self.api.list('ordering').json())
|
||||
nbnames = {n['name'] for n in nbs}
|
||||
expected = {'A.ipynb', 'b.ipynb', 'C.ipynb'}
|
||||
self.assertEqual(nbnames, expected)
|
||||
|
||||
def test_list_dirs(self):
|
||||
dirs = dirs_only(self.api.list().json())
|
||||
dir_names = {normalize('NFC', d['name']) for d in dirs}
|
||||
self.assertEqual(dir_names, self.top_level_dirs) # Excluding hidden dirs
|
||||
|
||||
def test_get_dir_no_content(self):
|
||||
for d in self.dirs:
|
||||
model = self.api.read(d, content=False).json()
|
||||
self.assertEqual(model['path'], d)
|
||||
self.assertEqual(model['type'], 'directory')
|
||||
self.assertIn('content', model)
|
||||
self.assertEqual(model['content'], None)
|
||||
|
||||
def test_list_nonexistant_dir(self):
|
||||
with assert_http_error(404):
|
||||
self.api.list('nonexistant')
|
||||
|
||||
def test_get_nb_contents(self):
|
||||
for d, name in self.dirs_nbs:
|
||||
path = url_path_join(d, name + '.ipynb')
|
||||
nb = self.api.read(path).json()
|
||||
self.assertEqual(nb['name'], f'{name}.ipynb')
|
||||
self.assertEqual(nb['path'], path)
|
||||
self.assertEqual(nb['type'], 'notebook')
|
||||
self.assertIn('content', nb)
|
||||
self.assertEqual(nb['format'], 'json')
|
||||
self.assertIn('metadata', nb['content'])
|
||||
self.assertIsInstance(nb['content']['metadata'], dict)
|
||||
|
||||
def test_get_nb_no_content(self):
|
||||
for d, name in self.dirs_nbs:
|
||||
path = url_path_join(d, name + '.ipynb')
|
||||
nb = self.api.read(path, content=False).json()
|
||||
self.assertEqual(nb['name'], f'{name}.ipynb')
|
||||
self.assertEqual(nb['path'], path)
|
||||
self.assertEqual(nb['type'], 'notebook')
|
||||
self.assertIn('content', nb)
|
||||
self.assertEqual(nb['content'], None)
|
||||
|
||||
def test_get_nb_invalid(self):
|
||||
nb = {
|
||||
'nbformat': 4,
|
||||
'metadata': {},
|
||||
'cells': [{
|
||||
'cell_type': 'wrong',
|
||||
'metadata': {},
|
||||
}],
|
||||
}
|
||||
path = 'å b/Validate tést.ipynb'
|
||||
self.make_txt(path, py3compat.cast_unicode(json.dumps(nb)))
|
||||
model = self.api.read(path).json()
|
||||
self.assertEqual(model['path'], path)
|
||||
self.assertEqual(model['type'], 'notebook')
|
||||
self.assertIn('content', model)
|
||||
self.assertIn('message', model)
|
||||
self.assertIn("validation failed", model['message'].lower())
|
||||
|
||||
def test_get_contents_no_such_file(self):
|
||||
# Name that doesn't exist - should be a 404
|
||||
with assert_http_error(404):
|
||||
self.api.read('foo/q.ipynb')
|
||||
|
||||
def test_get_text_file_contents(self):
|
||||
for d, name in self.dirs_nbs:
|
||||
path = url_path_join(d, name + '.txt')
|
||||
model = self.api.read(path).json()
|
||||
self.assertEqual(model['name'], f'{name}.txt')
|
||||
self.assertEqual(model['path'], path)
|
||||
self.assertIn('content', model)
|
||||
self.assertEqual(model['format'], 'text')
|
||||
self.assertEqual(model['type'], 'file')
|
||||
self.assertEqual(model['content'], self._txt_for_name(name))
|
||||
|
||||
# Name that doesn't exist - should be a 404
|
||||
with assert_http_error(404):
|
||||
self.api.read('foo/q.txt')
|
||||
|
||||
# Specifying format=text should fail on a non-UTF-8 file
|
||||
with assert_http_error(400):
|
||||
self.api.read('foo/bar/baz.blob', type='file', format='text')
|
||||
|
||||
def test_get_binary_file_contents(self):
|
||||
for d, name in self.dirs_nbs:
|
||||
path = url_path_join(d, name + '.blob')
|
||||
model = self.api.read(path).json()
|
||||
self.assertEqual(model['name'], f'{name}.blob')
|
||||
self.assertEqual(model['path'], path)
|
||||
self.assertIn('content', model)
|
||||
self.assertEqual(model['format'], 'base64')
|
||||
self.assertEqual(model['type'], 'file')
|
||||
self.assertEqual(
|
||||
decodebytes(model['content'].encode('ascii')),
|
||||
self._blob_for_name(name),
|
||||
)
|
||||
|
||||
# Name that doesn't exist - should be a 404
|
||||
with assert_http_error(404):
|
||||
self.api.read('foo/q.txt')
|
||||
|
||||
def test_get_bad_type(self):
|
||||
with assert_http_error(400):
|
||||
self.api.read('unicodé', type='file') # this is a directory
|
||||
|
||||
with assert_http_error(400):
|
||||
self.api.read('unicodé/innonascii.ipynb', type='directory')
|
||||
|
||||
def _check_created(self, resp, path, type='notebook'):
|
||||
self.assertEqual(resp.status_code, 201)
|
||||
location_header = py3compat.str_to_unicode(resp.headers['Location'])
|
||||
self.assertEqual(location_header, url_path_join(self.url_prefix, 'api/contents', url_escape(path)))
|
||||
rjson = resp.json()
|
||||
self.assertEqual(rjson['name'], path.rsplit('/', 1)[-1])
|
||||
self.assertEqual(rjson['path'], path)
|
||||
self.assertEqual(rjson['type'], type)
|
||||
isright = self.isdir if type == 'directory' else self.isfile
|
||||
assert isright(path)
|
||||
|
||||
def test_create_untitled(self):
|
||||
resp = self.api.create_untitled(path='å b')
|
||||
self._check_created(resp, 'å b/Untitled.ipynb')
|
||||
|
||||
# Second time
|
||||
resp = self.api.create_untitled(path='å b')
|
||||
self._check_created(resp, 'å b/Untitled1.ipynb')
|
||||
|
||||
# And two directories down
|
||||
resp = self.api.create_untitled(path='foo/bar')
|
||||
self._check_created(resp, 'foo/bar/Untitled.ipynb')
|
||||
|
||||
def test_create_untitled_txt(self):
|
||||
resp = self.api.create_untitled(path='foo/bar', ext='.txt')
|
||||
self._check_created(resp, 'foo/bar/untitled.txt', type='file')
|
||||
|
||||
resp = self.api.read(path='foo/bar/untitled.txt')
|
||||
model = resp.json()
|
||||
self.assertEqual(model['type'], 'file')
|
||||
self.assertEqual(model['format'], 'text')
|
||||
self.assertEqual(model['content'], '')
|
||||
|
||||
def test_upload(self):
|
||||
nb = new_notebook()
|
||||
nbmodel = {'content': nb, 'type': 'notebook'}
|
||||
path = 'å b/Upload tést.ipynb'
|
||||
resp = self.api.upload(path, body=json.dumps(nbmodel))
|
||||
self._check_created(resp, path)
|
||||
|
||||
def test_mkdir_untitled(self):
|
||||
resp = self.api.mkdir_untitled(path='å b')
|
||||
self._check_created(resp, 'å b/Untitled Folder', type='directory')
|
||||
|
||||
# Second time
|
||||
resp = self.api.mkdir_untitled(path='å b')
|
||||
self._check_created(resp, 'å b/Untitled Folder 1', type='directory')
|
||||
|
||||
# And two directories down
|
||||
resp = self.api.mkdir_untitled(path='foo/bar')
|
||||
self._check_created(resp, 'foo/bar/Untitled Folder', type='directory')
|
||||
|
||||
def test_mkdir(self):
|
||||
path = 'å b/New ∂ir'
|
||||
resp = self.api.mkdir(path)
|
||||
self._check_created(resp, path, type='directory')
|
||||
|
||||
def test_mkdir_hidden_400(self):
|
||||
with assert_http_error(400):
|
||||
resp = self.api.mkdir('å b/.hidden')
|
||||
|
||||
def test_upload_txt(self):
|
||||
body = 'ünicode téxt'
|
||||
model = {
|
||||
'content' : body,
|
||||
'format' : 'text',
|
||||
'type' : 'file',
|
||||
}
|
||||
path = 'å b/Upload tést.txt'
|
||||
resp = self.api.upload(path, body=json.dumps(model))
|
||||
|
||||
# check roundtrip
|
||||
resp = self.api.read(path)
|
||||
model = resp.json()
|
||||
self.assertEqual(model['type'], 'file')
|
||||
self.assertEqual(model['format'], 'text')
|
||||
self.assertEqual(model['content'], body)
|
||||
|
||||
def test_upload_b64(self):
|
||||
body = b'\xFFblob'
|
||||
b64body = encodebytes(body).decode('ascii')
|
||||
model = {
|
||||
'content' : b64body,
|
||||
'format' : 'base64',
|
||||
'type' : 'file',
|
||||
}
|
||||
path = 'å b/Upload tést.blob'
|
||||
resp = self.api.upload(path, body=json.dumps(model))
|
||||
|
||||
# check roundtrip
|
||||
resp = self.api.read(path)
|
||||
model = resp.json()
|
||||
self.assertEqual(model['type'], 'file')
|
||||
self.assertEqual(model['path'], path)
|
||||
self.assertEqual(model['format'], 'base64')
|
||||
decoded = decodebytes(model['content'].encode('ascii'))
|
||||
self.assertEqual(decoded, body)
|
||||
|
||||
def test_upload_v2(self):
|
||||
nb = v2.new_notebook()
|
||||
ws = v2.new_worksheet()
|
||||
nb.worksheets.append(ws)
|
||||
ws.cells.append(v2.new_code_cell(input='print("hi")'))
|
||||
nbmodel = {'content': nb, 'type': 'notebook'}
|
||||
path = 'å b/Upload tést.ipynb'
|
||||
resp = self.api.upload(path, body=json.dumps(nbmodel))
|
||||
self._check_created(resp, path)
|
||||
resp = self.api.read(path)
|
||||
data = resp.json()
|
||||
self.assertEqual(data['content']['nbformat'], 4)
|
||||
|
||||
def test_copy(self):
|
||||
resp = self.api.copy('å b/ç d.ipynb', 'å b')
|
||||
self._check_created(resp, 'å b/ç d-Copy1.ipynb')
|
||||
|
||||
resp = self.api.copy('å b/ç d.ipynb', 'å b')
|
||||
self._check_created(resp, 'å b/ç d-Copy2.ipynb')
|
||||
|
||||
def test_copy_copy(self):
|
||||
resp = self.api.copy('å b/ç d.ipynb', 'å b')
|
||||
self._check_created(resp, 'å b/ç d-Copy1.ipynb')
|
||||
|
||||
resp = self.api.copy('å b/ç d-Copy1.ipynb', 'å b')
|
||||
self._check_created(resp, 'å b/ç d-Copy2.ipynb')
|
||||
|
||||
def test_copy_path(self):
|
||||
resp = self.api.copy('foo/a.ipynb', 'å b')
|
||||
self._check_created(resp, 'å b/a.ipynb')
|
||||
|
||||
resp = self.api.copy('foo/a.ipynb', 'å b')
|
||||
self._check_created(resp, 'å b/a-Copy1.ipynb')
|
||||
|
||||
def test_copy_put_400(self):
|
||||
with assert_http_error(400):
|
||||
resp = self.api.copy_put('å b/ç d.ipynb', 'å b/cøpy.ipynb')
|
||||
|
||||
def test_copy_dir_400(self):
|
||||
# can't copy directories
|
||||
with assert_http_error(400):
|
||||
resp = self.api.copy('å b', 'foo')
|
||||
|
||||
def test_delete(self):
|
||||
for d, name in self.dirs_nbs:
|
||||
print(f'{d!r}, {name!r}')
|
||||
resp = self.api.delete(url_path_join(d, name + '.ipynb'))
|
||||
self.assertEqual(resp.status_code, 204)
|
||||
|
||||
for d in self.dirs + ['/']:
|
||||
nbs = notebooks_only(self.api.list(d).json())
|
||||
print('------')
|
||||
print(d)
|
||||
print(nbs)
|
||||
self.assertEqual(nbs, [])
|
||||
|
||||
def test_delete_dirs(self):
|
||||
# depth-first delete everything, so we don't try to delete empty directories
|
||||
for name in sorted(self.dirs + ['/'], key=len, reverse=True):
|
||||
listing = self.api.list(name).json()['content']
|
||||
for model in listing:
|
||||
self.api.delete(model['path'])
|
||||
listing = self.api.list('/').json()['content']
|
||||
self.assertEqual(listing, [])
|
||||
|
||||
def test_delete_non_empty_dir(self):
|
||||
if sys.platform == 'win32':
|
||||
self.skipTest("Disabled deleting non-empty dirs on Windows")
|
||||
# Test that non empty directory can be deleted
|
||||
try:
|
||||
self.api.delete('å b')
|
||||
except requests.HTTPError as e:
|
||||
if e.response.status_code == 400:
|
||||
if not self.can_send2trash('å b'):
|
||||
self.skipTest("Dir can't be sent to trash")
|
||||
raise
|
||||
# Check if directory has actually been deleted
|
||||
with assert_http_error(404):
|
||||
self.api.list('å b')
|
||||
|
||||
def test_rename(self):
|
||||
resp = self.api.rename('foo/a.ipynb', 'foo/z.ipynb')
|
||||
self.assertEqual(resp.headers['Location'].split('/')[-1], 'z.ipynb')
|
||||
self.assertEqual(resp.json()['name'], 'z.ipynb')
|
||||
self.assertEqual(resp.json()['path'], 'foo/z.ipynb')
|
||||
assert self.isfile('foo/z.ipynb')
|
||||
|
||||
nbs = notebooks_only(self.api.list('foo').json())
|
||||
nbnames = {n['name'] for n in nbs}
|
||||
self.assertIn('z.ipynb', nbnames)
|
||||
self.assertNotIn('a.ipynb', nbnames)
|
||||
|
||||
def test_checkpoints_follow_file(self):
|
||||
|
||||
# Read initial file state
|
||||
orig = self.api.read('foo/a.ipynb')
|
||||
|
||||
# Create a checkpoint of initial state
|
||||
r = self.api.new_checkpoint('foo/a.ipynb')
|
||||
cp1 = r.json()
|
||||
|
||||
# Modify file and save
|
||||
nbcontent = json.loads(orig.text)['content']
|
||||
nb = from_dict(nbcontent)
|
||||
hcell = new_markdown_cell('Created by test')
|
||||
nb.cells.append(hcell)
|
||||
nbmodel = {'content': nb, 'type': 'notebook'}
|
||||
self.api.save('foo/a.ipynb', body=json.dumps(nbmodel))
|
||||
|
||||
# Rename the file.
|
||||
self.api.rename('foo/a.ipynb', 'foo/z.ipynb')
|
||||
|
||||
# Looking for checkpoints in the old location should yield no results.
|
||||
self.assertEqual(self.api.get_checkpoints('foo/a.ipynb').json(), [])
|
||||
|
||||
# Looking for checkpoints in the new location should work.
|
||||
cps = self.api.get_checkpoints('foo/z.ipynb').json()
|
||||
self.assertEqual(cps, [cp1])
|
||||
|
||||
# Delete the file. The checkpoint should be deleted as well.
|
||||
self.api.delete('foo/z.ipynb')
|
||||
cps = self.api.get_checkpoints('foo/z.ipynb').json()
|
||||
self.assertEqual(cps, [])
|
||||
|
||||
def test_rename_existing(self):
|
||||
with assert_http_error(409):
|
||||
self.api.rename('foo/a.ipynb', 'foo/b.ipynb')
|
||||
|
||||
def test_save(self):
|
||||
resp = self.api.read('foo/a.ipynb')
|
||||
nbcontent = json.loads(resp.text)['content']
|
||||
nb = from_dict(nbcontent)
|
||||
nb.cells.append(new_markdown_cell('Created by test ³'))
|
||||
|
||||
nbmodel = {'content': nb, 'type': 'notebook'}
|
||||
resp = self.api.save('foo/a.ipynb', body=json.dumps(nbmodel))
|
||||
|
||||
nbcontent = self.api.read('foo/a.ipynb').json()['content']
|
||||
newnb = from_dict(nbcontent)
|
||||
self.assertEqual(newnb.cells[0].source,
|
||||
'Created by test ³')
|
||||
|
||||
def test_checkpoints(self):
|
||||
resp = self.api.read('foo/a.ipynb')
|
||||
r = self.api.new_checkpoint('foo/a.ipynb')
|
||||
self.assertEqual(r.status_code, 201)
|
||||
cp1 = r.json()
|
||||
self.assertEqual(set(cp1), {'id', 'last_modified'})
|
||||
self.assertEqual(r.headers['Location'].split('/')[-1], cp1['id'])
|
||||
|
||||
# Modify it
|
||||
nbcontent = json.loads(resp.text)['content']
|
||||
nb = from_dict(nbcontent)
|
||||
hcell = new_markdown_cell('Created by test')
|
||||
nb.cells.append(hcell)
|
||||
# Save
|
||||
nbmodel= {'content': nb, 'type': 'notebook'}
|
||||
resp = self.api.save('foo/a.ipynb', body=json.dumps(nbmodel))
|
||||
|
||||
# List checkpoints
|
||||
cps = self.api.get_checkpoints('foo/a.ipynb').json()
|
||||
self.assertEqual(cps, [cp1])
|
||||
|
||||
nbcontent = self.api.read('foo/a.ipynb').json()['content']
|
||||
nb = from_dict(nbcontent)
|
||||
self.assertEqual(nb.cells[0].source, 'Created by test')
|
||||
|
||||
# Restore cp1
|
||||
r = self.api.restore_checkpoint('foo/a.ipynb', cp1['id'])
|
||||
self.assertEqual(r.status_code, 204)
|
||||
nbcontent = self.api.read('foo/a.ipynb').json()['content']
|
||||
nb = from_dict(nbcontent)
|
||||
self.assertEqual(nb.cells, [])
|
||||
|
||||
# Delete cp1
|
||||
r = self.api.delete_checkpoint('foo/a.ipynb', cp1['id'])
|
||||
self.assertEqual(r.status_code, 204)
|
||||
cps = self.api.get_checkpoints('foo/a.ipynb').json()
|
||||
self.assertEqual(cps, [])
|
||||
|
||||
def test_file_checkpoints(self):
|
||||
"""
|
||||
Test checkpointing of non-notebook files.
|
||||
"""
|
||||
filename = 'foo/a.txt'
|
||||
resp = self.api.read(filename)
|
||||
orig_content = json.loads(resp.text)['content']
|
||||
|
||||
# Create a checkpoint.
|
||||
r = self.api.new_checkpoint(filename)
|
||||
self.assertEqual(r.status_code, 201)
|
||||
cp1 = r.json()
|
||||
self.assertEqual(set(cp1), {'id', 'last_modified'})
|
||||
self.assertEqual(r.headers['Location'].split('/')[-1], cp1['id'])
|
||||
|
||||
# Modify the file and save.
|
||||
new_content = orig_content + '\nsecond line'
|
||||
model = {
|
||||
'content': new_content,
|
||||
'type': 'file',
|
||||
'format': 'text',
|
||||
}
|
||||
resp = self.api.save(filename, body=json.dumps(model))
|
||||
|
||||
# List checkpoints
|
||||
cps = self.api.get_checkpoints(filename).json()
|
||||
self.assertEqual(cps, [cp1])
|
||||
|
||||
content = self.api.read(filename).json()['content']
|
||||
self.assertEqual(content, new_content)
|
||||
|
||||
# Restore cp1
|
||||
r = self.api.restore_checkpoint(filename, cp1['id'])
|
||||
self.assertEqual(r.status_code, 204)
|
||||
restored_content = self.api.read(filename).json()['content']
|
||||
self.assertEqual(restored_content, orig_content)
|
||||
|
||||
# Delete cp1
|
||||
r = self.api.delete_checkpoint(filename, cp1['id'])
|
||||
self.assertEqual(r.status_code, 204)
|
||||
cps = self.api.get_checkpoints(filename).json()
|
||||
self.assertEqual(cps, [])
|
||||
|
||||
@contextmanager
|
||||
def patch_cp_root(self, dirname):
|
||||
"""
|
||||
Temporarily patch the root dir of our checkpoint manager.
|
||||
"""
|
||||
cpm = self.notebook.contents_manager.checkpoints
|
||||
old_dirname = cpm.root_dir
|
||||
cpm.root_dir = dirname
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
cpm.root_dir = old_dirname
|
||||
|
||||
def test_checkpoints_separate_root(self):
|
||||
"""
|
||||
Test that FileCheckpoints functions correctly even when it's
|
||||
using a different root dir from FileContentsManager. This also keeps
|
||||
the implementation honest for use with ContentsManagers that don't map
|
||||
models to the filesystem
|
||||
|
||||
Override this method to a no-op when testing other managers.
|
||||
"""
|
||||
with TemporaryDirectory() as td:
|
||||
with self.patch_cp_root(td):
|
||||
self.test_checkpoints()
|
||||
|
||||
with TemporaryDirectory() as td:
|
||||
with self.patch_cp_root(td):
|
||||
self.test_file_checkpoints()
|
||||
|
||||
|
||||
class GenericFileCheckpointsAPITest(APITest):
|
||||
"""
|
||||
Run the tests from APITest with GenericFileCheckpoints.
|
||||
"""
|
||||
config = Config()
|
||||
config.FileContentsManager.checkpoints_class = GenericFileCheckpoints
|
||||
|
||||
def test_config_did_something(self):
|
||||
|
||||
self.assertIsInstance(
|
||||
self.notebook.contents_manager.checkpoints,
|
||||
GenericFileCheckpoints,
|
||||
)
|
||||
|
||||
|
@ -0,0 +1,132 @@
|
||||
"""Tests for file IO"""
|
||||
|
||||
# Copyright (c) Jupyter Development Team.
|
||||
# Distributed under the terms of the Modified BSD License.
|
||||
|
||||
import os.path
|
||||
import unittest
|
||||
import pytest
|
||||
import stat
|
||||
import sys
|
||||
|
||||
from ..fileio import atomic_writing
|
||||
|
||||
from ipython_genutils.tempdir import TemporaryDirectory
|
||||
|
||||
umask = 0
|
||||
|
||||
def test_atomic_writing():
|
||||
class CustomExc(Exception):
|
||||
pass
|
||||
|
||||
with TemporaryDirectory() as td:
|
||||
f1 = os.path.join(td, 'penguin')
|
||||
with open(f1, 'w') as f:
|
||||
f.write('Before')
|
||||
|
||||
if os.name != 'nt':
|
||||
os.chmod(f1, 0o701)
|
||||
orig_mode = stat.S_IMODE(os.stat(f1).st_mode)
|
||||
|
||||
f2 = os.path.join(td, 'flamingo')
|
||||
try:
|
||||
os.symlink(f1, f2)
|
||||
have_symlink = True
|
||||
except (AttributeError, NotImplementedError, OSError):
|
||||
# AttributeError: Python doesn't support it
|
||||
# NotImplementedError: The system doesn't support it
|
||||
# OSError: The user lacks the privilege (Windows)
|
||||
have_symlink = False
|
||||
|
||||
with pytest.raises(CustomExc):
|
||||
with atomic_writing(f1) as f:
|
||||
f.write('Failing write')
|
||||
raise CustomExc
|
||||
|
||||
# Because of the exception, the file should not have been modified
|
||||
with open(f1) as f:
|
||||
assert f.read() == 'Before'
|
||||
|
||||
with atomic_writing(f1) as f:
|
||||
f.write('Overwritten')
|
||||
|
||||
with open(f1) as f:
|
||||
assert f.read() == 'Overwritten'
|
||||
|
||||
if os.name != 'nt':
|
||||
mode = stat.S_IMODE(os.stat(f1).st_mode)
|
||||
assert mode == orig_mode
|
||||
|
||||
if have_symlink:
|
||||
# Check that writing over a file preserves a symlink
|
||||
with atomic_writing(f2) as f:
|
||||
f.write('written from symlink')
|
||||
|
||||
with open(f1) as f:
|
||||
assert f.read() == 'written from symlink'
|
||||
|
||||
class TestWithSetUmask(unittest.TestCase):
|
||||
def setUp(self):
|
||||
# save umask
|
||||
global umask
|
||||
umask = os.umask(0)
|
||||
os.umask(umask)
|
||||
|
||||
def tearDown(self):
|
||||
# restore umask
|
||||
os.umask(umask)
|
||||
|
||||
@pytest.mark.skipif(sys.platform == "win32", reason="do not run on windows")
|
||||
def test_atomic_writing_umask(self):
|
||||
with TemporaryDirectory() as td:
|
||||
os.umask(0o022)
|
||||
f1 = os.path.join(td, '1')
|
||||
with atomic_writing(f1) as f:
|
||||
f.write('1')
|
||||
mode = stat.S_IMODE(os.stat(f1).st_mode)
|
||||
assert mode == 0o644
|
||||
|
||||
os.umask(0o057)
|
||||
f2 = os.path.join(td, '2')
|
||||
with atomic_writing(f2) as f:
|
||||
f.write('2')
|
||||
mode = stat.S_IMODE(os.stat(f2).st_mode)
|
||||
assert mode == 0o620
|
||||
|
||||
|
||||
def test_atomic_writing_newlines():
|
||||
with TemporaryDirectory() as td:
|
||||
path = os.path.join(td, 'testfile')
|
||||
|
||||
lf = 'a\nb\nc\n'
|
||||
plat = lf.replace('\n', os.linesep)
|
||||
crlf = lf.replace('\n', '\r\n')
|
||||
|
||||
# test default
|
||||
with open(path, 'w') as f:
|
||||
f.write(lf)
|
||||
with open(path, newline='') as f:
|
||||
read = f.read()
|
||||
assert read == plat
|
||||
|
||||
# test newline=LF
|
||||
with open(path, 'w', newline='\n') as f:
|
||||
f.write(lf)
|
||||
with open(path, newline='') as f:
|
||||
read = f.read()
|
||||
assert read == lf
|
||||
|
||||
# test newline=CRLF
|
||||
with atomic_writing(path, newline='\r\n') as f:
|
||||
f.write(lf)
|
||||
with open(path, newline='') as f:
|
||||
read = f.read()
|
||||
assert read == crlf
|
||||
|
||||
# test newline=no convert
|
||||
text = 'crlf\r\ncr\rlf\n'
|
||||
with atomic_writing(path, newline='') as f:
|
||||
f.write(text)
|
||||
with open(path, newline='') as f:
|
||||
read = f.read()
|
||||
assert read == text
|
@ -0,0 +1,113 @@
|
||||
from unittest import TestCase
|
||||
from ipython_genutils.tempdir import TemporaryDirectory
|
||||
from ..largefilemanager import LargeFileManager
|
||||
import os
|
||||
from tornado import web
|
||||
|
||||
|
||||
def _make_dir(contents_manager, api_path):
|
||||
"""
|
||||
Make a directory.
|
||||
"""
|
||||
os_path = contents_manager._get_os_path(api_path)
|
||||
try:
|
||||
os.makedirs(os_path)
|
||||
except OSError:
|
||||
print(f"Directory already exists: {os_path!r}")
|
||||
|
||||
|
||||
class TestLargeFileManager(TestCase):
|
||||
|
||||
def setUp(self):
|
||||
self._temp_dir = TemporaryDirectory()
|
||||
self.td = self._temp_dir.name
|
||||
self.contents_manager = LargeFileManager(root_dir=self.td)
|
||||
|
||||
def make_dir(self, api_path):
|
||||
"""make a subdirectory at api_path
|
||||
|
||||
override in subclasses if contents are not on the filesystem.
|
||||
"""
|
||||
_make_dir(self.contents_manager, api_path)
|
||||
|
||||
def test_save(self):
|
||||
|
||||
cm = self.contents_manager
|
||||
# Create a notebook
|
||||
model = cm.new_untitled(type='notebook')
|
||||
name = model['name']
|
||||
path = model['path']
|
||||
|
||||
# Get the model with 'content'
|
||||
full_model = cm.get(path)
|
||||
# Save the notebook
|
||||
model = cm.save(full_model, path)
|
||||
assert isinstance(model, dict)
|
||||
self.assertIn('name', model)
|
||||
self.assertIn('path', model)
|
||||
self.assertEqual(model['name'], name)
|
||||
self.assertEqual(model['path'], path)
|
||||
|
||||
try:
|
||||
model = {'name': 'test', 'path': 'test', 'chunk': 1}
|
||||
cm.save(model, model['path'])
|
||||
except web.HTTPError as e:
|
||||
self.assertEqual('HTTP 400: Bad Request (No file type provided)', str(e))
|
||||
|
||||
try:
|
||||
model = {'name': 'test', 'path': 'test', 'chunk': 1, 'type': 'notebook'}
|
||||
cm.save(model, model['path'])
|
||||
except web.HTTPError as e:
|
||||
self.assertEqual('HTTP 400: Bad Request (File type "notebook" is not supported for large file transfer)', str(e))
|
||||
|
||||
try:
|
||||
model = {'name': 'test', 'path': 'test', 'chunk': 1, 'type': 'file'}
|
||||
cm.save(model, model['path'])
|
||||
except web.HTTPError as e:
|
||||
self.assertEqual('HTTP 400: Bad Request (No file content provided)', str(e))
|
||||
|
||||
try:
|
||||
model = {'name': 'test', 'path': 'test', 'chunk': 2, 'type': 'file',
|
||||
'content': 'test', 'format': 'json'}
|
||||
cm.save(model, model['path'])
|
||||
except web.HTTPError as e:
|
||||
self.assertEqual("HTTP 400: Bad Request (Must specify format of file contents as 'text' or 'base64')",
|
||||
str(e))
|
||||
|
||||
# Save model for different chunks
|
||||
model = {'name': 'test', 'path': 'test', 'type': 'file',
|
||||
'content': 'test==', 'format': 'text'}
|
||||
name = model['name']
|
||||
path = model['path']
|
||||
cm.save(model, path)
|
||||
|
||||
for chunk in (1, 2, -1):
|
||||
for fm in ('text', 'base64'):
|
||||
full_model = cm.get(path)
|
||||
full_model['chunk'] = chunk
|
||||
full_model['format'] = fm
|
||||
model_res = cm.save(full_model, path)
|
||||
assert isinstance(model_res, dict)
|
||||
|
||||
self.assertIn('name', model_res)
|
||||
self.assertIn('path', model_res)
|
||||
self.assertNotIn('chunk', model_res)
|
||||
self.assertEqual(model_res['name'], name)
|
||||
self.assertEqual(model_res['path'], path)
|
||||
|
||||
# Test in sub-directory
|
||||
# Create a directory and notebook in that directory
|
||||
sub_dir = '/foo/'
|
||||
self.make_dir('foo')
|
||||
model = cm.new_untitled(path=sub_dir, type='notebook')
|
||||
name = model['name']
|
||||
path = model['path']
|
||||
model = cm.get(path)
|
||||
|
||||
# Change the name in the model for rename
|
||||
model = cm.save(model, path)
|
||||
assert isinstance(model, dict)
|
||||
self.assertIn('name', model)
|
||||
self.assertIn('path', model)
|
||||
self.assertEqual(model['name'], 'Untitled.ipynb')
|
||||
self.assertEqual(model['path'], 'foo/Untitled.ipynb')
|
@ -0,0 +1,658 @@
|
||||
"""Tests for the notebook manager."""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
from contextlib import contextmanager
|
||||
from itertools import combinations
|
||||
|
||||
from tornado.web import HTTPError
|
||||
from unittest import TestCase, skipIf
|
||||
from tempfile import NamedTemporaryFile
|
||||
|
||||
from nbformat import v4 as nbformat
|
||||
|
||||
from ipython_genutils.tempdir import TemporaryDirectory
|
||||
from traitlets import TraitError
|
||||
|
||||
from ..filemanager import FileContentsManager
|
||||
|
||||
|
||||
def _make_dir(contents_manager, api_path):
|
||||
"""
|
||||
Make a directory.
|
||||
"""
|
||||
os_path = contents_manager._get_os_path(api_path)
|
||||
try:
|
||||
os.makedirs(os_path)
|
||||
except OSError:
|
||||
print(f"Directory already exists: {os_path!r}")
|
||||
|
||||
|
||||
class TestFileContentsManager(TestCase):
|
||||
|
||||
@contextmanager
|
||||
def assertRaisesHTTPError(self, status, msg=None):
|
||||
msg = msg or f"Should have raised HTTPError({status})"
|
||||
try:
|
||||
yield
|
||||
except HTTPError as e:
|
||||
self.assertEqual(e.status_code, status)
|
||||
else:
|
||||
self.fail(msg)
|
||||
|
||||
def symlink(self, contents_manager, src, dst):
|
||||
"""Make a symlink to src from dst
|
||||
|
||||
src and dst are api_paths
|
||||
"""
|
||||
src_os_path = contents_manager._get_os_path(src)
|
||||
dst_os_path = contents_manager._get_os_path(dst)
|
||||
print(src_os_path, dst_os_path, os.path.isfile(src_os_path))
|
||||
os.symlink(src_os_path, dst_os_path)
|
||||
|
||||
def test_root_dir(self):
|
||||
with TemporaryDirectory() as td:
|
||||
fm = FileContentsManager(root_dir=td)
|
||||
self.assertEqual(fm.root_dir, td)
|
||||
|
||||
def test_missing_root_dir(self):
|
||||
with TemporaryDirectory() as td:
|
||||
root = os.path.join(td, 'notebook', 'dir', 'is', 'missing')
|
||||
self.assertRaises(TraitError, FileContentsManager, root_dir=root)
|
||||
|
||||
def test_invalid_root_dir(self):
|
||||
with NamedTemporaryFile() as tf:
|
||||
self.assertRaises(TraitError, FileContentsManager, root_dir=tf.name)
|
||||
|
||||
def test_get_os_path(self):
|
||||
# full filesystem path should be returned with correct operating system
|
||||
# separators.
|
||||
with TemporaryDirectory() as td:
|
||||
root = td
|
||||
fm = FileContentsManager(root_dir=root)
|
||||
path = fm._get_os_path('/path/to/notebook/test.ipynb')
|
||||
rel_path_list = '/path/to/notebook/test.ipynb'.split('/')
|
||||
fs_path = os.path.join(fm.root_dir, *rel_path_list)
|
||||
self.assertEqual(path, fs_path)
|
||||
|
||||
fm = FileContentsManager(root_dir=root)
|
||||
path = fm._get_os_path('test.ipynb')
|
||||
fs_path = os.path.join(fm.root_dir, 'test.ipynb')
|
||||
self.assertEqual(path, fs_path)
|
||||
|
||||
fm = FileContentsManager(root_dir=root)
|
||||
path = fm._get_os_path('////test.ipynb')
|
||||
fs_path = os.path.join(fm.root_dir, 'test.ipynb')
|
||||
self.assertEqual(path, fs_path)
|
||||
|
||||
def test_checkpoint_subdir(self):
|
||||
subd = 'sub ∂ir'
|
||||
cp_name = 'test-cp.ipynb'
|
||||
with TemporaryDirectory() as td:
|
||||
root = td
|
||||
os.mkdir(os.path.join(td, subd))
|
||||
fm = FileContentsManager(root_dir=root)
|
||||
cpm = fm.checkpoints
|
||||
cp_dir = cpm.checkpoint_path(
|
||||
'cp', 'test.ipynb'
|
||||
)
|
||||
cp_subdir = cpm.checkpoint_path(
|
||||
'cp', f'/{subd}/test.ipynb'
|
||||
)
|
||||
self.assertNotEqual(cp_dir, cp_subdir)
|
||||
self.assertEqual(cp_dir, os.path.join(root, cpm.checkpoint_dir, cp_name))
|
||||
self.assertEqual(cp_subdir, os.path.join(root, subd, cpm.checkpoint_dir, cp_name))
|
||||
|
||||
def test_bad_symlink(self):
|
||||
with TemporaryDirectory() as td:
|
||||
cm = FileContentsManager(root_dir=td)
|
||||
path = 'test bad symlink'
|
||||
_make_dir(cm, path)
|
||||
|
||||
file_model = cm.new_untitled(path=path, ext='.txt')
|
||||
|
||||
# create a broken symlink
|
||||
self.symlink(cm, "target", f'{path}/{"bad symlink"}')
|
||||
model = cm.get(path)
|
||||
|
||||
contents = {
|
||||
content['name']: content for content in model['content']
|
||||
}
|
||||
self.assertTrue('untitled.txt' in contents)
|
||||
self.assertEqual(contents['untitled.txt'], file_model)
|
||||
# broken symlinks should still be shown in the contents manager
|
||||
self.assertTrue('bad symlink' in contents)
|
||||
|
||||
@skipIf(sys.platform == 'win32', "will not run on windows")
|
||||
def test_recursive_symlink(self):
|
||||
with TemporaryDirectory() as td:
|
||||
cm = FileContentsManager(root_dir=td)
|
||||
path = 'test recursive symlink'
|
||||
_make_dir(cm, path)
|
||||
os_path = cm._get_os_path(path)
|
||||
os.symlink("recursive", os.path.join(os_path, "recursive"))
|
||||
file_model = cm.new_untitled(path=path, ext='.txt')
|
||||
|
||||
model = cm.get(path)
|
||||
|
||||
contents = {
|
||||
content['name']: content for content in model['content']
|
||||
}
|
||||
self.assertIn('untitled.txt', contents)
|
||||
self.assertEqual(contents['untitled.txt'], file_model)
|
||||
# recursive symlinks should not be shown in the contents manager
|
||||
self.assertNotIn('recursive', contents)
|
||||
|
||||
def test_good_symlink(self):
|
||||
with TemporaryDirectory() as td:
|
||||
cm = FileContentsManager(root_dir=td)
|
||||
parent = 'test good symlink'
|
||||
name = 'good symlink'
|
||||
path = f'{parent}/{name}'
|
||||
_make_dir(cm, parent)
|
||||
|
||||
file_model = cm.new(path=parent + '/zfoo.txt')
|
||||
|
||||
# create a good symlink
|
||||
self.symlink(cm, file_model['path'], path)
|
||||
symlink_model = cm.get(path, content=False)
|
||||
dir_model = cm.get(parent)
|
||||
self.assertEqual(
|
||||
sorted(dir_model['content'], key=lambda x: x['name']),
|
||||
[symlink_model, file_model],
|
||||
)
|
||||
|
||||
|
||||
@skipIf(hasattr(os, 'getuid') and os.getuid() == 0, "Can't test permissions as root")
|
||||
@skipIf(sys.platform.startswith('win'), "Can't test permissions on Windows")
|
||||
def test_403(self):
|
||||
with TemporaryDirectory() as td:
|
||||
cm = FileContentsManager(root_dir=td)
|
||||
model = cm.new_untitled(type='file')
|
||||
os_path = cm._get_os_path(model['path'])
|
||||
|
||||
os.chmod(os_path, 0o400)
|
||||
try:
|
||||
with cm.open(os_path, 'w') as f:
|
||||
f.write("don't care")
|
||||
except HTTPError as e:
|
||||
self.assertEqual(e.status_code, 403)
|
||||
else:
|
||||
self.fail("Should have raised HTTPError(403)")
|
||||
|
||||
def test_escape_root(self):
|
||||
with TemporaryDirectory() as td:
|
||||
cm = FileContentsManager(root_dir=td)
|
||||
# make foo, bar next to root
|
||||
with open(os.path.join(cm.root_dir, '..', 'foo'), 'w') as f:
|
||||
f.write('foo')
|
||||
with open(os.path.join(cm.root_dir, '..', 'bar'), 'w') as f:
|
||||
f.write('bar')
|
||||
|
||||
with self.assertRaisesHTTPError(404):
|
||||
cm.get('..')
|
||||
with self.assertRaisesHTTPError(404):
|
||||
cm.get('foo/../../../bar')
|
||||
with self.assertRaisesHTTPError(404):
|
||||
cm.delete('../foo')
|
||||
with self.assertRaisesHTTPError(404):
|
||||
cm.rename('../foo', '../bar')
|
||||
with self.assertRaisesHTTPError(404):
|
||||
cm.save(model={
|
||||
'type': 'file',
|
||||
'content': '',
|
||||
'format': 'text',
|
||||
}, path='../foo')
|
||||
|
||||
|
||||
class TestContentsManager(TestCase):
|
||||
@contextmanager
|
||||
def assertRaisesHTTPError(self, status, msg=None):
|
||||
msg = msg or f"Should have raised HTTPError({status})"
|
||||
try:
|
||||
yield
|
||||
except HTTPError as e:
|
||||
self.assertEqual(e.status_code, status)
|
||||
else:
|
||||
self.fail(msg)
|
||||
|
||||
def make_populated_dir(self, api_path):
|
||||
cm = self.contents_manager
|
||||
|
||||
self.make_dir(api_path)
|
||||
|
||||
cm.new(path="/".join([api_path, "nb.ipynb"]))
|
||||
cm.new(path="/".join([api_path, "file.txt"]))
|
||||
|
||||
def check_populated_dir_files(self, api_path):
|
||||
dir_model = self.contents_manager.get(api_path)
|
||||
|
||||
self.assertEqual(dir_model['path'], api_path)
|
||||
self.assertEqual(dir_model['type'], "directory")
|
||||
|
||||
for entry in dir_model['content']:
|
||||
if entry['type'] == "directory":
|
||||
continue
|
||||
elif entry['type'] == "file":
|
||||
self.assertEqual(entry['name'], "file.txt")
|
||||
complete_path = "/".join([api_path, "file.txt"])
|
||||
self.assertEqual(entry["path"], complete_path)
|
||||
elif entry['type'] == "notebook":
|
||||
self.assertEqual(entry['name'], "nb.ipynb")
|
||||
complete_path = "/".join([api_path, "nb.ipynb"])
|
||||
self.assertEqual(entry["path"], complete_path)
|
||||
|
||||
def setUp(self):
|
||||
self._temp_dir = TemporaryDirectory()
|
||||
self.td = self._temp_dir.name
|
||||
self.contents_manager = FileContentsManager(
|
||||
root_dir=self.td,
|
||||
)
|
||||
|
||||
def tearDown(self):
|
||||
self._temp_dir.cleanup()
|
||||
|
||||
def make_dir(self, api_path):
|
||||
"""make a subdirectory at api_path
|
||||
|
||||
override in subclasses if contents are not on the filesystem.
|
||||
"""
|
||||
_make_dir(self.contents_manager, api_path)
|
||||
|
||||
def add_code_cell(self, nb):
|
||||
output = nbformat.new_output("display_data", {'application/javascript': "alert('hi');"})
|
||||
cell = nbformat.new_code_cell("print('hi')", outputs=[output])
|
||||
nb.cells.append(cell)
|
||||
|
||||
def new_notebook(self):
|
||||
cm = self.contents_manager
|
||||
model = cm.new_untitled(type='notebook')
|
||||
name = model['name']
|
||||
path = model['path']
|
||||
|
||||
full_model = cm.get(path)
|
||||
nb = full_model['content']
|
||||
nb['metadata']['counter'] = int(1e6 * time.time())
|
||||
self.add_code_cell(nb)
|
||||
|
||||
cm.save(full_model, path)
|
||||
return nb, name, path
|
||||
|
||||
def test_new_untitled(self):
|
||||
cm = self.contents_manager
|
||||
# Test in root directory
|
||||
model = cm.new_untitled(type='notebook')
|
||||
assert isinstance(model, dict)
|
||||
self.assertIn('name', model)
|
||||
self.assertIn('path', model)
|
||||
self.assertIn('type', model)
|
||||
self.assertEqual(model['type'], 'notebook')
|
||||
self.assertEqual(model['name'], 'Untitled.ipynb')
|
||||
self.assertEqual(model['path'], 'Untitled.ipynb')
|
||||
|
||||
# Test in sub-directory
|
||||
model = cm.new_untitled(type='directory')
|
||||
assert isinstance(model, dict)
|
||||
self.assertIn('name', model)
|
||||
self.assertIn('path', model)
|
||||
self.assertIn('type', model)
|
||||
self.assertEqual(model['type'], 'directory')
|
||||
self.assertEqual(model['name'], 'Untitled Folder')
|
||||
self.assertEqual(model['path'], 'Untitled Folder')
|
||||
sub_dir = model['path']
|
||||
|
||||
model = cm.new_untitled(path=sub_dir)
|
||||
assert isinstance(model, dict)
|
||||
self.assertIn('name', model)
|
||||
self.assertIn('path', model)
|
||||
self.assertIn('type', model)
|
||||
self.assertEqual(model['type'], 'file')
|
||||
self.assertEqual(model['name'], 'untitled')
|
||||
self.assertEqual(model['path'], f'{sub_dir}/untitled')
|
||||
|
||||
# Test with a compound extension
|
||||
model = cm.new_untitled(path=sub_dir, ext='.foo.bar')
|
||||
self.assertEqual(model['name'], 'untitled.foo.bar')
|
||||
model = cm.new_untitled(path=sub_dir, ext='.foo.bar')
|
||||
self.assertEqual(model['name'], 'untitled1.foo.bar')
|
||||
|
||||
def test_modified_date(self):
|
||||
|
||||
cm = self.contents_manager
|
||||
|
||||
# Create a new notebook.
|
||||
nb, name, path = self.new_notebook()
|
||||
model = cm.get(path)
|
||||
|
||||
# Add a cell and save.
|
||||
self.add_code_cell(model['content'])
|
||||
cm.save(model, path)
|
||||
|
||||
# Reload notebook and verify that last_modified incremented.
|
||||
saved = cm.get(path)
|
||||
self.assertGreaterEqual(saved['last_modified'], model['last_modified'])
|
||||
|
||||
# Move the notebook and verify that last_modified stayed the same.
|
||||
# (The frontend fires a warning if last_modified increases on the
|
||||
# renamed file.)
|
||||
new_path = 'renamed.ipynb'
|
||||
cm.rename(path, new_path)
|
||||
renamed = cm.get(new_path)
|
||||
self.assertGreaterEqual(
|
||||
renamed['last_modified'],
|
||||
saved['last_modified'],
|
||||
)
|
||||
|
||||
def test_get(self):
|
||||
cm = self.contents_manager
|
||||
# Create a notebook
|
||||
model = cm.new_untitled(type='notebook')
|
||||
name = model['name']
|
||||
path = model['path']
|
||||
|
||||
# Check that we 'get' on the notebook we just created
|
||||
model2 = cm.get(path)
|
||||
assert isinstance(model2, dict)
|
||||
self.assertIn('name', model2)
|
||||
self.assertIn('path', model2)
|
||||
self.assertEqual(model['name'], name)
|
||||
self.assertEqual(model['path'], path)
|
||||
|
||||
nb_as_file = cm.get(path, content=True, type='file')
|
||||
self.assertEqual(nb_as_file['path'], path)
|
||||
self.assertEqual(nb_as_file['type'], 'file')
|
||||
self.assertEqual(nb_as_file['format'], 'text')
|
||||
self.assertNotIsInstance(nb_as_file['content'], dict)
|
||||
|
||||
nb_as_bin_file = cm.get(path, content=True, type='file', format='base64')
|
||||
self.assertEqual(nb_as_bin_file['format'], 'base64')
|
||||
|
||||
# Test in sub-directory
|
||||
sub_dir = '/foo/'
|
||||
self.make_dir('foo')
|
||||
model = cm.new_untitled(path=sub_dir, ext='.ipynb')
|
||||
model2 = cm.get(sub_dir + name)
|
||||
assert isinstance(model2, dict)
|
||||
self.assertIn('name', model2)
|
||||
self.assertIn('path', model2)
|
||||
self.assertIn('content', model2)
|
||||
self.assertEqual(model2['name'], 'Untitled.ipynb')
|
||||
self.assertEqual(model2['path'], f'{sub_dir.strip("/")}/{name}')
|
||||
|
||||
# Test with a regular file.
|
||||
file_model_path = cm.new_untitled(path=sub_dir, ext='.txt')['path']
|
||||
file_model = cm.get(file_model_path)
|
||||
self.assertDictContainsSubset(
|
||||
{
|
||||
'content': '',
|
||||
'format': 'text',
|
||||
'mimetype': 'text/plain',
|
||||
'name': 'untitled.txt',
|
||||
'path': 'foo/untitled.txt',
|
||||
'type': 'file',
|
||||
'writable': True,
|
||||
},
|
||||
file_model,
|
||||
)
|
||||
self.assertIn('created', file_model)
|
||||
self.assertIn('last_modified', file_model)
|
||||
|
||||
# Test getting directory model
|
||||
|
||||
# Create a sub-sub directory to test getting directory contents with a
|
||||
# subdir.
|
||||
self.make_dir('foo/bar')
|
||||
dirmodel = cm.get('foo')
|
||||
self.assertEqual(dirmodel['type'], 'directory')
|
||||
self.assertIsInstance(dirmodel['content'], list)
|
||||
self.assertEqual(len(dirmodel['content']), 3)
|
||||
self.assertEqual(dirmodel['path'], 'foo')
|
||||
self.assertEqual(dirmodel['name'], 'foo')
|
||||
|
||||
# Directory contents should match the contents of each individual entry
|
||||
# when requested with content=False.
|
||||
model2_no_content = cm.get(sub_dir + name, content=False)
|
||||
file_model_no_content = cm.get('foo/untitled.txt', content=False)
|
||||
sub_sub_dir_no_content = cm.get('foo/bar', content=False)
|
||||
self.assertEqual(sub_sub_dir_no_content['path'], 'foo/bar')
|
||||
self.assertEqual(sub_sub_dir_no_content['name'], 'bar')
|
||||
|
||||
for entry in dirmodel['content']:
|
||||
# Order isn't guaranteed by the spec, so this is a hacky way of
|
||||
# verifying that all entries are matched.
|
||||
if entry['path'] == sub_sub_dir_no_content['path']:
|
||||
self.assertEqual(entry, sub_sub_dir_no_content)
|
||||
elif entry['path'] == model2_no_content['path']:
|
||||
self.assertEqual(entry, model2_no_content)
|
||||
elif entry['path'] == file_model_no_content['path']:
|
||||
self.assertEqual(entry, file_model_no_content)
|
||||
else:
|
||||
self.fail(f"Unexpected directory entry: {entry()}")
|
||||
|
||||
with self.assertRaises(HTTPError):
|
||||
cm.get('foo', type='file')
|
||||
|
||||
def test_update(self):
|
||||
cm = self.contents_manager
|
||||
# Create a notebook
|
||||
model = cm.new_untitled(type='notebook')
|
||||
name = model['name']
|
||||
path = model['path']
|
||||
|
||||
# Change the name in the model for rename
|
||||
model['path'] = 'test.ipynb'
|
||||
model = cm.update(model, path)
|
||||
assert isinstance(model, dict)
|
||||
self.assertIn('name', model)
|
||||
self.assertIn('path', model)
|
||||
self.assertEqual(model['name'], 'test.ipynb')
|
||||
|
||||
# Make sure the old name is gone
|
||||
self.assertRaises(HTTPError, cm.get, path)
|
||||
|
||||
# Test in sub-directory
|
||||
# Create a directory and notebook in that directory
|
||||
sub_dir = '/foo/'
|
||||
self.make_dir('foo')
|
||||
model = cm.new_untitled(path=sub_dir, type='notebook')
|
||||
path = model['path']
|
||||
|
||||
# Change the name in the model for rename
|
||||
d = path.rsplit('/', 1)[0]
|
||||
new_path = model['path'] = d + '/test_in_sub.ipynb'
|
||||
model = cm.update(model, path)
|
||||
assert isinstance(model, dict)
|
||||
self.assertIn('name', model)
|
||||
self.assertIn('path', model)
|
||||
self.assertEqual(model['name'], 'test_in_sub.ipynb')
|
||||
self.assertEqual(model['path'], new_path)
|
||||
|
||||
# Make sure the old name is gone
|
||||
self.assertRaises(HTTPError, cm.get, path)
|
||||
|
||||
def test_save(self):
|
||||
cm = self.contents_manager
|
||||
# Create a notebook
|
||||
model = cm.new_untitled(type='notebook')
|
||||
name = model['name']
|
||||
path = model['path']
|
||||
|
||||
# Get the model with 'content'
|
||||
full_model = cm.get(path)
|
||||
|
||||
# Save the notebook
|
||||
model = cm.save(full_model, path)
|
||||
assert isinstance(model, dict)
|
||||
self.assertIn('name', model)
|
||||
self.assertIn('path', model)
|
||||
self.assertEqual(model['name'], name)
|
||||
self.assertEqual(model['path'], path)
|
||||
|
||||
# Test in sub-directory
|
||||
# Create a directory and notebook in that directory
|
||||
sub_dir = '/foo/'
|
||||
self.make_dir('foo')
|
||||
model = cm.new_untitled(path=sub_dir, type='notebook')
|
||||
name = model['name']
|
||||
path = model['path']
|
||||
model = cm.get(path)
|
||||
|
||||
# Change the name in the model for rename
|
||||
model = cm.save(model, path)
|
||||
assert isinstance(model, dict)
|
||||
self.assertIn('name', model)
|
||||
self.assertIn('path', model)
|
||||
self.assertEqual(model['name'], 'Untitled.ipynb')
|
||||
self.assertEqual(model['path'], 'foo/Untitled.ipynb')
|
||||
|
||||
def test_delete(self):
|
||||
cm = self.contents_manager
|
||||
# Create a notebook
|
||||
nb, name, path = self.new_notebook()
|
||||
|
||||
# Delete the notebook
|
||||
cm.delete(path)
|
||||
|
||||
# Check that deleting a non-existent path raises an error.
|
||||
self.assertRaises(HTTPError, cm.delete, path)
|
||||
|
||||
# Check that a 'get' on the deleted notebook raises and error
|
||||
self.assertRaises(HTTPError, cm.get, path)
|
||||
|
||||
def test_rename(self):
|
||||
cm = self.contents_manager
|
||||
# Create a new notebook
|
||||
nb, name, path = self.new_notebook()
|
||||
|
||||
# Rename the notebook
|
||||
cm.rename(path, "changed_path")
|
||||
|
||||
# Attempting to get the notebook under the old name raises an error
|
||||
self.assertRaises(HTTPError, cm.get, path)
|
||||
# Fetching the notebook under the new name is successful
|
||||
assert isinstance(cm.get("changed_path"), dict)
|
||||
|
||||
# Test validation. Currently, only Windows has a non-empty set of invalid characters
|
||||
if sys.platform == 'win32' and isinstance(cm, FileContentsManager):
|
||||
with self.assertRaisesHTTPError(400):
|
||||
cm.rename("changed_path", "prevent: in name")
|
||||
|
||||
# Ported tests on nested directory renaming from pgcontents
|
||||
all_dirs = ['foo', 'bar', 'foo/bar', 'foo/bar/foo', 'foo/bar/foo/bar']
|
||||
unchanged_dirs = all_dirs[:2]
|
||||
changed_dirs = all_dirs[2:]
|
||||
|
||||
for _dir in all_dirs:
|
||||
self.make_populated_dir(_dir)
|
||||
self.check_populated_dir_files(_dir)
|
||||
|
||||
# Renaming to an existing directory should fail
|
||||
for src, dest in combinations(all_dirs, 2):
|
||||
with self.assertRaisesHTTPError(409):
|
||||
cm.rename(src, dest)
|
||||
|
||||
# Creating a notebook in a non_existant directory should fail
|
||||
with self.assertRaisesHTTPError(404):
|
||||
cm.new_untitled("foo/bar_diff", ext=".ipynb")
|
||||
|
||||
cm.rename("foo/bar", "foo/bar_diff")
|
||||
|
||||
# Assert that unchanged directories remain so
|
||||
for unchanged in unchanged_dirs:
|
||||
self.check_populated_dir_files(unchanged)
|
||||
|
||||
# Assert changed directories can no longer be accessed under old names
|
||||
for changed_dirname in changed_dirs:
|
||||
with self.assertRaisesHTTPError(404):
|
||||
cm.get(changed_dirname)
|
||||
|
||||
new_dirname = changed_dirname.replace("foo/bar", "foo/bar_diff", 1)
|
||||
|
||||
self.check_populated_dir_files(new_dirname)
|
||||
|
||||
# Created a notebook in the renamed directory should work
|
||||
cm.new_untitled("foo/bar_diff", ext=".ipynb")
|
||||
|
||||
def test_delete_root(self):
|
||||
cm = self.contents_manager
|
||||
with self.assertRaises(HTTPError) as err:
|
||||
cm.delete('')
|
||||
self.assertEqual(err.exception.status_code, 400)
|
||||
|
||||
def test_copy(self):
|
||||
cm = self.contents_manager
|
||||
parent = 'å b'
|
||||
name = 'nb √.ipynb'
|
||||
path = f'{parent}/{name}'
|
||||
self.make_dir(parent)
|
||||
|
||||
orig = cm.new(path=path)
|
||||
# copy with unspecified name
|
||||
copy = cm.copy(path)
|
||||
self.assertEqual(copy['name'], orig['name'].replace('.ipynb', '-Copy1.ipynb'))
|
||||
|
||||
# copy with specified name
|
||||
copy2 = cm.copy(path, 'å b/copy 2.ipynb')
|
||||
self.assertEqual(copy2['name'], 'copy 2.ipynb')
|
||||
self.assertEqual(copy2['path'], 'å b/copy 2.ipynb')
|
||||
# copy with specified path
|
||||
copy2 = cm.copy(path, '/')
|
||||
self.assertEqual(copy2['name'], name)
|
||||
self.assertEqual(copy2['path'], name)
|
||||
|
||||
def test_trust_notebook(self):
|
||||
cm = self.contents_manager
|
||||
nb, name, path = self.new_notebook()
|
||||
|
||||
untrusted = cm.get(path)['content']
|
||||
assert not cm.notary.check_cells(untrusted)
|
||||
|
||||
# print(untrusted)
|
||||
cm.trust_notebook(path)
|
||||
trusted = cm.get(path)['content']
|
||||
# print(trusted)
|
||||
assert cm.notary.check_cells(trusted)
|
||||
|
||||
def test_mark_trusted_cells(self):
|
||||
cm = self.contents_manager
|
||||
nb, name, path = self.new_notebook()
|
||||
|
||||
cm.mark_trusted_cells(nb, path)
|
||||
for cell in nb.cells:
|
||||
if cell.cell_type == 'code':
|
||||
assert not cell.metadata.trusted
|
||||
|
||||
cm.trust_notebook(path)
|
||||
nb = cm.get(path)['content']
|
||||
for cell in nb.cells:
|
||||
if cell.cell_type == 'code':
|
||||
assert cell.metadata.trusted
|
||||
|
||||
def test_check_and_sign(self):
|
||||
cm = self.contents_manager
|
||||
nb, name, path = self.new_notebook()
|
||||
|
||||
cm.mark_trusted_cells(nb, path)
|
||||
cm.check_and_sign(nb, path)
|
||||
assert not cm.notary.check_signature(nb)
|
||||
|
||||
cm.trust_notebook(path)
|
||||
nb = cm.get(path)['content']
|
||||
cm.mark_trusted_cells(nb, path)
|
||||
cm.check_and_sign(nb, path)
|
||||
assert cm.notary.check_signature(nb)
|
||||
|
||||
|
||||
class TestContentsManagerNoAtomic(TestContentsManager):
|
||||
"""
|
||||
Make same test in no atomic case than in atomic case, using inheritance
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
self._temp_dir = TemporaryDirectory()
|
||||
self.td = self._temp_dir.name
|
||||
self.contents_manager = FileContentsManager(
|
||||
root_dir = self.td,
|
||||
)
|
||||
self.contents_manager.use_atomic_writing = False
|
Reference in New Issue
Block a user