potluck.context_utils

Support functions for contexts that are also needed by modules which the potluck.contexts module depends on.

context_utils.py

  1"""
  2Support functions for contexts that are also needed by modules which the
  3`potluck.contexts` module depends on.
  4
  5context_utils.py
  6"""
  7
  8import io, os
  9
 10from . import html_tools
 11
 12
 13#---------#
 14# Globals #
 15#---------#
 16
 17BASE_CONTEXT_SLOTS = [
 18    "task_info",
 19    "username",
 20    "submission_root",
 21    "default_file",
 22    "actual_file"
 23]
 24"""
 25The context slots which can be expected to always be available, and which
 26are provided not via a `Context` object but by the evaluation system
 27itself.
 28"""
 29
 30
 31#---------------#
 32# Error classes #
 33#---------------#
 34
 35class ContextError(Exception):
 36    """
 37    Custom exception class to use when context builders fail.
 38    """
 39    pass
 40
 41
 42class MissingContextError(ContextError):
 43    """
 44    Error indicating that required context is missing during
 45    testing/checking (i.e., an internal error in the construction of a
 46    Goal or Rubric).
 47    """
 48    pass
 49
 50
 51class ContextCreationError(ContextError):
 52    """
 53    Error indicating that context could not be created, usually because a
 54    submission was missing the required function or module, or because
 55    attempting to evaluate a function/module caused an error.
 56    """
 57    def __init__(self, context, message, cause=None):
 58        """
 59        Supply a Context object within which this error was generated, a
 60        message describing the error, and if there is one, an earlier
 61        Exception that caused this error.
 62        """
 63        self.context = context
 64        self.message = message
 65        self.cause = cause
 66
 67    def __str__(self):
 68        return (
 69            "(arising in context '{}') {}"
 70        ).format(
 71            self.context.feedback_topic(), # errors won't appear in rubric
 72            (
 73                "<br>\n  caused by: {}".format(self.cause)
 74                if self.cause
 75                else ""
 76            )
 77        )
 78
 79    def __repr__(self):
 80        return "ContextCreationError({}, {}, {})".format(
 81            repr(self.context), repr(self.message), repr(self.cause)
 82        )
 83
 84    def explanation(self):
 85        """
 86        Chains through causes to return an HTML string describing why
 87        this context could not be established. Makes use of the html_tb
 88        property of our cause if it's present and the cause is not a
 89        ContextCreationError.
 90        """
 91        result = "Could not get '{}' because:".format(
 92            self.context.feedback_topic() # errors won't appear in rubric
 93        )
 94        if self.cause:
 95            # Short-circuit to the root of the chain if we can
 96            current = self
 97            chain = []
 98            while hasattr(current, "cause") and current.cause:
 99                chain.append(current)
100                current = current.cause
101
102            # Add the root cause's message first
103            root_cause = current
104            result += "<br>\n" + str(root_cause)
105
106            # Try to get an HTML traceback for the root cause
107            root_tb = str(root_cause)
108            if hasattr(root_cause, "html_tb"):
109                root_tb = root_cause.html_tb
110            elif hasattr(root_cause, "__traceback__"):
111                if self.context.cached_value is not None:
112                    linkable = linkmap(self.context.cached_value)
113                elif self.context.working_from is not None:
114                    linkable = linkmap(self.context.working_from)
115                else:
116                    linkable = None
117                root_tb = html_tools.html_traceback(
118                    root_cause,
119                    linkable=linkable
120                )
121
122            result += html_tools.build_html_details(
123                "Details (click me):",
124                html_tools.build_list(
125                    [
126                        "{}: {}".format(
127                            error.context.feedback_topic(),
128                            error.message
129                        )
130                        for error in chain
131                    ] + [ root_tb ]
132                )
133            )
134        else:
135            result += "<br>\n" + self.message
136        return result
137
138
139#-------#
140# Utils #
141#-------#
142
143def extract(context, slot):
144    """
145    Returns the value for the given slot of the given Context, raising a
146    MissingContextError if there is no such value.
147    """
148    if slot in context:
149        return context[slot]
150    else:
151        raise MissingContextError(
152            f"Required context slot '{slot}' not found."
153        )
154
155
156def linkmap(context):
157    """
158    Returns a link dictionary suitable for the `linkable` parameter of
159    `html_traceback`, based on the "task_info" and "filename" slots of
160    the given context dictionary, or just based on the "task_info" if
161    "filename" is not present. Returns None if "task_info" is missing.
162    """
163    if "task_info" in context and "filename" in context:
164        return { context["filename"]: context["task_info"]["id"] }
165    elif "task_info" in context:
166        ti = context["task_info"]
167        return { ti["target"]: ti["id"] }
168    else:
169        return None
170
171
172class MashEnter(io.TextIOBase):
173    """
174    A fake input designed to be used in place of stdin. Reading anything
175    just returns newlines.
176    """
177    def read(size=-1):
178        """
179        Returns a newline, or if a specific size is specified, that many
180        newlines. Behavior is weird since normally you can't call read
181        multiple times.
182        """
183        if size == -1:
184            return '\n'
185        else:
186            return '\n' * size
187
188    def readline(size=-1):
189        """
190        Returns a newline.
191        """
192        return '\n'
193
194
195class AllOnes(io.TextIOBase):
196    """
197    A fake input designed to be used in place of stdin. Reading anything
198    just returns the string '1' with a newline at the end.
199    """
200    def read(size=-1):
201        """
202        Returns a string containing the digit 1 followed by a newline, or
203        if a specific size is specified, that many ones minus one, plus a
204        single newline. Behavior is weird since normally you can't call
205        read multiple times.
206        """
207        if size == -1:
208            return '1\n'
209        else:
210            return '1' * (size - 1) + '\n'
211
212    def readline(size=-1):
213        """
214        Returns the digit 1 followed by a newline.
215        """
216        return '1\n'
217
218
219def sandbox_filemap(spec):
220    """
221    Extracts the standard symlink mapping for sandbox files from the
222    given specification, based on its helper_files and starter_path
223    properties. The result maps absolute filenames to sandbox-relative
224    filenames and specifies how to copy helper files into a sandbox.
225    """
226    return {
227        os.path.abspath(os.path.join(spec.starter_path, helper)): helper
228        for helper in spec.helper_files
229    }
230
231
232#---------------------------#
233# ContextualValue machinery #
234#---------------------------#
235
236class ContextualValue:
237    """
238    This class and its subclasses represent values that may appear as
239    part of arguments in a code behavior test (see
240    create_code_behavior_context_builder). Before testing, those
241    arguments will be replaced (using the instance's replace method). The
242    replace method will receive the current context as its first
243    argument, along with a boolean indicating whether the value is being
244    requested to test the submitted module (True) or solution module
245    (False).
246
247    The default behavior (this class) is to accept a function when
248    constructed and run that function on the provided context dictionary,
249    using the function's return value as the actual argument to the
250    function being tested.
251
252    Your extractor function should ideally raise MissingContextError in
253    cases where a context value that it depends on is not present,
254    although this is not critical.
255    """
256    def __init__(self, value_extractor):
257        """
258        There is one required argument: the value extractor function,
259        which will be given a context dictionary and a boolean indicating
260        submitted vs. solution testing, and will be expected to produce
261        an argument value.
262        """
263        self.extractor = value_extractor
264
265    def __str__(self):
266        return "a contextual value based on {}".format(
267            self.extractor.__name__
268        )
269
270    def __repr__(self):
271        return "<a ContextualValue based on {}>".format(
272            self.extractor.__name__
273        )
274
275    def replace(self, context):
276        """
277        This method is used to provide an actual argument value to take
278        the place of this object.
279
280        The argument is the context to use to retrieve a value.
281
282        This implementation simply runs the provided extractor function
283        on the two arguments it gets.
284        """
285        return self.extractor(context)
286
287
288class ContextSlot(ContextualValue):
289    """
290    A special case ContextualValue where the value to be used is simply
291    stored in a slot in the context dictionary, with no extra processing
292    necessary. This class just needs the string name of the slot to be
293    used.
294    """
295    def __init__(self, slot_name):
296        """
297        One required argument: the name of the context slot to use.
298        """
299        self.slot = slot_name
300
301    def __str__(self):
302        return "the current '{}' value".format(self.slot)
303
304    def __repr__(self):
305        return "<a ContextSlot for " + str(self) + ">"
306
307    def replace(self, context):
308        """
309        We retrieve the slot value from the context. Notice that if the
310        value is missing, we generate a MissingContextError that should
311        eventually bubble out.
312        """
313        # Figure out which slot we're using
314        slot = self.slot
315
316        if slot not in context:
317            raise MissingContextError(
318                (
319                    "Context slot '{}' is required by a ContextSlot dynamic"
320                    " value, but it is not present in the testing context."
321                ).format(slot)
322            )
323        return context[slot]
324
325
326class ModuleValue(ContextualValue):
327    """
328    A kind of ContextualValue that evaluates a string containing Python
329    code within the module stored in the "module" context slot.
330    """
331    def __init__(self, expression):
332        """
333        One required argument: the expression to evaluate, which must be
334        a string that contains a valid Python expression.
335        """
336        self.expression = expression
337
338    def __str__(self):
339        return "the result of " + self.expression
340
341    def __repr__(self):
342        return "<a ModuleValue based on {}>".format(str(self))
343
344    def replace(self, context):
345        """
346        We retrieve the "module" slot value from the provided context.
347
348        We then evaluate our expression within the retrieved module, and
349        return that result.
350        """
351        if "module" not in context:
352            raise MissingContextError(
353                "ModuleValue argument requires a 'module' context"
354              + " key, but there isn't one."
355            )
356        module = context["module"]
357
358        return eval(self.expression, module.__dict__)
359
360
361class SolnValue(ContextualValue):
362    """
363    Like a ModuleValue, but *always* takes the value from the solution
364    module, even when we're testing submitted code.
365    """
366    def __init__(self, expression):
367        """
368        One required argument: the expression to evaluate, which must be
369        a string that contains a valid Python expression.
370        """
371        self.expression = expression
372
373    def __str__(self):
374        return "the correct value of " + self.expression
375
376    def __repr__(self):
377        return "<a SolnValue based on {}>".format(str(self))
378
379    def replace(self, context):
380        """
381        We retrieve the "ref_module" slot value from the provided context.
382
383        We then evaluate our expression within the retrieved module, and
384        return that result.
385        """
386        if "ref_module" not in context:
387            raise MissingContextError(
388                f"SolnValue argument requires a 'ref_module' context"
389                f" key, but there isn't one in: {list(context.keys())}"
390            )
391        module = context["ref_module"]
392
393        return eval(self.expression, module.__dict__)
BASE_CONTEXT_SLOTS = ['task_info', 'username', 'submission_root', 'default_file', 'actual_file']

The context slots which can be expected to always be available, and which are provided not via a Context object but by the evaluation system itself.

class ContextError(builtins.Exception):
36class ContextError(Exception):
37    """
38    Custom exception class to use when context builders fail.
39    """
40    pass

Custom exception class to use when context builders fail.

Inherited Members
builtins.Exception
Exception
builtins.BaseException
with_traceback
class MissingContextError(ContextError):
43class MissingContextError(ContextError):
44    """
45    Error indicating that required context is missing during
46    testing/checking (i.e., an internal error in the construction of a
47    Goal or Rubric).
48    """
49    pass

Error indicating that required context is missing during testing/checking (i.e., an internal error in the construction of a Goal or Rubric).

Inherited Members
builtins.Exception
Exception
builtins.BaseException
with_traceback
class ContextCreationError(ContextError):
 52class ContextCreationError(ContextError):
 53    """
 54    Error indicating that context could not be created, usually because a
 55    submission was missing the required function or module, or because
 56    attempting to evaluate a function/module caused an error.
 57    """
 58    def __init__(self, context, message, cause=None):
 59        """
 60        Supply a Context object within which this error was generated, a
 61        message describing the error, and if there is one, an earlier
 62        Exception that caused this error.
 63        """
 64        self.context = context
 65        self.message = message
 66        self.cause = cause
 67
 68    def __str__(self):
 69        return (
 70            "(arising in context '{}') {}"
 71        ).format(
 72            self.context.feedback_topic(), # errors won't appear in rubric
 73            (
 74                "<br>\n  caused by: {}".format(self.cause)
 75                if self.cause
 76                else ""
 77            )
 78        )
 79
 80    def __repr__(self):
 81        return "ContextCreationError({}, {}, {})".format(
 82            repr(self.context), repr(self.message), repr(self.cause)
 83        )
 84
 85    def explanation(self):
 86        """
 87        Chains through causes to return an HTML string describing why
 88        this context could not be established. Makes use of the html_tb
 89        property of our cause if it's present and the cause is not a
 90        ContextCreationError.
 91        """
 92        result = "Could not get '{}' because:".format(
 93            self.context.feedback_topic() # errors won't appear in rubric
 94        )
 95        if self.cause:
 96            # Short-circuit to the root of the chain if we can
 97            current = self
 98            chain = []
 99            while hasattr(current, "cause") and current.cause:
100                chain.append(current)
101                current = current.cause
102
103            # Add the root cause's message first
104            root_cause = current
105            result += "<br>\n" + str(root_cause)
106
107            # Try to get an HTML traceback for the root cause
108            root_tb = str(root_cause)
109            if hasattr(root_cause, "html_tb"):
110                root_tb = root_cause.html_tb
111            elif hasattr(root_cause, "__traceback__"):
112                if self.context.cached_value is not None:
113                    linkable = linkmap(self.context.cached_value)
114                elif self.context.working_from is not None:
115                    linkable = linkmap(self.context.working_from)
116                else:
117                    linkable = None
118                root_tb = html_tools.html_traceback(
119                    root_cause,
120                    linkable=linkable
121                )
122
123            result += html_tools.build_html_details(
124                "Details (click me):",
125                html_tools.build_list(
126                    [
127                        "{}: {}".format(
128                            error.context.feedback_topic(),
129                            error.message
130                        )
131                        for error in chain
132                    ] + [ root_tb ]
133                )
134            )
135        else:
136            result += "<br>\n" + self.message
137        return result

Error indicating that context could not be created, usually because a submission was missing the required function or module, or because attempting to evaluate a function/module caused an error.

ContextCreationError(context, message, cause=None)
58    def __init__(self, context, message, cause=None):
59        """
60        Supply a Context object within which this error was generated, a
61        message describing the error, and if there is one, an earlier
62        Exception that caused this error.
63        """
64        self.context = context
65        self.message = message
66        self.cause = cause

Supply a Context object within which this error was generated, a message describing the error, and if there is one, an earlier Exception that caused this error.

def explanation(self):
 85    def explanation(self):
 86        """
 87        Chains through causes to return an HTML string describing why
 88        this context could not be established. Makes use of the html_tb
 89        property of our cause if it's present and the cause is not a
 90        ContextCreationError.
 91        """
 92        result = "Could not get '{}' because:".format(
 93            self.context.feedback_topic() # errors won't appear in rubric
 94        )
 95        if self.cause:
 96            # Short-circuit to the root of the chain if we can
 97            current = self
 98            chain = []
 99            while hasattr(current, "cause") and current.cause:
100                chain.append(current)
101                current = current.cause
102
103            # Add the root cause's message first
104            root_cause = current
105            result += "<br>\n" + str(root_cause)
106
107            # Try to get an HTML traceback for the root cause
108            root_tb = str(root_cause)
109            if hasattr(root_cause, "html_tb"):
110                root_tb = root_cause.html_tb
111            elif hasattr(root_cause, "__traceback__"):
112                if self.context.cached_value is not None:
113                    linkable = linkmap(self.context.cached_value)
114                elif self.context.working_from is not None:
115                    linkable = linkmap(self.context.working_from)
116                else:
117                    linkable = None
118                root_tb = html_tools.html_traceback(
119                    root_cause,
120                    linkable=linkable
121                )
122
123            result += html_tools.build_html_details(
124                "Details (click me):",
125                html_tools.build_list(
126                    [
127                        "{}: {}".format(
128                            error.context.feedback_topic(),
129                            error.message
130                        )
131                        for error in chain
132                    ] + [ root_tb ]
133                )
134            )
135        else:
136            result += "<br>\n" + self.message
137        return result

Chains through causes to return an HTML string describing why this context could not be established. Makes use of the html_tb property of our cause if it's present and the cause is not a ContextCreationError.

Inherited Members
builtins.BaseException
with_traceback
def extract(context, slot):
144def extract(context, slot):
145    """
146    Returns the value for the given slot of the given Context, raising a
147    MissingContextError if there is no such value.
148    """
149    if slot in context:
150        return context[slot]
151    else:
152        raise MissingContextError(
153            f"Required context slot '{slot}' not found."
154        )

Returns the value for the given slot of the given Context, raising a MissingContextError if there is no such value.

def linkmap(context):
157def linkmap(context):
158    """
159    Returns a link dictionary suitable for the `linkable` parameter of
160    `html_traceback`, based on the "task_info" and "filename" slots of
161    the given context dictionary, or just based on the "task_info" if
162    "filename" is not present. Returns None if "task_info" is missing.
163    """
164    if "task_info" in context and "filename" in context:
165        return { context["filename"]: context["task_info"]["id"] }
166    elif "task_info" in context:
167        ti = context["task_info"]
168        return { ti["target"]: ti["id"] }
169    else:
170        return None

Returns a link dictionary suitable for the linkable parameter of html_traceback, based on the "task_info" and "filename" slots of the given context dictionary, or just based on the "task_info" if "filename" is not present. Returns None if "task_info" is missing.

class MashEnter(io.TextIOBase):
173class MashEnter(io.TextIOBase):
174    """
175    A fake input designed to be used in place of stdin. Reading anything
176    just returns newlines.
177    """
178    def read(size=-1):
179        """
180        Returns a newline, or if a specific size is specified, that many
181        newlines. Behavior is weird since normally you can't call read
182        multiple times.
183        """
184        if size == -1:
185            return '\n'
186        else:
187            return '\n' * size
188
189    def readline(size=-1):
190        """
191        Returns a newline.
192        """
193        return '\n'

A fake input designed to be used in place of stdin. Reading anything just returns newlines.

MashEnter()
def read(size=-1):
178    def read(size=-1):
179        """
180        Returns a newline, or if a specific size is specified, that many
181        newlines. Behavior is weird since normally you can't call read
182        multiple times.
183        """
184        if size == -1:
185            return '\n'
186        else:
187            return '\n' * size

Returns a newline, or if a specific size is specified, that many newlines. Behavior is weird since normally you can't call read multiple times.

def readline(size=-1):
189    def readline(size=-1):
190        """
191        Returns a newline.
192        """
193        return '\n'

Returns a newline.

Inherited Members
_io._TextIOBase
detach
write
encoding
newlines
errors
_io._IOBase
seek
tell
truncate
flush
close
seekable
readable
writable
fileno
isatty
readlines
writelines
class AllOnes(io.TextIOBase):
196class AllOnes(io.TextIOBase):
197    """
198    A fake input designed to be used in place of stdin. Reading anything
199    just returns the string '1' with a newline at the end.
200    """
201    def read(size=-1):
202        """
203        Returns a string containing the digit 1 followed by a newline, or
204        if a specific size is specified, that many ones minus one, plus a
205        single newline. Behavior is weird since normally you can't call
206        read multiple times.
207        """
208        if size == -1:
209            return '1\n'
210        else:
211            return '1' * (size - 1) + '\n'
212
213    def readline(size=-1):
214        """
215        Returns the digit 1 followed by a newline.
216        """
217        return '1\n'

A fake input designed to be used in place of stdin. Reading anything just returns the string '1' with a newline at the end.

AllOnes()
def read(size=-1):
201    def read(size=-1):
202        """
203        Returns a string containing the digit 1 followed by a newline, or
204        if a specific size is specified, that many ones minus one, plus a
205        single newline. Behavior is weird since normally you can't call
206        read multiple times.
207        """
208        if size == -1:
209            return '1\n'
210        else:
211            return '1' * (size - 1) + '\n'

Returns a string containing the digit 1 followed by a newline, or if a specific size is specified, that many ones minus one, plus a single newline. Behavior is weird since normally you can't call read multiple times.

def readline(size=-1):
213    def readline(size=-1):
214        """
215        Returns the digit 1 followed by a newline.
216        """
217        return '1\n'

Returns the digit 1 followed by a newline.

Inherited Members
_io._TextIOBase
detach
write
encoding
newlines
errors
_io._IOBase
seek
tell
truncate
flush
close
seekable
readable
writable
fileno
isatty
readlines
writelines
def sandbox_filemap(spec):
220def sandbox_filemap(spec):
221    """
222    Extracts the standard symlink mapping for sandbox files from the
223    given specification, based on its helper_files and starter_path
224    properties. The result maps absolute filenames to sandbox-relative
225    filenames and specifies how to copy helper files into a sandbox.
226    """
227    return {
228        os.path.abspath(os.path.join(spec.starter_path, helper)): helper
229        for helper in spec.helper_files
230    }

Extracts the standard symlink mapping for sandbox files from the given specification, based on its helper_files and starter_path properties. The result maps absolute filenames to sandbox-relative filenames and specifies how to copy helper files into a sandbox.

class ContextualValue:
237class ContextualValue:
238    """
239    This class and its subclasses represent values that may appear as
240    part of arguments in a code behavior test (see
241    create_code_behavior_context_builder). Before testing, those
242    arguments will be replaced (using the instance's replace method). The
243    replace method will receive the current context as its first
244    argument, along with a boolean indicating whether the value is being
245    requested to test the submitted module (True) or solution module
246    (False).
247
248    The default behavior (this class) is to accept a function when
249    constructed and run that function on the provided context dictionary,
250    using the function's return value as the actual argument to the
251    function being tested.
252
253    Your extractor function should ideally raise MissingContextError in
254    cases where a context value that it depends on is not present,
255    although this is not critical.
256    """
257    def __init__(self, value_extractor):
258        """
259        There is one required argument: the value extractor function,
260        which will be given a context dictionary and a boolean indicating
261        submitted vs. solution testing, and will be expected to produce
262        an argument value.
263        """
264        self.extractor = value_extractor
265
266    def __str__(self):
267        return "a contextual value based on {}".format(
268            self.extractor.__name__
269        )
270
271    def __repr__(self):
272        return "<a ContextualValue based on {}>".format(
273            self.extractor.__name__
274        )
275
276    def replace(self, context):
277        """
278        This method is used to provide an actual argument value to take
279        the place of this object.
280
281        The argument is the context to use to retrieve a value.
282
283        This implementation simply runs the provided extractor function
284        on the two arguments it gets.
285        """
286        return self.extractor(context)

This class and its subclasses represent values that may appear as part of arguments in a code behavior test (see create_code_behavior_context_builder). Before testing, those arguments will be replaced (using the instance's replace method). The replace method will receive the current context as its first argument, along with a boolean indicating whether the value is being requested to test the submitted module (True) or solution module (False).

The default behavior (this class) is to accept a function when constructed and run that function on the provided context dictionary, using the function's return value as the actual argument to the function being tested.

Your extractor function should ideally raise MissingContextError in cases where a context value that it depends on is not present, although this is not critical.

ContextualValue(value_extractor)
257    def __init__(self, value_extractor):
258        """
259        There is one required argument: the value extractor function,
260        which will be given a context dictionary and a boolean indicating
261        submitted vs. solution testing, and will be expected to produce
262        an argument value.
263        """
264        self.extractor = value_extractor

There is one required argument: the value extractor function, which will be given a context dictionary and a boolean indicating submitted vs. solution testing, and will be expected to produce an argument value.

def replace(self, context):
276    def replace(self, context):
277        """
278        This method is used to provide an actual argument value to take
279        the place of this object.
280
281        The argument is the context to use to retrieve a value.
282
283        This implementation simply runs the provided extractor function
284        on the two arguments it gets.
285        """
286        return self.extractor(context)

This method is used to provide an actual argument value to take the place of this object.

The argument is the context to use to retrieve a value.

This implementation simply runs the provided extractor function on the two arguments it gets.

class ContextSlot(ContextualValue):
289class ContextSlot(ContextualValue):
290    """
291    A special case ContextualValue where the value to be used is simply
292    stored in a slot in the context dictionary, with no extra processing
293    necessary. This class just needs the string name of the slot to be
294    used.
295    """
296    def __init__(self, slot_name):
297        """
298        One required argument: the name of the context slot to use.
299        """
300        self.slot = slot_name
301
302    def __str__(self):
303        return "the current '{}' value".format(self.slot)
304
305    def __repr__(self):
306        return "<a ContextSlot for " + str(self) + ">"
307
308    def replace(self, context):
309        """
310        We retrieve the slot value from the context. Notice that if the
311        value is missing, we generate a MissingContextError that should
312        eventually bubble out.
313        """
314        # Figure out which slot we're using
315        slot = self.slot
316
317        if slot not in context:
318            raise MissingContextError(
319                (
320                    "Context slot '{}' is required by a ContextSlot dynamic"
321                    " value, but it is not present in the testing context."
322                ).format(slot)
323            )
324        return context[slot]

A special case ContextualValue where the value to be used is simply stored in a slot in the context dictionary, with no extra processing necessary. This class just needs the string name of the slot to be used.

ContextSlot(slot_name)
296    def __init__(self, slot_name):
297        """
298        One required argument: the name of the context slot to use.
299        """
300        self.slot = slot_name

One required argument: the name of the context slot to use.

def replace(self, context):
308    def replace(self, context):
309        """
310        We retrieve the slot value from the context. Notice that if the
311        value is missing, we generate a MissingContextError that should
312        eventually bubble out.
313        """
314        # Figure out which slot we're using
315        slot = self.slot
316
317        if slot not in context:
318            raise MissingContextError(
319                (
320                    "Context slot '{}' is required by a ContextSlot dynamic"
321                    " value, but it is not present in the testing context."
322                ).format(slot)
323            )
324        return context[slot]

We retrieve the slot value from the context. Notice that if the value is missing, we generate a MissingContextError that should eventually bubble out.

class ModuleValue(ContextualValue):
327class ModuleValue(ContextualValue):
328    """
329    A kind of ContextualValue that evaluates a string containing Python
330    code within the module stored in the "module" context slot.
331    """
332    def __init__(self, expression):
333        """
334        One required argument: the expression to evaluate, which must be
335        a string that contains a valid Python expression.
336        """
337        self.expression = expression
338
339    def __str__(self):
340        return "the result of " + self.expression
341
342    def __repr__(self):
343        return "<a ModuleValue based on {}>".format(str(self))
344
345    def replace(self, context):
346        """
347        We retrieve the "module" slot value from the provided context.
348
349        We then evaluate our expression within the retrieved module, and
350        return that result.
351        """
352        if "module" not in context:
353            raise MissingContextError(
354                "ModuleValue argument requires a 'module' context"
355              + " key, but there isn't one."
356            )
357        module = context["module"]
358
359        return eval(self.expression, module.__dict__)

A kind of ContextualValue that evaluates a string containing Python code within the module stored in the "module" context slot.

ModuleValue(expression)
332    def __init__(self, expression):
333        """
334        One required argument: the expression to evaluate, which must be
335        a string that contains a valid Python expression.
336        """
337        self.expression = expression

One required argument: the expression to evaluate, which must be a string that contains a valid Python expression.

def replace(self, context):
345    def replace(self, context):
346        """
347        We retrieve the "module" slot value from the provided context.
348
349        We then evaluate our expression within the retrieved module, and
350        return that result.
351        """
352        if "module" not in context:
353            raise MissingContextError(
354                "ModuleValue argument requires a 'module' context"
355              + " key, but there isn't one."
356            )
357        module = context["module"]
358
359        return eval(self.expression, module.__dict__)

We retrieve the "module" slot value from the provided context.

We then evaluate our expression within the retrieved module, and return that result.

class SolnValue(ContextualValue):
362class SolnValue(ContextualValue):
363    """
364    Like a ModuleValue, but *always* takes the value from the solution
365    module, even when we're testing submitted code.
366    """
367    def __init__(self, expression):
368        """
369        One required argument: the expression to evaluate, which must be
370        a string that contains a valid Python expression.
371        """
372        self.expression = expression
373
374    def __str__(self):
375        return "the correct value of " + self.expression
376
377    def __repr__(self):
378        return "<a SolnValue based on {}>".format(str(self))
379
380    def replace(self, context):
381        """
382        We retrieve the "ref_module" slot value from the provided context.
383
384        We then evaluate our expression within the retrieved module, and
385        return that result.
386        """
387        if "ref_module" not in context:
388            raise MissingContextError(
389                f"SolnValue argument requires a 'ref_module' context"
390                f" key, but there isn't one in: {list(context.keys())}"
391            )
392        module = context["ref_module"]
393
394        return eval(self.expression, module.__dict__)

Like a ModuleValue, but always takes the value from the solution module, even when we're testing submitted code.

SolnValue(expression)
367    def __init__(self, expression):
368        """
369        One required argument: the expression to evaluate, which must be
370        a string that contains a valid Python expression.
371        """
372        self.expression = expression

One required argument: the expression to evaluate, which must be a string that contains a valid Python expression.

def replace(self, context):
380    def replace(self, context):
381        """
382        We retrieve the "ref_module" slot value from the provided context.
383
384        We then evaluate our expression within the retrieved module, and
385        return that result.
386        """
387        if "ref_module" not in context:
388            raise MissingContextError(
389                f"SolnValue argument requires a 'ref_module' context"
390                f" key, but there isn't one in: {list(context.keys())}"
391            )
392        module = context["ref_module"]
393
394        return eval(self.expression, module.__dict__)

We retrieve the "ref_module" slot value from the provided context.

We then evaluate our expression within the retrieved module, and return that result.