class ValidateApp(NbGrader, NbConvertApp): name = u'nbgrader-validate' description = u'Validate a notebook by running it' aliases = aliases flags = flags examples = """ You can run `nbgrader validate` on just a single file, e.g.: nbgrader validate "Problem 1.ipynb" Or, you can run it on multiple files using shell globs: nbgrader validate "Problem Set 1/*.ipynb" If you want to test instead that none of the tests pass (rather than that all of the tests pass, which is the default), you can use --invert: nbgrader validate --invert "Problem 1.ipynb" """ preprocessors = List([ClearOutput, Execute, DisplayAutoGrades]) export_format = Unicode('notebook') use_output_suffix = Bool(False) postprocessor_class = DottedOrNone('') notebooks = List([]) writer_class = DottedOrNone('FilesWriter') output_base = Unicode('') def _log_level_default(self): return 'ERROR' def _classes_default(self): classes = super(ValidateApp, self)._classes_default() for pp in self.preprocessors: if len(pp.class_traits(config=True)) > 0: classes.append(pp) return classes def build_extra_config(self): extra_config = super(ValidateApp, self).build_extra_config() extra_config.Exporter.default_preprocessors = self.preprocessors return extra_config def init_single_notebook_resources(self, notebook_filename): resources = super( ValidateApp, self).init_single_notebook_resources(notebook_filename) resources['nbgrader'] = {} return resources def write_single_notebook(self, output, resources): return
class BaseNbConvertApp(NbGrader, NbConvertApp): """A base class for all the nbgrader apps that utilize nbconvert. This inherits defaults from NbGrader, while exposing nbconvert's functionality of running preprocessors and writing a new file. The default export format is 'assignment', which is a special export format defined in nbgrader (see nbgrader.exporters.assignmentexporter) that includes a few things that nbgrader needs (such as the path to the file). """ aliases = nbconvert_aliases flags = nbconvert_flags use_output_suffix = Bool(False) postprocessor_class = DottedOrNone('') notebooks = List([]) assignments = Dict({}) writer_class = DottedOrNone('FilesWriter') output_base = Unicode('') preprocessors = List([]) force = Bool(False, config=True, help="Whether to overwrite existing assignments/submissions") permissions = Integer(config=True, help=dedent(""" Permissions to set on files output by nbgrader. The default is generally read-only (444), with the exception of nbgrader assign, in which case the user also has write permission. """)) def _permissions_default(self): return 444 def _classes_default(self): classes = super(BaseNbConvertApp, self)._classes_default() for pp in self.preprocessors: if len(pp.class_traits(config=True)) > 0: classes.append(pp) return classes @property def _input_directory(self): raise NotImplementedError @property def _output_directory(self): raise NotImplementedError def _format_source(self, assignment_id, student_id, escape=False): return self._format_path(self._input_directory, student_id, assignment_id, escape=escape) def _format_dest(self, assignment_id, student_id, escape=False): return self._format_path(self._output_directory, student_id, assignment_id, escape=escape) def build_extra_config(self): extra_config = super(BaseNbConvertApp, self).build_extra_config() extra_config.Exporter.default_preprocessors = self.preprocessors return extra_config def init_notebooks(self): # the assignment can be set via extra args if len(self.extra_args) > 1: self.fail("Only one argument (the assignment id) may be specified") elif len(self.extra_args) == 1 and self.assignment_id != "": self.fail( "The assignment cannot both be specified in config and as an argument" ) elif len(self.extra_args) == 0 and self.assignment_id == "": self.fail( "An assignment id must be specified, either as an argument or with --assignment" ) elif len(self.extra_args) == 1: self.assignment_id = self.extra_args[0] self.assignments = {} self.notebooks = [] fullglob = self._format_source(self.assignment_id, self.student_id) for assignment in glob.glob(fullglob): self.assignments[assignment] = glob.glob( os.path.join(assignment, self.notebook_id + ".ipynb")) if len(self.assignments[assignment]) == 0: self.fail("No notebooks were matched in '%s'", assignment) if len(self.assignments) == 0: self.fail("No notebooks were matched by '%s'", fullglob) def init_single_notebook_resources(self, notebook_filename): regexp = re.escape(os.path.sep).join([ self._format_source("(?P<assignment_id>.*)", "(?P<student_id>.*)", escape=True), "(?P<notebook_id>.*).ipynb" ]) m = re.match(regexp, notebook_filename) if m is None: self.fail("Could not match '%s' with regexp '%s'", notebook_filename, regexp) gd = m.groupdict() self.log.debug("Student: %s", gd['student_id']) self.log.debug("Assignment: %s", gd['assignment_id']) self.log.debug("Notebook: %s", gd['notebook_id']) resources = {} resources['unique_key'] = gd['notebook_id'] resources['output_files_dir'] = '%s_files' % gd['notebook_id'] resources['nbgrader'] = {} resources['nbgrader']['student'] = gd['student_id'] resources['nbgrader']['assignment'] = gd['assignment_id'] resources['nbgrader']['notebook'] = gd['notebook_id'] resources['nbgrader']['db_url'] = self.db_url return resources def write_single_notebook(self, output, resources): # configure the writer build directory self.writer.build_directory = self._format_dest( resources['nbgrader']['assignment'], resources['nbgrader']['student']) return super(BaseNbConvertApp, self).write_single_notebook(output, resources) def init_destination(self, assignment_id, student_id): """Initialize the destination for an assignment. Returns whether the assignment should actually be processed or not (i.e. whether the initialization was successful). """ dest = os.path.normpath(self._format_dest(assignment_id, student_id)) # the destination doesn't exist, so we haven't processed it if self.notebook_id == "*": if not os.path.exists(dest): return True else: # if any of the notebooks don't exist, then we want to process them for notebook in self.notebooks: filename = os.path.splitext(os.path.basename( notebook))[0] + self.exporter.file_extension path = os.path.join(dest, filename) if not os.path.exists(path): return True # if we have specified --force, then always remove existing stuff if self.force: if self.notebook_id == "*": self.log.warning( "Removing existing assignment: {}".format(dest)) rmtree(dest) else: for notebook in self.notebooks: filename = os.path.splitext(os.path.basename( notebook))[0] + self.exporter.file_extension path = os.path.join(dest, filename) if os.path.exists(path): self.log.warning( "Removing existing notebook: {}".format(path)) remove(path) return True src = self._format_source(assignment_id, student_id) new_timestamp = self._get_existing_timestamp(src) old_timestamp = self._get_existing_timestamp(dest) # if --force hasn't been specified, but the source assignment is newer, # then we want to overwrite it if new_timestamp is not None and old_timestamp is not None and new_timestamp > old_timestamp: if self.notebook_id == "*": self.log.warning( "Updating existing assignment: {}".format(dest)) rmtree(dest) else: for notebook in self.notebooks: filename = os.path.splitext(os.path.basename( notebook))[0] + self.exporter.file_extension path = os.path.join(dest, filename) if os.path.exists(path): self.log.warning( "Updating existing notebook: {}".format(path)) remove(path) return True # otherwise, we should skip the assignment self.log.info("Skipping existing assignment: {}".format(dest)) return False def init_assignment(self, assignment_id, student_id): """Initializes resources/dependencies/etc. that are common to all notebooks in an assignment. """ source = self._format_source(assignment_id, student_id) dest = self._format_dest(assignment_id, student_id) # detect other files in the source directory for filename in find_all_files(source, self.ignore + ["*.ipynb"]): # Make sure folder exists. path = os.path.join(dest, os.path.relpath(filename, source)) if not os.path.exists(os.path.dirname(path)): os.makedirs(os.path.dirname(path)) if os.path.exists(path): remove(path) self.log.info("Copying %s -> %s", filename, path) shutil.copy(filename, path) def set_permissions(self, assignment_id, student_id): self.log.info("Setting destination file permissions to %s", self.permissions) dest = os.path.normpath(self._format_dest(assignment_id, student_id)) permissions = int(str(self.permissions), 8) for dirname, dirnames, filenames in os.walk(dest): for filename in filenames: os.chmod(os.path.join(dirname, filename), permissions) def convert_notebooks(self): errors = [] for assignment in sorted(self.assignments.keys()): # initialize the list of notebooks and the exporter self.notebooks = sorted(self.assignments[assignment]) self.exporter = exporter_map[self.export_format]( config=self.config) # parse out the assignment and student ids regexp = self._format_source("(?P<assignment_id>.*)", "(?P<student_id>.*)", escape=True) m = re.match(regexp, assignment) if m is None: self.fail("Could not match '%s' with regexp '%s'", assignment, regexp) gd = m.groupdict() try: # determine whether we actually even want to process this submission should_process = self.init_destination(gd['assignment_id'], gd['student_id']) if not should_process: continue # initialize the destination and convert self.init_assignment(gd['assignment_id'], gd['student_id']) super(BaseNbConvertApp, self).convert_notebooks() self.set_permissions(gd['assignment_id'], gd['student_id']) except Exception: self.log.error("There was an error processing assignment: %s", assignment) self.log.error(traceback.format_exc()) errors.append((gd['assignment_id'], gd['student_id'])) dest = os.path.normpath( self._format_dest(gd['assignment_id'], gd['student_id'])) if self.notebook_id == "*": if os.path.exists(dest): self.log.warning( "Removing failed assignment: {}".format(dest)) rmtree(dest) else: for notebook in self.notebooks: filename = os.path.splitext(os.path.basename( notebook))[0] + self.exporter.file_extension path = os.path.join(dest, filename) if os.path.exists(path): self.log.warning( "Removing failed notebook: {}".format(path)) remove(path) if len(errors) > 0: for assignment_id, student_id in errors: self.log.error( "There was an error processing assignment '{}' for student '{}'" .format(assignment_id, student_id)) if self.logfile: self.fail( "Please see the error log ({}) for details on the specific " "errors on the above failures.".format(self.logfile))