Repository: ipython/ipython

Issue:
Multi-Directory notebooks overwrite each other's checkpoints
IPython version: 71ff8f9523e438402f696104be44e90394bbf429 (after 2.0)

Even though I haven't seen any open issue about this yet, I believe it might be already known, given that it is mentioned here: https://github.com/ipython/ipython/wiki/IPEP-16%3A-Notebook-multi-directory-dashboard-and-URL-mapping#create-a-checkpoint

In brief, the problem is that notebooks that have the same name will write/read the same checkpoint file, regardless of the notebooks' path.

Example:
Notebooks `/test.ipynb` and `/some_path/test.ipynb` will both have their checkpoint file named `test-checkpoint.ipynb` directly under `.ipynb_checkpoints`.

The consequence of this is that if two or more notebooks located in different subdirectories happen to have the same name they will overwrite each other's checkpoints. A potential solution could be to save the checkpoints in subdirectories as well.



Discussion:

Resolving patch:
diff --git a/IPython/html/services/notebooks/filenbmanager.py b/IPython/html/services/notebooks/filenbmanager.py
--- a/IPython/html/services/notebooks/filenbmanager.py
+++ b/IPython/html/services/notebooks/filenbmanager.py
@@ -60,31 +60,15 @@ def _notebook_dir_changed(self, name, old, new):
             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(config=True,
-        help="""The location in which to keep notebook checkpoints
+    
+    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 notebook-dir/.ipynb_checkpoints
+        By default, it is .ipynb_checkpoints
         """
     )
-    def _checkpoint_dir_default(self):
-        return os.path.join(self.notebook_dir, '.ipynb_checkpoints')
-    
-    def _checkpoint_dir_changed(self, name, old, new):
-        """do a bit of validation of the checkpoint dir"""
-        if not os.path.isabs(new):
-            # If we receive a non-absolute path, make it absolute.
-            abs_new = os.path.abspath(new)
-            self.checkpoint_dir = abs_new
-            return
-        if os.path.exists(new) and not os.path.isdir(new):
-            raise TraitError("checkpoint dir %r is not a directory" % new)
-        if not os.path.exists(new):
-            self.log.info("Creating checkpoint dir %s", new)
-            try:
-                os.mkdir(new)
-            except:
-                raise TraitError("Couldn't create checkpoint dir %r" % new)
     
     def _copy(self, src, dest):
         """copy src to dest
@@ -416,7 +400,11 @@ def get_checkpoint_path(self, checkpoint_id, name, path=''):
             checkpoint_id=checkpoint_id,
             ext=self.filename_ext,
         )
-        cp_path = os.path.join(path, self.checkpoint_dir, filename)
+        os_path = self._get_os_path(path=path)
+        cp_dir = os.path.join(os_path, self.checkpoint_dir)
+        if not os.path.exists(cp_dir):
+            os.mkdir(cp_dir)
+        cp_path = os.path.join(cp_dir, filename)
         return cp_path
 
     def get_checkpoint_model(self, checkpoint_id, name, path=''):
@@ -455,8 +443,8 @@ def list_checkpoints(self, name, path=''):
         """
         path = path.strip('/')
         checkpoint_id = "checkpoint"
-        path = self.get_checkpoint_path(checkpoint_id, name, path)
-        if not os.path.exists(path):
+        os_path = self.get_checkpoint_path(checkpoint_id, name, path)
+        if not os.path.exists(os_path):
             return []
         else:
             return [self.get_checkpoint_model(checkpoint_id, name, path)]


Test patch:
diff --git a/IPython/html/services/notebooks/tests/test_nbmanager.py b/IPython/html/services/notebooks/tests/test_nbmanager.py
--- a/IPython/html/services/notebooks/tests/test_nbmanager.py
+++ b/IPython/html/services/notebooks/tests/test_nbmanager.py
@@ -55,6 +55,20 @@ def test_get_os_path(self):
             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'
+        with TemporaryDirectory() as td:
+            nbdir = td
+            os.mkdir(os.path.join(td, subd))
+            fm = FileNotebookManager(notebook_dir=nbdir)
+            cp_dir = fm.get_checkpoint_path('cp', 'test.ipynb', '/')
+            cp_subdir = fm.get_checkpoint_path('cp', 'test.ipynb', '/%s/' % subd)
+        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):