autograder.assignment
1import inspect 2import traceback 3import typing 4 5import edq.util.code 6import edq.util.serial 7 8import autograder.question 9import autograder.util.prepare_submission 10 11class GradedAssignment(edq.util.serial.DictConverter): 12 """ 13 The result of an assignment being graded with a submission. 14 """ 15 16 def __init__(self, 17 name: str, 18 questions: typing.Sequence[autograder.question.GradedQuestion], 19 message: typing.Union[str, None] = None, 20 prologue: typing.Union[str, None] = None, 21 epilogue: typing.Union[str, None] = None, 22 grading_start_time: typing.Union[edq.util.time.Timestamp, int, None] = None, 23 grading_end_time: typing.Union[edq.util.time.Timestamp, int, None] = None, 24 proxy_start_time: typing.Union[edq.util.time.Timestamp, int, None] = None, 25 proxy_end_time: typing.Union[edq.util.time.Timestamp, int, None] = None, 26 **kwargs: typing.Any) -> None: 27 self.name: str = name 28 """ The name of the assignment. """ 29 30 self.questions: typing.List[autograder.question.GradedQuestion] = list(questions) 31 """ The result of grading for each question. """ 32 33 self.message: typing.Union[str, None] = prologue 34 """ Text to include with the grading result. """ 35 36 self.prologue: typing.Union[str, None] = prologue 37 """ Text to include before the grading result. """ 38 39 self.epilogue: typing.Union[str, None] = epilogue 40 """ Text to include after the grading result. """ 41 42 if (grading_start_time is None): 43 grading_start_time = edq.util.time.Timestamp() 44 45 self.grading_start_time: typing.Any = edq.util.time.Timestamp(grading_start_time) 46 """ When grading started. """ 47 48 if (grading_end_time is None): 49 grading_end_time = edq.util.time.Timestamp() 50 51 self.grading_end_time: typing.Any = edq.util.time.Timestamp(grading_end_time) 52 """ When grading ended. """ 53 54 if (proxy_start_time is None): 55 proxy_start_time = edq.util.time.Timestamp() 56 57 self.proxy_start_time: typing.Any = edq.util.time.Timestamp(proxy_start_time) 58 """ When proxy grading started. """ 59 60 if (proxy_end_time is None): 61 proxy_end_time = edq.util.time.Timestamp() 62 63 self.proxy_end_time: typing.Any = edq.util.time.Timestamp(proxy_end_time) 64 """ When proxy grading ended. """ 65 66 def to_test_submission(self, options: typing.Union[typing.Dict[str, typing.Any], None] = None) -> typing.Dict[str, typing.Any]: 67 """ 68 Output a dict that can be used as a test submission. 69 """ 70 71 if (options is None): 72 options = {} 73 74 results = self.to_dict() 75 76 del results['grading_start_time'] 77 del results['grading_end_time'] 78 del results['proxy_start_time'] 79 del results['proxy_end_time'] 80 81 questions = typing.cast(typing.List[typing.Dict[str, edq.util.serial.PODType]], results.get('questions', [])) 82 83 for question in questions: 84 del question['grading_start_time'] 85 del question['grading_end_time'] 86 87 test_submission = { 88 'result': results, 89 } 90 91 test_submission.update(options) 92 93 return test_submission 94 95 def get_score(self) -> typing.Tuple[float, float]: 96 """ 97 Return (total score, max score). 98 """ 99 100 total_score: float = 0 101 max_points: float = 0 102 103 for question in self.questions: 104 total_score += question.score 105 max_points += question.max_points 106 107 return (total_score, max_points) 108 109 def report(self, prefix: str = '', precision: int = 2) -> str: 110 """ 111 Return a string representation of the grading for this assignment. 112 """ 113 114 if ((prefix != '') and (not prefix.endswith(' '))): 115 prefix += ' ' 116 117 output: typing.List[str] = [] 118 119 output += self._format_logue(self.prologue, prefix) 120 121 output += [ 122 prefix + f"Autograder transcript for assignment: {self.name}", 123 prefix + f"Grading started at {self.grading_start_time.pretty()} and ended at {self.grading_end_time.pretty()}.", 124 ] 125 126 if ((self.message is not None) and (len(self.message) > 0)): 127 output.append(f"\n{self.message}\n") 128 129 total_score: float = 0 130 total_max_points: float = 0 131 132 for question in self.questions: 133 total_score += question.score 134 total_max_points += question.max_points 135 136 output.append(question.scoring_report(prefix = prefix, precision = precision)) 137 138 total_score_str = autograder.util.math.number_to_str(total_score, precision = precision) 139 total_max_points_str = autograder.util.math.number_to_str(total_max_points, precision = precision) 140 141 output.append('') 142 output.append(f"{prefix}Total: {total_score_str} / {total_max_points_str}") 143 144 output += self._format_logue(self.epilogue, prefix) 145 146 return "\n".join(output) 147 148 def _format_logue(self, text: typing.Union[str, None], prefix: str) -> typing.List[str]: 149 """ Format the output logue (prologe/epilogue). """ 150 151 if ((text is None) or (text == '')): 152 return [] 153 154 lines = text.splitlines() 155 return [prefix + line.rstrip() for line in lines] 156 157 def __eq__(self, other: object) -> bool: 158 return self.equals(other) 159 160 def equals(self, other: object, **kwargs: typing.Any) -> bool: 161 """ Check two graded assignments for equality. """ 162 163 if (not isinstance(other, GradedAssignment)): 164 return False 165 166 if ((self.name != other.name) or (len(self.questions) != len(other.questions))): 167 return False 168 169 for (i, question) in enumerate(self.questions): 170 if (not question.equals(other.questions[i], **kwargs)): 171 return False 172 173 return True 174 175class Assignment: 176 """ 177 A collection of questions to be scored. 178 """ 179 180 def __init__(self, 181 name: typing.Union[str, None] = None, 182 questions: typing.Union[typing.Sequence[autograder.question.Question], None] = None, 183 input_dir: str = '.', 184 output_dir: str = '.', 185 work_dir: str = '.', 186 prep_submission: bool = True, 187 additional_data: typing.Union[typing.Dict[str, typing.Any], None] = None, 188 **kwargs: typing.Any) -> None: 189 if (name is None): 190 name = type(self).__name__ 191 192 self.name: str = name 193 """ 194 The display name for this assignment. 195 Defaults to the (unqualified) name of this class. 196 """ 197 198 if (questions is None): 199 questions = [] 200 201 self.questions = list(questions) 202 """ The questions for this assignment. """ 203 204 self.input_dir: str = input_dir 205 """ The base directory that houses student inputs for this assignment. """ 206 207 self.output_dir: str = output_dir 208 """ 209 The base directory where output will be written for this assignment. 210 A successful grading should result in a `result.json` created in this directory. 211 """ 212 213 self.work_dir: str = work_dir 214 """ The base directory that the grader is run in. """ 215 216 self.prep_submission: bool = prep_submission 217 """ Whether or not to call self.prepare_submission() to prepare the input directory before grading. """ 218 219 if (additional_data is None): 220 additional_data = {} 221 222 self.additional_data: typing.Dict[str, typing.Any] = additional_data 223 """ Additional data that can be passed to the grader. """ 224 225 self.result: typing.Union[GradedAssignment, None] = None 226 """ The result of grading. """ 227 228 def grade(self, **kwargs: typing.Any) -> GradedAssignment: 229 """ Grade this assignment. """ 230 231 try: 232 return self._grade_submission(self._prepare_submission(), **kwargs) 233 except Exception: 234 now = edq.util.time.Timestamp.now() 235 236 questions = [] 237 for question in self.questions: 238 questions.append(autograder.question.GradedQuestion( 239 name = question.name, 240 max_points = question.max_points, 241 score = 0, 242 message = "Submission could not be graded.", 243 grading_start_time = now, 244 grading_end_time = now)) 245 246 epilogue = (f"\nSubmission could not be graded because of the following error:\n---\n{traceback.format_exc()}---") 247 248 return GradedAssignment( 249 name = self.name, 250 questions = questions, 251 grading_start_time = now, 252 grading_end_time = now, 253 epilogue = epilogue) 254 255 def _grade_submission(self, 256 submission: typing.Union[object, None], 257 show_exceptions: bool = False, 258 **kwargs: typing.Any) -> GradedAssignment: 259 """ 260 Grade an assignment by grading all the questions. 261 262 The submission argument is the result of _prepare_submission(). 263 """ 264 265 self.result = GradedAssignment(name = self.name, questions = []) 266 self.result.grading_start_time = edq.util.time.Timestamp.now() 267 268 stop_grading = False 269 for question in self.questions: 270 if (stop_grading): 271 now = edq.util.time.Timestamp.now() 272 273 self.result.questions.append(autograder.question.GradedQuestion( 274 name = question.name, 275 max_points = question.max_points, 276 score = 0, 277 message = "Grading stopped because of a hard error, skipping question...", 278 grading_start_time = now, 279 grading_end_time = now, 280 skipped = True)) 281 else: 282 result = question.grade(submission, 283 additional_data = self.additional_data, 284 show_exceptions = show_exceptions) 285 286 self.result.questions.append(result) 287 288 stop_grading = result.hard_fail 289 290 self.result.grading_end_time = edq.util.time.Timestamp.now() 291 292 return self.result 293 294 def _prepare_submission(self) -> typing.Union[object, None]: 295 """ 296 Prepare the submission in the input directory for grading. 297 The result of this is what will be passed to grade(). 298 Child classes may leave this default behavior or override. 299 300 This implementation will check the prep_submission argument passed in the constructor. 301 If true the input directory will be prepared, otherwise None will be returned. 302 """ 303 304 if (self.prep_submission): 305 return autograder.util.prepare_submission.prepare(self.input_dir) 306 307 return None 308 309def load_assignment_classes(path: str) -> typing.List[typing.Type[Assignment]]: 310 """ 311 Recursively load all the assignment classes in a path (file or dir). 312 """ 313 314 module = edq.util.code.sanitize_and_import_path(path) 315 assignments = [] 316 317 for name in dir(module): 318 obj = getattr(module, name) 319 320 if (not inspect.isclass(obj)): 321 continue 322 323 if (obj == autograder.assignment.Assignment): 324 continue 325 326 if (issubclass(obj, autograder.assignment.Assignment)): 327 assignments.append(obj) 328 329 return assignments 330 331def fetch_assignment_class(path: str) -> typing.Type[Assignment]: 332 """ 333 Recursively fetch a single assignment class from a path. 334 If exactly one assignment is not found, then raise an error. 335 """ 336 337 assignments = load_assignment_classes(path) 338 339 if (len(assignments) == 0): 340 raise ValueError(f"Assignment file ('{path}') does not contain any instances of autograder.assignment.Assignment.") 341 342 if (len(assignments) > 1): 343 raise ValueError(f"Assignment file ('{path}') contains more than one ({len(assignments)}) instances of autograder.assignment.Assignment.") 344 345 return assignments[0]
class
GradedAssignment(edq.util.serial.DictConverter):
12class GradedAssignment(edq.util.serial.DictConverter): 13 """ 14 The result of an assignment being graded with a submission. 15 """ 16 17 def __init__(self, 18 name: str, 19 questions: typing.Sequence[autograder.question.GradedQuestion], 20 message: typing.Union[str, None] = None, 21 prologue: typing.Union[str, None] = None, 22 epilogue: typing.Union[str, None] = None, 23 grading_start_time: typing.Union[edq.util.time.Timestamp, int, None] = None, 24 grading_end_time: typing.Union[edq.util.time.Timestamp, int, None] = None, 25 proxy_start_time: typing.Union[edq.util.time.Timestamp, int, None] = None, 26 proxy_end_time: typing.Union[edq.util.time.Timestamp, int, None] = None, 27 **kwargs: typing.Any) -> None: 28 self.name: str = name 29 """ The name of the assignment. """ 30 31 self.questions: typing.List[autograder.question.GradedQuestion] = list(questions) 32 """ The result of grading for each question. """ 33 34 self.message: typing.Union[str, None] = prologue 35 """ Text to include with the grading result. """ 36 37 self.prologue: typing.Union[str, None] = prologue 38 """ Text to include before the grading result. """ 39 40 self.epilogue: typing.Union[str, None] = epilogue 41 """ Text to include after the grading result. """ 42 43 if (grading_start_time is None): 44 grading_start_time = edq.util.time.Timestamp() 45 46 self.grading_start_time: typing.Any = edq.util.time.Timestamp(grading_start_time) 47 """ When grading started. """ 48 49 if (grading_end_time is None): 50 grading_end_time = edq.util.time.Timestamp() 51 52 self.grading_end_time: typing.Any = edq.util.time.Timestamp(grading_end_time) 53 """ When grading ended. """ 54 55 if (proxy_start_time is None): 56 proxy_start_time = edq.util.time.Timestamp() 57 58 self.proxy_start_time: typing.Any = edq.util.time.Timestamp(proxy_start_time) 59 """ When proxy grading started. """ 60 61 if (proxy_end_time is None): 62 proxy_end_time = edq.util.time.Timestamp() 63 64 self.proxy_end_time: typing.Any = edq.util.time.Timestamp(proxy_end_time) 65 """ When proxy grading ended. """ 66 67 def to_test_submission(self, options: typing.Union[typing.Dict[str, typing.Any], None] = None) -> typing.Dict[str, typing.Any]: 68 """ 69 Output a dict that can be used as a test submission. 70 """ 71 72 if (options is None): 73 options = {} 74 75 results = self.to_dict() 76 77 del results['grading_start_time'] 78 del results['grading_end_time'] 79 del results['proxy_start_time'] 80 del results['proxy_end_time'] 81 82 questions = typing.cast(typing.List[typing.Dict[str, edq.util.serial.PODType]], results.get('questions', [])) 83 84 for question in questions: 85 del question['grading_start_time'] 86 del question['grading_end_time'] 87 88 test_submission = { 89 'result': results, 90 } 91 92 test_submission.update(options) 93 94 return test_submission 95 96 def get_score(self) -> typing.Tuple[float, float]: 97 """ 98 Return (total score, max score). 99 """ 100 101 total_score: float = 0 102 max_points: float = 0 103 104 for question in self.questions: 105 total_score += question.score 106 max_points += question.max_points 107 108 return (total_score, max_points) 109 110 def report(self, prefix: str = '', precision: int = 2) -> str: 111 """ 112 Return a string representation of the grading for this assignment. 113 """ 114 115 if ((prefix != '') and (not prefix.endswith(' '))): 116 prefix += ' ' 117 118 output: typing.List[str] = [] 119 120 output += self._format_logue(self.prologue, prefix) 121 122 output += [ 123 prefix + f"Autograder transcript for assignment: {self.name}", 124 prefix + f"Grading started at {self.grading_start_time.pretty()} and ended at {self.grading_end_time.pretty()}.", 125 ] 126 127 if ((self.message is not None) and (len(self.message) > 0)): 128 output.append(f"\n{self.message}\n") 129 130 total_score: float = 0 131 total_max_points: float = 0 132 133 for question in self.questions: 134 total_score += question.score 135 total_max_points += question.max_points 136 137 output.append(question.scoring_report(prefix = prefix, precision = precision)) 138 139 total_score_str = autograder.util.math.number_to_str(total_score, precision = precision) 140 total_max_points_str = autograder.util.math.number_to_str(total_max_points, precision = precision) 141 142 output.append('') 143 output.append(f"{prefix}Total: {total_score_str} / {total_max_points_str}") 144 145 output += self._format_logue(self.epilogue, prefix) 146 147 return "\n".join(output) 148 149 def _format_logue(self, text: typing.Union[str, None], prefix: str) -> typing.List[str]: 150 """ Format the output logue (prologe/epilogue). """ 151 152 if ((text is None) or (text == '')): 153 return [] 154 155 lines = text.splitlines() 156 return [prefix + line.rstrip() for line in lines] 157 158 def __eq__(self, other: object) -> bool: 159 return self.equals(other) 160 161 def equals(self, other: object, **kwargs: typing.Any) -> bool: 162 """ Check two graded assignments for equality. """ 163 164 if (not isinstance(other, GradedAssignment)): 165 return False 166 167 if ((self.name != other.name) or (len(self.questions) != len(other.questions))): 168 return False 169 170 for (i, question) in enumerate(self.questions): 171 if (not question.equals(other.questions[i], **kwargs)): 172 return False 173 174 return True
The result of an assignment being graded with a submission.
GradedAssignment( name: str, questions: Sequence[autograder.question.GradedQuestion], message: Optional[str] = None, prologue: Optional[str] = None, epilogue: Optional[str] = None, grading_start_time: Union[edq.util.time.Timestamp, int, NoneType] = None, grading_end_time: Union[edq.util.time.Timestamp, int, NoneType] = None, proxy_start_time: Union[edq.util.time.Timestamp, int, NoneType] = None, proxy_end_time: Union[edq.util.time.Timestamp, int, NoneType] = None, **kwargs: Any)
17 def __init__(self, 18 name: str, 19 questions: typing.Sequence[autograder.question.GradedQuestion], 20 message: typing.Union[str, None] = None, 21 prologue: typing.Union[str, None] = None, 22 epilogue: typing.Union[str, None] = None, 23 grading_start_time: typing.Union[edq.util.time.Timestamp, int, None] = None, 24 grading_end_time: typing.Union[edq.util.time.Timestamp, int, None] = None, 25 proxy_start_time: typing.Union[edq.util.time.Timestamp, int, None] = None, 26 proxy_end_time: typing.Union[edq.util.time.Timestamp, int, None] = None, 27 **kwargs: typing.Any) -> None: 28 self.name: str = name 29 """ The name of the assignment. """ 30 31 self.questions: typing.List[autograder.question.GradedQuestion] = list(questions) 32 """ The result of grading for each question. """ 33 34 self.message: typing.Union[str, None] = prologue 35 """ Text to include with the grading result. """ 36 37 self.prologue: typing.Union[str, None] = prologue 38 """ Text to include before the grading result. """ 39 40 self.epilogue: typing.Union[str, None] = epilogue 41 """ Text to include after the grading result. """ 42 43 if (grading_start_time is None): 44 grading_start_time = edq.util.time.Timestamp() 45 46 self.grading_start_time: typing.Any = edq.util.time.Timestamp(grading_start_time) 47 """ When grading started. """ 48 49 if (grading_end_time is None): 50 grading_end_time = edq.util.time.Timestamp() 51 52 self.grading_end_time: typing.Any = edq.util.time.Timestamp(grading_end_time) 53 """ When grading ended. """ 54 55 if (proxy_start_time is None): 56 proxy_start_time = edq.util.time.Timestamp() 57 58 self.proxy_start_time: typing.Any = edq.util.time.Timestamp(proxy_start_time) 59 """ When proxy grading started. """ 60 61 if (proxy_end_time is None): 62 proxy_end_time = edq.util.time.Timestamp() 63 64 self.proxy_end_time: typing.Any = edq.util.time.Timestamp(proxy_end_time) 65 """ When proxy grading ended. """
def
to_test_submission(self, options: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
67 def to_test_submission(self, options: typing.Union[typing.Dict[str, typing.Any], None] = None) -> typing.Dict[str, typing.Any]: 68 """ 69 Output a dict that can be used as a test submission. 70 """ 71 72 if (options is None): 73 options = {} 74 75 results = self.to_dict() 76 77 del results['grading_start_time'] 78 del results['grading_end_time'] 79 del results['proxy_start_time'] 80 del results['proxy_end_time'] 81 82 questions = typing.cast(typing.List[typing.Dict[str, edq.util.serial.PODType]], results.get('questions', [])) 83 84 for question in questions: 85 del question['grading_start_time'] 86 del question['grading_end_time'] 87 88 test_submission = { 89 'result': results, 90 } 91 92 test_submission.update(options) 93 94 return test_submission
Output a dict that can be used as a test submission.
def
get_score(self) -> Tuple[float, float]:
96 def get_score(self) -> typing.Tuple[float, float]: 97 """ 98 Return (total score, max score). 99 """ 100 101 total_score: float = 0 102 max_points: float = 0 103 104 for question in self.questions: 105 total_score += question.score 106 max_points += question.max_points 107 108 return (total_score, max_points)
Return (total score, max score).
def
report(self, prefix: str = '', precision: int = 2) -> str:
110 def report(self, prefix: str = '', precision: int = 2) -> str: 111 """ 112 Return a string representation of the grading for this assignment. 113 """ 114 115 if ((prefix != '') and (not prefix.endswith(' '))): 116 prefix += ' ' 117 118 output: typing.List[str] = [] 119 120 output += self._format_logue(self.prologue, prefix) 121 122 output += [ 123 prefix + f"Autograder transcript for assignment: {self.name}", 124 prefix + f"Grading started at {self.grading_start_time.pretty()} and ended at {self.grading_end_time.pretty()}.", 125 ] 126 127 if ((self.message is not None) and (len(self.message) > 0)): 128 output.append(f"\n{self.message}\n") 129 130 total_score: float = 0 131 total_max_points: float = 0 132 133 for question in self.questions: 134 total_score += question.score 135 total_max_points += question.max_points 136 137 output.append(question.scoring_report(prefix = prefix, precision = precision)) 138 139 total_score_str = autograder.util.math.number_to_str(total_score, precision = precision) 140 total_max_points_str = autograder.util.math.number_to_str(total_max_points, precision = precision) 141 142 output.append('') 143 output.append(f"{prefix}Total: {total_score_str} / {total_max_points_str}") 144 145 output += self._format_logue(self.epilogue, prefix) 146 147 return "\n".join(output)
Return a string representation of the grading for this assignment.
def
equals(self, other: object, **kwargs: Any) -> bool:
161 def equals(self, other: object, **kwargs: typing.Any) -> bool: 162 """ Check two graded assignments for equality. """ 163 164 if (not isinstance(other, GradedAssignment)): 165 return False 166 167 if ((self.name != other.name) or (len(self.questions) != len(other.questions))): 168 return False 169 170 for (i, question) in enumerate(self.questions): 171 if (not question.equals(other.questions[i], **kwargs)): 172 return False 173 174 return True
Check two graded assignments for equality.
class
Assignment:
176class Assignment: 177 """ 178 A collection of questions to be scored. 179 """ 180 181 def __init__(self, 182 name: typing.Union[str, None] = None, 183 questions: typing.Union[typing.Sequence[autograder.question.Question], None] = None, 184 input_dir: str = '.', 185 output_dir: str = '.', 186 work_dir: str = '.', 187 prep_submission: bool = True, 188 additional_data: typing.Union[typing.Dict[str, typing.Any], None] = None, 189 **kwargs: typing.Any) -> None: 190 if (name is None): 191 name = type(self).__name__ 192 193 self.name: str = name 194 """ 195 The display name for this assignment. 196 Defaults to the (unqualified) name of this class. 197 """ 198 199 if (questions is None): 200 questions = [] 201 202 self.questions = list(questions) 203 """ The questions for this assignment. """ 204 205 self.input_dir: str = input_dir 206 """ The base directory that houses student inputs for this assignment. """ 207 208 self.output_dir: str = output_dir 209 """ 210 The base directory where output will be written for this assignment. 211 A successful grading should result in a `result.json` created in this directory. 212 """ 213 214 self.work_dir: str = work_dir 215 """ The base directory that the grader is run in. """ 216 217 self.prep_submission: bool = prep_submission 218 """ Whether or not to call self.prepare_submission() to prepare the input directory before grading. """ 219 220 if (additional_data is None): 221 additional_data = {} 222 223 self.additional_data: typing.Dict[str, typing.Any] = additional_data 224 """ Additional data that can be passed to the grader. """ 225 226 self.result: typing.Union[GradedAssignment, None] = None 227 """ The result of grading. """ 228 229 def grade(self, **kwargs: typing.Any) -> GradedAssignment: 230 """ Grade this assignment. """ 231 232 try: 233 return self._grade_submission(self._prepare_submission(), **kwargs) 234 except Exception: 235 now = edq.util.time.Timestamp.now() 236 237 questions = [] 238 for question in self.questions: 239 questions.append(autograder.question.GradedQuestion( 240 name = question.name, 241 max_points = question.max_points, 242 score = 0, 243 message = "Submission could not be graded.", 244 grading_start_time = now, 245 grading_end_time = now)) 246 247 epilogue = (f"\nSubmission could not be graded because of the following error:\n---\n{traceback.format_exc()}---") 248 249 return GradedAssignment( 250 name = self.name, 251 questions = questions, 252 grading_start_time = now, 253 grading_end_time = now, 254 epilogue = epilogue) 255 256 def _grade_submission(self, 257 submission: typing.Union[object, None], 258 show_exceptions: bool = False, 259 **kwargs: typing.Any) -> GradedAssignment: 260 """ 261 Grade an assignment by grading all the questions. 262 263 The submission argument is the result of _prepare_submission(). 264 """ 265 266 self.result = GradedAssignment(name = self.name, questions = []) 267 self.result.grading_start_time = edq.util.time.Timestamp.now() 268 269 stop_grading = False 270 for question in self.questions: 271 if (stop_grading): 272 now = edq.util.time.Timestamp.now() 273 274 self.result.questions.append(autograder.question.GradedQuestion( 275 name = question.name, 276 max_points = question.max_points, 277 score = 0, 278 message = "Grading stopped because of a hard error, skipping question...", 279 grading_start_time = now, 280 grading_end_time = now, 281 skipped = True)) 282 else: 283 result = question.grade(submission, 284 additional_data = self.additional_data, 285 show_exceptions = show_exceptions) 286 287 self.result.questions.append(result) 288 289 stop_grading = result.hard_fail 290 291 self.result.grading_end_time = edq.util.time.Timestamp.now() 292 293 return self.result 294 295 def _prepare_submission(self) -> typing.Union[object, None]: 296 """ 297 Prepare the submission in the input directory for grading. 298 The result of this is what will be passed to grade(). 299 Child classes may leave this default behavior or override. 300 301 This implementation will check the prep_submission argument passed in the constructor. 302 If true the input directory will be prepared, otherwise None will be returned. 303 """ 304 305 if (self.prep_submission): 306 return autograder.util.prepare_submission.prepare(self.input_dir) 307 308 return None
A collection of questions to be scored.
Assignment( name: Optional[str] = None, questions: Optional[Sequence[autograder.question.Question]] = None, input_dir: str = '.', output_dir: str = '.', work_dir: str = '.', prep_submission: bool = True, additional_data: Optional[Dict[str, Any]] = None, **kwargs: Any)
181 def __init__(self, 182 name: typing.Union[str, None] = None, 183 questions: typing.Union[typing.Sequence[autograder.question.Question], None] = None, 184 input_dir: str = '.', 185 output_dir: str = '.', 186 work_dir: str = '.', 187 prep_submission: bool = True, 188 additional_data: typing.Union[typing.Dict[str, typing.Any], None] = None, 189 **kwargs: typing.Any) -> None: 190 if (name is None): 191 name = type(self).__name__ 192 193 self.name: str = name 194 """ 195 The display name for this assignment. 196 Defaults to the (unqualified) name of this class. 197 """ 198 199 if (questions is None): 200 questions = [] 201 202 self.questions = list(questions) 203 """ The questions for this assignment. """ 204 205 self.input_dir: str = input_dir 206 """ The base directory that houses student inputs for this assignment. """ 207 208 self.output_dir: str = output_dir 209 """ 210 The base directory where output will be written for this assignment. 211 A successful grading should result in a `result.json` created in this directory. 212 """ 213 214 self.work_dir: str = work_dir 215 """ The base directory that the grader is run in. """ 216 217 self.prep_submission: bool = prep_submission 218 """ Whether or not to call self.prepare_submission() to prepare the input directory before grading. """ 219 220 if (additional_data is None): 221 additional_data = {} 222 223 self.additional_data: typing.Dict[str, typing.Any] = additional_data 224 """ Additional data that can be passed to the grader. """ 225 226 self.result: typing.Union[GradedAssignment, None] = None 227 """ The result of grading. """
output_dir: str
The base directory where output will be written for this assignment.
A successful grading should result in a result.json created in this directory.
prep_submission: bool
Whether or not to call self.prepare_submission() to prepare the input directory before grading.
229 def grade(self, **kwargs: typing.Any) -> GradedAssignment: 230 """ Grade this assignment. """ 231 232 try: 233 return self._grade_submission(self._prepare_submission(), **kwargs) 234 except Exception: 235 now = edq.util.time.Timestamp.now() 236 237 questions = [] 238 for question in self.questions: 239 questions.append(autograder.question.GradedQuestion( 240 name = question.name, 241 max_points = question.max_points, 242 score = 0, 243 message = "Submission could not be graded.", 244 grading_start_time = now, 245 grading_end_time = now)) 246 247 epilogue = (f"\nSubmission could not be graded because of the following error:\n---\n{traceback.format_exc()}---") 248 249 return GradedAssignment( 250 name = self.name, 251 questions = questions, 252 grading_start_time = now, 253 grading_end_time = now, 254 epilogue = epilogue)
Grade this assignment.
310def load_assignment_classes(path: str) -> typing.List[typing.Type[Assignment]]: 311 """ 312 Recursively load all the assignment classes in a path (file or dir). 313 """ 314 315 module = edq.util.code.sanitize_and_import_path(path) 316 assignments = [] 317 318 for name in dir(module): 319 obj = getattr(module, name) 320 321 if (not inspect.isclass(obj)): 322 continue 323 324 if (obj == autograder.assignment.Assignment): 325 continue 326 327 if (issubclass(obj, autograder.assignment.Assignment)): 328 assignments.append(obj) 329 330 return assignments
Recursively load all the assignment classes in a path (file or dir).
332def fetch_assignment_class(path: str) -> typing.Type[Assignment]: 333 """ 334 Recursively fetch a single assignment class from a path. 335 If exactly one assignment is not found, then raise an error. 336 """ 337 338 assignments = load_assignment_classes(path) 339 340 if (len(assignments) == 0): 341 raise ValueError(f"Assignment file ('{path}') does not contain any instances of autograder.assignment.Assignment.") 342 343 if (len(assignments) > 1): 344 raise ValueError(f"Assignment file ('{path}') contains more than one ({len(assignments)}) instances of autograder.assignment.Assignment.") 345 346 return assignments[0]
Recursively fetch a single assignment class from a path. If exactly one assignment is not found, then raise an error.