diff --git a/IPython/html/services/notebooks/__init__.py b/IPython/html/services/contents/__init__.py similarity index 100% rename from IPython/html/services/notebooks/__init__.py rename to IPython/html/services/contents/__init__.py diff --git a/IPython/html/services/notebooks/filenbmanager.py b/IPython/html/services/contents/filenbmanager.py similarity index 100% rename from IPython/html/services/notebooks/filenbmanager.py rename to IPython/html/services/contents/filenbmanager.py index b9bd389..118f02d 100644 --- a/IPython/html/services/notebooks/filenbmanager.py +++ b/IPython/html/services/contents/filenbmanager.py @@ -27,10 +27,10 @@ def sort_key(item): #----------------------------------------------------------------------------- class FileNotebookManager(NotebookManager): - + save_script = Bool(False, config=True, help="""Automatically create a Python script when saving the notebook. - + For easier use of import, %run and %load across notebooks, a .py script will be created next to any .ipynb on each save. This can also be set with the @@ -38,7 +38,7 @@ class FileNotebookManager(NotebookManager): """ ) notebook_dir = Unicode(getcwd(), config=True) - + def _notebook_dir_changed(self, name, old, new): """Do a bit of validation of the notebook dir.""" if not os.path.isabs(new): @@ -47,19 +47,19 @@ class FileNotebookManager(NotebookManager): return if not os.path.exists(new) or not os.path.isdir(new): raise TraitError("notebook dir %r is not a directory" % new) - + checkpoint_dir = Unicode('.ipynb_checkpoints', config=True, help="""The directory name in which to keep notebook checkpoints - + This is a path relative to the notebook's own directory. - + By default, it is .ipynb_checkpoints """ ) - + def _copy(self, src, dest): """copy src to dest - + like shutil.copy2, but log errors in copystat """ shutil.copyfile(src, dest) @@ -67,7 +67,7 @@ class FileNotebookManager(NotebookManager): shutil.copystat(src, dest) except OSError as e: self.log.debug("copystat on %s failed", dest, exc_info=True) - + def get_notebook_names(self, path=''): """List all notebook names in the notebook dir and path.""" path = path.strip('/') @@ -80,13 +80,13 @@ class FileNotebookManager(NotebookManager): def path_exists(self, path): """Does the API-style path (directory) actually exist? - + Parameters ---------- path : string The path to check. This is an API path (`/` separated, relative to base notebook-dir). - + Returns ------- exists : bool @@ -98,18 +98,18 @@ class FileNotebookManager(NotebookManager): def is_hidden(self, path): """Does the API style path correspond to a hidden directory or file? - + Parameters ---------- path : string The path to check. This is an API path (`/` separated, relative to base notebook-dir). - + Returns ------- exists : bool Whether the path is hidden. - + """ path = path.strip('/') os_path = self._get_os_path(path=path) @@ -204,13 +204,13 @@ class FileNotebookManager(NotebookManager): def list_notebooks(self, path): """Returns a list of dictionaries that are the standard model for all notebooks in the relative 'path'. - + Parameters ---------- path : str the URL path that describes the relative path for the listed notebooks - + Returns ------- notebooks : list of dicts @@ -225,7 +225,7 @@ class FileNotebookManager(NotebookManager): def get_notebook(self, name, path='', content=True): """ Takes a path and name for a notebook and returns its model - + Parameters ---------- name : str @@ -233,11 +233,11 @@ class FileNotebookManager(NotebookManager): path : str the URL path that describes the relative path for the notebook - + Returns ------- model : dict - the notebook model. If contents=True, returns the 'contents' + the notebook model. If contents=True, returns the 'contents' dict in the model as well. """ path = path.strip('/') @@ -284,9 +284,9 @@ class FileNotebookManager(NotebookManager): # Save the notebook file os_path = self._get_os_path(new_name, new_path) nb = current.to_notebook_json(model['content']) - + self.check_and_sign(nb, new_name, new_path) - + if 'name' in nb['metadata']: nb['metadata']['name'] = u'' try: @@ -325,7 +325,7 @@ class FileNotebookManager(NotebookManager): os_path = self._get_os_path(name, path) if not os.path.isfile(os_path): raise web.HTTPError(404, u'Notebook does not exist: %s' % os_path) - + # clear checkpoints for checkpoint in self.list_checkpoints(name, path): checkpoint_id = checkpoint['id'] @@ -333,7 +333,7 @@ class FileNotebookManager(NotebookManager): if os.path.isfile(cp_path): self.log.debug("Unlinking checkpoint %s", cp_path) os.unlink(cp_path) - + self.log.debug("Unlinking notebook %s", os_path) os.unlink(os_path) @@ -343,7 +343,7 @@ class FileNotebookManager(NotebookManager): new_path = new_path.strip('/') if new_name == old_name and new_path == old_path: return - + new_os_path = self._get_os_path(new_name, new_path) old_os_path = self._get_os_path(old_name, old_path) @@ -375,9 +375,9 @@ class FileNotebookManager(NotebookManager): # Move the .py script if self.save_script: shutil.move(old_py_path, new_py_path) - + # Checkpoint-related utilities - + def get_checkpoint_path(self, checkpoint_id, name, path=''): """find the path to a checkpoint""" path = path.strip('/') @@ -404,9 +404,9 @@ class FileNotebookManager(NotebookManager): last_modified = last_modified, ) return info - + # public checkpoint API - + def create_checkpoint(self, name, path=''): """Create a checkpoint from the current state of a notebook""" path = path.strip('/') @@ -416,13 +416,13 @@ class FileNotebookManager(NotebookManager): cp_path = self.get_checkpoint_path(checkpoint_id, name, path) self.log.debug("creating checkpoint for notebook %s", name) self._copy(nb_path, cp_path) - + # return the checkpoint info return self.get_checkpoint_model(checkpoint_id, name, path) - + def list_checkpoints(self, name, path=''): """list the checkpoints for a given notebook - + This notebook manager currently only supports one checkpoint per notebook. """ path = path.strip('/') @@ -432,8 +432,8 @@ class FileNotebookManager(NotebookManager): return [] else: return [self.get_checkpoint_model(checkpoint_id, name, path)] - - + + def restore_checkpoint(self, checkpoint_id, name, path=''): """restore a notebook to a checkpointed state""" path = path.strip('/') @@ -450,7 +450,7 @@ class FileNotebookManager(NotebookManager): current.read(f, u'json') self._copy(cp_path, nb_path) self.log.debug("copying %s -> %s", cp_path, nb_path) - + def delete_checkpoint(self, checkpoint_id, name, path=''): """delete a notebook's checkpoint""" path = path.strip('/') @@ -461,7 +461,7 @@ class FileNotebookManager(NotebookManager): ) self.log.debug("unlinking %s", cp_path) os.unlink(cp_path) - + def info_string(self): return "Serving notebooks from local directory: %s" % self.notebook_dir diff --git a/IPython/html/services/notebooks/handlers.py b/IPython/html/services/contents/handlers.py similarity index 100% rename from IPython/html/services/notebooks/handlers.py rename to IPython/html/services/contents/handlers.py index dab6849..5647ce7 100644 --- a/IPython/html/services/notebooks/handlers.py +++ b/IPython/html/services/contents/handlers.py @@ -38,7 +38,7 @@ class NotebookHandler(IPythonHandler): def notebook_location(self, name, path=''): """Return the full URL location of a notebook based. - + Parameters ---------- name : unicode @@ -57,7 +57,7 @@ class NotebookHandler(IPythonHandler): self.set_header('Location', location) self.set_header('Last-Modified', model['last_modified']) self.finish(json.dumps(model, default=date_default)) - + @web.authenticated @json_errors def get(self, path='', name=None): @@ -99,10 +99,10 @@ class NotebookHandler(IPythonHandler): raise web.HTTPError(400, u'JSON body missing') model = nbm.update_notebook(model, name, path) self._finish_model(model) - + def _copy_notebook(self, copy_from, path, copy_to=None): """Copy a notebook in path, optionally specifying the new name. - + Only support copying within the same directory. """ self.log.info(u"Copying notebook from %s/%s to %s/%s", @@ -112,23 +112,23 @@ class NotebookHandler(IPythonHandler): model = self.notebook_manager.copy_notebook(copy_from, copy_to, path) self.set_status(201) self._finish_model(model) - + def _upload_notebook(self, model, path, name=None): """Upload a notebook - + If name specified, create it in path/name. """ self.log.info(u"Uploading notebook to %s/%s", path, name or '') if name: model['name'] = name - + model = self.notebook_manager.create_notebook(model, path) self.set_status(201) self._finish_model(model) - + def _create_empty_notebook(self, path, name=None): """Create an empty notebook in path - + If name specified, create it in path/name. """ self.log.info(u"Creating new notebook in %s/%s", path, name or '') @@ -138,7 +138,7 @@ class NotebookHandler(IPythonHandler): model = self.notebook_manager.create_notebook(model, path=path) self.set_status(201) self._finish_model(model) - + def _save_notebook(self, model, path, name): """Save an existing notebook.""" self.log.info(u"Saving notebook at %s/%s", path, name) @@ -149,26 +149,26 @@ class NotebookHandler(IPythonHandler): else: location = False self._finish_model(model, location) - + @web.authenticated @json_errors def post(self, path='', name=None): """Create a new notebook in the specified path. - + POST creates new notebooks. The server always decides on the notebook name. - + POST /api/notebooks/path New untitled notebook in path. If content specified, upload a notebook, otherwise start empty. POST /api/notebooks/path?copy=OtherNotebook.ipynb New copy of OtherNotebook in path """ - + if name is not None: raise web.HTTPError(400, "Only POST to directories. Use PUT for full names.") - + model = self.get_json_body() - + if model is not None: copy_from = model.get('copy_from') if copy_from: @@ -184,10 +184,10 @@ class NotebookHandler(IPythonHandler): @json_errors def put(self, path='', name=None): """Saves the notebook in the location specified by name and path. - + PUT is very similar to POST, but the requester specifies the name, whereas with POST, the server picks the name. - + PUT /api/notebooks/path/Name.ipynb Save notebook at ``path/Name.ipynb``. Notebook structure is specified in `content` key of JSON request body. If content is not specified, @@ -197,7 +197,7 @@ class NotebookHandler(IPythonHandler): """ if name is None: raise web.HTTPError(400, "Only PUT to full names. Use POST for directories.") - + model = self.get_json_body() if model: copy_from = model.get('copy_from') @@ -223,9 +223,9 @@ class NotebookHandler(IPythonHandler): class NotebookCheckpointsHandler(IPythonHandler): - + SUPPORTED_METHODS = ('GET', 'POST') - + @web.authenticated @json_errors def get(self, path='', name=None): @@ -234,7 +234,7 @@ class NotebookCheckpointsHandler(IPythonHandler): checkpoints = nbm.list_checkpoints(name, path) data = json.dumps(checkpoints, default=date_default) self.finish(data) - + @web.authenticated @json_errors def post(self, path='', name=None): @@ -250,9 +250,9 @@ class NotebookCheckpointsHandler(IPythonHandler): class ModifyNotebookCheckpointsHandler(IPythonHandler): - + SUPPORTED_METHODS = ('POST', 'DELETE') - + @web.authenticated @json_errors def post(self, path, name, checkpoint_id): @@ -261,7 +261,7 @@ class ModifyNotebookCheckpointsHandler(IPythonHandler): nbm.restore_checkpoint(checkpoint_id, name, path) self.set_status(204) self.finish() - + @web.authenticated @json_errors def delete(self, path, name, checkpoint_id): @@ -270,7 +270,7 @@ class ModifyNotebookCheckpointsHandler(IPythonHandler): nbm.delete_checkpoint(checkpoint_id, name, path) self.set_status(204) self.finish() - + #----------------------------------------------------------------------------- # URL to handler mappings #----------------------------------------------------------------------------- @@ -285,4 +285,3 @@ default_handlers = [ (r"/api/notebooks%s" % notebook_path_regex, NotebookHandler), (r"/api/notebooks%s" % path_regex, NotebookHandler), ] - diff --git a/IPython/html/services/notebooks/nbmanager.py b/IPython/html/services/contents/nbmanager.py similarity index 100% rename from IPython/html/services/notebooks/nbmanager.py rename to IPython/html/services/contents/nbmanager.py index d5b6907..5f8bd97 100644 --- a/IPython/html/services/notebooks/nbmanager.py +++ b/IPython/html/services/contents/nbmanager.py @@ -32,11 +32,11 @@ from IPython.utils.traitlets import Instance, Unicode, List class NotebookManager(LoggingConfigurable): filename_ext = Unicode(u'.ipynb') - + notary = Instance(sign.NotebookNotary) def _notary_default(self): return sign.NotebookNotary(parent=self) - + hide_globs = List(Unicode, [u'__pycache__'], config=True, help=""" Glob patterns to hide in file and directory listings. """) @@ -46,14 +46,14 @@ class NotebookManager(LoggingConfigurable): def path_exists(self, path): """Does the API-style path (directory) actually exist? - + Override this method in subclasses. - + Parameters ---------- path : string The path to check - + Returns ------- exists : bool @@ -63,18 +63,18 @@ class NotebookManager(LoggingConfigurable): def is_hidden(self, path): """Does the API style path correspond to a hidden directory or file? - + Parameters ---------- path : string The path to check. This is an API path (`/` separated, relative to base notebook-dir). - + Returns ------- exists : bool Whether the path is hidden. - + """ raise NotImplementedError @@ -104,7 +104,7 @@ class NotebookManager(LoggingConfigurable): # no longer listed by the notebook web service. def get_dir_model(self, name, path=''): """Get the directory model given a directory name and its API style path. - + The keys in the model should be: * name * path @@ -145,15 +145,15 @@ class NotebookManager(LoggingConfigurable): def create_checkpoint(self, name, path=''): """Create a checkpoint of the current state of a notebook - + Returns a checkpoint_id for the new checkpoint. """ raise NotImplementedError("must be implemented in a subclass") - + def list_checkpoints(self, name, path=''): """Return a list of checkpoints for a given notebook""" return [] - + def restore_checkpoint(self, checkpoint_id, name, path=''): """Restore a notebook from one of its checkpoints""" raise NotImplementedError("must be implemented in a subclass") @@ -161,7 +161,7 @@ class NotebookManager(LoggingConfigurable): def delete_checkpoint(self, checkpoint_id, name, path=''): """delete a checkpoint for a notebook""" raise NotImplementedError("must be implemented in a subclass") - + def info_string(self): return "Serving notebooks" @@ -174,7 +174,7 @@ class NotebookManager(LoggingConfigurable): def increment_filename(self, basename, path=''): """Increment a notebook filename without the .ipynb to make it unique. - + Parameters ---------- basename : unicode @@ -206,14 +206,14 @@ class NotebookManager(LoggingConfigurable): model['content'] = current.new_notebook(metadata=metadata) if 'name' not in model: model['name'] = self.increment_filename('Untitled', path) - + model['path'] = path model = self.save_notebook(model, model['name'], model['path']) return model def copy_notebook(self, from_name, to_name=None, path=''): """Copy an existing notebook and return its new model. - + If to_name not specified, increment `from_name-Copy#.ipynb`. """ path = path.strip('/') @@ -224,13 +224,13 @@ class NotebookManager(LoggingConfigurable): model['name'] = to_name model = self.save_notebook(model, to_name, path) return model - + def log_info(self): self.log.info(self.info_string()) def trust_notebook(self, name, path=''): """Explicitly trust a notebook - + Parameters ---------- name : string @@ -243,12 +243,12 @@ class NotebookManager(LoggingConfigurable): self.log.warn("Trusting notebook %s/%s", path, name) self.notary.mark_cells(nb, True) self.save_notebook(model, name, path) - + def check_and_sign(self, nb, name, path=''): """Check for trusted cells, and sign the notebook. - + Called as a part of saving notebooks. - + Parameters ---------- nb : dict @@ -262,12 +262,12 @@ class NotebookManager(LoggingConfigurable): self.notary.sign(nb) else: self.log.warn("Saving untrusted notebook %s/%s", path, name) - + def mark_trusted_cells(self, nb, name, path=''): """Mark cells as trusted if the notebook signature matches. - + Called as a part of loading notebooks. - + Parameters ---------- nb : dict diff --git a/IPython/html/services/notebooks/tests/__init__.py b/IPython/html/services/contents/tests/__init__.py similarity index 100% rename from IPython/html/services/notebooks/tests/__init__.py rename to IPython/html/services/contents/tests/__init__.py diff --git a/IPython/html/services/notebooks/tests/test_nbmanager.py b/IPython/html/services/contents/tests/test_nbmanager.py similarity index 100% rename from IPython/html/services/notebooks/tests/test_nbmanager.py rename to IPython/html/services/contents/tests/test_nbmanager.py index bc03a87..c4b85b9 100644 --- a/IPython/html/services/notebooks/tests/test_nbmanager.py +++ b/IPython/html/services/contents/tests/test_nbmanager.py @@ -55,7 +55,7 @@ class TestFileNotebookManager(TestCase): path = fm._get_os_path('test.ipynb', '////') fs_path = os.path.join(fm.notebook_dir, 'test.ipynb') self.assertEqual(path, fs_path) - + def test_checkpoint_subdir(self): subd = u'sub ∂ir' cp_name = 'test-cp.ipynb' @@ -68,10 +68,10 @@ class TestFileNotebookManager(TestCase): self.assertNotEqual(cp_dir, cp_subdir) self.assertEqual(cp_dir, os.path.join(nbdir, fm.checkpoint_dir, cp_name)) self.assertEqual(cp_subdir, os.path.join(nbdir, subd, fm.checkpoint_dir, cp_name)) - + class TestNotebookManager(TestCase): - + def setUp(self): self._temp_dir = TemporaryDirectory() self.td = self._temp_dir.name @@ -79,10 +79,10 @@ class TestNotebookManager(TestCase): notebook_dir=self.td, log=logging.getLogger() ) - + def tearDown(self): self._temp_dir.cleanup() - + def make_dir(self, abs_path, rel_path): """make subdirectory, rel_path is the relative path to that directory from the location where the server started""" @@ -91,27 +91,27 @@ class TestNotebookManager(TestCase): os.makedirs(os_path) except OSError: print("Directory already exists: %r" % os_path) - + def add_code_cell(self, nb): output = current.new_output("display_data", output_javascript="alert('hi');") cell = current.new_code_cell("print('hi')", outputs=[output]) if not nb.worksheets: nb.worksheets.append(current.new_worksheet()) nb.worksheets[0].cells.append(cell) - + def new_notebook(self): nbm = self.notebook_manager model = nbm.create_notebook() name = model['name'] path = model['path'] - + full_model = nbm.get_notebook(name, path) nb = full_model['content'] self.add_code_cell(nb) - + nbm.save_notebook(full_model, name, path) return nb, name, path - + def test_create_notebook(self): nm = self.notebook_manager # Test in root directory @@ -158,7 +158,7 @@ class TestNotebookManager(TestCase): self.assertIn('content', model2) self.assertEqual(model2['name'], 'Untitled0.ipynb') self.assertEqual(model2['path'], sub_dir.strip('/')) - + def test_update_notebook(self): nm = self.notebook_manager # Create a notebook @@ -184,7 +184,7 @@ class TestNotebookManager(TestCase): model = nm.create_notebook(None, sub_dir) name = model['name'] path = model['path'] - + # Change the name in the model for rename model['name'] = 'test_in_sub.ipynb' model = nm.update_notebook(model, name, path) @@ -193,7 +193,7 @@ class TestNotebookManager(TestCase): self.assertIn('path', model) self.assertEqual(model['name'], 'test_in_sub.ipynb') self.assertEqual(model['path'], sub_dir.strip('/')) - + # Make sure the old name is gone self.assertRaises(HTTPError, nm.get_notebook, name, path) @@ -255,50 +255,50 @@ class TestNotebookManager(TestCase): nm = self.notebook_manager # Create a notebook nb, name, path = self.new_notebook() - + # Delete the notebook nm.delete_notebook(name, path) - + # Check that a 'get' on the deleted notebook raises and error self.assertRaises(HTTPError, nm.get_notebook, name, path) - + def test_copy_notebook(self): nm = self.notebook_manager path = u'å b' name = u'nb √.ipynb' os.mkdir(os.path.join(nm.notebook_dir, path)) orig = nm.create_notebook({'name' : name}, path=path) - + # copy with unspecified name copy = nm.copy_notebook(name, path=path) self.assertEqual(copy['name'], orig['name'].replace('.ipynb', '-Copy0.ipynb')) - + # copy with specified name copy2 = nm.copy_notebook(name, u'copy 2.ipynb', path=path) self.assertEqual(copy2['name'], u'copy 2.ipynb') - + def test_trust_notebook(self): nbm = self.notebook_manager nb, name, path = self.new_notebook() - + untrusted = nbm.get_notebook(name, path)['content'] assert not nbm.notary.check_cells(untrusted) - + # print(untrusted) nbm.trust_notebook(name, path) trusted = nbm.get_notebook(name, path)['content'] # print(trusted) assert nbm.notary.check_cells(trusted) - + def test_mark_trusted_cells(self): nbm = self.notebook_manager nb, name, path = self.new_notebook() - + nbm.mark_trusted_cells(nb, name, path) for cell in nb.worksheets[0].cells: if cell.cell_type == 'code': assert not cell.trusted - + nbm.trust_notebook(name, path) nb = nbm.get_notebook(name, path)['content'] for cell in nb.worksheets[0].cells: @@ -308,11 +308,11 @@ class TestNotebookManager(TestCase): def test_check_and_sign(self): nbm = self.notebook_manager nb, name, path = self.new_notebook() - + nbm.mark_trusted_cells(nb, name, path) nbm.check_and_sign(nb, name, path) assert not nbm.notary.check_signature(nb) - + nbm.trust_notebook(name, path) nb = nbm.get_notebook(name, path)['content'] nbm.mark_trusted_cells(nb, name, path) diff --git a/IPython/html/services/notebooks/tests/test_notebooks_api.py b/IPython/html/services/contents/tests/test_notebooks_api.py similarity index 100% rename from IPython/html/services/notebooks/tests/test_notebooks_api.py rename to IPython/html/services/contents/tests/test_notebooks_api.py index c8c82e8..74c9a25 100644 --- a/IPython/html/services/notebooks/tests/test_notebooks_api.py +++ b/IPython/html/services/contents/tests/test_notebooks_api.py @@ -163,7 +163,7 @@ class APITest(NotebookTestBase): expected = [ u'a.ipynb', u'b.ipynb', u'name with spaces.ipynb', u'unicodé.ipynb'] expected = { normalize('NFC', name) for name in expected } self.assertEqual(nbnames, expected) - + nbs = notebooks_only(self.nb_api.list('ordering').json()) nbnames = [n['name'] for n in nbs] expected = ['A.ipynb', 'b.ipynb', 'C.ipynb'] @@ -344,4 +344,3 @@ class APITest(NotebookTestBase): self.assertEqual(r.status_code, 204) cps = self.nb_api.get_checkpoints('a.ipynb', 'foo').json() self.assertEqual(cps, []) -