diff --git a/IPython/html/services/contents/manager.py b/IPython/html/services/contents/manager.py
index ec2f7db7c..f8aeec2c5 100644
--- a/IPython/html/services/contents/manager.py
+++ b/IPython/html/services/contents/manager.py
@@ -30,6 +30,21 @@ from IPython.utils.py3compat import string_types
copy_pat = re.compile(r'\-Copy\d*\.')
+def _separate_dirs_files(models):
+ """
+ Split an iterable of models into a list of file paths and a list of
+ directory paths.
+ """
+ dirs = []
+ files = []
+ for model in models:
+ if model['type'] == 'directory':
+ dirs.append(model['path'])
+ else:
+ files.append(model['path'])
+ return dirs, files
+
+
class CheckpointManager(LoggingConfigurable):
"""
Base class for managing checkpoints for a ContentsManager.
@@ -499,6 +514,31 @@ class ContentsManager(LoggingConfigurable):
"""Should this file/directory name be displayed in a listing?"""
return not any(fnmatch(name, glob) for glob in self.hide_globs)
+ def walk(self):
+ """
+ Like os.walk, but written in terms of the ContentsAPI.
+
+ Returns a generator of tuples of the form:
+ (directory name, [subdirectories], [files in directory])
+ """
+ return self._walk([''])
+
+ def _walk(self, dirs):
+ """
+ Recursive helper for walk.
+ """
+ for directory in dirs:
+ children = self.get(
+ directory,
+ content=True,
+ type='directory',
+ )['content']
+ dirs, files = map(sorted, _separate_dirs_files(children))
+ yield (directory, dirs, files)
+ if dirs:
+ for entry in self._walk(dirs):
+ yield(entry)
+
# Part 3: Checkpoints API
def create_checkpoint(self, path):
"""Create a checkpoint."""
diff --git a/IPython/html/services/contents/tests/test_contents_api.py b/IPython/html/services/contents/tests/test_contents_api.py
index 3c0d33aa0..6fa2ec9ea 100644
--- a/IPython/html/services/contents/tests/test_contents_api.py
+++ b/IPython/html/services/contents/tests/test_contents_api.py
@@ -614,3 +614,82 @@ class APITest(NotebookTestBase):
with TemporaryDirectory() as td:
with self.patch_cp_root(td):
self.test_file_checkpoints()
+
+ def test_walk(self):
+ """
+ Test ContentsManager.walk.
+ """
+ results = list(self.notebook.contents_manager.walk())
+ expected = [
+ (
+ '',
+ [
+ 'Directory with spaces in',
+ 'foo',
+ 'ordering',
+ u'unicodé',
+ u'å b',
+ ],
+ ['inroot.blob', 'inroot.ipynb', 'inroot.txt'],
+ ),
+ (
+ 'Directory with spaces in',
+ [],
+ ['inspace.blob', 'inspace.ipynb', 'inspace.txt'],
+ ),
+ (
+ 'foo',
+ ['bar'],
+ [
+ 'a.blob', 'a.ipynb', 'a.txt',
+ 'b.blob', 'b.ipynb', 'b.txt',
+ 'name with spaces.blob',
+ 'name with spaces.ipynb',
+ 'name with spaces.txt',
+ u'unicodé.blob', u'unicodé.ipynb', u'unicodé.txt'
+ ]
+ ),
+ (
+ 'foo/bar',
+ [],
+ ['baz.blob', 'baz.ipynb', 'baz.txt'],
+ ),
+ (
+ 'ordering',
+ [],
+ [
+ 'A.blob', 'A.ipynb', 'A.txt',
+ 'C.blob', 'C.ipynb', 'C.txt',
+ 'b.blob', 'b.ipynb', 'b.txt',
+ ],
+ ),
+ (
+ u'unicodé',
+ [],
+ ['innonascii.blob', 'innonascii.ipynb', 'innonascii.txt'],
+ ),
+ (
+ u'å b',
+ [],
+ [u'ç d.blob', u'ç d.ipynb', u'ç d.txt'],
+ ),
+ ]
+
+ for idx, (dname, subdirs, files) in enumerate(expected):
+ result_dname, result_subdirs, result_files = results[idx]
+ if dname == '':
+ sep = ''
+ else:
+ sep = '/'
+ self.assertEqual(
+ dname,
+ result_dname,
+ )
+ self.assertEqual(
+ [sep.join([dname, sub]) for sub in subdirs],
+ result_subdirs,
+ )
+ self.assertEqual(
+ [sep.join([dname, fname]) for fname in files],
+ result_files,
+ )