Repository: jupyter/nbgrader

Issue:
Allow nbgrader quickstart to work on an existing directory
Rather than refusing to run if the directory exists, quickstart should check if it is going to overwrite any files and if it is only then throw an error.

cc @ncclementi

Discussion:

Patch:
diff --git a/nbgrader/apps/quickstartapp.py b/nbgrader/apps/quickstartapp.py
index 462e1cd7..62d57441 100644
--- a/nbgrader/apps/quickstartapp.py
+++ b/nbgrader/apps/quickstartapp.py
@@ -79,6 +79,14 @@ class QuickStartApp(NbGrader):
         classes.append(QuickStartApp)
         return classes
 
+    def _course_folder_content_exists(self, course_path):
+        course_folder_content = {
+            "source_dir": os.path.join(course_path, "source"),
+            "config_file": os.path.join(course_path, "nbgrader_config.py")
+        }
+        exists = os.path.isdir(course_folder_content['source_dir']) or os.path.isfile(course_folder_content['config_file'])
+        return exists
+
     def start(self):
         super(QuickStartApp, self).start()
 
@@ -89,19 +97,23 @@ class QuickStartApp(NbGrader):
         # make sure it doesn't exist
         course_id = self.extra_args[0]
         course_path = os.path.abspath(course_id)
+
         if os.path.exists(course_path):
             if self.force:
                 self.log.warning("Removing existing directory '%s'", course_path)
                 utils.rmtree(course_path)
             else:
-                self.fail(
-                    "Directory '{}' already exists! Rerun with --force to remove "
-                    "this directory first (warning: this will remove the ENTIRE "
-                    "directory and all files in it.) ".format(course_path))
+                if self._course_folder_content_exists(course_path):
+                    self.fail(
+                        "Directory '{}' and it's content already exists! Rerun with --force to remove "
+                        "this directory first (warning: this will remove the ENTIRE "
+                        "directory and all files in it.) ".format(course_path))
 
         # create the directory
         self.log.info("Creating directory '%s'...", course_path)
-        os.mkdir(course_path)
+
+        if not os.path.isdir(course_path):
+            os.mkdir(course_path)
 
         # populating it with an example
         self.log.info("Copying example from the user guide...")


Test patch:
diff --git a/nbgrader/tests/apps/test_nbgrader_quickstart.py b/nbgrader/tests/apps/test_nbgrader_quickstart.py
index d9a9f705..f8257119 100644
--- a/nbgrader/tests/apps/test_nbgrader_quickstart.py
+++ b/nbgrader/tests/apps/test_nbgrader_quickstart.py
@@ -1,5 +1,5 @@
 import os
-import sys
+import shutil
 
 from .. import run_nbgrader
 from .base import BaseTestApp
@@ -39,6 +39,61 @@ class TestNbGraderQuickStart(BaseTestApp):
         # nbgrader assign should work
         run_nbgrader(["assign", "ps1"])
 
+    def test_quickstart_overwrite_course_folder_if_structure_not_present(self):
+        """Is the quickstart example properly generated?"""
+
+        run_nbgrader(["quickstart", "example_without_folder_and_config_file"])
+
+        # it should fail if it already exists
+        run_nbgrader(["quickstart", "example_without_folder_and_config_file"], retcode=1)
+
+        # should succeed if both source folder and config file are not present.
+        shutil.rmtree(os.path.join("example_without_folder_and_config_file", "source"))
+        os.remove(os.path.join("example_without_folder_and_config_file", "nbgrader_config.py"))
+
+        run_nbgrader(["quickstart", "example_without_folder_and_config_file"])
+        assert os.path.exists(os.path.join("example_without_folder_and_config_file", "nbgrader_config.py"))
+        assert os.path.exists(os.path.join("example_without_folder_and_config_file", "source"))
+
+        # nbgrader validate should work
+        os.chdir("example_without_folder_and_config_file")
+        for nb in os.listdir(os.path.join("source", "ps1")):
+            if not nb.endswith(".ipynb"):
+                continue
+            output = run_nbgrader(["validate", os.path.join("source", "ps1", nb)], stdout=True)
+            assert output.strip() == "Success! Your notebook passes all the tests."
+
+        # nbgrader assign should work
+        run_nbgrader(["assign", "ps1"])
+
+    def test_quickstart_fails_with_source_folder_removed(self):
+        """Is the quickstart example properly generated if source folder removed?"""
+
+        run_nbgrader(["quickstart", "example_source_folder_fail"])
+
+        # it should fail if it already exists
+        run_nbgrader(["quickstart", "example_source_folder_fail"], retcode=1)
+
+        # it should succeed if source folder not present and create it
+        shutil.rmtree(os.path.join("example_source_folder_fail", "source"))
+
+        # it should fail if it already source folder or config file exists
+        run_nbgrader(["quickstart", "example_source_folder_fail"], retcode=1)
+
+    def test_quickstart_fails_with_config_file_removed(self):
+        """Is the quickstart example properly generated if source folder removed?"""
+
+        run_nbgrader(["quickstart", "example_source_folder_fail"])
+
+        # it should fail if it already exists
+        run_nbgrader(["quickstart", "example_source_folder_fail"], retcode=1)
+
+        # it should succeed if source folder not present and create it
+        os.remove(os.path.join("example_source_folder_fail", "nbgrader_config.py"))
+
+        # it should fail if it already source folder or config file exists
+        run_nbgrader(["quickstart", "example_source_folder_fail"], retcode=1)
+
     def test_quickstart_f(self):
         """Is the quickstart example properly generated?"""