autograder.question

A single question (test case) for an assignment.

  1"""
  2A single question (test case) for an assignment.
  3"""
  4
  5import abc
  6import functools
  7import numbers
  8import traceback
  9import typing
 10
 11import edq.util.serial
 12import edq.util.time
 13
 14import autograder.util.invoke
 15import autograder.util.math
 16
 17DEFAULT_TIMEOUT_SEC: float = 60
 18""" Default timeout for grading a question. """
 19
 20class AutograderFailError(RuntimeError):
 21    """
 22    This error indicates that fail() has been called on a question
 23    and execution should be stopped.
 24    """
 25
 26class AutograderHardFailError(RuntimeError):
 27    """
 28    This error indicates that hard_fail() has been called on a question
 29    and execution should be stopped for all questions in the assignment.
 30    """
 31
 32class GradedQuestion(edq.util.serial.DictConverter):
 33    """
 34    The result of a question being graded with a submission.
 35    """
 36
 37    def __init__(self,
 38            name: str,
 39            max_points: float,
 40            score: float = 0,
 41            message: str = '',
 42            hard_fail: bool = False,
 43            skipped: bool = False,
 44            grading_start_time: typing.Union[edq.util.time.Timestamp, int, None] = None,
 45            grading_end_time: typing.Union[edq.util.time.Timestamp, int, None] = None,
 46            **kwargs: typing.Any) -> None:
 47        self.name: str = name
 48        """ The name of the question. """
 49
 50        self.max_points: float = max_points
 51        """ The max points possible for this question. """
 52
 53        self.score: float = score
 54        """ The score earned for this question. """
 55
 56        self.message: str = message
 57        """ A message/feedback for the student. """
 58
 59        self.hard_fail = hard_fail
 60        """ Whether this question triggered a hard fail during grading. """
 61
 62        self.skipped = skipped
 63        """ Whether this question was skipped during grading. """
 64
 65        # Default the grading time to deal with situations where the grader throws an exception.
 66        now = edq.util.time.Timestamp.now()
 67
 68        if (grading_start_time is None):
 69            grading_start_time = now
 70
 71        self.grading_start_time: typing.Any = edq.util.time.Timestamp(grading_start_time)
 72        """ When grading started. """
 73
 74        if (grading_end_time is None):
 75            grading_end_time = now
 76
 77        self.grading_end_time: typing.Any = edq.util.time.Timestamp(grading_end_time)
 78        """ When grading ended. """
 79
 80    def scoring_report(self, prefix: str = '', precision: int = 2) -> str:
 81        """
 82        Get a string that represents the scoring for this question.
 83        """
 84
 85        if ((prefix != '') and (not prefix.endswith(' '))):
 86            prefix += ' '
 87
 88        score_str = autograder.util.math.number_to_str(self.score, precision = precision)
 89        max_points_str = autograder.util.math.number_to_str(self.max_points, precision = precision)
 90
 91        lines = [f"{prefix}{self.name}: {score_str} / {max_points_str}"]
 92        if (self.message != ''):
 93            for line in self.message.split("\n"):
 94                lines.append(prefix + '   ' + line)
 95
 96        return "\n".join(lines)
 97
 98    def __eq__(self, other: typing.Any) -> bool:
 99        return self.equals(other)
100
101    def equals(self, other: typing.Any, ignore_messages: bool = False, **kwargs: typing.Any) -> bool:
102        """ Check another graded question for equality. """
103
104        if (not isinstance(other, GradedQuestion)):
105            return False
106
107        if (
108                (self.name != other.name)
109                or (self.max_points != other.max_points)
110                or (self.score != other.score)
111                or (self.hard_fail != other.hard_fail)
112                or (self.skipped != other.skipped)):
113            return False
114
115        if (ignore_messages):
116            return True
117
118        return self.message == other.message
119
120class Question:
121    """
122    Questions are grade-able portions of an assignment.
123    They can also be thought of as "test cases".
124    Note that all scoring is in ints.
125    """
126
127    def __init__(self,
128            max_points: float = 0,
129            name: typing.Union[str, None] = None,
130            timeout: typing.Union[float, None] = DEFAULT_TIMEOUT_SEC,
131            ) -> None:
132        if (name is None):
133            name = type(self).__name__
134
135        self.name: str = name
136        """
137        The name of this question.
138        Defaults to the name of the question class.
139        """
140
141        if ((not isinstance(max_points, numbers.Real)) or (max_points < 0)):
142            raise ValueError("max_points must be a real number, got '{max_points}' (type: '{type(max_points)}).")
143
144        self.max_points: float = max_points
145        """ The maximum number of points possible for this question (does not include extra credit). """
146
147        self._timeout: typing.Union[float, None] = timeout
148        """ The number of seconds allowed when grading this question. """
149
150        # Create the base scoring artifact.
151        self.result: GradedQuestion = GradedQuestion(name = self.name, max_points = self.max_points)
152        """
153        The result of grading this question.
154        A default/empty one is created on construction and is added to during the grading process.
155        """
156
157    @abc.abstractmethod
158    def score_question(self, submission: typing.Any, **kwargs: typing.Any) -> None:
159        """
160        Assign an actual score to this question.
161        The implementer has full access to instance variables.
162        However, users should generally just call the grading methods to manipulate the result.
163        """
164
165    def grade(self, submission: typing.Any,
166            additional_data: typing.Union[typing.Dict[str, typing.Any], None] = None,
167            show_exceptions: bool = False) -> GradedQuestion:
168        """
169        Invoke the scoring method using a timeout and cleanup.
170        Return the graded question.
171        """
172
173        if (additional_data is None):
174            additional_data = {}
175
176        helper = functools.partial(self._score_helper, submission,
177                additional_data = additional_data)
178
179        self._internal_grade(helper, show_exceptions)
180
181        return self.result
182
183    def _internal_grade(self, helper: typing.Callable, show_exceptions: bool) -> None:
184        """
185        Handle the internal process for grading a question.
186        """
187
188        try:
189            success, value = autograder.util.invoke.with_timeout(self._timeout, helper)
190        except Exception:
191            if (show_exceptions):
192                traceback.print_exc()
193
194            self.set_result(0, "Raised an exception: " + traceback.format_exc())
195            return
196
197        if (not success):
198            if (value is None):
199                self.set_result(0, f"Timeout ({self._timeout} seconds).")
200            else:
201                self.set_result(0, f"Error during execution: '{value}'.")
202
203            return
204
205        # Because we use the helper method, we can only get None back if there was an error.
206        if (value is None):
207            self.set_result(0, "Error running scoring.")
208            return
209
210        self.result = value
211
212    def _score_helper(self, submission: typing.Any,
213            additional_data: typing.Union[typing.Dict[str, typing.Any], None] = None,
214            ) -> GradedQuestion:
215        """
216        Score the question, but make sure to return the result so
217        multiprocessing can properly pass it back.
218        """
219
220        if (additional_data is None):
221            additional_data = {}
222
223        self.result = GradedQuestion(name = self.name, max_points = self.max_points)
224
225        self.result.grading_start_time = edq.util.time.Timestamp.now()
226
227        try:
228            self.score_question(submission, **additional_data)
229        except AutograderFailError:
230            # The question has been failed, no additional output is required.
231            pass
232        except AutograderHardFailError:
233            # The question has been failed hard, signal to stop grading.
234            self.result.hard_fail = True
235
236        self.result.grading_end_time = edq.util.time.Timestamp.now()
237
238        return self.result
239
240    def get_last_result(self) -> GradedQuestion:
241        """ Get the current grading result. """
242
243        return self.result
244
245    def get_score(self) -> float:
246        """ Get the current assigned score for this grading. """
247
248        return self.result.score
249
250    # Grading functions.
251
252    def check_not_implemented(self, value: typing.Any) -> bool:
253        """
254        Check if the given value is a marker for not implemented.
255        By default, this checked for a NotImplemented value.
256        Children may override this to define their own not implemented values.
257        """
258
259        if (value is None):
260            self.fail("None returned.")
261
262        if (isinstance(value, type(NotImplemented))):
263            self.fail("NotImplemented returned.")
264
265        return False
266
267    def set_result(self, score: float, message: str) -> None:
268        """ Set the score and message for the current grading (overrides any other values). """
269
270        self.result.score = score
271        self.result.message = message
272
273    def set_score(self, score: float) -> None:
274        """ Set the score for the current grading (overrides any other values). """
275
276        self.result.score = score
277
278    def set_message(self, message: str) -> None:
279        """ Set the message for the current grading (overrides any other values). """
280
281        self.result.message = message
282
283    def fail(self, message: str) -> None:
284        """
285        Immediately fail this question, no partial credit.
286        """
287
288        self.set_result(0, message)
289        raise AutograderFailError()
290
291    def hard_fail(self, message: str) -> None:
292        """
293        Immediately hard fail this question, no partial credit.
294        Grading will be stopped for the rest of the assignment.
295        """
296
297        self.set_result(0, message)
298        raise AutograderHardFailError()
299
300    def full_credit(self, message: str = '') -> None:
301        """ Assign full credit for this question. """
302
303        self.set_score(self.max_points)
304
305        if (message != ''):
306            self.set_message(message)
307
308    def add_score(self, add_score: float) -> None:
309        """ Add the given score to the current score for this question. """
310
311        self.result.score += add_score
312
313    def add_message(self, message: str, add_score: float = 0) -> None:
314        """ Add the given message (and optional score) to the current grading for this question. """
315
316        if (self.result.message != ''):
317            self.result.message += "\n"
318
319        self.result.message += str(message)
320        self.result.score += add_score
321
322    def cap_score(self) -> None:
323        """
324        Cap the current score so it is in [0, self.max_points].
325        """
326
327        self.result.score = max(0, min(self.max_points, self.result.score))
DEFAULT_TIMEOUT_SEC: float = 60

Default timeout for grading a question.

class AutograderFailError(builtins.RuntimeError):
21class AutograderFailError(RuntimeError):
22    """
23    This error indicates that fail() has been called on a question
24    and execution should be stopped.
25    """

This error indicates that fail() has been called on a question and execution should be stopped.

class AutograderHardFailError(builtins.RuntimeError):
27class AutograderHardFailError(RuntimeError):
28    """
29    This error indicates that hard_fail() has been called on a question
30    and execution should be stopped for all questions in the assignment.
31    """

This error indicates that hard_fail() has been called on a question and execution should be stopped for all questions in the assignment.

class GradedQuestion(edq.util.serial.DictConverter):
 33class GradedQuestion(edq.util.serial.DictConverter):
 34    """
 35    The result of a question being graded with a submission.
 36    """
 37
 38    def __init__(self,
 39            name: str,
 40            max_points: float,
 41            score: float = 0,
 42            message: str = '',
 43            hard_fail: bool = False,
 44            skipped: bool = False,
 45            grading_start_time: typing.Union[edq.util.time.Timestamp, int, None] = None,
 46            grading_end_time: typing.Union[edq.util.time.Timestamp, int, None] = None,
 47            **kwargs: typing.Any) -> None:
 48        self.name: str = name
 49        """ The name of the question. """
 50
 51        self.max_points: float = max_points
 52        """ The max points possible for this question. """
 53
 54        self.score: float = score
 55        """ The score earned for this question. """
 56
 57        self.message: str = message
 58        """ A message/feedback for the student. """
 59
 60        self.hard_fail = hard_fail
 61        """ Whether this question triggered a hard fail during grading. """
 62
 63        self.skipped = skipped
 64        """ Whether this question was skipped during grading. """
 65
 66        # Default the grading time to deal with situations where the grader throws an exception.
 67        now = edq.util.time.Timestamp.now()
 68
 69        if (grading_start_time is None):
 70            grading_start_time = now
 71
 72        self.grading_start_time: typing.Any = edq.util.time.Timestamp(grading_start_time)
 73        """ When grading started. """
 74
 75        if (grading_end_time is None):
 76            grading_end_time = now
 77
 78        self.grading_end_time: typing.Any = edq.util.time.Timestamp(grading_end_time)
 79        """ When grading ended. """
 80
 81    def scoring_report(self, prefix: str = '', precision: int = 2) -> str:
 82        """
 83        Get a string that represents the scoring for this question.
 84        """
 85
 86        if ((prefix != '') and (not prefix.endswith(' '))):
 87            prefix += ' '
 88
 89        score_str = autograder.util.math.number_to_str(self.score, precision = precision)
 90        max_points_str = autograder.util.math.number_to_str(self.max_points, precision = precision)
 91
 92        lines = [f"{prefix}{self.name}: {score_str} / {max_points_str}"]
 93        if (self.message != ''):
 94            for line in self.message.split("\n"):
 95                lines.append(prefix + '   ' + line)
 96
 97        return "\n".join(lines)
 98
 99    def __eq__(self, other: typing.Any) -> bool:
100        return self.equals(other)
101
102    def equals(self, other: typing.Any, ignore_messages: bool = False, **kwargs: typing.Any) -> bool:
103        """ Check another graded question for equality. """
104
105        if (not isinstance(other, GradedQuestion)):
106            return False
107
108        if (
109                (self.name != other.name)
110                or (self.max_points != other.max_points)
111                or (self.score != other.score)
112                or (self.hard_fail != other.hard_fail)
113                or (self.skipped != other.skipped)):
114            return False
115
116        if (ignore_messages):
117            return True
118
119        return self.message == other.message

The result of a question being graded with a submission.

GradedQuestion( name: str, max_points: float, score: float = 0, message: str = '', hard_fail: bool = False, skipped: bool = False, grading_start_time: Union[edq.util.time.Timestamp, int, NoneType] = None, grading_end_time: Union[edq.util.time.Timestamp, int, NoneType] = None, **kwargs: Any)
38    def __init__(self,
39            name: str,
40            max_points: float,
41            score: float = 0,
42            message: str = '',
43            hard_fail: bool = False,
44            skipped: bool = False,
45            grading_start_time: typing.Union[edq.util.time.Timestamp, int, None] = None,
46            grading_end_time: typing.Union[edq.util.time.Timestamp, int, None] = None,
47            **kwargs: typing.Any) -> None:
48        self.name: str = name
49        """ The name of the question. """
50
51        self.max_points: float = max_points
52        """ The max points possible for this question. """
53
54        self.score: float = score
55        """ The score earned for this question. """
56
57        self.message: str = message
58        """ A message/feedback for the student. """
59
60        self.hard_fail = hard_fail
61        """ Whether this question triggered a hard fail during grading. """
62
63        self.skipped = skipped
64        """ Whether this question was skipped during grading. """
65
66        # Default the grading time to deal with situations where the grader throws an exception.
67        now = edq.util.time.Timestamp.now()
68
69        if (grading_start_time is None):
70            grading_start_time = now
71
72        self.grading_start_time: typing.Any = edq.util.time.Timestamp(grading_start_time)
73        """ When grading started. """
74
75        if (grading_end_time is None):
76            grading_end_time = now
77
78        self.grading_end_time: typing.Any = edq.util.time.Timestamp(grading_end_time)
79        """ When grading ended. """
name: str

The name of the question.

max_points: float

The max points possible for this question.

score: float

The score earned for this question.

message: str

A message/feedback for the student.

hard_fail

Whether this question triggered a hard fail during grading.

skipped

Whether this question was skipped during grading.

grading_start_time: Any

When grading started.

grading_end_time: Any

When grading ended.

def scoring_report(self, prefix: str = '', precision: int = 2) -> str:
81    def scoring_report(self, prefix: str = '', precision: int = 2) -> str:
82        """
83        Get a string that represents the scoring for this question.
84        """
85
86        if ((prefix != '') and (not prefix.endswith(' '))):
87            prefix += ' '
88
89        score_str = autograder.util.math.number_to_str(self.score, precision = precision)
90        max_points_str = autograder.util.math.number_to_str(self.max_points, precision = precision)
91
92        lines = [f"{prefix}{self.name}: {score_str} / {max_points_str}"]
93        if (self.message != ''):
94            for line in self.message.split("\n"):
95                lines.append(prefix + '   ' + line)
96
97        return "\n".join(lines)

Get a string that represents the scoring for this question.

def equals(self, other: Any, ignore_messages: bool = False, **kwargs: Any) -> bool:
102    def equals(self, other: typing.Any, ignore_messages: bool = False, **kwargs: typing.Any) -> bool:
103        """ Check another graded question for equality. """
104
105        if (not isinstance(other, GradedQuestion)):
106            return False
107
108        if (
109                (self.name != other.name)
110                or (self.max_points != other.max_points)
111                or (self.score != other.score)
112                or (self.hard_fail != other.hard_fail)
113                or (self.skipped != other.skipped)):
114            return False
115
116        if (ignore_messages):
117            return True
118
119        return self.message == other.message

Check another graded question for equality.

class Question:
121class Question:
122    """
123    Questions are grade-able portions of an assignment.
124    They can also be thought of as "test cases".
125    Note that all scoring is in ints.
126    """
127
128    def __init__(self,
129            max_points: float = 0,
130            name: typing.Union[str, None] = None,
131            timeout: typing.Union[float, None] = DEFAULT_TIMEOUT_SEC,
132            ) -> None:
133        if (name is None):
134            name = type(self).__name__
135
136        self.name: str = name
137        """
138        The name of this question.
139        Defaults to the name of the question class.
140        """
141
142        if ((not isinstance(max_points, numbers.Real)) or (max_points < 0)):
143            raise ValueError("max_points must be a real number, got '{max_points}' (type: '{type(max_points)}).")
144
145        self.max_points: float = max_points
146        """ The maximum number of points possible for this question (does not include extra credit). """
147
148        self._timeout: typing.Union[float, None] = timeout
149        """ The number of seconds allowed when grading this question. """
150
151        # Create the base scoring artifact.
152        self.result: GradedQuestion = GradedQuestion(name = self.name, max_points = self.max_points)
153        """
154        The result of grading this question.
155        A default/empty one is created on construction and is added to during the grading process.
156        """
157
158    @abc.abstractmethod
159    def score_question(self, submission: typing.Any, **kwargs: typing.Any) -> None:
160        """
161        Assign an actual score to this question.
162        The implementer has full access to instance variables.
163        However, users should generally just call the grading methods to manipulate the result.
164        """
165
166    def grade(self, submission: typing.Any,
167            additional_data: typing.Union[typing.Dict[str, typing.Any], None] = None,
168            show_exceptions: bool = False) -> GradedQuestion:
169        """
170        Invoke the scoring method using a timeout and cleanup.
171        Return the graded question.
172        """
173
174        if (additional_data is None):
175            additional_data = {}
176
177        helper = functools.partial(self._score_helper, submission,
178                additional_data = additional_data)
179
180        self._internal_grade(helper, show_exceptions)
181
182        return self.result
183
184    def _internal_grade(self, helper: typing.Callable, show_exceptions: bool) -> None:
185        """
186        Handle the internal process for grading a question.
187        """
188
189        try:
190            success, value = autograder.util.invoke.with_timeout(self._timeout, helper)
191        except Exception:
192            if (show_exceptions):
193                traceback.print_exc()
194
195            self.set_result(0, "Raised an exception: " + traceback.format_exc())
196            return
197
198        if (not success):
199            if (value is None):
200                self.set_result(0, f"Timeout ({self._timeout} seconds).")
201            else:
202                self.set_result(0, f"Error during execution: '{value}'.")
203
204            return
205
206        # Because we use the helper method, we can only get None back if there was an error.
207        if (value is None):
208            self.set_result(0, "Error running scoring.")
209            return
210
211        self.result = value
212
213    def _score_helper(self, submission: typing.Any,
214            additional_data: typing.Union[typing.Dict[str, typing.Any], None] = None,
215            ) -> GradedQuestion:
216        """
217        Score the question, but make sure to return the result so
218        multiprocessing can properly pass it back.
219        """
220
221        if (additional_data is None):
222            additional_data = {}
223
224        self.result = GradedQuestion(name = self.name, max_points = self.max_points)
225
226        self.result.grading_start_time = edq.util.time.Timestamp.now()
227
228        try:
229            self.score_question(submission, **additional_data)
230        except AutograderFailError:
231            # The question has been failed, no additional output is required.
232            pass
233        except AutograderHardFailError:
234            # The question has been failed hard, signal to stop grading.
235            self.result.hard_fail = True
236
237        self.result.grading_end_time = edq.util.time.Timestamp.now()
238
239        return self.result
240
241    def get_last_result(self) -> GradedQuestion:
242        """ Get the current grading result. """
243
244        return self.result
245
246    def get_score(self) -> float:
247        """ Get the current assigned score for this grading. """
248
249        return self.result.score
250
251    # Grading functions.
252
253    def check_not_implemented(self, value: typing.Any) -> bool:
254        """
255        Check if the given value is a marker for not implemented.
256        By default, this checked for a NotImplemented value.
257        Children may override this to define their own not implemented values.
258        """
259
260        if (value is None):
261            self.fail("None returned.")
262
263        if (isinstance(value, type(NotImplemented))):
264            self.fail("NotImplemented returned.")
265
266        return False
267
268    def set_result(self, score: float, message: str) -> None:
269        """ Set the score and message for the current grading (overrides any other values). """
270
271        self.result.score = score
272        self.result.message = message
273
274    def set_score(self, score: float) -> None:
275        """ Set the score for the current grading (overrides any other values). """
276
277        self.result.score = score
278
279    def set_message(self, message: str) -> None:
280        """ Set the message for the current grading (overrides any other values). """
281
282        self.result.message = message
283
284    def fail(self, message: str) -> None:
285        """
286        Immediately fail this question, no partial credit.
287        """
288
289        self.set_result(0, message)
290        raise AutograderFailError()
291
292    def hard_fail(self, message: str) -> None:
293        """
294        Immediately hard fail this question, no partial credit.
295        Grading will be stopped for the rest of the assignment.
296        """
297
298        self.set_result(0, message)
299        raise AutograderHardFailError()
300
301    def full_credit(self, message: str = '') -> None:
302        """ Assign full credit for this question. """
303
304        self.set_score(self.max_points)
305
306        if (message != ''):
307            self.set_message(message)
308
309    def add_score(self, add_score: float) -> None:
310        """ Add the given score to the current score for this question. """
311
312        self.result.score += add_score
313
314    def add_message(self, message: str, add_score: float = 0) -> None:
315        """ Add the given message (and optional score) to the current grading for this question. """
316
317        if (self.result.message != ''):
318            self.result.message += "\n"
319
320        self.result.message += str(message)
321        self.result.score += add_score
322
323    def cap_score(self) -> None:
324        """
325        Cap the current score so it is in [0, self.max_points].
326        """
327
328        self.result.score = max(0, min(self.max_points, self.result.score))

Questions are grade-able portions of an assignment. They can also be thought of as "test cases". Note that all scoring is in ints.

Question( max_points: float = 0, name: Optional[str] = None, timeout: Optional[float] = 60)
128    def __init__(self,
129            max_points: float = 0,
130            name: typing.Union[str, None] = None,
131            timeout: typing.Union[float, None] = DEFAULT_TIMEOUT_SEC,
132            ) -> None:
133        if (name is None):
134            name = type(self).__name__
135
136        self.name: str = name
137        """
138        The name of this question.
139        Defaults to the name of the question class.
140        """
141
142        if ((not isinstance(max_points, numbers.Real)) or (max_points < 0)):
143            raise ValueError("max_points must be a real number, got '{max_points}' (type: '{type(max_points)}).")
144
145        self.max_points: float = max_points
146        """ The maximum number of points possible for this question (does not include extra credit). """
147
148        self._timeout: typing.Union[float, None] = timeout
149        """ The number of seconds allowed when grading this question. """
150
151        # Create the base scoring artifact.
152        self.result: GradedQuestion = GradedQuestion(name = self.name, max_points = self.max_points)
153        """
154        The result of grading this question.
155        A default/empty one is created on construction and is added to during the grading process.
156        """
name: str

The name of this question. Defaults to the name of the question class.

max_points: float

The maximum number of points possible for this question (does not include extra credit).

result: GradedQuestion

The result of grading this question. A default/empty one is created on construction and is added to during the grading process.

@abc.abstractmethod
def score_question(self, submission: Any, **kwargs: Any) -> None:
158    @abc.abstractmethod
159    def score_question(self, submission: typing.Any, **kwargs: typing.Any) -> None:
160        """
161        Assign an actual score to this question.
162        The implementer has full access to instance variables.
163        However, users should generally just call the grading methods to manipulate the result.
164        """

Assign an actual score to this question. The implementer has full access to instance variables. However, users should generally just call the grading methods to manipulate the result.

def grade( self, submission: Any, additional_data: Optional[Dict[str, Any]] = None, show_exceptions: bool = False) -> GradedQuestion:
166    def grade(self, submission: typing.Any,
167            additional_data: typing.Union[typing.Dict[str, typing.Any], None] = None,
168            show_exceptions: bool = False) -> GradedQuestion:
169        """
170        Invoke the scoring method using a timeout and cleanup.
171        Return the graded question.
172        """
173
174        if (additional_data is None):
175            additional_data = {}
176
177        helper = functools.partial(self._score_helper, submission,
178                additional_data = additional_data)
179
180        self._internal_grade(helper, show_exceptions)
181
182        return self.result

Invoke the scoring method using a timeout and cleanup. Return the graded question.

def get_last_result(self) -> GradedQuestion:
241    def get_last_result(self) -> GradedQuestion:
242        """ Get the current grading result. """
243
244        return self.result

Get the current grading result.

def get_score(self) -> float:
246    def get_score(self) -> float:
247        """ Get the current assigned score for this grading. """
248
249        return self.result.score

Get the current assigned score for this grading.

def check_not_implemented(self, value: Any) -> bool:
253    def check_not_implemented(self, value: typing.Any) -> bool:
254        """
255        Check if the given value is a marker for not implemented.
256        By default, this checked for a NotImplemented value.
257        Children may override this to define their own not implemented values.
258        """
259
260        if (value is None):
261            self.fail("None returned.")
262
263        if (isinstance(value, type(NotImplemented))):
264            self.fail("NotImplemented returned.")
265
266        return False

Check if the given value is a marker for not implemented. By default, this checked for a NotImplemented value. Children may override this to define their own not implemented values.

def set_result(self, score: float, message: str) -> None:
268    def set_result(self, score: float, message: str) -> None:
269        """ Set the score and message for the current grading (overrides any other values). """
270
271        self.result.score = score
272        self.result.message = message

Set the score and message for the current grading (overrides any other values).

def set_score(self, score: float) -> None:
274    def set_score(self, score: float) -> None:
275        """ Set the score for the current grading (overrides any other values). """
276
277        self.result.score = score

Set the score for the current grading (overrides any other values).

def set_message(self, message: str) -> None:
279    def set_message(self, message: str) -> None:
280        """ Set the message for the current grading (overrides any other values). """
281
282        self.result.message = message

Set the message for the current grading (overrides any other values).

def fail(self, message: str) -> None:
284    def fail(self, message: str) -> None:
285        """
286        Immediately fail this question, no partial credit.
287        """
288
289        self.set_result(0, message)
290        raise AutograderFailError()

Immediately fail this question, no partial credit.

def hard_fail(self, message: str) -> None:
292    def hard_fail(self, message: str) -> None:
293        """
294        Immediately hard fail this question, no partial credit.
295        Grading will be stopped for the rest of the assignment.
296        """
297
298        self.set_result(0, message)
299        raise AutograderHardFailError()

Immediately hard fail this question, no partial credit. Grading will be stopped for the rest of the assignment.

def full_credit(self, message: str = '') -> None:
301    def full_credit(self, message: str = '') -> None:
302        """ Assign full credit for this question. """
303
304        self.set_score(self.max_points)
305
306        if (message != ''):
307            self.set_message(message)

Assign full credit for this question.

def add_score(self, add_score: float) -> None:
309    def add_score(self, add_score: float) -> None:
310        """ Add the given score to the current score for this question. """
311
312        self.result.score += add_score

Add the given score to the current score for this question.

def add_message(self, message: str, add_score: float = 0) -> None:
314    def add_message(self, message: str, add_score: float = 0) -> None:
315        """ Add the given message (and optional score) to the current grading for this question. """
316
317        if (self.result.message != ''):
318            self.result.message += "\n"
319
320        self.result.message += str(message)
321        self.result.score += add_score

Add the given message (and optional score) to the current grading for this question.

def cap_score(self) -> None:
323    def cap_score(self) -> None:
324        """
325        Cap the current score so it is in [0, self.max_points].
326        """
327
328        self.result.score = max(0, min(self.max_points, self.result.score))

Cap the current score so it is in [0, self.max_points].