Repository: jupyter/nbgrader

Issue:
Have submitted notebooks import from local directory
I had students edit a python file and then submit it along with the notebooks. However, when I run the autograder, nbgrader imports the python file from my source directory instead of the submitted one. This, of course, leads to the test cells that test their implementation always passing no matter what they do (and also makes it so that, if they added and rely on any further functionality that's not in my solution, then those blocks fail!). Is there any way to have the submitted notebooks import from the submitted .py file? 


Discussion:

Patch:
diff --git a/nbgrader/converters/autograde.py b/nbgrader/converters/autograde.py
index 62327f17..57c662ab 100644
--- a/nbgrader/converters/autograde.py
+++ b/nbgrader/converters/autograde.py
@@ -2,7 +2,7 @@ import os
 import shutil
 
 from textwrap import dedent
-from traitlets import Bool, List
+from traitlets import Bool, List, Dict
 
 from .base import BaseConverter, NbGraderException
 from ..preprocessors import (
@@ -24,6 +24,19 @@ class Autograde(BaseConverter):
         )
     ).tag(config=True)
 
+    exclude_overwriting = Dict(
+        {},
+        help=dedent(
+            """
+            A dictionary with keys corresponding to assignment names and values
+            being a list of filenames (relative to the assignment's source
+            directory) that should NOT be overwritten with the source version.
+            This is to allow students to e.g. edit a python file and submit it
+            alongside the notebooks in their assignment.
+            """
+        )
+    ).tag(config=True)
+
     _sanitizing = True
 
     @property
@@ -109,7 +122,9 @@ class Autograde(BaseConverter):
         self.log.info("Overwriting files with master versions from the source directory")
         dest_path = self._format_dest(assignment_id, student_id)
         source_path = self.coursedir.format_path(self.coursedir.source_directory, '.', assignment_id)
-        source_files = utils.find_all_files(source_path, self.coursedir.ignore + ["*.ipynb"])
+        source_files = set(utils.find_all_files(source_path, self.coursedir.ignore + ["*.ipynb"]))
+        exclude_files = set([os.path.join(source_path, x) for x in self.exclude_overwriting.get(assignment_id, [])])
+        source_files = list(source_files - exclude_files)
 
         # copy them to the build directory
         for filename in source_files:


Test patch:
diff --git a/nbgrader/tests/apps/test_nbgrader_autograde.py b/nbgrader/tests/apps/test_nbgrader_autograde.py
index 091564e7..ba44d44b 100644
--- a/nbgrader/tests/apps/test_nbgrader_autograde.py
+++ b/nbgrader/tests/apps/test_nbgrader_autograde.py
@@ -389,20 +389,24 @@ class TestNbGraderAutograde(BaseTestApp):
         """Are dependent files properly linked and overwritten?"""
         with open("nbgrader_config.py", "a") as fh:
             fh.write("""c.CourseDirectory.db_assignments = [dict(name='ps1', duedate='2015-02-02 14:58:23.948203 PST')]\n""")
-            fh.write("""c.CourseDirectory.db_students = [dict(id="foo"), dict(id="bar")]""")
+            fh.write("""c.CourseDirectory.db_students = [dict(id="foo"), dict(id="bar")]\n""")
+            fh.write("""c.Autograde.exclude_overwriting = {"ps1": ["helper.py"]}\n""")
 
         self._copy_file(join("files", "submitted-unchanged.ipynb"), join(course_dir, "source", "ps1", "p1.ipynb"))
         self._make_file(join(course_dir, "source", "ps1", "data.csv"), "some,data\n")
+        self._make_file(join(course_dir, "source", "ps1", "helper.py"), "print('hello!')\n")
         run_nbgrader(["assign", "ps1", "--db", db])
 
         self._copy_file(join("files", "submitted-unchanged.ipynb"), join(course_dir, "submitted", "foo", "ps1", "p1.ipynb"))
         self._make_file(join(course_dir, "submitted", "foo", "ps1", "timestamp.txt"), "2015-02-02 15:58:23.948203 PST")
         self._make_file(join(course_dir, "submitted", "foo", "ps1", "data.csv"), "some,other,data\n")
+        self._make_file(join(course_dir, "submitted", "foo", "ps1", "helper.py"), "print('this is different!')\n")
         run_nbgrader(["autograde", "ps1", "--db", db])
 
         assert os.path.isfile(join(course_dir, "autograded", "foo", "ps1", "p1.ipynb"))
         assert os.path.isfile(join(course_dir, "autograded", "foo", "ps1", "timestamp.txt"))
         assert os.path.isfile(join(course_dir, "autograded", "foo", "ps1", "data.csv"))
+        assert os.path.isfile(join(course_dir, "autograded", "foo", "ps1", "helper.py"))
 
         with open(join(course_dir, "autograded", "foo", "ps1", "timestamp.txt"), "r") as fh:
             contents = fh.read()
@@ -412,6 +416,45 @@ class TestNbGraderAutograde(BaseTestApp):
             contents = fh.read()
         assert contents == "some,data\n"
 
+        with open(join(course_dir, "autograded", "foo", "ps1", "helper.py"), "r") as fh:
+            contents = fh.read()
+        assert contents == "print('this is different!')\n"
+
+    def test_grade_overwrite_files_subdirs(self, db, course_dir):
+        """Are dependent files properly linked and overwritten?"""
+        with open("nbgrader_config.py", "a") as fh:
+            fh.write("""c.CourseDirectory.db_assignments = [dict(name='ps1', duedate='2015-02-02 14:58:23.948203 PST')]\n""")
+            fh.write("""c.CourseDirectory.db_students = [dict(id="foo"), dict(id="bar")]\n""")
+            fh.write("""c.Autograde.exclude_overwriting = {{"ps1": ["{}"]}}\n""".format(os.path.join("subdir", "helper.py")))
+
+        self._copy_file(join("files", "submitted-unchanged.ipynb"), join(course_dir, "source", "ps1", "p1.ipynb"))
+        self._make_file(join(course_dir, "source", "ps1", "subdir", "data.csv"), "some,data\n")
+        self._make_file(join(course_dir, "source", "ps1", "subdir", "helper.py"), "print('hello!')\n")
+        run_nbgrader(["assign", "ps1", "--db", db])
+
+        self._copy_file(join("files", "submitted-unchanged.ipynb"), join(course_dir, "submitted", "foo", "ps1", "p1.ipynb"))
+        self._make_file(join(course_dir, "submitted", "foo", "ps1", "timestamp.txt"), "2015-02-02 15:58:23.948203 PST")
+        self._make_file(join(course_dir, "submitted", "foo", "ps1", "subdir", "data.csv"), "some,other,data\n")
+        self._make_file(join(course_dir, "submitted", "foo", "ps1", "subdir", "helper.py"), "print('this is different!')\n")
+        run_nbgrader(["autograde", "ps1", "--db", db])
+
+        assert os.path.isfile(join(course_dir, "autograded", "foo", "ps1", "p1.ipynb"))
+        assert os.path.isfile(join(course_dir, "autograded", "foo", "ps1", "timestamp.txt"))
+        assert os.path.isfile(join(course_dir, "autograded", "foo", "ps1", "subdir", "data.csv"))
+        assert os.path.isfile(join(course_dir, "autograded", "foo", "ps1", "subdir", "helper.py"))
+
+        with open(join(course_dir, "autograded", "foo", "ps1", "timestamp.txt"), "r") as fh:
+            contents = fh.read()
+        assert contents == "2015-02-02 15:58:23.948203 PST"
+
+        with open(join(course_dir, "autograded", "foo", "ps1", "subdir", "data.csv"), "r") as fh:
+            contents = fh.read()
+        assert contents == "some,data\n"
+
+        with open(join(course_dir, "autograded", "foo", "ps1", "subdir", "helper.py"), "r") as fh:
+            contents = fh.read()
+        assert contents == "print('this is different!')\n"
+
     def test_side_effects(self, db, course_dir):
         with open("nbgrader_config.py", "a") as fh:
             fh.write("""c.CourseDirectory.db_assignments = [dict(name='ps1', duedate='2015-02-02 14:58:23.948203 PST')]\n""")