Student Implementations#

For tracking and managing student implementations, stored in PyBryt as a list of 2-tuples of the objects observed while tracing students’ code and the timestamps of those objects, PyBryt provides the StudentImplementation class. The constructor for this class takes in either a path to a Jupyter Notebook file or a notebook object read in with nbformat.

stu = pybryt.StudentImplementation("subm.ipynb")

# or...
nb = nbformat.read("subm.ipynb")
stu = pybryt.StudentImplementation(nb)

Notebook Execution#

The constructor reads the notebook file and stores the student’s code. It then proceeds to execute the student’s notebook using nbformat’s ExecutePreprocessor. The memory footprint of the student’s code is constructed by executing the notebook with a trace function that tracks every value created and accessed by the student’s code and the timestamps at which those values were observed. PyBryt also tracks all of the function calls that occur during execution.

To trace into code written in specific files, use the addl_filenames argument of the constructor to pass a list of absolute paths of files to trace inside of. This can be useful for cases in which a testing harness is being used to test student’s code and the student’s actual submission is written in a Python script (which PyBryt would by default not trace).

stu = pybryt.StudentImplementation("harness.ipynb", addl_filenames=["subm.py"])

To prevent notebooks from getting stuck in a loop or from taking up too many resources, PyBryt automatically sets a timeout of 1200 seconds for each notebook to execute. This cap can be changed using the timeout argument to the constructor, and can be removed by setting that value to None:

stu = pybryt.StudentImplementation("subm.ipynb", timeout=2000)

# no timeout
stu = pybryt.StudentImplementation("subm.ipynb", timeout=None)

PyBryt also employs various custom notebook preprocessors for handling special cases that occur in the code to allow different types of values to be checked. To see the exact version of the code that PyBryt executes, set output to a path to a notebook that PyBryt will write with the executed notebook. You can also access this notebook as an nbformat.NotebookNode object using StudentImplementation.executed_nb This can be useful e.g. for debugging reference implementations by inserting print statements that show the values at various stages of execution.

stu = pybryt.StudentImplementation("subm.ipynb", output="executed-subm.ipynb")

If there is code in a student notebook that should not be traced by PyBryt, wrap it PyBryt’s no_tracing context manager. Any code inside this context will not be traced (if PyBryt is tracing the call stack). If no tracing is occurring, no action is taken.

with pybryt.no_tracing():
    foo(1)

Checking Implementations#

To reconcile a student implementation with a set of reference implementations, use the StudentImplementation.check method, which takes in a single ReferenceImplementation object, or a list of them, and returns a ReferenceResult object (or a list of them). This method simply abstracts away managing the memory footprint tracked by the StudentImplementation object and calls the ReferenceImplementation.run method for each provided reference implementation.

ref = pybryt.ReferenceImplementation.load("reference.pkl")
stu = pybryt.StudentImplementation("subm.ipynb")
stu.check(ref)

To run the references for a single group of annotations, pass the group argument, which should be a string that corresponds to the name of a group of annotations. For example, to run the checks for a single question in a reference that contains multiple questions, the pattern might be

stu.check(ref, group="q1")

Checking from the Notebook#

For running checks against a reference implementation from inside the notebook, PyBryt also provides the context manager check. This context manager initializes PyBryt’s tracing function for any code executed inside of the context and generates a memory footprint of that code, which can be reconciled against a reference implementation. The context manager prints a report when it exits to inform students of any messages and the passing or failing of each reference.

A general pattern for using this context manager would be to have students encapsulate some logic in a function and then write test cases that are checked by the reference implementation inside the context manager. For exmaple, consider the median example below:

def median(S):
    sorted_S = sorted(S)
    size_of_set = len(S)
    middle = size_of_set // 2
    is_set_size_even = (size_of_set % 2) == 0
    if is_set_size_even:
        return (sorted_S[middle-1] + sorted_S[middle]) / 2
    else:
        return sorted_S[middle]

with pybryt.check("median.pkl"):
    import numpy as np
    np.random.seed(42)
    for _ in range(10):
        vals = [np.random.randint(-1000, 1000) for _ in range(np.random.randint(1, 1000))]
        val = median(vals)

The check context manager takes as its arguments a path to a reference implementation, a reference implementation object, or lists thereof. By default, the report printed out at the end includes the results of all reference implementations being checked; this can be changed using the show_only argument, which takes on 3 values: {"satisfied", "unsatisfied", None}. If it is set to "satisfied", only the results of satisfied reference will be included (unless there are none and fill_empty is True), and similarly for "unsatisfied".

Working with Memory Footprints#

Once the notebook has been executed, which happens when the constructor is called, the submission’s memory footprint can be found in the footprint field of the StudentImplementation object. This field contains a MemoryFootprint object, which has fields and method for accessing the values in the footprint, the function calls observed by the trace function, the set of imported modules, and the processed notebook that was executed by PyBryt. See the API reference for MemoryFootprint objects for more information.

Storing Implementations#

Because generating the memory footprints of students’ code can be time consuming and computationally expensive, StudentImplementation objects can also be serialized to make multiple runs across sessions easier. The StudentImplementation class provides the dump and load methods, which function the same as with reference implementations. StudentImplementation objects can also be serialized to base-64-encoded strings using the dumps and loads methods.