From f1cf8edcba4818849469b790ab1b2c24ba6645fb Mon Sep 17 00:00:00 2001 From: Stoney Jackson Date: Thu, 10 Jul 2014 17:09:54 -0400 Subject: [PATCH 01/11] General reorganization. * Move all JSON files to configs/. * Move all Python files except assignmentconvert.py under grade/. * grade/ is now the root python package for this project. * Add run_tests.py to run all the tests. Ultimately the JSON files should probably be stored along with their courses and be removed from this repository. That way, if there is a change to a course, this repository should not change. --- assignmentconvert.py | 5 ++-- lab10config.json => configs/lab10config.json | 0 lab11config.json => configs/lab11config.json | 0 lab1config.json => configs/lab1config.json | 0 lab2config.json => configs/lab2config.json | 0 lab3config.json => configs/lab3config.json | 0 lab4config.json => configs/lab4config.json | 0 lab5config.json => configs/lab5config.json | 0 lab6config.json => configs/lab6config.json | 0 lab7config.json => configs/lab7config.json | 0 lab9config.json => configs/lab9config.json | 0 .../project1config.json | 0 .../project2config.json | 0 .../project3config.json | 0 .../project4config.json | 0 .../student1/file1.txt => grade/__init__.py | 0 assignment.py => grade/assignment.py | 0 command.py => grade/command.py | 2 +- .../file2.txt => grade/tests/__init__.py | 0 sandbox.py => grade/tests/sandbox.py | 0 .../tests/sandbox}/assignment1.json | 0 .../sandbox/assignment1/student1}/file1.txt | 0 .../assignment1/student1}/subdir/file2.txt | 0 .../sandbox/assignment1/student2}/file1.txt | 0 .../assignment1/student2}/subdir/file2.txt | 0 .../sandbox/assignment1/student3}/file1.txt | 0 .../assignment1/student3}/subdir/file2.txt | 0 {sandbox => grade/tests/sandbox}/issue28.json | 0 .../tests/sandbox/issue28/student1}/file1.txt | 0 .../issue28/student1}/subdir/file2.txt | 0 .../issue28/student2}/subdir/file2.txt | 0 .../tests/sandbox/issue28/student3/file1.txt | 0 .../sandbox/issue28/student3/subdir/file2.txt | 0 .../tests/test_assignment.py | 4 ++-- .../tests/test_command.py | 4 ++-- .../tests/test_issue28.py | 6 ++--- test_json.py => grade/tests/test_json.py | 2 +- run_tests.py | 23 +++++++++++++++++++ 38 files changed, 35 insertions(+), 11 deletions(-) rename lab10config.json => configs/lab10config.json (100%) rename lab11config.json => configs/lab11config.json (100%) rename lab1config.json => configs/lab1config.json (100%) rename lab2config.json => configs/lab2config.json (100%) rename lab3config.json => configs/lab3config.json (100%) rename lab4config.json => configs/lab4config.json (100%) rename lab5config.json => configs/lab5config.json (100%) rename lab6config.json => configs/lab6config.json (100%) rename lab7config.json => configs/lab7config.json (100%) rename lab9config.json => configs/lab9config.json (100%) rename project1config.json => configs/project1config.json (100%) rename project2config.json => configs/project2config.json (100%) rename project3config.json => configs/project3config.json (100%) rename project4config.json => configs/project4config.json (100%) rename sandbox/assignment1/student1/file1.txt => grade/__init__.py (100%) rename assignment.py => grade/assignment.py (100%) rename command.py => grade/command.py (98%) rename sandbox/assignment1/student1/subdir/file2.txt => grade/tests/__init__.py (100%) rename sandbox.py => grade/tests/sandbox.py (100%) rename {sandbox => grade/tests/sandbox}/assignment1.json (100%) rename {sandbox/assignment1/student2 => grade/tests/sandbox/assignment1/student1}/file1.txt (100%) rename {sandbox/assignment1/student2 => grade/tests/sandbox/assignment1/student1}/subdir/file2.txt (100%) rename {sandbox/assignment1/student3 => grade/tests/sandbox/assignment1/student2}/file1.txt (100%) rename {sandbox/assignment1/student3 => grade/tests/sandbox/assignment1/student2}/subdir/file2.txt (100%) rename {sandbox/issue28/student1 => grade/tests/sandbox/assignment1/student3}/file1.txt (100%) rename {sandbox/issue28/student1 => grade/tests/sandbox/assignment1/student3}/subdir/file2.txt (100%) rename {sandbox => grade/tests/sandbox}/issue28.json (100%) rename {sandbox/issue28/student3 => grade/tests/sandbox/issue28/student1}/file1.txt (100%) rename {sandbox/issue28/student2 => grade/tests/sandbox/issue28/student1}/subdir/file2.txt (100%) rename {sandbox/issue28/student3 => grade/tests/sandbox/issue28/student2}/subdir/file2.txt (100%) create mode 100644 grade/tests/sandbox/issue28/student3/file1.txt create mode 100644 grade/tests/sandbox/issue28/student3/subdir/file2.txt rename test_assignment.py => grade/tests/test_assignment.py (96%) rename test_command.py => grade/tests/test_command.py (98%) rename test_issue28.py => grade/tests/test_issue28.py (92%) rename test_json.py => grade/tests/test_json.py (97%) create mode 100644 run_tests.py diff --git a/assignmentconvert.py b/assignmentconvert.py index 388f233..5c3cd0e 100644 --- a/assignmentconvert.py +++ b/assignmentconvert.py @@ -13,10 +13,11 @@ # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA + import argparse import os -from assignment import Assignment -from command import Command +from grade.assignment import Assignment +from grade.command import Command class LabConvert(object): diff --git a/lab10config.json b/configs/lab10config.json similarity index 100% rename from lab10config.json rename to configs/lab10config.json diff --git a/lab11config.json b/configs/lab11config.json similarity index 100% rename from lab11config.json rename to configs/lab11config.json diff --git a/lab1config.json b/configs/lab1config.json similarity index 100% rename from lab1config.json rename to configs/lab1config.json diff --git a/lab2config.json b/configs/lab2config.json similarity index 100% rename from lab2config.json rename to configs/lab2config.json diff --git a/lab3config.json b/configs/lab3config.json similarity index 100% rename from lab3config.json rename to configs/lab3config.json diff --git a/lab4config.json b/configs/lab4config.json similarity index 100% rename from lab4config.json rename to configs/lab4config.json diff --git a/lab5config.json b/configs/lab5config.json similarity index 100% rename from lab5config.json rename to configs/lab5config.json diff --git a/lab6config.json b/configs/lab6config.json similarity index 100% rename from lab6config.json rename to configs/lab6config.json diff --git a/lab7config.json b/configs/lab7config.json similarity index 100% rename from lab7config.json rename to configs/lab7config.json diff --git a/lab9config.json b/configs/lab9config.json similarity index 100% rename from lab9config.json rename to configs/lab9config.json diff --git a/project1config.json b/configs/project1config.json similarity index 100% rename from project1config.json rename to configs/project1config.json diff --git a/project2config.json b/configs/project2config.json similarity index 100% rename from project2config.json rename to configs/project2config.json diff --git a/project3config.json b/configs/project3config.json similarity index 100% rename from project3config.json rename to configs/project3config.json diff --git a/project4config.json b/configs/project4config.json similarity index 100% rename from project4config.json rename to configs/project4config.json diff --git a/sandbox/assignment1/student1/file1.txt b/grade/__init__.py similarity index 100% rename from sandbox/assignment1/student1/file1.txt rename to grade/__init__.py diff --git a/assignment.py b/grade/assignment.py similarity index 100% rename from assignment.py rename to grade/assignment.py diff --git a/command.py b/grade/command.py similarity index 98% rename from command.py rename to grade/command.py index 7f0f674..9c975f5 100644 --- a/command.py +++ b/grade/command.py @@ -15,7 +15,7 @@ # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA import argparse import os -import assignment +import grade.assignment as assignment class Command(object): diff --git a/sandbox/assignment1/student1/subdir/file2.txt b/grade/tests/__init__.py similarity index 100% rename from sandbox/assignment1/student1/subdir/file2.txt rename to grade/tests/__init__.py diff --git a/sandbox.py b/grade/tests/sandbox.py similarity index 100% rename from sandbox.py rename to grade/tests/sandbox.py diff --git a/sandbox/assignment1.json b/grade/tests/sandbox/assignment1.json similarity index 100% rename from sandbox/assignment1.json rename to grade/tests/sandbox/assignment1.json diff --git a/sandbox/assignment1/student2/file1.txt b/grade/tests/sandbox/assignment1/student1/file1.txt similarity index 100% rename from sandbox/assignment1/student2/file1.txt rename to grade/tests/sandbox/assignment1/student1/file1.txt diff --git a/sandbox/assignment1/student2/subdir/file2.txt b/grade/tests/sandbox/assignment1/student1/subdir/file2.txt similarity index 100% rename from sandbox/assignment1/student2/subdir/file2.txt rename to grade/tests/sandbox/assignment1/student1/subdir/file2.txt diff --git a/sandbox/assignment1/student3/file1.txt b/grade/tests/sandbox/assignment1/student2/file1.txt similarity index 100% rename from sandbox/assignment1/student3/file1.txt rename to grade/tests/sandbox/assignment1/student2/file1.txt diff --git a/sandbox/assignment1/student3/subdir/file2.txt b/grade/tests/sandbox/assignment1/student2/subdir/file2.txt similarity index 100% rename from sandbox/assignment1/student3/subdir/file2.txt rename to grade/tests/sandbox/assignment1/student2/subdir/file2.txt diff --git a/sandbox/issue28/student1/file1.txt b/grade/tests/sandbox/assignment1/student3/file1.txt similarity index 100% rename from sandbox/issue28/student1/file1.txt rename to grade/tests/sandbox/assignment1/student3/file1.txt diff --git a/sandbox/issue28/student1/subdir/file2.txt b/grade/tests/sandbox/assignment1/student3/subdir/file2.txt similarity index 100% rename from sandbox/issue28/student1/subdir/file2.txt rename to grade/tests/sandbox/assignment1/student3/subdir/file2.txt diff --git a/sandbox/issue28.json b/grade/tests/sandbox/issue28.json similarity index 100% rename from sandbox/issue28.json rename to grade/tests/sandbox/issue28.json diff --git a/sandbox/issue28/student3/file1.txt b/grade/tests/sandbox/issue28/student1/file1.txt similarity index 100% rename from sandbox/issue28/student3/file1.txt rename to grade/tests/sandbox/issue28/student1/file1.txt diff --git a/sandbox/issue28/student2/subdir/file2.txt b/grade/tests/sandbox/issue28/student1/subdir/file2.txt similarity index 100% rename from sandbox/issue28/student2/subdir/file2.txt rename to grade/tests/sandbox/issue28/student1/subdir/file2.txt diff --git a/sandbox/issue28/student3/subdir/file2.txt b/grade/tests/sandbox/issue28/student2/subdir/file2.txt similarity index 100% rename from sandbox/issue28/student3/subdir/file2.txt rename to grade/tests/sandbox/issue28/student2/subdir/file2.txt diff --git a/grade/tests/sandbox/issue28/student3/file1.txt b/grade/tests/sandbox/issue28/student3/file1.txt new file mode 100644 index 0000000..e69de29 diff --git a/grade/tests/sandbox/issue28/student3/subdir/file2.txt b/grade/tests/sandbox/issue28/student3/subdir/file2.txt new file mode 100644 index 0000000..e69de29 diff --git a/test_assignment.py b/grade/tests/test_assignment.py similarity index 96% rename from test_assignment.py rename to grade/tests/test_assignment.py index 95f68ac..8a7d6ee 100644 --- a/test_assignment.py +++ b/grade/tests/test_assignment.py @@ -14,9 +14,9 @@ # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA import unittest -import sandbox import os.path -from assignment import Assignment +import grade.tests.sandbox as sandbox +from grade.assignment import Assignment class test_Assignment(unittest.TestCase): diff --git a/test_command.py b/grade/tests/test_command.py similarity index 98% rename from test_command.py rename to grade/tests/test_command.py index a7bc8a6..08c4338 100644 --- a/test_command.py +++ b/grade/tests/test_command.py @@ -14,8 +14,8 @@ # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA import unittest -import sandbox -from command import Command +import grade.tests.sandbox as sandbox +from grade.command import Command from pathlib import Path diff --git a/test_issue28.py b/grade/tests/test_issue28.py similarity index 92% rename from test_issue28.py rename to grade/tests/test_issue28.py index 038a266..cf332dc 100644 --- a/test_issue28.py +++ b/grade/tests/test_issue28.py @@ -1,11 +1,11 @@ import unittest -import sandbox import os.path import os from io import StringIO import sys -from assignment import Assignment -from command import Command +import grade.tests.sandbox as sandbox +from grade.assignment import Assignment +from grade.command import Command OUTFILE = sandbox.dir('issue28.txt') diff --git a/test_json.py b/grade/tests/test_json.py similarity index 97% rename from test_json.py rename to grade/tests/test_json.py index 2300c3b..dbc172c 100644 --- a/test_json.py +++ b/grade/tests/test_json.py @@ -17,7 +17,7 @@ import json import os.path import os -import sandbox +import grade.tests.sandbox as sandbox CONFIG_FILENAME = sandbox.dir('assignment1.json') diff --git a/run_tests.py b/run_tests.py new file mode 100644 index 0000000..1130550 --- /dev/null +++ b/run_tests.py @@ -0,0 +1,23 @@ +# Copyright (C) 2014 Karl R. Wurst +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA + +from unittest import TestLoader +from unittest.runner import TextTestRunner +from os.path import dirname, abspath + + +if __name__ == '__main__': + TextTestRunner().run(TestLoader().discover(abspath(dirname(__file__)))) From cfee8d4c00f3da15dce2a8f2c0a37ec56a6bec5a Mon Sep 17 00:00:00 2001 From: Stoney Jackson Date: Thu, 10 Jul 2014 17:51:57 -0400 Subject: [PATCH 02/11] Add documentation. --- README.md | 53 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 53 insertions(+) diff --git a/README.md b/README.md index 527aa88..5e49a68 100644 --- a/README.md +++ b/README.md @@ -3,3 +3,56 @@ Scripts for preparing student programs for grading. Scripts convert student repository code to a PDF file for grading. +## Example + +In this example we have the following structure: + + hw1 + ├── config.json + └── submissions + ├── jh + │   └── Lab10Code + │   ├── Employee.java + │   ├── Faculty.java + │   ├── Person.java + │   ├── Staff.java + │   └── Student.java + └── wk + └── Lab10Code + ├── Employee.java + ├── Faculty.java + ├── Person.java + ├── Staff.java + └── Student.java + + +`config.json` contains the following: + + { + "directory" : "/Users/karl/GoogleDrive/Courses/CS-140/StudentSubmissions/S1-2014/grading/Lab10", + "files" : [ + "Lab10Code/Person.java", + "Lab10Code/Student.java", + "Lab10Code/Employee.java", + "Lab10Code/Faculty.java", + "Lab10Code/Staff.java" + ] + } + +* `directory` - Path to the directory containing student submission + subdirectories. Paths are relative to the configuration file's location. +* `files` - List of files to process for each submission directory. Paths are + relative to each student's submission directory. + +`submissions` contians a subdirectory for each students' work. The names of each +subdirectories is not important. In this example they are the initials of each +student. + +We process the assignment as follows: + + $ python path/to/assignmentconvert.py hw1/config.json + +## Run tests + + $ cd grading-scripts + $ python run_tests.py From 5fae2777e5bc731e5b4266eee79baa72c0660956 Mon Sep 17 00:00:00 2001 From: Stoney Jackson Date: Sat, 12 Jul 2014 20:12:04 -0400 Subject: [PATCH 03/11] Combine command.py and assignment.py into engine.py. In Python, modules are used to group related classes (and functions, attributes, constants, etc.). When importing a module, one wants to import everything related to that topic. In our case, Assignment and Command work together to form the assignment processing engine. --- assignmentconvert.py | 3 +- grade/command.py | 79 ------------------- grade/{assignment.py => engine.py} | 64 ++++++++++++++- grade/tests/test_assignment.py | 57 ------------- .../tests/{test_command.py => test_engine.py} | 39 ++++++++- grade/tests/test_issue28.py | 3 +- 6 files changed, 103 insertions(+), 142 deletions(-) delete mode 100644 grade/command.py rename grade/{assignment.py => engine.py} (67%) delete mode 100644 grade/tests/test_assignment.py rename grade/tests/{test_command.py => test_engine.py} (75%) diff --git a/assignmentconvert.py b/assignmentconvert.py index 5c3cd0e..d1e22e4 100644 --- a/assignmentconvert.py +++ b/assignmentconvert.py @@ -16,8 +16,7 @@ import argparse import os -from grade.assignment import Assignment -from grade.command import Command +from grade.engine import Assignment, Command class LabConvert(object): diff --git a/grade/command.py b/grade/command.py deleted file mode 100644 index 9c975f5..0000000 --- a/grade/command.py +++ /dev/null @@ -1,79 +0,0 @@ -# Copyright (C) 2014 Karl R. Wurst -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation; either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software -# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA -import argparse -import os -import grade.assignment as assignment - - -class Command(object): - ''' - Command represents a reusable shell command. - - Examples creating a command: - - concat = Command('cat "{ins}" > "{outs}"') - concat2 = Command('cat "{ins}" > "{ins}.2"') - - IMPORTANT: Always encluse placeholders in double quotes. - - Example calls: - - concat('f1', 'f2') # copies f1 to f2 - concat(['f1', 'f2'], 'f3') # lists, concats f1 and f2 into f3 - concat(pathlib.Path('f1'), 'f2') # you may pass Paths. - concat(o1, o2) # o1 and o2 will be converted to a string via str(). - concat(Path('.').glob('*.txt'), 'out.txt') # globbing - concat(outs='f3', ins=['f1', 'f2']) # named parameters - concat2('f1') # copies f1 to f1.2 ; optional parameters - concat2.each(['f1', 'f2]) # copies f1 to f1.2 and f2 to f2.2 - concat.each(['f1', 'f2'], ['f3', 'f4']) # copies f1 to f3 and f2 to f4 - ''' - _default_verbosity = False - - @classmethod - def set_default_verbosity(cls, flag): - cls._default_verbosity = flag - - def __init__(self, command_string, verbose=None): - self._verbose = \ - verbose if verbose is not None else Command._default_verbosity - self._command_string = command_string - - def __call__(self, ins=None, outs=None): - ins = self._format_args(ins) - outs = self._format_args(outs) - command = self._command_string.format(ins=ins, outs=outs) - self._run_command(command) - - def _format_args(self, args): - if args is None: - return None - if isinstance(args, list): - return '" "'.join([str(i) for i in args]) - elif not isinstance(args, str): - return str(args) - return args - - def _run_command(self, command): - if self._verbose: - print(command) - os.system(command) - - def each(self, ins=None, outs=None): - for i in range(len(ins)): - ins_i = ins[i] - outs_i = outs[i] if isinstance(outs, list) else outs - self(ins_i, outs_i) diff --git a/grade/assignment.py b/grade/engine.py similarity index 67% rename from grade/assignment.py rename to grade/engine.py index a0da186..aa5d3a3 100644 --- a/grade/assignment.py +++ b/grade/engine.py @@ -13,9 +13,10 @@ # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA -import pathlib +import argparse import json import os +import pathlib class Assignment(object): @@ -120,3 +121,64 @@ def _resolve_path_or_none(self, path, root=None): except FileNotFoundError: print('Not found: ' + str(root/path)) return resolved + + +class Command(object): + ''' + Command represents a reusable shell command. + + Examples creating a command: + + concat = Command('cat "{ins}" > "{outs}"') + concat2 = Command('cat "{ins}" > "{ins}.2"') + + IMPORTANT: Always encluse placeholders in double quotes. + + Example calls: + + concat('f1', 'f2') # copies f1 to f2 + concat(['f1', 'f2'], 'f3') # lists, concats f1 and f2 into f3 + concat(pathlib.Path('f1'), 'f2') # you may pass Paths. + concat(o1, o2) # o1 and o2 will be converted to a string via str(). + concat(Path('.').glob('*.txt'), 'out.txt') # globbing + concat(outs='f3', ins=['f1', 'f2']) # named parameters + concat2('f1') # copies f1 to f1.2 ; optional parameters + concat2.each(['f1', 'f2]) # copies f1 to f1.2 and f2 to f2.2 + concat.each(['f1', 'f2'], ['f3', 'f4']) # copies f1 to f3 and f2 to f4 + ''' + _default_verbosity = False + + @classmethod + def set_default_verbosity(cls, flag): + cls._default_verbosity = flag + + def __init__(self, command_string, verbose=None): + self._verbose = \ + verbose if verbose is not None else Command._default_verbosity + self._command_string = command_string + + def __call__(self, ins=None, outs=None): + ins = self._format_args(ins) + outs = self._format_args(outs) + command = self._command_string.format(ins=ins, outs=outs) + self._run_command(command) + + def _format_args(self, args): + if args is None: + return None + if isinstance(args, list): + return '" "'.join([str(i) for i in args]) + elif not isinstance(args, str): + return str(args) + return args + + def _run_command(self, command): + if self._verbose: + print(command) + os.system(command) + + def each(self, ins=None, outs=None): + for i in range(len(ins)): + ins_i = ins[i] + outs_i = outs[i] if isinstance(outs, list) else outs + self(ins_i, outs_i) diff --git a/grade/tests/test_assignment.py b/grade/tests/test_assignment.py deleted file mode 100644 index 8a7d6ee..0000000 --- a/grade/tests/test_assignment.py +++ /dev/null @@ -1,57 +0,0 @@ -# Copyright (C) 2014 Karl R. Wurst -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation; either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software -# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA -import unittest -import os.path -import grade.tests.sandbox as sandbox -from grade.assignment import Assignment - - -class test_Assignment(unittest.TestCase): - - def setUp(self): - self.collector = Collector() - - def test_each(self): - the_assignment = Assignment(sandbox.dir('assignment1.json')) - the_assignment.accept(self.collector.visit) - self.assertPathsExist() - - def assertPathsExist(self): - self.assertTrue( - self.collector.directories, msg="No directories collected.") - self.assertTrue(self.collector.files, msg="No files collected.") - for d in self.collector.directories: - self.assertTrue( - d.exists(), msg="Directory does not exist: " + str(d)) - self.assertTrue(d.is_dir(), msg="Not a directory: " + str(d)) - for f in self.collector.files: - self.assertTrue(f.exists(), msg="File does not exist: " + str(f)) - self.assertTrue(f.is_file(), msg="Not a file: " + str(f)) - - -class Collector(object): - def __init__(self): - self.directories = [] - self.files = [] - - def visit(self, submission_directory, files_to_collect): - self.directories.append(submission_directory) - for file in files_to_collect: - self.files.append(file) - - -if __name__ == '__main__': - unittest.main() diff --git a/grade/tests/test_command.py b/grade/tests/test_engine.py similarity index 75% rename from grade/tests/test_command.py rename to grade/tests/test_engine.py index 08c4338..e6b2e40 100644 --- a/grade/tests/test_command.py +++ b/grade/tests/test_engine.py @@ -14,11 +14,48 @@ # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA import unittest +import os.path import grade.tests.sandbox as sandbox -from grade.command import Command +from grade.engine import Assignment, Command from pathlib import Path +class test_Assignment(unittest.TestCase): + + def setUp(self): + self.collector = Collector() + + def test_each(self): + the_assignment = Assignment(sandbox.dir('assignment1.json')) + the_assignment.accept(self.collector.visit) + self.assertPathsExist() + + def assertPathsExist(self): + self.assertTrue( + self.collector.directories, msg="No directories collected.") + self.assertTrue(self.collector.files, msg="No files collected.") + for d in self.collector.directories: + self.assertTrue( + d.exists(), msg="Directory does not exist: " + str(d)) + self.assertTrue(d.is_dir(), msg="Not a directory: " + str(d)) + for f in self.collector.files: + self.assertTrue(f.exists(), msg="File does not exist: " + str(f)) + self.assertTrue(f.is_file(), msg="Not a file: " + str(f)) + + +class Collector(object): + def __init__(self): + self.directories = [] + self.files = [] + + def visit(self, submission_directory, files_to_collect): + self.directories.append(submission_directory) + for file in files_to_collect: + self.files.append(file) + + + + class test_Command(unittest.TestCase): def setUp(self): diff --git a/grade/tests/test_issue28.py b/grade/tests/test_issue28.py index cf332dc..35f294e 100644 --- a/grade/tests/test_issue28.py +++ b/grade/tests/test_issue28.py @@ -4,8 +4,7 @@ from io import StringIO import sys import grade.tests.sandbox as sandbox -from grade.assignment import Assignment -from grade.command import Command +from grade.engine import Assignment, Command OUTFILE = sandbox.dir('issue28.txt') From b808906a80a058a4874c5569dbd2d7c9e07382e2 Mon Sep 17 00:00:00 2001 From: Stoney Jackson Date: Sat, 12 Jul 2014 21:24:26 -0400 Subject: [PATCH 04/11] Add logging with verbosity control (closes #22). --- assignmentconvert.py | 17 +++++++++++++++-- grade/engine.py | 24 +++++++----------------- grade/tests/test_engine.py | 20 -------------------- grade/tests/test_issue28.py | 2 -- 4 files changed, 22 insertions(+), 41 deletions(-) diff --git a/assignmentconvert.py b/assignmentconvert.py index d1e22e4..4be18cb 100644 --- a/assignmentconvert.py +++ b/assignmentconvert.py @@ -15,6 +15,7 @@ # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA import argparse +import logging import os from grade.engine import Assignment, Command @@ -27,10 +28,22 @@ def __init__(self): parser.add_argument( '-v', '--verbose', help='increase output verbosity', - action='store_true' + action='store_true', + default=False + ) + parser.add_argument( + '-b', '--brief', + help='decrease output verbosity', + action='store_true', + default=False ) args = parser.parse_args() - Command.set_default_verbosity(args.verbose) + if args.verbose: + logging.basicConfig(level='DEBUG') + elif args.brief: + logging.basicConfig(level='WARNING') + else: + logging.basicConfig(level='INFO') self._a2pdf = Command( 'a2pdf --noperl-syntax --noline-numbers "{ins}" -o "{ins}.pdf"') self._pdfcat = Command('pdftk "{ins}" cat output "{outs}"') diff --git a/grade/engine.py b/grade/engine.py index aa5d3a3..ebd5e8d 100644 --- a/grade/engine.py +++ b/grade/engine.py @@ -17,6 +17,7 @@ import json import os import pathlib +import logging class Assignment(object): @@ -24,9 +25,8 @@ class Assignment(object): directory. ''' - def __init__(self, config_file, verbose=False, cd=True): + def __init__(self, config_file, cd=True): self._config_file = pathlib.Path(config_file).resolve() - self._verbose = verbose self._cd = cd self._config_dict = None self._submission_directories = [] @@ -86,12 +86,10 @@ def accept(self, visit, cd=None): self._notify_end_process_directory(directory) def _notify_start_process_directory(self, directory): - if self._verbose: - print('Processing', directory) + logging.info('Processing {directory}'.format(directory=directory)) def _notify_end_process_directory(self, directory): - if self._verbose: - print('Done processing', directory) + logging.info('Done processing {directory}'.format(directory=directory)) def _process_directory(self, visit, directory): self._enter_directory(directory) @@ -119,7 +117,7 @@ def _resolve_path_or_none(self, path, root=None): try: resolved = self._resolve_path(path, root=root) except FileNotFoundError: - print('Not found: ' + str(root/path)) + logging.warning('Not found: {path}'.format(path=str(root/path))) return resolved @@ -146,15 +144,8 @@ class Command(object): concat2.each(['f1', 'f2]) # copies f1 to f1.2 and f2 to f2.2 concat.each(['f1', 'f2'], ['f3', 'f4']) # copies f1 to f3 and f2 to f4 ''' - _default_verbosity = False - @classmethod - def set_default_verbosity(cls, flag): - cls._default_verbosity = flag - - def __init__(self, command_string, verbose=None): - self._verbose = \ - verbose if verbose is not None else Command._default_verbosity + def __init__(self, command_string): self._command_string = command_string def __call__(self, ins=None, outs=None): @@ -173,8 +164,7 @@ def _format_args(self, args): return args def _run_command(self, command): - if self._verbose: - print(command) + logging.debug(command) os.system(command) def each(self, ins=None, outs=None): diff --git a/grade/tests/test_engine.py b/grade/tests/test_engine.py index e6b2e40..1991528 100644 --- a/grade/tests/test_engine.py +++ b/grade/tests/test_engine.py @@ -59,14 +59,12 @@ def visit(self, submission_directory, files_to_collect): class test_Command(unittest.TestCase): def setUp(self): - Command.set_default_verbosity(False) self.directory = Path(sandbox.dir('test_Command')) self.directory.mkdir() self.file_ = self.directory / 'test_simple' self.filename = str(self.file_) def tearDown(self): - Command.set_default_verbosity(False) for f in self.directory.glob('*'): f.unlink() self.directory.rmdir() @@ -126,24 +124,6 @@ def test_only_outs(self): write_hi(outs=self.file_) self.assertTrue(self.file_.exists()) - def test_default_verbosity_is_false(self): - ls = Command('ls') - self.assertFalse(ls._verbose) - - def test_verbosity_true(self): - ls = Command('ls', verbose=True) - self.assertTrue(ls._verbose) - - def test_set_default_verbosity_true(self): - Command.set_default_verbosity(True) - ls = Command('ls') - self.assertTrue(ls._verbose) - - def test_override_default_verbosity_false(self): - Command.set_default_verbosity(True) - ls = Command('ls', False) - self.assertFalse(ls._verbose) - if __name__ == '__main__': unittest.main() diff --git a/grade/tests/test_issue28.py b/grade/tests/test_issue28.py index 35f294e..7157c0c 100644 --- a/grade/tests/test_issue28.py +++ b/grade/tests/test_issue28.py @@ -23,8 +23,6 @@ def test_missingFile_keepGoing(self): Assignment(sandbox.dir('issue28.json')).accept(collector.visit) self.assertTrue(len(collector.get_lines()) == 5) output = out.getvalue().strip() - self.assertRegex(output, 'Not found: .*student2/file1.txt', - msg=output) finally: self._restore_stdout() From cb8c6bc8b27c43bab8ebc4437e6d62598d707ec8 Mon Sep 17 00:00:00 2001 From: Stoney Jackson Date: Sun, 13 Jul 2014 07:26:00 -0400 Subject: [PATCH 05/11] Simplify client code and overall structure. --- README.md | 4 ++ assignmentconvert.py | 42 +++---------- grade/engine.py => grade.py | 59 ++++++++++++++----- grade/tests/__init__.py | 0 {grade => tests}/__init__.py | 0 {grade/tests => tests}/sandbox.py | 0 .../tests => tests}/sandbox/assignment1.json | 0 .../sandbox/assignment1/student1/file1.txt | 0 .../assignment1/student1/subdir/file2.txt | 0 .../sandbox/assignment1/student2/file1.txt | 0 .../assignment1/student2/subdir/file2.txt | 0 .../sandbox/assignment1/student3/file1.txt | 0 .../assignment1/student3/subdir/file2.txt | 0 {grade/tests => tests}/sandbox/issue28.json | 0 .../sandbox/issue28/student1/file1.txt | 0 .../sandbox/issue28/student1/subdir/file2.txt | 0 .../sandbox/issue28/student2/subdir/file2.txt | 0 .../sandbox/issue28/student3/file1.txt | 0 .../sandbox/issue28/student3/subdir/file2.txt | 0 .../test_engine.py => tests/test_grade.py | 4 +- {grade/tests => tests}/test_issue28.py | 4 +- {grade/tests => tests}/test_json.py | 2 +- 22 files changed, 61 insertions(+), 54 deletions(-) rename grade/engine.py => grade.py (80%) delete mode 100644 grade/tests/__init__.py rename {grade => tests}/__init__.py (100%) rename {grade/tests => tests}/sandbox.py (100%) rename {grade/tests => tests}/sandbox/assignment1.json (100%) rename {grade/tests => tests}/sandbox/assignment1/student1/file1.txt (100%) rename {grade/tests => tests}/sandbox/assignment1/student1/subdir/file2.txt (100%) rename {grade/tests => tests}/sandbox/assignment1/student2/file1.txt (100%) rename {grade/tests => tests}/sandbox/assignment1/student2/subdir/file2.txt (100%) rename {grade/tests => tests}/sandbox/assignment1/student3/file1.txt (100%) rename {grade/tests => tests}/sandbox/assignment1/student3/subdir/file2.txt (100%) rename {grade/tests => tests}/sandbox/issue28.json (100%) rename {grade/tests => tests}/sandbox/issue28/student1/file1.txt (100%) rename {grade/tests => tests}/sandbox/issue28/student1/subdir/file2.txt (100%) rename {grade/tests => tests}/sandbox/issue28/student2/subdir/file2.txt (100%) rename {grade/tests => tests}/sandbox/issue28/student3/file1.txt (100%) rename {grade/tests => tests}/sandbox/issue28/student3/subdir/file2.txt (100%) rename grade/tests/test_engine.py => tests/test_grade.py (98%) rename {grade/tests => tests}/test_issue28.py (94%) rename {grade/tests => tests}/test_json.py (97%) diff --git a/README.md b/README.md index 5e49a68..63eadc3 100644 --- a/README.md +++ b/README.md @@ -56,3 +56,7 @@ We process the assignment as follows: $ cd grading-scripts $ python run_tests.py + +## Writting a processor + +See `assignmentconvert.py` for an example. diff --git a/assignmentconvert.py b/assignmentconvert.py index 4be18cb..90d1c47 100644 --- a/assignmentconvert.py +++ b/assignmentconvert.py @@ -13,43 +13,17 @@ # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA +from grade import App -import argparse -import logging -import os -from grade.engine import Assignment, Command - - -class LabConvert(object): +class LabConvert(App): def __init__(self): - parser = argparse.ArgumentParser() - parser.add_argument('config', help='JSON configuration file') - parser.add_argument( - '-v', '--verbose', - help='increase output verbosity', - action='store_true', - default=False - ) - parser.add_argument( - '-b', '--brief', - help='decrease output verbosity', - action='store_true', - default=False - ) - args = parser.parse_args() - if args.verbose: - logging.basicConfig(level='DEBUG') - elif args.brief: - logging.basicConfig(level='WARNING') - else: - logging.basicConfig(level='INFO') - self._a2pdf = Command( + super().__init__(self) + self._a2pdf = self.command( 'a2pdf --noperl-syntax --noline-numbers "{ins}" -o "{ins}.pdf"') - self._pdfcat = Command('pdftk "{ins}" cat output "{outs}"') - self._create_log = Command('git log > log.txt') - self._rm = Command('rm "{ins}"') - Assignment(args.config).accept(self.process_submission) + self._pdfcat = self.command('pdftk "{ins}" cat output "{outs}"') + self._create_log = self.command('git log > log.txt') + self._rm = self.command('rm "{ins}"') def process_submission(self, directory, files): self._create_log() @@ -62,4 +36,4 @@ def process_submission(self, directory, files): if __name__ == '__main__': - LabConvert() + LabConvert().run() diff --git a/grade/engine.py b/grade.py similarity index 80% rename from grade/engine.py rename to grade.py index ebd5e8d..56111c9 100644 --- a/grade/engine.py +++ b/grade.py @@ -1,25 +1,54 @@ -# Copyright (C) 2014 Karl R. Wurst -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation; either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software -# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA import argparse +import logging import json import os import pathlib import logging +class App(object): + def __init__(self): + self._arguments = CommandLineArguments() + self._init_logging() + self._assignment = Assignment(self._arguments.config) + + def _init_logging(self): + if self._arguments.verbose: + logging.basicConfig(level='DEBUG') + elif self._arguments.brief: + logging.basicConfig(level='WARNING') + else: + logging.basicConfig(level='INFO') + + def run(self): + self._assignment.accept(self.process_submissions) + + def process_submissions(self, directory, files): + raise NotImplementedError('Must implement.') + + def command(self, shell_string): + return Command(shell_string) + + +class CommandLineArguments(object): + def __init__(self): + self._parser = argparse.ArgumentParser() + self._parser.add_argument('config', help='JSON configuration file') + self._parser.add_argument( + '-v', '--verbose', + help='increase output verbosity', + action='store_true', + default=False + ) + self._parser.add_argument( + '-b', '--brief', + help='decrease output verbosity', + action='store_true', + default=False + ) + self._parser.parse_args(namespace=self) + + class Assignment(object): '''Provides traversal and path resolution for visitors over an assignment directory. diff --git a/grade/tests/__init__.py b/grade/tests/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/grade/__init__.py b/tests/__init__.py similarity index 100% rename from grade/__init__.py rename to tests/__init__.py diff --git a/grade/tests/sandbox.py b/tests/sandbox.py similarity index 100% rename from grade/tests/sandbox.py rename to tests/sandbox.py diff --git a/grade/tests/sandbox/assignment1.json b/tests/sandbox/assignment1.json similarity index 100% rename from grade/tests/sandbox/assignment1.json rename to tests/sandbox/assignment1.json diff --git a/grade/tests/sandbox/assignment1/student1/file1.txt b/tests/sandbox/assignment1/student1/file1.txt similarity index 100% rename from grade/tests/sandbox/assignment1/student1/file1.txt rename to tests/sandbox/assignment1/student1/file1.txt diff --git a/grade/tests/sandbox/assignment1/student1/subdir/file2.txt b/tests/sandbox/assignment1/student1/subdir/file2.txt similarity index 100% rename from grade/tests/sandbox/assignment1/student1/subdir/file2.txt rename to tests/sandbox/assignment1/student1/subdir/file2.txt diff --git a/grade/tests/sandbox/assignment1/student2/file1.txt b/tests/sandbox/assignment1/student2/file1.txt similarity index 100% rename from grade/tests/sandbox/assignment1/student2/file1.txt rename to tests/sandbox/assignment1/student2/file1.txt diff --git a/grade/tests/sandbox/assignment1/student2/subdir/file2.txt b/tests/sandbox/assignment1/student2/subdir/file2.txt similarity index 100% rename from grade/tests/sandbox/assignment1/student2/subdir/file2.txt rename to tests/sandbox/assignment1/student2/subdir/file2.txt diff --git a/grade/tests/sandbox/assignment1/student3/file1.txt b/tests/sandbox/assignment1/student3/file1.txt similarity index 100% rename from grade/tests/sandbox/assignment1/student3/file1.txt rename to tests/sandbox/assignment1/student3/file1.txt diff --git a/grade/tests/sandbox/assignment1/student3/subdir/file2.txt b/tests/sandbox/assignment1/student3/subdir/file2.txt similarity index 100% rename from grade/tests/sandbox/assignment1/student3/subdir/file2.txt rename to tests/sandbox/assignment1/student3/subdir/file2.txt diff --git a/grade/tests/sandbox/issue28.json b/tests/sandbox/issue28.json similarity index 100% rename from grade/tests/sandbox/issue28.json rename to tests/sandbox/issue28.json diff --git a/grade/tests/sandbox/issue28/student1/file1.txt b/tests/sandbox/issue28/student1/file1.txt similarity index 100% rename from grade/tests/sandbox/issue28/student1/file1.txt rename to tests/sandbox/issue28/student1/file1.txt diff --git a/grade/tests/sandbox/issue28/student1/subdir/file2.txt b/tests/sandbox/issue28/student1/subdir/file2.txt similarity index 100% rename from grade/tests/sandbox/issue28/student1/subdir/file2.txt rename to tests/sandbox/issue28/student1/subdir/file2.txt diff --git a/grade/tests/sandbox/issue28/student2/subdir/file2.txt b/tests/sandbox/issue28/student2/subdir/file2.txt similarity index 100% rename from grade/tests/sandbox/issue28/student2/subdir/file2.txt rename to tests/sandbox/issue28/student2/subdir/file2.txt diff --git a/grade/tests/sandbox/issue28/student3/file1.txt b/tests/sandbox/issue28/student3/file1.txt similarity index 100% rename from grade/tests/sandbox/issue28/student3/file1.txt rename to tests/sandbox/issue28/student3/file1.txt diff --git a/grade/tests/sandbox/issue28/student3/subdir/file2.txt b/tests/sandbox/issue28/student3/subdir/file2.txt similarity index 100% rename from grade/tests/sandbox/issue28/student3/subdir/file2.txt rename to tests/sandbox/issue28/student3/subdir/file2.txt diff --git a/grade/tests/test_engine.py b/tests/test_grade.py similarity index 98% rename from grade/tests/test_engine.py rename to tests/test_grade.py index 1991528..28280c1 100644 --- a/grade/tests/test_engine.py +++ b/tests/test_grade.py @@ -15,8 +15,8 @@ # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA import unittest import os.path -import grade.tests.sandbox as sandbox -from grade.engine import Assignment, Command +import tests.sandbox as sandbox +from grade import Assignment, Command from pathlib import Path diff --git a/grade/tests/test_issue28.py b/tests/test_issue28.py similarity index 94% rename from grade/tests/test_issue28.py rename to tests/test_issue28.py index 7157c0c..a8fec1d 100644 --- a/grade/tests/test_issue28.py +++ b/tests/test_issue28.py @@ -3,8 +3,8 @@ import os from io import StringIO import sys -import grade.tests.sandbox as sandbox -from grade.engine import Assignment, Command +import tests.sandbox as sandbox +from grade import Assignment, Command OUTFILE = sandbox.dir('issue28.txt') diff --git a/grade/tests/test_json.py b/tests/test_json.py similarity index 97% rename from grade/tests/test_json.py rename to tests/test_json.py index dbc172c..925329f 100644 --- a/grade/tests/test_json.py +++ b/tests/test_json.py @@ -17,7 +17,7 @@ import json import os.path import os -import grade.tests.sandbox as sandbox +import tests.sandbox as sandbox CONFIG_FILENAME = sandbox.dir('assignment1.json') From b8c8ab41a5bbf5fd95c660eec7ac77f6f4db3082 Mon Sep 17 00:00:00 2001 From: Stoney Jackson Date: Sun, 13 Jul 2014 07:37:21 -0400 Subject: [PATCH 06/11] Add copyright headers. --- grade.py | 15 +++++++++++++++ tests/test_issue28.py | 15 +++++++++++++++ 2 files changed, 30 insertions(+) diff --git a/grade.py b/grade.py index 56111c9..70e77ba 100644 --- a/grade.py +++ b/grade.py @@ -1,3 +1,18 @@ +# Copyright (C) 2014 Karl R. Wurst +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA import argparse import logging import json diff --git a/tests/test_issue28.py b/tests/test_issue28.py index a8fec1d..80ab128 100644 --- a/tests/test_issue28.py +++ b/tests/test_issue28.py @@ -1,3 +1,18 @@ +# Copyright (C) 2014 Karl R. Wurst +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA import unittest import os.path import os From 23dc6a16bad1444e04ac38a9efedd30ae3b8396e Mon Sep 17 00:00:00 2001 From: Stoney Jackson Date: Sun, 13 Jul 2014 21:04:00 -0400 Subject: [PATCH 07/11] Remove double import. --- grade.py | 1 - 1 file changed, 1 deletion(-) diff --git a/grade.py b/grade.py index 70e77ba..4bfcdad 100644 --- a/grade.py +++ b/grade.py @@ -18,7 +18,6 @@ import json import os import pathlib -import logging class App(object): From faf315747583ef3458bc0f19533a1a631a86da6a Mon Sep 17 00:00:00 2001 From: Stoney Jackson Date: Sun, 13 Jul 2014 21:24:18 -0400 Subject: [PATCH 08/11] Improve naming and add documentation. I think from submissions import Processor ... class ToPdf(Processor): reads better than from grade import App ... class ToPdf(App): --- assignmentconvert.py | 4 ++-- grade.py => submissions.py | 27 +++++++++++++++++++++------ tests/test_grade.py | 2 +- tests/test_issue28.py | 2 +- 4 files changed, 25 insertions(+), 10 deletions(-) rename grade.py => submissions.py (89%) diff --git a/assignmentconvert.py b/assignmentconvert.py index 90d1c47..97b8eea 100644 --- a/assignmentconvert.py +++ b/assignmentconvert.py @@ -13,10 +13,10 @@ # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA -from grade import App +from submissions import Processor -class LabConvert(App): +class LabConvert(Processor): def __init__(self): super().__init__(self) self._a2pdf = self.command( diff --git a/grade.py b/submissions.py similarity index 89% rename from grade.py rename to submissions.py index 4bfcdad..5a60af7 100644 --- a/grade.py +++ b/submissions.py @@ -20,8 +20,17 @@ import pathlib -class App(object): +class Processor(object): + ''' + Base class for submission processors. To use: + 1) Inherit. + 2) Call super constructor. + 3) Optionally set self.cd = False to prevent changing into each + submission directory before calling process_submissions. + 4) Implement process_submission. + ''' def __init__(self): + self.cd = True self._arguments = CommandLineArguments() self._init_logging() self._assignment = Assignment(self._arguments.config) @@ -35,12 +44,21 @@ def _init_logging(self): logging.basicConfig(level='INFO') def run(self): - self._assignment.accept(self.process_submissions) + self._assignment.accept(self.process_submission, self.cd) - def process_submissions(self, directory, files): + def process_submission(self, directory, files): + ''' + Called once for each submission directory. `directory` is a + `pathlib.Path` to the submission directory. `files` is a possibly empy + list of `pathlib.Path`s to files in `directory` to process. The paths + in `files` have been resolved against directory. All `files` exist. + ''' raise NotImplementedError('Must implement.') def command(self, shell_string): + ''' + Returns a `Command` for the given `shell_string`. + ''' return Command(shell_string) @@ -64,9 +82,6 @@ def __init__(self): class Assignment(object): - '''Provides traversal and path resolution for visitors over an assignment - directory. - ''' def __init__(self, config_file, cd=True): self._config_file = pathlib.Path(config_file).resolve() diff --git a/tests/test_grade.py b/tests/test_grade.py index 28280c1..6d73e64 100644 --- a/tests/test_grade.py +++ b/tests/test_grade.py @@ -16,7 +16,7 @@ import unittest import os.path import tests.sandbox as sandbox -from grade import Assignment, Command +from submissions import Assignment, Command from pathlib import Path diff --git a/tests/test_issue28.py b/tests/test_issue28.py index 80ab128..c0f1277 100644 --- a/tests/test_issue28.py +++ b/tests/test_issue28.py @@ -19,7 +19,7 @@ from io import StringIO import sys import tests.sandbox as sandbox -from grade import Assignment, Command +from submissions import Assignment, Command OUTFILE = sandbox.dir('issue28.txt') From 8cd09100632c16239c2134363e6b209dc9d0b719 Mon Sep 17 00:00:00 2001 From: Stoney Jackson Date: Sun, 13 Jul 2014 21:32:12 -0400 Subject: [PATCH 09/11] Another attempt to improve a name. --- assignmentconvert.py => collect_pdfs.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename assignmentconvert.py => collect_pdfs.py (100%) diff --git a/assignmentconvert.py b/collect_pdfs.py similarity index 100% rename from assignmentconvert.py rename to collect_pdfs.py From 64c2376507c3805520a3a74a971f49c545735dc6 Mon Sep 17 00:00:00 2001 From: Stoney Jackson Date: Sun, 13 Jul 2014 21:39:40 -0400 Subject: [PATCH 10/11] More name massaging. --- collect_pdfs.py | 4 ++-- submissions.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/collect_pdfs.py b/collect_pdfs.py index 97b8eea..65a2c7c 100644 --- a/collect_pdfs.py +++ b/collect_pdfs.py @@ -13,10 +13,10 @@ # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA -from submissions import Processor +from submissions import SubmissionProcessor -class LabConvert(Processor): +class LabConvert(SubmissionProcessor): def __init__(self): super().__init__(self) self._a2pdf = self.command( diff --git a/submissions.py b/submissions.py index 5a60af7..36cb92f 100644 --- a/submissions.py +++ b/submissions.py @@ -20,7 +20,7 @@ import pathlib -class Processor(object): +class SubmissionProcessor(object): ''' Base class for submission processors. To use: 1) Inherit. From 0d6d3e9ebe86c7c65649112d8a99977aee5db39e Mon Sep 17 00:00:00 2001 From: Stoney Jackson Date: Sun, 13 Jul 2014 22:25:57 -0400 Subject: [PATCH 11/11] Parameters to Commands are now quoted automatically. Command('ls "{ins}"') should now be Command('ls {ins}') --- collect_pdfs.py | 6 ++-- submissions.py | 19 +++++++---- tests/{test_grade.py => test_submissions.py} | 34 ++++++++++++++++---- 3 files changed, 44 insertions(+), 15 deletions(-) rename tests/{test_grade.py => test_submissions.py} (79%) diff --git a/collect_pdfs.py b/collect_pdfs.py index 65a2c7c..aabe43f 100644 --- a/collect_pdfs.py +++ b/collect_pdfs.py @@ -20,10 +20,10 @@ class LabConvert(SubmissionProcessor): def __init__(self): super().__init__(self) self._a2pdf = self.command( - 'a2pdf --noperl-syntax --noline-numbers "{ins}" -o "{ins}.pdf"') - self._pdfcat = self.command('pdftk "{ins}" cat output "{outs}"') + 'a2pdf --noperl-syntax --noline-numbers {ins} -o {ins}.pdf') + self._pdfcat = self.command('pdftk {ins} cat output {outs}') self._create_log = self.command('git log > log.txt') - self._rm = self.command('rm "{ins}"') + self._rm = self.command('rm {ins}') def process_submission(self, directory, files): self._create_log() diff --git a/submissions.py b/submissions.py index 36cb92f..0ebaba9 100644 --- a/submissions.py +++ b/submissions.py @@ -185,10 +185,12 @@ class Command(object): Examples creating a command: - concat = Command('cat "{ins}" > "{outs}"') - concat2 = Command('cat "{ins}" > "{ins}.2"') + concat = Command('cat {ins} > {outs}') + concat2 = Command('cat {ins} > {ins}.2') - IMPORTANT: Always encluse placeholders in double quotes. + {ins} and {outs} will be replaced by the parameters passed to the command + when called. These parameters will be quoted when substituted in. So, do not + quote. Example calls: @@ -216,10 +218,15 @@ def _format_args(self, args): if args is None: return None if isinstance(args, list): - return '" "'.join([str(i) for i in args]) + return ' '.join([self._format_args(arg) for arg in args]) elif not isinstance(args, str): - return str(args) - return args + args_string = str(args) + else: + args_string = args + args_string = args_string.replace('"', '\\"') + args_string = '"' + args_string + '"' + return args_string + def _run_command(self, command): logging.debug(command) diff --git a/tests/test_grade.py b/tests/test_submissions.py similarity index 79% rename from tests/test_grade.py rename to tests/test_submissions.py index 6d73e64..590a2d5 100644 --- a/tests/test_grade.py +++ b/tests/test_submissions.py @@ -85,8 +85,8 @@ def test_implicit_ins(self): self.assertTrue(not self.file_.exists()) def _get_parameterized_commands(self): - touch = Command('touch "{ins}"') - remove = Command('rm "{ins}"') + touch = Command('touch {ins}') + remove = Command('rm {ins}') return (touch, remove) def test_explicit_ins(self): @@ -99,7 +99,7 @@ def test_explicit_ins(self): def test_list_ins(self): touch, remove = self._get_parameterized_commands() touch(ins=[self.file_, self.filename+'2']) - self.assertTrue(self.file_.exists()) + self.assertTrue(self.file_.exists(), msg="File: {}".format(self.file_)) self.assertTrue(Path(self.filename+'2').exists()) remove(ins=[self.file_, self.filename+'2']) self.assertTrue(not self.file_.exists()) @@ -108,22 +108,44 @@ def test_list_ins(self): def test_explicit_outs(self): touch, remove = self._get_parameterized_commands() touch(self.file_) - copy = Command('cat "{ins}" > "{outs}"') + copy = Command('cat {ins} > {outs}') copy(self.file_, outs=self.filename+'2') self.assertTrue(Path(self.filename+'2').exists()) def test_implicit_outs(self): touch, remove = self._get_parameterized_commands() touch(self.file_) - copy = Command('cat "{ins}" > "{outs}"') + copy = Command('cat {ins} > {outs}') copy(self.file_, self.filename+'2') self.assertTrue(Path(self.filename+'2').exists()) def test_only_outs(self): - write_hi = Command('echo hi > "{outs}"') + write_hi = Command('echo hi > {outs}') write_hi(outs=self.file_) self.assertTrue(self.file_.exists()) + def test_format_args_noneReturnsNone(self): + command = Command('') + self.assertIsNone(command._format_args(None)) + + def test_format_args_oneReturnsQuoted(self): + command = Command('') + result = command._format_args('hi') + self.assertEquals('"hi"', result) + + def test_format_args_quotesEscaped(self): + command = Command('') + original = '"hi"' + expected = '"\\"hi\\""' + result = command._format_args(original) + self.assertEquals(expected, result) + + def test_format_args_list(self): + command = Command('') + original = [ 'hi', 'mom', '"how"' ] + expected = '"hi" "mom" "\\"how\\""' + result = command._format_args(original) + self.assertEquals(expected, result) if __name__ == '__main__': unittest.main()