Source code for pedal.sandbox.commands

"""
The collection of top-level commands for the Sandbox module. Note that
most of these simply act on the current MAIN_REPORT's Sandbox instance, without
doing any logic themselves.
"""

from pedal.core.report import MAIN_REPORT
from pedal.sandbox.constants import TOOL_NAME
from pedal.sandbox.sandbox import Sandbox
from pedal.source.constants import TOOL_NAME as SOURCE_TOOL_NAME
from pedal.utilities.ast_tools import FindExecutableLines


[docs] def run(code=None, filename=None, inputs=None, threaded=None, after=None, before=None, report=MAIN_REPORT): """ If both ``code`` and ``filename`` are None, then the submission's main file will be executed. If ``code`` is given but ``filename`` is not, then it is assumed to be instructor code. Args: code (str or :py:class:`~pedal.cait.cait_node.CaitNode` or None): The code to execute. filename (str or None): The filename to use for this code. inputs (list[str]): Optional inputs to be passed to :py:func:`~pedal.sandbox.Sandbox.set_input`. threaded (bool): Whether to run this code in a threaded environment, which allows for timeouts. after (str): Code to run after this code (without a filename). before (str): Code to run before this code (without a filename). report (:py:class:`pedal.core.report.Report`): The report with the sandbox instance. Returns: Sandbox: The sandbox instance that this was run in. """ sandbox: Sandbox = report[TOOL_NAME]['sandbox'] return sandbox.run(code=code, filename=filename, inputs=inputs, threaded=threaded, after=after, before=before)
[docs] def call(function, *args, target="_", threaded=None, inputs=None, function_kwargs=None, args_locals=None, kwargs_locals=None, report=MAIN_REPORT, **kwargs): """ Execute the given function from the student's namespace. The ``args_locals`` and ``kwargs_locals`` values allow you to use student's local variables as arguments, instead of literal values. They actually support any arbitrary Python code, it will be injected without modification. Args: function (str): The name of the function to call. *args (Any): Any number of positional arguments to be passed to the called function. target (str): The variable to assign the result of this call to. Defaults to ``"_"``. threaded (bool): Whether or not to run this code in a threaded environment, which allows for timeouts. inputs (str or list[str]): function_kwargs (dict[str, Any]): Additional keyword arguments that could not be passed in directly via ``**kwargs`` (perhaps because they conflict with an existing parameter like ``target``). args_locals (list[str]): A list of names (or any valid Python expression) that will be passed as positional arguments to the function (as opposed to actual values). If any value is None (or if the list is too short), then the corresponding position argument from ``*args`` will be used instead. kwargs_locals (dict[str, str]): A dictionary of keyword argument names mapped to local names (or any valid Python expression), that will be used as keyword parameters similar to ``args_locals``. **kwargs (): Additional keyword arguments passed to the called function. report (:py:class:`pedal.core.report.Report`): The report with the sandbox instance. Returns: Exception or :py:class:`~pedal.sandbox.sandbox.SandboxResult`: The result of calling the function will be returned, proxied behind a SandboxResult (which attempts to perfectly emulate that value). If the function call failed, the exception will be returned instead. """ sandbox: Sandbox = report[TOOL_NAME]['sandbox'] return sandbox.call(function, *args, target=target, threaded=threaded, inputs=inputs, function_kwargs=function_kwargs, args_locals=args_locals, kwargs_locals=kwargs_locals, **kwargs)
[docs] def evaluate(code, target="_", threaded=None, report=MAIN_REPORT): """ Evaluates the given code and assigns the result to the given target. Will cause an error if ``code`` is not a valid expression. Args: code (str or :py:class:`~pedal.cait.cait_node.CaitNode`): The code to execute. If a CaitNode, then that will be executed directly instead of being compiled. target (str): The name of the variable to assign the result to. Note that the result is also returned by this function. threaded (bool): Whether or not to run this code in a threaded environment, which allows for timeouts. report (:py:class:`pedal.core.report.Report`): The report with the sandbox instance. Returns: Exception or :py:class:`~pedal.sandbox.sandbox.SandboxResult`: The result of evaluating the code will be returned, proxied behind a SandboxResult (which attempts to perfectly emulate that value). If the function call failed, the exception will be returned instead. """ sandbox: Sandbox = report[TOOL_NAME]['sandbox'] return sandbox.evaluate(code, target=target, threaded=threaded)
[docs] def clear_input(report=MAIN_REPORT): """ Removes any existing inputs set up for the sandbox. Args: report (:py:class:`pedal.core.report.Report`): The report to clear inputs for """ sandbox: Sandbox = report[TOOL_NAME]['sandbox'] sandbox.clear_input()
[docs] def queue_input(*inputs, report=MAIN_REPORT): """ Adds the given ``*inputs`` to be used as input during subsequent executions (e.g., :py:func:`pedal.sandbox.commands.run`) if the student's code calls :py:func:`input`. This does not remove existing inputs. Args: *inputs (str): One or more strings that will be set. The first string passed in is the first string that will be passed in as input. report (:py:class:`pedal.core.report.Report`): The report with the sandbox instance. """ sandbox: Sandbox = report[TOOL_NAME]['sandbox'] sandbox.set_input(inputs, clear=False)
[docs] def set_input(inputs, clear=True, report=MAIN_REPORT): """ Sets the given ``inputs`` to be used as input during subsequent executions (e.g., :py:func:`pedal.sandbox.commands.run`) if the student's code calls :py:func:`input`. Unless the ``clear`` parameter is set to False, this removes existing inputs. Args: inputs (str or list[str]): One or more strings that will be set. The first string passed in is the first string that will be passed in as input. clear (bool): Whether or not to remove any existing inputs set up in the sandbox. report (:py:class:`pedal.core.report.Report`): The report with the sandbox instance. """ sandbox: Sandbox = report[TOOL_NAME]['sandbox'] sandbox.set_input(inputs, clear=clear)
[docs] def get_input(report=MAIN_REPORT): """ Retrieves the current inputs that are available for execution. Args: report (:py:class:`pedal.core.report.Report`): The report with the sandbox instance. Returns: list[str]: The current list of inputs available for execution. The first element of this list is the next string that will be passed to the next student call of :py:func:`input`. """ sandbox: Sandbox = report[TOOL_NAME]['sandbox'] return sandbox.inputs
[docs] def clear_output(report=MAIN_REPORT): """ Removes any existing outputs associated with the sandbox. Args: report (:py:class:`pedal.core.report.Report`): The report to clear outputs for. """ sandbox: Sandbox = report[TOOL_NAME]['sandbox'] sandbox.clear_output()
[docs] def get_output(report=MAIN_REPORT): """ Retrieves the current output (whatever the student has printed) since execution began. Args: report (:py:class:`pedal.core.report.Report`): The report to get the output for. Returns: list[str]: The current output. """ sandbox: Sandbox = report[TOOL_NAME]['sandbox'] return sandbox.output
[docs] def get_raw_output(report=MAIN_REPORT): """ Retrieves the current output (whatever the student has printed) since execution began, without any formatting. The result is a single string. Args: report (:py:class:`pedal.core.report.Report`): The report to get the output for. Returns: str: The current output. """ sandbox: Sandbox = report[TOOL_NAME]['sandbox'] return sandbox.raw_output
[docs] def get_exception(report=MAIN_REPORT): """ Returns an exception if one occurred during the last execution, otherwise returns None. Args: report (:py:class:`pedal.core.report.Report`): The report to get the exception for. Returns: Exception or None: The exception that occurred, or None if no exception """ sandbox: Sandbox = report[TOOL_NAME]['sandbox'] return sandbox.exception
[docs] def clear_student_data(report=MAIN_REPORT): """ Removes any data in the student namespace. Args: report (:py:class:`pedal.core.report.Report`): The report to clear the data for. """ sandbox: Sandbox = report[TOOL_NAME]['sandbox'] sandbox.clear_data()
[docs] def get_student_data(report=MAIN_REPORT): """ Retrieves the current data in the student namespace. Note that this is the data itself - modifying the dictionary will modify the data in the students' namespace for subsequent executions! Args: report (:py:class:`pedal.core.report.Report`): The report with the sandbox instance. Returns: dict[str, Any]: The student's data namespace, mapping names to the values themselves. """ sandbox: Sandbox = report[TOOL_NAME]['sandbox'] return sandbox.data
[docs] def get_sandbox(report=MAIN_REPORT): """ Retrieves the current sandbox instance attached to this report. Typically, this is used to retrieve the sandbox without running the students' code. Args: report (:py:class:`pedal.core.report.Report`): The report with the sandbox instance. Returns: :py:class:`pedal.sandbox.Sandbox`: The sandbox instance. """ return report[TOOL_NAME]['sandbox']
[docs] def clear_sandbox(report=MAIN_REPORT): """ Removes any existing data within the current sandbox instance. """ sandbox: Sandbox = report[TOOL_NAME]['sandbox'] sandbox.clear()
[docs] def get_trace(report=MAIN_REPORT): """ Retrieves the list of line numbers that have been traced (recognized as executed). Args: report (:py:class:`pedal.core.report.Report`): The report with the sandbox instance. Returns: list[int]: The list of lines that have been executed in the student's code. """ sandbox: Sandbox = report[TOOL_NAME]['sandbox'] return sandbox.trace.lines
[docs] def get_call_arguments(function_name, report=MAIN_REPORT): """ Retrieves all the arguments that were passed into the given function when it was called. Args: function_name (str): The name of the function to check. report (:py:class:`pedal.core.report.Report`): The report with the sandbox instance. Returns: list[dict[str, any]]: The outer list will have any entry for each function call. The inner list will be the arguments passed into that call. Pretty sure that any *args and **kwargs are the last elements of that list, if they exist. """ sandbox: Sandbox = report[TOOL_NAME]['sandbox'] return sandbox.trace.calls.get(function_name, [])
[docs] def count_unique_calls(function_name, report=MAIN_REPORT): """ Counts how many times the given function was called with unique arguments. TODO: What about also tracking the number of "top-level" calls? So we know whether they were actually testing it explicitly or not. Args: function_name (str): The name of the function to check for calls. report (:py:class:`pedal.core.report.Report`): The report with the sandbox instance. Returns: int: The number of unique calls """ calls = get_call_arguments(function_name, report) return len(set([tuple(args.values()) for args in calls]))
[docs] def start_trace(tracer_style='native', report=MAIN_REPORT): """ Start tracing using the coverage module. """ sandbox: Sandbox = report[TOOL_NAME]['sandbox'] sandbox.tracer_style = tracer_style
[docs] def stop_trace(report=MAIN_REPORT): """ Stop whatever tracing is going on. """ sandbox: Sandbox = report[TOOL_NAME]['sandbox'] sandbox.clear_tracer()
[docs] def check_coverage(report=MAIN_REPORT): """ Checks that all the statements in the program have been executed. This function only works when a tracer_style has been set in the sandbox, or you are using an environment that automatically traces calls (e.g., BlockPy). Args: report (Report): The Report to draw source code from; if not given, defaults to MAIN_REPORT. Returns: set[int]: If the source file was not parsed, None is returned. Otherwise, returnes the set of unexecuted lines. float: The ratio of unexected to total executable lines. """ if not report[SOURCE_TOOL_NAME]['success']: return None, 0 lines_executed = set(get_trace()) # TODO: Why... does -1 get returned sometimes? if -1 in lines_executed: lines_executed.remove(-1) student_ast = report[SOURCE_TOOL_NAME]['ast'] visitor = FindExecutableLines() visitor.visit(student_ast) lines_in_code = set(visitor.lines) unexecuted_lines = lines_in_code - lines_executed if lines_in_code: coverage_ratio = len(lines_executed)/len(lines_in_code) else: coverage_ratio = 0 return unexecuted_lines, coverage_ratio
[docs] def clear_mocks(report=MAIN_REPORT): """ Reset mocked modules and functions to their defaults. """ sandbox: Sandbox = report[TOOL_NAME]['sandbox'] sandbox.clear_mocks()
[docs] def mock_function(function_name, new_version, report=MAIN_REPORT): """ Provide a custom version of the built-in function. """ sandbox: Sandbox = report[TOOL_NAME]['sandbox'] sandbox.mock_function(function_name, new_version)
[docs] def allow_function(function_name, report=MAIN_REPORT): """ Explicitly allow students to use the given function. """ sandbox: Sandbox = report[TOOL_NAME]['sandbox'] sandbox.allow_function(function_name)
[docs] def block_function(function_name, report=MAIN_REPORT): """ Cause an error if students call the given function. """ sandbox: Sandbox = report[TOOL_NAME]['sandbox'] sandbox.block_function(function_name)
[docs] def allow_module(module_name, report=MAIN_REPORT): """ Explicitly allow students to use the given module. """ sandbox: Sandbox = report[TOOL_NAME]['sandbox'] sandbox.allow_module(module_name)
[docs] def allow_real_io(report=MAIN_REPORT): """ Allow the input() and print() functions to actually write to the real stdout. By default, Pedal will block this kind of writing/reading normally (although students can still use those functions). """ sandbox: Sandbox = report[TOOL_NAME]['sandbox'] sandbox.allow_function('print') sandbox.set_input(input)
[docs] def block_real_io(report=MAIN_REPORT): """ Explicitly block students from using the input() and print() functions to write/read to the real stdout. This does not prevent them from using the functions, but it does prevent the output from appearing in the real console. """ sandbox: Sandbox = report[TOOL_NAME]['sandbox'] sandbox.clear_mocked_function('print') sandbox.clear_input()
[docs] def mock_module(module_name, new_version, friendly_name=None, report=MAIN_REPORT): """ Provide an alternative version of the given module. Args: module_name (str): The importable name of the module. new_version (dict | :py:class:`pedal.sandbox.mocked.MockModule`): The new version of the module, either as a dictionary of fields/values or a fully created MockModule. friendly_name (str): The internal name to use to store the data for this module, accessible via Sandbox's `modules` field. report (:py:class:`pedal.core.report.Report`): The report with the sandbox instance. """ sandbox: Sandbox = report[TOOL_NAME]['sandbox'] sandbox.mock_module(module_name, new_version, friendly_name=friendly_name)
[docs] def block_module(module_name, report=MAIN_REPORT): """ Cause an error if students use the given module. """ sandbox: Sandbox = report[TOOL_NAME]['sandbox'] sandbox.block_module(module_name)
[docs] def get_module(module_name, report=MAIN_REPORT): """ Loads the data for the given mocked module, if available (otherwise raises exception) """ sandbox: Sandbox = report[TOOL_NAME]['sandbox'] if hasattr(sandbox.modules, module_name): return getattr(sandbox.modules, module_name) raise ValueError(f"Unknown Sandbox Module: `{module_name}`")
[docs] def get_function(function_name, report=MAIN_REPORT): """ Creates an executable function from the given name, based on the students' namespace. This will be executed using the call method. """ sandbox: Sandbox = report[TOOL_NAME]['sandbox'] return sandbox.get_function(function_name)
[docs] def get_python_errors(report=MAIN_REPORT): """ Retrieves any runtime or syntax errors from the report. Args: report: Returns: list[Feedback]: A list of runtime and syntax errors that were detected """ for feedback in report.feedback: if feedback.category == 'runtime' or feedback.category == 'syntax': yield feedback
[docs] class CommandBlock: """ Context Manager for creating instructor blocks of code that will be shown together to the student. TODO: What about named points where you can "rewind" the state to? """ sandbox: Sandbox def __init__(self, report=MAIN_REPORT): self.sandbox = report[TOOL_NAME]['sandbox'] self.context_id = None self.context = None def __enter__(self): self.sandbox.start_grouping_context() return self def __exit__(self, exc_type, exc_val, exc_tb): self.context = self.sandbox.get_context() self.context_id = self.sandbox.stop_grouping_context()
# TODO: Deprecate these! reset_output = clear_output # TODO: Add function to get PREVIOUS inputs