diff --git a/IPython/html/services/contents/filemanager.py b/IPython/html/services/contents/filemanager.py index e9fab0b16..2c36e05cd 100644 --- a/IPython/html/services/contents/filemanager.py +++ b/IPython/html/services/contents/filemanager.py @@ -19,10 +19,6 @@ from IPython.utils.py3compat import getcwd from IPython.utils import tz from IPython.html.utils import is_hidden, to_os_path -def sort_key(item): - """Case-insensitive sorting.""" - return item['name'].lower() - class FileContentsManager(ContentsManager): @@ -38,9 +34,9 @@ class FileContentsManager(ContentsManager): raise TraitError("%r is not a directory" % new) checkpoint_dir = Unicode('.ipynb_checkpoints', config=True, - help="""The directory name in which to keep notebook checkpoints + help="""The directory name in which to keep file checkpoints - This is a path relative to the notebook's own directory. + This is a path relative to the file's own directory. By default, it is .ipynb_checkpoints """ @@ -157,7 +153,7 @@ class FileContentsManager(ContentsManager): info = os.stat(os_path) last_modified = tz.utcfromtimestamp(info.st_mtime) created = tz.utcfromtimestamp(info.st_ctime) - # Create the notebook model. + # Create the base model. model = {} model['name'] = name model['path'] = path @@ -189,13 +185,12 @@ class FileContentsManager(ContentsManager): model['type'] = 'directory' dir_path = u'{}/{}'.format(path, name) if content: - contents = [] + model['content'] = contents = [] for os_path in glob.glob(self._get_os_path('*', dir_path)): name = os.path.basename(os_path) if self.should_list(name) and not is_hidden(os_path, self.root_dir): contents.append(self.get_model(name=name, path=dir_path, content=False)) - model['content'] = sorted(contents, key=sort_key) model['format'] = 'json' return model @@ -204,7 +199,7 @@ class FileContentsManager(ContentsManager): """Build a model for a file if content is requested, include the file contents. - Text files will be unicode, binary files will be base64-encoded. + UTF-8 text files will be unicode, binary files will be base64-encoded. """ model = self._base_model(name, path) model['type'] = 'file' @@ -251,8 +246,7 @@ class FileContentsManager(ContentsManager): name : str the name of the target path : str - the URL path that describes the relative path for - the notebook + the URL path that describes the relative path for the target Returns ------- @@ -275,6 +269,7 @@ class FileContentsManager(ContentsManager): return model def _save_notebook(self, os_path, model, name='', path=''): + """save a notebook file""" # Save the notebook file nb = current.to_notebook_json(model['content']) @@ -287,6 +282,7 @@ class FileContentsManager(ContentsManager): current.write(nb, f, u'json') def _save_file(self, os_path, model, name='', path=''): + """save a non-notebook file""" fmt = model.get('format', None) if fmt not in {'text', 'base64'}: raise web.HTTPError(400, "Must specify format of file contents as 'text' or 'base64'") @@ -303,6 +299,7 @@ class FileContentsManager(ContentsManager): f.write(bcontent) def _save_directory(self, os_path, model, name='', path=''): + """create a directory""" if not os.path.exists(os_path): os.mkdir(os_path) elif not os.path.isdir(os_path): @@ -442,7 +439,7 @@ class FileContentsManager(ContentsManager): # only the one checkpoint ID: checkpoint_id = u"checkpoint" cp_path = self.get_checkpoint_path(checkpoint_id, name, path) - self.log.debug("creating checkpoint for notebook %s", name) + self.log.debug("creating checkpoint for %s", name) self._copy(src_path, cp_path) # return the checkpoint info diff --git a/IPython/html/services/contents/handlers.py b/IPython/html/services/contents/handlers.py index e6f08ed25..7f394f3ba 100644 --- a/IPython/html/services/contents/handlers.py +++ b/IPython/html/services/contents/handlers.py @@ -15,6 +15,16 @@ from IPython.html.base.handlers import (IPythonHandler, json_errors, file_name_regex) +def sort_key(model): + """key function for case-insensitive sort by name and type""" + iname = model['name'].lower() + type_key = { + 'directory' : '0', + 'notebook' : '1', + 'file' : '2', + }.get(model['type'], '9') + return u'%s%s' % (type_key, iname) + class ContentsHandler(IPythonHandler): SUPPORTED_METHODS = (u'GET', u'PUT', u'PATCH', u'POST', u'DELETE') @@ -52,16 +62,9 @@ class ContentsHandler(IPythonHandler): path = path or '' model = self.contents_manager.get_model(name=name, path=path) if model['type'] == 'directory': - # resort listing to group directories at the top - dirs = [] - files = [] - for entry in model['content']: - if entry['type'] == 'directory': - dirs.append(entry) - else: - # do we also want to group notebooks separate from files? - files.append(entry) - model['content'] = dirs + files + # group listing by type, then by name (case-insensitive) + # FIXME: front-ends shouldn't rely on this sorting + model['content'].sort(key=sort_key) self._finish_model(model, location=False) @web.authenticated @@ -130,9 +133,9 @@ class ContentsHandler(IPythonHandler): @web.authenticated @json_errors def post(self, path='', name=None): - """Create a new notebook in the specified path. + """Create a new file or directory in the specified path. - POST creates new notebooks. The server always decides on the notebook name. + POST creates new files or directories. The server always decides on the name. POST /api/contents/path New untitled notebook in path. If content specified, upload a diff --git a/IPython/html/services/contents/manager.py b/IPython/html/services/contents/manager.py index 8cec3983c..cd4231c79 100644 --- a/IPython/html/services/contents/manager.py +++ b/IPython/html/services/contents/manager.py @@ -18,7 +18,10 @@ class ContentsManager(LoggingConfigurable): def _notary_default(self): return sign.NotebookNotary(parent=self) - hide_globs = List(Unicode, [u'__pycache__'], config=True, help=""" + hide_globs = List(Unicode, [ + u'__pycache__', '*.pyc', '*.pyo', + '.DS_Store', '*.so', '*.dylib', '*~', + ], config=True, help=""" Glob patterns to hide in file and directory listings. """) @@ -60,14 +63,14 @@ class ContentsManager(LoggingConfigurable): raise NotImplementedError def file_exists(self, name, path=''): - """Returns a True if the notebook exists. Else, returns False. + """Returns a True if the file exists. Else, returns False. Parameters ---------- name : string - The name of the notebook you are checking. + The name of the file you are checking. path : string - The relative path to the notebook (with '/' as separator) + The relative path to the file's directory (with '/' as separator) Returns ------- @@ -87,38 +90,38 @@ class ContentsManager(LoggingConfigurable): raise NotImplementedError('must be implemented in a subclass') def get_model(self, name, path='', content=True): - """Get the notebook model with or without content.""" + """Get the model of a file or directory with or without content.""" raise NotImplementedError('must be implemented in a subclass') def save(self, model, name, path=''): - """Save the notebook and return the model with no content.""" + """Save the file or directory and return the model with no content.""" raise NotImplementedError('must be implemented in a subclass') def update(self, model, name, path=''): - """Update the notebook and return the model with no content.""" + """Update the file or directory and return the model with no content.""" raise NotImplementedError('must be implemented in a subclass') def delete(self, name, path=''): - """Delete notebook by name and path.""" + """Delete file or directory by name and path.""" raise NotImplementedError('must be implemented in a subclass') def create_checkpoint(self, name, path=''): - """Create a checkpoint of the current state of a notebook + """Create a checkpoint of the current state of a file 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 a list of checkpoints for a given file""" return [] def restore_checkpoint(self, checkpoint_id, name, path=''): - """Restore a notebook from one of its checkpoints""" + """Restore a file from one of its checkpoints""" raise NotImplementedError("must be implemented in a subclass") def delete_checkpoint(self, checkpoint_id, name, path=''): - """delete a checkpoint for a notebook""" + """delete a checkpoint for a file""" raise NotImplementedError("must be implemented in a subclass") def info_string(self): @@ -139,7 +142,7 @@ class ContentsManager(LoggingConfigurable): filename : unicode The name of a file, including extension path : unicode - The URL path of the notebooks directory + The URL path of the target's directory Returns ------- @@ -156,7 +159,7 @@ class ContentsManager(LoggingConfigurable): return name def create_file(self, model=None, path='', ext='.ipynb'): - """Create a new notebook and return its model with no content.""" + """Create a new file or directory and return its model with no content.""" path = path.strip('/') if model is None: model = {}