forked from ucbds-infra/otter-grader
-
Notifications
You must be signed in to change notification settings - Fork 0
/
__init__.py
146 lines (118 loc) · 5.72 KB
/
__init__.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
"""
Execution and grading internals for Otter-Grader
"""
import json
import itertools
import inspect
import nbformat
from IPython import get_ipython
from .execute_log import execute_log
from .execute_notebook import execute_notebook, filter_ignored_cells
from .execute_script import execute_script
# from .results import GradingResults
from ..test_files import GradingResults, NotebookMetadataOKTestFile, OKTestFile
from ..utils import id_generator
NBFORMAT_VERSION = 4
def check(nb_or_test_path, test_name=None, global_env=None):
"""
Checks a global environment against given test file. If global_env is ``None``, the global
environment of the calling frame is used; i.e., the following two calls are equivalent:
.. code-block:: python
check('tests/q1.py')
check('tests/q1.py', globals())
Args:
nb_or_test_path (``str``): path to test file or notebook
test_name (``str``, optional): the name of the test if a notebook metadata test
global_env (``dict``, optional): a global environment resulting from the execution
of a python script or notebook
Returns:
``otter.test_files.abstract_test.TestFile``: result of running the tests in the
given global environment
"""
if test_name is None:
test = OKTestFile.from_file(nb_or_test_path)
else:
test = NotebookMetadataOKTestFile.from_file(nb_or_test_path, test_name)
if global_env is None:
# Get the global env of our callers - one level below us in the stack
# The grade method should only be called directly from user / notebook
# code. If some other method is calling it, it should also use the
# inspect trick to pass in its parents' global env.
global_env = inspect.currentframe().f_back.f_globals
test.run(global_env)
return test
def grade_notebook(notebook_path, *, tests_glob=None, name=None, ignore_errors=True, script=False,
cwd=None, test_dir=None, seed=None, log=None, variables=None, plugin_collection=None):
"""
Grade an assignment file and return grade information
Args:
notebook_path (``str``): path to a single notebook or Python script
tests_glob (``list`` of ``str``, optional): paths to test files to run
name (``str``, optional): initial environment name
ignore_errors (``bool``, optional): whether errors in execution should be ignored
script (``bool``, optional): whether the ``notebook_path`` is a Python script
cwd (``str``, optional): working directory of execution to be appended to ``sys.path`` in
grading environment
test_dir (``str``, optional): path to directory of tests in grading environment
seed (``int``, optional): random seed for intercell seeding
log (``otter.check.logs.Log``, optional): log from which to grade questions
variables (``dict``, optional): map of variable names -> type string to check type of deserialized
object to prevent arbitrary code from being put into the environment; ignored if log is ``None``
plugin_collection (``otter.plugins.PluginCollection``, optional): a set of plugins to run on
this assignment during execution and grading
Returns:
``otter.test_files.GradingResults``: the results of grading
"""
# # ensure this is not being executed inside a notebook
# assert get_ipython() is None, "Cannot execute inside Jupyter Notebook"
if not script:
try:
with open(notebook_path) as f:
nb = nbformat.read(f, as_version=NBFORMAT_VERSION)
except UnicodeDecodeError:
with open(notebook_path, encoding='utf-8') as f:
nb = nbformat.read(f, as_version=NBFORMAT_VERSION)
else:
with open(notebook_path) as f:
nb = f.read()
if plugin_collection is not None:
nb = plugin_collection.before_execution(nb)
# remove any ignored cells from the notebook
if not script:
nb = filter_ignored_cells(nb)
secret = id_generator()
results_array = "check_results_{}".format(secret)
initial_env = {
results_array: []
}
if name:
initial_env["__name__"] = name
if log is not None:
global_env = execute_log(nb, log, secret, initial_env, ignore_errors=ignore_errors, cwd=cwd, test_dir=test_dir, variables=variables)
elif script:
global_env = execute_script(nb, secret, initial_env, ignore_errors=ignore_errors, cwd=cwd, test_dir=test_dir, seed=seed)
else:
global_env = execute_notebook(nb, secret, initial_env, ignore_errors=ignore_errors, cwd=cwd, test_dir=test_dir, seed=seed)
if plugin_collection is not None:
plugin_collection.run("after_execution", global_env)
tests_run = global_env[results_array]
# Check for tests which were not included in the notebook and specified by tests_globs
# Allows instructors to run notebooks with additional tests not accessible to user
if tests_glob:
# unpack list of paths into a single list
tested_set = [test.path for test in tests_run]
extra_tests = []
for t in sorted(tests_glob):
include = True
for tested in tested_set:
if tested in t or t in tested: # e.g. if 'tests/q1.py' is in /srv/repo/lab01/tests/q1.py
include = False
if include:
extra_tests.append(OKTestFile.from_file(t))
extra_tests[-1].run(global_env)
# extra_results = [t.run(global_env, include_grade=False) for t in extra_tests]
tests_run += extra_tests
results = GradingResults(tests_run)
if plugin_collection is not None:
plugin_collection.run("after_grading", results)
return results