dlai-grader 1.22.2__py3-none-any.whl → 2.0b1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of dlai-grader might be problematic. Click here for more details.
- dlai_grader/__init__.py +1 -1
- dlai_grader/config.py +12 -5
- dlai_grader/grading.py +58 -30
- dlai_grader/io.py +46 -18
- dlai_grader/notebook.py +10 -10
- dlai_grader/templates.py +336 -160
- dlai_grader/types.py +6 -5
- {dlai_grader-1.22.2.dist-info → dlai_grader-2.0b1.dist-info}/METADATA +10 -5
- dlai_grader-2.0b1.dist-info/RECORD +16 -0
- {dlai_grader-1.22.2.dist-info → dlai_grader-2.0b1.dist-info}/WHEEL +1 -1
- dlai_grader-1.22.2.dist-info/RECORD +0 -16
- {dlai_grader-1.22.2.dist-info → dlai_grader-2.0b1.dist-info}/entry_points.txt +0 -0
- {dlai_grader-1.22.2.dist-info → dlai_grader-2.0b1.dist-info/licenses}/LICENSE +0 -0
- {dlai_grader-1.22.2.dist-info → dlai_grader-2.0b1.dist-info}/top_level.txt +0 -0
dlai_grader/__init__.py
CHANGED
dlai_grader/config.py
CHANGED
|
@@ -2,12 +2,11 @@ import os
|
|
|
2
2
|
import re
|
|
3
3
|
import csv
|
|
4
4
|
from dataclasses import dataclass
|
|
5
|
-
from typing import Dict
|
|
6
5
|
|
|
7
6
|
|
|
8
7
|
def parse_conf(
|
|
9
8
|
path: str = "./.conf",
|
|
10
|
-
) ->
|
|
9
|
+
) -> dict[str, str]:
|
|
11
10
|
"""Parses variables from .conf file
|
|
12
11
|
|
|
13
12
|
Args:
|
|
@@ -45,10 +44,9 @@ class Config:
|
|
|
45
44
|
submission_workdir: str = "./submission/"
|
|
46
45
|
solution_workdir: str = "./solution/"
|
|
47
46
|
solution_file: str = "solution.ipynb"
|
|
48
|
-
solution_file_path: str =
|
|
47
|
+
solution_file_path: str = ""
|
|
49
48
|
submission_file: str = "submission.ipynb"
|
|
50
|
-
submission_file_path: str =
|
|
51
|
-
feedback_file_path: str = "/shared/feedback.json"
|
|
49
|
+
submission_file_path: str = ""
|
|
52
50
|
part_id: str = get_part_id()
|
|
53
51
|
latest_version: str = (
|
|
54
52
|
parse_conf()["GRADER_VERSION"] if os.path.exists("./.conf") else ""
|
|
@@ -56,3 +54,12 @@ class Config:
|
|
|
56
54
|
assignment_name: str = (
|
|
57
55
|
parse_conf()["ASSIGNMENT_NAME"] if os.path.exists("./.conf") else ""
|
|
58
56
|
)
|
|
57
|
+
|
|
58
|
+
def __post_init__(self):
|
|
59
|
+
# This is where we set the file paths after the instance variables are initialized
|
|
60
|
+
self.solution_file_path = os.path.join(
|
|
61
|
+
self.solution_workdir, self.solution_file
|
|
62
|
+
)
|
|
63
|
+
self.submission_file_path = os.path.join(
|
|
64
|
+
self.submission_workdir, self.submission_file
|
|
65
|
+
)
|
dlai_grader/grading.py
CHANGED
|
@@ -1,75 +1,96 @@
|
|
|
1
1
|
from dataclasses import dataclass
|
|
2
2
|
from functools import wraps
|
|
3
|
-
from typing import Any, Callable
|
|
3
|
+
from typing import Any, Callable
|
|
4
4
|
from types import ModuleType
|
|
5
5
|
|
|
6
6
|
|
|
7
7
|
@dataclass
|
|
8
8
|
class LearnerSubmission:
|
|
9
|
-
"""Class that represents a file from the learner. Useful when grading does not depend on the notebook"""
|
|
9
|
+
"""Class that represents a file from the learner. Useful when grading does not depend on the notebook."""
|
|
10
10
|
|
|
11
11
|
submission: Any = None
|
|
12
12
|
|
|
13
13
|
|
|
14
|
-
learner_submission =
|
|
14
|
+
learner_submission = ModuleType | LearnerSubmission
|
|
15
15
|
|
|
16
16
|
|
|
17
17
|
@dataclass
|
|
18
18
|
class test_case:
|
|
19
|
-
"""Class that represents a test case"""
|
|
19
|
+
"""Class that represents a test case."""
|
|
20
20
|
|
|
21
21
|
msg: str = ""
|
|
22
22
|
want: Any = None
|
|
23
23
|
got: Any = None
|
|
24
24
|
failed: bool = False
|
|
25
25
|
|
|
26
|
+
def fail(self):
|
|
27
|
+
"""Sets the failed attribute to True."""
|
|
28
|
+
self.failed = True
|
|
29
|
+
|
|
26
30
|
|
|
27
31
|
@dataclass
|
|
28
32
|
class aggregated_test_case:
|
|
29
|
-
"""Class that represents an aggregated collection of test cases"""
|
|
33
|
+
"""Class that represents an aggregated collection of test cases."""
|
|
30
34
|
|
|
31
35
|
test_name: str
|
|
32
|
-
tests:
|
|
36
|
+
tests: list[test_case]
|
|
37
|
+
|
|
33
38
|
|
|
39
|
+
def compute_grading_score(test_cases: list[test_case]) -> tuple[float, str]:
|
|
40
|
+
"""
|
|
41
|
+
Computes the score based on the number of failed and total cases.
|
|
34
42
|
|
|
35
|
-
def compute_grading_score(
|
|
36
|
-
test_cases: List[test_case],
|
|
37
|
-
) -> Tuple[float, str]:
|
|
38
|
-
"""Computes the score based on the number of failed and total cases.
|
|
39
43
|
Args:
|
|
40
44
|
test_cases (List): Test cases.
|
|
45
|
+
|
|
41
46
|
Returns:
|
|
42
47
|
Tuple[float, str]: The grade and feedback message.
|
|
43
|
-
"""
|
|
44
48
|
|
|
49
|
+
"""
|
|
45
50
|
num_cases = len(test_cases)
|
|
46
51
|
if num_cases == 0:
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
"The grader was unable to generate test cases for your implementation. This suggests a bug with your code, please revise your solution and try again.",
|
|
50
|
-
)
|
|
52
|
+
msg = "The grader was unable to generate test cases for your implementation. This suggests a bug with your code, please revise your solution and try again."
|
|
53
|
+
return 0.0, msg
|
|
51
54
|
|
|
52
55
|
failed_cases = [t for t in test_cases if t.failed]
|
|
53
56
|
score = 1.0 - len(failed_cases) / num_cases
|
|
54
|
-
|
|
57
|
+
score = round(score, 2)
|
|
58
|
+
|
|
59
|
+
if not failed_cases:
|
|
60
|
+
msg = "All tests passed! ✅ Congratulations! 🎉"
|
|
61
|
+
return 1.0, msg
|
|
62
|
+
|
|
63
|
+
feedback_msg = ""
|
|
64
|
+
# if print_passed_tests:
|
|
65
|
+
# passed_cases = [t for t in test_cases if not t.failed]
|
|
66
|
+
# for passed_case in passed_cases:
|
|
67
|
+
# feedback_msg += f"✅ {passed_case.msg}.\n"
|
|
55
68
|
|
|
56
69
|
if failed_cases:
|
|
57
|
-
feedback_msg = ""
|
|
58
70
|
for failed_case in failed_cases:
|
|
59
|
-
feedback_msg += f"
|
|
60
|
-
|
|
71
|
+
feedback_msg += f"❌ {failed_case.msg}.\n"
|
|
72
|
+
|
|
73
|
+
if failed_case.want:
|
|
74
|
+
feedback_msg += f"Expected:\n{failed_case.want}\n"
|
|
75
|
+
|
|
76
|
+
if failed_case.got:
|
|
77
|
+
feedback_msg += f"but got:\n{failed_case.got}\n\n"
|
|
61
78
|
|
|
62
79
|
return score, feedback_msg
|
|
63
80
|
|
|
64
81
|
|
|
65
82
|
def compute_aggregated_grading_score(
|
|
66
|
-
aggregated_test_cases:
|
|
67
|
-
) ->
|
|
68
|
-
"""
|
|
83
|
+
aggregated_test_cases: list[aggregated_test_case],
|
|
84
|
+
) -> tuple[float, str]:
|
|
85
|
+
"""
|
|
86
|
+
Computes the score based on the number of failed and total cases.
|
|
87
|
+
|
|
69
88
|
Args:
|
|
70
89
|
aggregated_test_cases (List): Aggregated test cases for every part.
|
|
90
|
+
|
|
71
91
|
Returns:
|
|
72
92
|
Tuple[float, str]: The grade and feedback message.
|
|
93
|
+
|
|
73
94
|
"""
|
|
74
95
|
scores = []
|
|
75
96
|
msgs = []
|
|
@@ -98,11 +119,14 @@ def compute_aggregated_grading_score(
|
|
|
98
119
|
def object_to_grade(
|
|
99
120
|
origin_module: learner_submission,
|
|
100
121
|
*attr_names: str,
|
|
101
|
-
) -> Callable[[Callable[[Any],
|
|
102
|
-
"""
|
|
122
|
+
) -> Callable[[Callable[[Any], list[test_case]]], Callable[[Any], list[test_case]]]:
|
|
123
|
+
"""
|
|
124
|
+
Used as a parameterized decorator to get any number of attributes from a module.
|
|
125
|
+
|
|
103
126
|
Args:
|
|
104
127
|
origin_module (ModuleType): A module.
|
|
105
|
-
attrs_name (Tuple[str, ...]): Names of the attributes to extract from the module.
|
|
128
|
+
*attrs_name (Tuple[str, ...]): Names of the attributes to extract from the module.
|
|
129
|
+
|
|
106
130
|
"""
|
|
107
131
|
|
|
108
132
|
def middle(func):
|
|
@@ -120,11 +144,13 @@ def object_to_grade(
|
|
|
120
144
|
return middle
|
|
121
145
|
|
|
122
146
|
|
|
123
|
-
def print_feedback(test_cases:
|
|
124
|
-
"""
|
|
147
|
+
def print_feedback(test_cases: list[test_case]) -> None:
|
|
148
|
+
"""
|
|
149
|
+
Prints feedback of public unit tests within notebook.
|
|
125
150
|
|
|
126
151
|
Args:
|
|
127
152
|
test_cases (List[test_case]): List of public test cases.
|
|
153
|
+
|
|
128
154
|
"""
|
|
129
155
|
failed_cases = [t for t in test_cases if t.failed]
|
|
130
156
|
feedback_msg = "\033[92m All tests passed!"
|
|
@@ -137,16 +163,18 @@ def print_feedback(test_cases: List[test_case]) -> None:
|
|
|
137
163
|
print(feedback_msg)
|
|
138
164
|
|
|
139
165
|
|
|
140
|
-
def graded_obj_missing(test_cases:
|
|
141
|
-
"""
|
|
166
|
+
def graded_obj_missing(test_cases: list[test_case]) -> bool:
|
|
167
|
+
"""
|
|
168
|
+
Check if the object to grade was found in the learned module.
|
|
142
169
|
|
|
143
170
|
Args:
|
|
144
171
|
test_cases (List[test_case]): List of test cases.
|
|
145
172
|
|
|
146
173
|
Returns:
|
|
147
174
|
bool: True if object is missing. False otherwise.
|
|
175
|
+
|
|
148
176
|
"""
|
|
149
|
-
if len(test_cases) == 1 and test_cases[0].got
|
|
177
|
+
if len(test_cases) == 1 and test_cases[0].got is type(None):
|
|
150
178
|
return True
|
|
151
179
|
|
|
152
180
|
return False
|
dlai_grader/io.py
CHANGED
|
@@ -1,27 +1,29 @@
|
|
|
1
|
-
import os
|
|
2
1
|
import json
|
|
2
|
+
import os
|
|
3
3
|
import shutil
|
|
4
|
-
import tarfile
|
|
5
|
-
import nbformat
|
|
6
|
-
import jupytext
|
|
7
4
|
import subprocess
|
|
5
|
+
import tarfile
|
|
6
|
+
from contextlib import contextmanager, redirect_stderr, redirect_stdout
|
|
8
7
|
from os import devnull
|
|
9
8
|
from textwrap import dedent
|
|
10
9
|
from zipfile import ZipFile
|
|
10
|
+
|
|
11
|
+
import jupytext
|
|
12
|
+
import nbformat
|
|
11
13
|
from nbformat.notebooknode import NotebookNode
|
|
12
|
-
|
|
14
|
+
|
|
13
15
|
from .notebook import (
|
|
14
16
|
add_metadata_all_code_cells,
|
|
15
17
|
add_metadata_code_cells_without_pattern,
|
|
16
|
-
tag_code_cells,
|
|
17
18
|
solution_to_learner_format,
|
|
19
|
+
tag_code_cells,
|
|
18
20
|
)
|
|
19
21
|
from .templates import load_templates
|
|
20
22
|
|
|
21
23
|
|
|
22
24
|
@contextmanager
|
|
23
25
|
def suppress_stdout_stderr():
|
|
24
|
-
"""A context manager that redirects stdout and stderr to devnull"""
|
|
26
|
+
"""A context manager that redirects stdout and stderr to devnull."""
|
|
25
27
|
with open(devnull, "w") as fnull:
|
|
26
28
|
with redirect_stderr(fnull) as err, redirect_stdout(fnull) as out:
|
|
27
29
|
yield (err, out)
|
|
@@ -30,11 +32,15 @@ def suppress_stdout_stderr():
|
|
|
30
32
|
def read_notebook(
|
|
31
33
|
path: str,
|
|
32
34
|
) -> NotebookNode:
|
|
33
|
-
"""
|
|
35
|
+
"""
|
|
36
|
+
Reads a notebook found in the given path and returns a serialized version.
|
|
37
|
+
|
|
34
38
|
Args:
|
|
35
39
|
path (str): Path of the notebook file to read.
|
|
40
|
+
|
|
36
41
|
Returns:
|
|
37
42
|
NotebookNode: Representation of the notebook following nbformat convention.
|
|
43
|
+
|
|
38
44
|
"""
|
|
39
45
|
return nbformat.read(path, as_version=nbformat.NO_CONVERT)
|
|
40
46
|
|
|
@@ -42,10 +48,12 @@ def read_notebook(
|
|
|
42
48
|
def tag_notebook(
|
|
43
49
|
path: str,
|
|
44
50
|
) -> None:
|
|
45
|
-
"""
|
|
51
|
+
"""
|
|
52
|
+
Adds 'graded' tag to all code cells of a notebook.
|
|
46
53
|
|
|
47
54
|
Args:
|
|
48
55
|
path (str): Path to the notebook.
|
|
56
|
+
|
|
49
57
|
"""
|
|
50
58
|
nb = read_notebook(path)
|
|
51
59
|
nb = tag_code_cells(nb)
|
|
@@ -53,10 +61,12 @@ def tag_notebook(
|
|
|
53
61
|
|
|
54
62
|
|
|
55
63
|
def undeletable_notebook(path: str) -> None:
|
|
56
|
-
"""
|
|
64
|
+
"""
|
|
65
|
+
Makes all code cells of a notebook non-deletable.
|
|
57
66
|
|
|
58
67
|
Args:
|
|
59
68
|
path (str): Path to the notebook.
|
|
69
|
+
|
|
60
70
|
"""
|
|
61
71
|
nb = read_notebook(path)
|
|
62
72
|
nb = add_metadata_all_code_cells(nb, {"deletable": False})
|
|
@@ -64,13 +74,17 @@ def undeletable_notebook(path: str) -> None:
|
|
|
64
74
|
|
|
65
75
|
|
|
66
76
|
def uneditable_notebook(path: str) -> None:
|
|
67
|
-
"""
|
|
77
|
+
"""
|
|
78
|
+
Makes all non-graded code cells of a notebook non-editable.
|
|
68
79
|
|
|
69
80
|
Args:
|
|
70
81
|
path (str): Path to the notebook.
|
|
82
|
+
|
|
71
83
|
"""
|
|
72
84
|
nb = read_notebook(path)
|
|
73
|
-
nb = add_metadata_code_cells_without_pattern(
|
|
85
|
+
nb = add_metadata_code_cells_without_pattern(
|
|
86
|
+
nb, {"editable": False}, ignore_pattern="^# EDITABLE"
|
|
87
|
+
)
|
|
74
88
|
jupytext.write(nb, path)
|
|
75
89
|
|
|
76
90
|
|
|
@@ -79,12 +93,14 @@ def extract_tar(
|
|
|
79
93
|
destination: str,
|
|
80
94
|
post_cleanup: bool = True,
|
|
81
95
|
) -> None:
|
|
82
|
-
"""
|
|
96
|
+
"""
|
|
97
|
+
Extracts a tar file unto the desired destination.
|
|
83
98
|
|
|
84
99
|
Args:
|
|
85
100
|
file_path (str): Path to tar file.
|
|
86
101
|
destination (str): Path where to save uncompressed files.
|
|
87
102
|
post_cleanup (bool, optional): If true, deletes the compressed tar file. Defaults to True.
|
|
103
|
+
|
|
88
104
|
"""
|
|
89
105
|
with tarfile.open(file_path, "r") as my_tar:
|
|
90
106
|
my_tar.extractall(destination)
|
|
@@ -118,14 +134,16 @@ def send_feedback(
|
|
|
118
134
|
feedback_path: str = "/shared/feedback.json",
|
|
119
135
|
err: bool = False,
|
|
120
136
|
) -> None:
|
|
121
|
-
"""
|
|
137
|
+
"""
|
|
138
|
+
Sends feedback to the learner.
|
|
139
|
+
|
|
122
140
|
Args:
|
|
123
141
|
score (float): Grading score to show on Coursera for the assignment.
|
|
124
142
|
msg (str): Message providing additional feedback.
|
|
125
143
|
feedback_path (str): Path where the json feedback will be saved. Defaults to /shared/feedback.json
|
|
126
144
|
err (bool, optional): True if there was an error while grading. Defaults to False.
|
|
127
|
-
"""
|
|
128
145
|
|
|
146
|
+
"""
|
|
129
147
|
post = {"fractionalScore": score, "feedback": msg}
|
|
130
148
|
print(json.dumps(post))
|
|
131
149
|
|
|
@@ -166,7 +184,7 @@ def update_grader_version() -> str:
|
|
|
166
184
|
|
|
167
185
|
new_lines = []
|
|
168
186
|
for l in lines:
|
|
169
|
-
if ("GRADER_VERSION" in l) and (
|
|
187
|
+
if ("GRADER_VERSION" in l) and ("TAG_ID" not in l):
|
|
170
188
|
_, v = l.split("=")
|
|
171
189
|
num_v = int(v)
|
|
172
190
|
new_v = num_v + 1
|
|
@@ -227,13 +245,23 @@ def init_grader() -> None:
|
|
|
227
245
|
write_file_from_template("./Makefile", template_dict["makefile"])
|
|
228
246
|
write_file_from_template("./.conf", template_dict["conf"])
|
|
229
247
|
write_file_from_template("./entry.py", template_dict["entry_py"])
|
|
248
|
+
write_file_from_template(
|
|
249
|
+
"./copy_assignment_to_submission.sh",
|
|
250
|
+
template_dict["copy_assignment_to_submission_sh"],
|
|
251
|
+
)
|
|
230
252
|
write_file_from_template("./requirements.txt", "dlai-grader")
|
|
231
|
-
|
|
253
|
+
write_file_from_template("./.env", "")
|
|
254
|
+
|
|
232
255
|
os.makedirs("learner")
|
|
233
256
|
os.makedirs("mount")
|
|
234
|
-
os.makedirs("solution")
|
|
235
257
|
os.makedirs("submission")
|
|
236
258
|
|
|
259
|
+
if "COPY data/ /grader/data/" in template_dict["dockerfile"]:
|
|
260
|
+
os.makedirs("data")
|
|
261
|
+
|
|
262
|
+
if "COPY solution/ /grader/solution/" in template_dict["dockerfile"]:
|
|
263
|
+
os.makedirs("solution")
|
|
264
|
+
|
|
237
265
|
|
|
238
266
|
def generate_learner_version(
|
|
239
267
|
filename_source: str,
|
dlai_grader/notebook.py
CHANGED
|
@@ -72,8 +72,8 @@ def keep_tagged_cells(
|
|
|
72
72
|
filtered_cells = []
|
|
73
73
|
|
|
74
74
|
for cell in notebook["cells"]:
|
|
75
|
-
if (
|
|
76
|
-
not
|
|
75
|
+
if ("tags" not in cell["metadata"]) or (
|
|
76
|
+
tag not in cell["metadata"].get("tags")
|
|
77
77
|
):
|
|
78
78
|
continue
|
|
79
79
|
filtered_cells.append(cell)
|
|
@@ -147,7 +147,7 @@ def get_named_cells(
|
|
|
147
147
|
named_cells = {}
|
|
148
148
|
for cell in notebook["cells"]:
|
|
149
149
|
metadata = cell["metadata"]
|
|
150
|
-
if
|
|
150
|
+
if "name" not in metadata:
|
|
151
151
|
continue
|
|
152
152
|
named_cells.update({metadata.get("name"): cell})
|
|
153
153
|
return named_cells
|
|
@@ -169,12 +169,12 @@ def tag_code_cells(
|
|
|
169
169
|
|
|
170
170
|
for cell in notebook["cells"]:
|
|
171
171
|
if cell["cell_type"] == "code":
|
|
172
|
-
if
|
|
172
|
+
if "tags" not in cell["metadata"]:
|
|
173
173
|
cell["metadata"]["tags"] = []
|
|
174
174
|
|
|
175
175
|
tags = cell["metadata"]["tags"]
|
|
176
176
|
|
|
177
|
-
if not
|
|
177
|
+
if tag not in tags:
|
|
178
178
|
tags.append(tag)
|
|
179
179
|
cell["metadata"]["tags"] = tags
|
|
180
180
|
|
|
@@ -240,17 +240,17 @@ def add_metadata_code_cells_with_pattern(
|
|
|
240
240
|
def add_metadata_code_cells_without_pattern(
|
|
241
241
|
notebook: NotebookNode,
|
|
242
242
|
metadata: dict,
|
|
243
|
-
match_pattern: str = "^# GRADED
|
|
244
|
-
ignore_pattern: str = None
|
|
243
|
+
match_pattern: str = "^# GRADED",
|
|
244
|
+
ignore_pattern: str = None,
|
|
245
245
|
) -> NotebookNode:
|
|
246
246
|
"""Adds metadata to code cells of a notebook that don't match a regexp pattern and aren't ignored by another pattern.
|
|
247
|
-
|
|
247
|
+
|
|
248
248
|
Args:
|
|
249
249
|
notebook (NotebookNode): Notebook to filter.
|
|
250
250
|
metadata (dict): The metadata which should be a key-value pair.
|
|
251
251
|
match_pattern (str, optional): Pattern to check which cells to add metadata to. Defaults to "^# GRADED ".
|
|
252
252
|
ignore_pattern (str, optional): Pattern to check which cells to ignore. If a cell matches this pattern, it will be skipped.
|
|
253
|
-
|
|
253
|
+
|
|
254
254
|
Returns:
|
|
255
255
|
NotebookNode: The notebook with the new metadata.
|
|
256
256
|
"""
|
|
@@ -258,7 +258,7 @@ def add_metadata_code_cells_without_pattern(
|
|
|
258
258
|
if cell["cell_type"] == "code":
|
|
259
259
|
if ignore_pattern and re.search(ignore_pattern, cell["source"]):
|
|
260
260
|
continue
|
|
261
|
-
|
|
261
|
+
|
|
262
262
|
if not re.search(match_pattern, cell["source"]):
|
|
263
263
|
current_metadata = cell.get("metadata", {})
|
|
264
264
|
current_metadata.update(metadata)
|
dlai_grader/templates.py
CHANGED
|
@@ -1,77 +1,339 @@
|
|
|
1
|
+
import sys
|
|
1
2
|
from textwrap import dedent
|
|
2
|
-
from typing import Dict
|
|
3
3
|
|
|
4
4
|
|
|
5
|
-
def
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
module = None
|
|
13
|
-
elif week_or_module == "2":
|
|
14
|
-
module = input("Number of the module: ")
|
|
15
|
-
week = None
|
|
16
|
-
else:
|
|
17
|
-
print("invalid option selected")
|
|
18
|
-
exit(1)
|
|
5
|
+
def generate_copy_assignment_script(
|
|
6
|
+
extra_file_required="n",
|
|
7
|
+
assignment_name="C1M2_Assignment.ipynb",
|
|
8
|
+
extra_file_name="foo.txt",
|
|
9
|
+
):
|
|
10
|
+
"""
|
|
11
|
+
Generate copy_assignment_to_submission.sh script with optional extra file copying.
|
|
19
12
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
13
|
+
Args:
|
|
14
|
+
extra_file_required (str): Include extra file copying if "y"
|
|
15
|
+
assignment_name (str): The name of the assignment notebook file
|
|
16
|
+
extra_file_name (str): The name of the extra file to copy (if required)
|
|
17
|
+
|
|
18
|
+
Returns:
|
|
19
|
+
str: The complete script content
|
|
20
|
+
|
|
21
|
+
"""
|
|
22
|
+
# Common script header and initial variables
|
|
23
|
+
header = [
|
|
24
|
+
"#!/bin/bash",
|
|
25
|
+
"set -euo pipefail",
|
|
26
|
+
"",
|
|
27
|
+
"# each grader should modify Assignment and Submission file to fulfill the grader setting",
|
|
28
|
+
f"Assignment={assignment_name}",
|
|
29
|
+
]
|
|
30
|
+
|
|
31
|
+
# Add extra file declaration if required
|
|
32
|
+
if extra_file_required == "y":
|
|
33
|
+
header.append(f"Extra_file={extra_file_name}")
|
|
34
|
+
|
|
35
|
+
# Common script variables
|
|
36
|
+
variables = [
|
|
37
|
+
"",
|
|
38
|
+
"SubmissionFile=submission.ipynb",
|
|
39
|
+
"SubmissionPath=/shared/submission",
|
|
40
|
+
"SharedDiskPath=/learner_workplace/$UserId/$CourseId/$LessonId",
|
|
41
|
+
"",
|
|
42
|
+
"# copy synced files (exam image typically sync all files in lesson folder)",
|
|
43
|
+
'echo "Copy learner submission from $SharedDiskPath/$Assignment to $SubmissionPath/$SubmissionFile"',
|
|
44
|
+
"cp $SharedDiskPath/$Assignment $SubmissionPath/$SubmissionFile",
|
|
45
|
+
]
|
|
46
|
+
|
|
47
|
+
# Add extra file copying if required
|
|
48
|
+
extra_file_copy = []
|
|
49
|
+
if extra_file_required == "y":
|
|
50
|
+
extra_file_copy = [
|
|
51
|
+
'echo "Copy learner submission from $SharedDiskPath/$Extra_file to $SubmissionPath/$Extra_file"',
|
|
52
|
+
"cp $SharedDiskPath/$Extra_file $SubmissionPath/$Extra_file",
|
|
53
|
+
]
|
|
54
|
+
|
|
55
|
+
# Combine all sections
|
|
56
|
+
content = header + variables + extra_file_copy
|
|
57
|
+
|
|
58
|
+
return "\n".join(content)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def generate_entry_py(
|
|
62
|
+
non_notebook_grading="n",
|
|
63
|
+
extra_file_name="foo.txt",
|
|
64
|
+
):
|
|
65
|
+
"""
|
|
66
|
+
Generate entry.py with optional non-notebook grading capability.
|
|
24
67
|
|
|
25
|
-
|
|
26
|
-
|
|
68
|
+
Args:
|
|
69
|
+
non_notebook_grading (str): Include non-notebook grading if "y"
|
|
70
|
+
extra_file_name (str): Name of extra file to grade
|
|
27
71
|
|
|
28
|
-
|
|
72
|
+
Returns:
|
|
73
|
+
str: The complete entry.py content
|
|
29
74
|
|
|
30
|
-
|
|
75
|
+
"""
|
|
76
|
+
# Common imports for both versions
|
|
77
|
+
imports = [
|
|
78
|
+
"from dlai_grader.config import Config, get_part_id",
|
|
79
|
+
"from dlai_grader.compiler import compile_partial_module",
|
|
80
|
+
"from dlai_grader.io import read_notebook, copy_submission_to_workdir, send_feedback",
|
|
81
|
+
"from dlai_grader.notebook import keep_tagged_cells",
|
|
82
|
+
"from dlai_grader.grading import (",
|
|
83
|
+
" compute_grading_score,",
|
|
84
|
+
" graded_obj_missing,",
|
|
85
|
+
" LearnerSubmission,",
|
|
86
|
+
")",
|
|
87
|
+
"from grader import handle_part_id",
|
|
88
|
+
"",
|
|
89
|
+
]
|
|
90
|
+
|
|
91
|
+
# Notebook grading function (common to both versions)
|
|
92
|
+
notebook_grading_func = [
|
|
93
|
+
"",
|
|
94
|
+
"def notebook_grading(config, compile_solution=False):",
|
|
95
|
+
" try:",
|
|
96
|
+
" nb = read_notebook(config.submission_file_path)",
|
|
97
|
+
" except Exception as e:",
|
|
98
|
+
' msg = f"There was a problem reading your notebook. Details:\\n{e!s}"',
|
|
99
|
+
" send_feedback(0.0, msg, err=True)",
|
|
100
|
+
"",
|
|
101
|
+
" transformations = [keep_tagged_cells()]",
|
|
102
|
+
" for t in transformations:",
|
|
103
|
+
" nb = t(nb)",
|
|
104
|
+
"",
|
|
105
|
+
" try:",
|
|
106
|
+
' learner_mod = compile_partial_module(nb, "learner_mod", verbose=False)',
|
|
107
|
+
" except Exception as e:",
|
|
108
|
+
' msg = f"There was a problem compiling the code from your notebook, please check that you saved before submitting. Details:\\n{e!s}"',
|
|
109
|
+
" send_feedback(0.0, msg, err=True)",
|
|
110
|
+
"",
|
|
111
|
+
" solution_mod = None",
|
|
112
|
+
" if compile_solution:",
|
|
113
|
+
" solution_nb = read_notebook(config.solution_file_path)",
|
|
114
|
+
" for t in transformations:",
|
|
115
|
+
" solution_nb = t(solution_nb)",
|
|
116
|
+
" solution_mod = compile_partial_module(",
|
|
117
|
+
' solution_nb, "solution_mod", verbose=False',
|
|
118
|
+
" )",
|
|
119
|
+
"",
|
|
120
|
+
" return learner_mod, solution_mod",
|
|
121
|
+
"",
|
|
122
|
+
]
|
|
123
|
+
|
|
124
|
+
# Non-notebook grading function (only for version with non_notebook_grading)
|
|
125
|
+
non_notebook_grading_func = [
|
|
126
|
+
"",
|
|
127
|
+
"def non_notebook_grading(config):",
|
|
128
|
+
" try:",
|
|
129
|
+
' with open(config.submission_file_path, "r") as file:',
|
|
130
|
+
" contents = file.read()",
|
|
131
|
+
" except Exception as e:",
|
|
132
|
+
' msg = f"There was an error reading your submission. Details:\\n{e!s}"',
|
|
133
|
+
" send_feedback(0.0, msg, err=True)",
|
|
134
|
+
"",
|
|
135
|
+
" return LearnerSubmission(submission=contents)",
|
|
136
|
+
"",
|
|
137
|
+
]
|
|
138
|
+
|
|
139
|
+
# Main function for version without non-notebook grading
|
|
140
|
+
main_func_simple = [
|
|
141
|
+
"def main() -> None:",
|
|
142
|
+
" copy_submission_to_workdir()",
|
|
143
|
+
"",
|
|
144
|
+
" part_id = get_part_id()",
|
|
145
|
+
"",
|
|
146
|
+
" c = Config()",
|
|
147
|
+
"",
|
|
148
|
+
" learner_mod, _ = notebook_grading(c)",
|
|
149
|
+
"",
|
|
150
|
+
" g_func = handle_part_id(part_id)(learner_mod)",
|
|
151
|
+
"",
|
|
152
|
+
" try:",
|
|
153
|
+
" cases = g_func()",
|
|
154
|
+
" except Exception as e:",
|
|
155
|
+
' msg = f"There was an error grading your submission. Details:\\n{e!s}"',
|
|
156
|
+
" send_feedback(0.0, msg, err=True)",
|
|
157
|
+
"",
|
|
158
|
+
" if graded_obj_missing(cases):",
|
|
159
|
+
' msg = "Object required for grading not found. If you haven\'t completed the exercise this might be expected. Otherwise, check your solution as grader omits cells that throw errors."',
|
|
160
|
+
" send_feedback(0.0, msg, err=True)",
|
|
161
|
+
"",
|
|
162
|
+
" score, feedback = compute_grading_score(cases)",
|
|
163
|
+
" send_feedback(score, feedback)",
|
|
164
|
+
"",
|
|
165
|
+
]
|
|
166
|
+
|
|
167
|
+
# Main function for version with non-notebook grading
|
|
168
|
+
main_func_with_non_notebook = [
|
|
169
|
+
"def main() -> None:",
|
|
170
|
+
" copy_submission_to_workdir()",
|
|
171
|
+
"",
|
|
172
|
+
" part_id = get_part_id()",
|
|
173
|
+
"",
|
|
174
|
+
" match part_id:",
|
|
175
|
+
' case "123":',
|
|
176
|
+
f' c = Config(submission_file="{extra_file_name}")',
|
|
177
|
+
" learner_mod = non_notebook_grading(c)",
|
|
178
|
+
" case _:",
|
|
179
|
+
" c = Config()",
|
|
180
|
+
" learner_mod, _ = notebook_grading(c)",
|
|
181
|
+
"",
|
|
182
|
+
" g_func = handle_part_id(part_id)(learner_mod)",
|
|
183
|
+
"",
|
|
184
|
+
" try:",
|
|
185
|
+
" cases = g_func()",
|
|
186
|
+
" except Exception as e:",
|
|
187
|
+
' msg = f"There was an error grading your submission. Details:\\n{e!s}"',
|
|
188
|
+
" send_feedback(0.0, msg, err=True)",
|
|
189
|
+
"",
|
|
190
|
+
" if graded_obj_missing(cases):",
|
|
191
|
+
' msg = "Object required for grading not found. If you haven\'t completed the exercise this might be expected. Otherwise, check your solution as grader omits cells that throw errors."',
|
|
192
|
+
" send_feedback(0.0, msg, err=True)",
|
|
193
|
+
"",
|
|
194
|
+
" score, feedback = compute_grading_score(cases)",
|
|
195
|
+
" send_feedback(score, feedback)",
|
|
196
|
+
"",
|
|
197
|
+
]
|
|
198
|
+
|
|
199
|
+
# Common script entry point
|
|
200
|
+
entry_point = [
|
|
201
|
+
'if __name__ == "__main__":',
|
|
202
|
+
" main()",
|
|
203
|
+
"",
|
|
204
|
+
]
|
|
205
|
+
|
|
206
|
+
# Combine all sections based on configuration
|
|
207
|
+
content = imports + notebook_grading_func
|
|
208
|
+
|
|
209
|
+
if non_notebook_grading == "y":
|
|
210
|
+
content.extend(non_notebook_grading_func)
|
|
211
|
+
content.extend(main_func_with_non_notebook)
|
|
212
|
+
else:
|
|
213
|
+
content.extend(main_func_simple)
|
|
31
214
|
|
|
32
|
-
|
|
33
|
-
rm requirements.txt
|
|
215
|
+
content.extend(entry_point)
|
|
34
216
|
|
|
35
|
-
|
|
36
|
-
mkdir /grader/submission
|
|
217
|
+
return "\n".join(content)
|
|
37
218
|
|
|
38
|
-
COPY .conf /grader/.conf
|
|
39
|
-
COPY data/ /grader/data/
|
|
40
|
-
COPY solution/ /grader/solution/
|
|
41
|
-
COPY entry.py /grader/entry.py
|
|
42
|
-
COPY grader.py /grader/grader.py
|
|
43
219
|
|
|
44
|
-
|
|
220
|
+
def generate_dockerfile(data_dir_required="n", sol_dir_required="n"):
|
|
221
|
+
"""
|
|
222
|
+
Generate a Dockerfile with optional data and solution directories.
|
|
45
223
|
|
|
46
|
-
|
|
224
|
+
Args:
|
|
225
|
+
data_dir_required (str): Include data directory if "y"
|
|
226
|
+
sol_dir_required (str): Include solution directory if "y"
|
|
227
|
+
|
|
228
|
+
Returns:
|
|
229
|
+
str: The complete Dockerfile content
|
|
47
230
|
|
|
48
|
-
ENTRYPOINT ["python", "entry.py"]
|
|
49
231
|
"""
|
|
232
|
+
base_content = [
|
|
233
|
+
"FROM continuumio/miniconda3@sha256:d601a04ea48fd45e60808c7072243d33703d29434d2067816b7f26b0705d889a",
|
|
234
|
+
"",
|
|
235
|
+
"RUN apk update && apk add libstdc++",
|
|
236
|
+
"",
|
|
237
|
+
"COPY requirements.txt .",
|
|
238
|
+
"",
|
|
239
|
+
"RUN pip install -r requirements.txt && rm requirements.txt",
|
|
240
|
+
"",
|
|
241
|
+
"RUN mkdir /grader && \\ \nmkdir /grader/submission",
|
|
242
|
+
"",
|
|
243
|
+
"COPY .conf /grader/.conf",
|
|
244
|
+
]
|
|
245
|
+
|
|
246
|
+
# Add optional file copies based on config
|
|
247
|
+
if data_dir_required == "y":
|
|
248
|
+
base_content.append("COPY data/ /grader/data/")
|
|
249
|
+
|
|
250
|
+
if sol_dir_required == "y":
|
|
251
|
+
base_content.append("COPY solution/ /grader/solution/")
|
|
252
|
+
|
|
253
|
+
# Add final common parts
|
|
254
|
+
base_content.extend(
|
|
255
|
+
[
|
|
256
|
+
"COPY entry.py /grader/entry.py",
|
|
257
|
+
"COPY grader.py /grader/grader.py",
|
|
258
|
+
"",
|
|
259
|
+
"RUN chmod a+rwx /grader/",
|
|
260
|
+
"",
|
|
261
|
+
"WORKDIR /grader/",
|
|
262
|
+
"",
|
|
263
|
+
'ENTRYPOINT ["python", "entry.py"]',
|
|
264
|
+
]
|
|
265
|
+
)
|
|
50
266
|
|
|
51
|
-
|
|
52
|
-
W_OR_M = "W"
|
|
53
|
-
W_OR_M_num = week
|
|
267
|
+
return "\n".join(base_content)
|
|
54
268
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
269
|
+
|
|
270
|
+
def load_templates() -> dict[str, str]:
|
|
271
|
+
specialization = input("Name of the specialization: ")
|
|
272
|
+
course = input("Number of the course: ")
|
|
273
|
+
module = input("Number of the module: ")
|
|
274
|
+
|
|
275
|
+
unit_test_filename = input("Filename for unit tests (leave empty for unittests): ")
|
|
276
|
+
unit_test_filename = unit_test_filename if unit_test_filename else "unittests"
|
|
277
|
+
version = input("Version of the grader (leave empty for version 1): ")
|
|
278
|
+
version = version if version else "1"
|
|
279
|
+
data_dir_required = input("Do you require a data dir? y/n (leave empty for n): ")
|
|
280
|
+
data_dir_required = data_dir_required if data_dir_required else "n"
|
|
281
|
+
|
|
282
|
+
if data_dir_required not in ["y", "n"]:
|
|
283
|
+
print("invalid option selected")
|
|
284
|
+
sys.exit(1)
|
|
285
|
+
|
|
286
|
+
sol_dir_required = input(
|
|
287
|
+
"Do you require a solution file? y/n (leave empty for n): "
|
|
288
|
+
)
|
|
289
|
+
sol_dir_required = sol_dir_required if sol_dir_required else "n"
|
|
290
|
+
if sol_dir_required not in ["y", "n"]:
|
|
291
|
+
print("invalid option selected")
|
|
292
|
+
sys.exit(1)
|
|
293
|
+
|
|
294
|
+
non_notebook_grading = input(
|
|
295
|
+
"Will you grade a file different from a notebook? y/n (leave empty for n): ",
|
|
296
|
+
)
|
|
297
|
+
non_notebook_grading = non_notebook_grading if non_notebook_grading else "n"
|
|
298
|
+
if non_notebook_grading not in ["y", "n"]:
|
|
299
|
+
print("invalid option selected")
|
|
300
|
+
sys.exit(1)
|
|
301
|
+
|
|
302
|
+
extra_file_name = ""
|
|
303
|
+
if non_notebook_grading == "y":
|
|
304
|
+
extra_file_name = input(
|
|
305
|
+
"Name of the extra file to grade: ",
|
|
306
|
+
)
|
|
307
|
+
|
|
308
|
+
dockerfile = generate_dockerfile(
|
|
309
|
+
data_dir_required=data_dir_required,
|
|
310
|
+
sol_dir_required=sol_dir_required,
|
|
311
|
+
)
|
|
58
312
|
|
|
59
313
|
conf = f"""
|
|
60
|
-
ASSIGNMENT_NAME=C{course}{
|
|
314
|
+
ASSIGNMENT_NAME=C{course}M{module}_Assignment
|
|
61
315
|
UNIT_TESTS_NAME={unit_test_filename}
|
|
62
|
-
IMAGE_NAME={specialization}c{course}{
|
|
316
|
+
IMAGE_NAME={specialization}c{course}m{module}-grader
|
|
63
317
|
GRADER_VERSION={version}
|
|
64
318
|
TAG_ID=V$(GRADER_VERSION)
|
|
65
319
|
SUB_DIR=mount
|
|
66
320
|
MEMORY_LIMIT=4096
|
|
67
321
|
"""
|
|
68
322
|
|
|
323
|
+
assignment_name = f"C{course}M{module}_Assignment.ipynb"
|
|
324
|
+
|
|
325
|
+
copy_assignment_to_submission_sh = generate_copy_assignment_script(
|
|
326
|
+
extra_file_required=non_notebook_grading,
|
|
327
|
+
assignment_name=assignment_name,
|
|
328
|
+
extra_file_name=extra_file_name,
|
|
329
|
+
)
|
|
330
|
+
|
|
69
331
|
makefile = """
|
|
70
|
-
.PHONY: learner build
|
|
332
|
+
.PHONY: sync learner build debug-unsafe debug-safe grade versioning tag undeletable uneditable init upgrade coursera zip
|
|
71
333
|
|
|
72
334
|
include .conf
|
|
73
335
|
|
|
74
|
-
PARTIDS =
|
|
336
|
+
PARTIDS = 123 456
|
|
75
337
|
OS := $(shell uname)
|
|
76
338
|
|
|
77
339
|
sync:
|
|
@@ -81,15 +343,19 @@ def load_templates() -> Dict[str, str]:
|
|
|
81
343
|
|
|
82
344
|
learner:
|
|
83
345
|
dlai_grader --learner --output_notebook=./learner/$(ASSIGNMENT_NAME).ipynb
|
|
346
|
+
rsync -a --exclude="submission.ipynb" --exclude="__pycache__" --exclude=".mypy_cache" ./mount/ ./learner/
|
|
84
347
|
|
|
85
348
|
build:
|
|
86
349
|
docker build -t $(IMAGE_NAME):$(TAG_ID) .
|
|
87
350
|
|
|
88
|
-
debug:
|
|
89
|
-
docker run -it --rm --mount type=bind,source=$(PWD)/mount,target=/shared/submission --mount type=bind,source=$(PWD),target=/grader/ --entrypoint ash $(IMAGE_NAME):$(TAG_ID)
|
|
351
|
+
debug-unsafe:
|
|
352
|
+
docker run -it --rm --mount type=bind,source=$(PWD)/mount,target=/shared/submission --mount type=bind,source=$(PWD),target=/grader/ --env-file $(PWD)/.env --entrypoint ash $(IMAGE_NAME):$(TAG_ID)
|
|
353
|
+
|
|
354
|
+
debug-safe:
|
|
355
|
+
docker run -it --rm --mount type=bind,source=$(PWD)/mount,target=/shared/submission --env-file $(PWD)/.env --entrypoint ash $(IMAGE_NAME):$(TAG_ID)
|
|
90
356
|
|
|
91
|
-
|
|
92
|
-
|
|
357
|
+
grade:
|
|
358
|
+
docker run -it --rm --mount type=bind,source=$(PWD)/mount,target=/shared/submission --env-file $(PWD)/.env --entrypoint ash $(IMAGE_NAME):$(TAG_ID) -c 'for partId in $(PARTIDS); do export partId=$$partId; echo "Processing part $$partId"; python entry.py; done'
|
|
93
359
|
|
|
94
360
|
versioning:
|
|
95
361
|
dlai_grader --versioning
|
|
@@ -103,160 +369,70 @@ def load_templates() -> Dict[str, str]:
|
|
|
103
369
|
uneditable:
|
|
104
370
|
dlai_grader --uneditable
|
|
105
371
|
|
|
372
|
+
init:
|
|
373
|
+
dlai_grader --versioning
|
|
374
|
+
dlai_grader --tag
|
|
375
|
+
dlai_grader --undeletable
|
|
376
|
+
dlai_grader --uneditable
|
|
377
|
+
|
|
106
378
|
upgrade:
|
|
107
379
|
dlai_grader --upgrade
|
|
108
380
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
grade:
|
|
113
|
-
dlai_grader --grade --partids=$(PARTIDS) --docker=$(IMAGE_NAME):$(TAG_ID) --memory=$(MEMORY_LIMIT) --submission=$(SUB_DIR)
|
|
114
|
-
|
|
115
|
-
mem:
|
|
116
|
-
memthis $(PARTIDS)
|
|
381
|
+
coursera:
|
|
382
|
+
dlai_grader --grade --partids="$(PARTIDS)" --docker=$(IMAGE_NAME):$(TAG_ID) --memory=$(MEMORY_LIMIT) --submission=$(SUB_DIR)
|
|
117
383
|
|
|
118
384
|
zip:
|
|
119
385
|
zip -r $(IMAGE_NAME)$(TAG_ID).zip .
|
|
120
386
|
|
|
121
|
-
clean:
|
|
122
|
-
find . -maxdepth 1 -type f -name "*.zip" -exec rm {} +
|
|
123
|
-
docker rm $$(docker ps -qa --no-trunc --filter "status=exited")
|
|
124
|
-
docker rmi $$(docker images --filter "dangling=true" -q --no-trunc)
|
|
125
|
-
|
|
126
|
-
upload:
|
|
127
|
-
coursera_autograder --timeout 1800 upload --grader-memory-limit $(MEMORY_LIMIT) --grading-timeout 1800 $(IMAGE_NAME)$(TAG_ID).zip $(COURSE_ID) $(ITEM_ID) $(PART_ID)
|
|
128
|
-
|
|
129
387
|
"""
|
|
130
388
|
|
|
131
389
|
grader_py = """
|
|
132
390
|
from types import ModuleType, FunctionType
|
|
133
|
-
from typing import Dict, List, Optional
|
|
134
391
|
from dlai_grader.grading import test_case, object_to_grade
|
|
135
392
|
from dlai_grader.types import grading_function, grading_wrapper, learner_submission
|
|
136
393
|
|
|
137
394
|
|
|
138
395
|
def part_1(
|
|
139
|
-
learner_mod: learner_submission,
|
|
396
|
+
learner_mod: learner_submission,
|
|
397
|
+
solution_mod: ModuleType | None = None,
|
|
140
398
|
) -> grading_function:
|
|
141
399
|
@object_to_grade(learner_mod, "learner_func")
|
|
142
|
-
def g(learner_func: FunctionType) ->
|
|
400
|
+
def g(learner_func: FunctionType) -> list[test_case]:
|
|
401
|
+
|
|
402
|
+
cases: list[test_case] = []
|
|
143
403
|
|
|
144
404
|
t = test_case()
|
|
145
405
|
if not isinstance(learner_func, FunctionType):
|
|
146
|
-
t.
|
|
406
|
+
t.fail()
|
|
147
407
|
t.msg = "learner_func has incorrect type"
|
|
148
408
|
t.want = FunctionType
|
|
149
409
|
t.got = type(learner_func)
|
|
150
410
|
return [t]
|
|
151
411
|
|
|
152
|
-
cases: List[test_case] = []
|
|
153
|
-
|
|
154
412
|
return cases
|
|
155
413
|
|
|
156
414
|
return g
|
|
157
415
|
|
|
158
416
|
|
|
159
417
|
def handle_part_id(part_id: str) -> grading_wrapper:
|
|
160
|
-
grader_dict:
|
|
418
|
+
grader_dict: dict[str, grading_wrapper] = {
|
|
161
419
|
"": part_1,
|
|
162
420
|
}
|
|
163
421
|
return grader_dict[part_id]
|
|
164
422
|
"""
|
|
165
423
|
|
|
166
|
-
entry_py =
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
from dlai_grader.io import read_notebook, copy_submission_to_workdir, send_feedback
|
|
170
|
-
|
|
171
|
-
from dlai_grader.notebook import (
|
|
172
|
-
notebook_to_script,
|
|
173
|
-
keep_tagged_cells,
|
|
174
|
-
notebook_is_up_to_date,
|
|
175
|
-
notebook_version,
|
|
176
|
-
cut_notebook,
|
|
177
|
-
partial_grading_enabled,
|
|
424
|
+
entry_py = generate_entry_py(
|
|
425
|
+
non_notebook_grading=non_notebook_grading,
|
|
426
|
+
extra_file_name=extra_file_name,
|
|
178
427
|
)
|
|
179
|
-
from dlai_grader.grading import compute_grading_score, graded_obj_missing
|
|
180
|
-
from grader import handle_part_id
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
def main() -> None:
|
|
184
|
-
c = Config()
|
|
185
|
-
|
|
186
|
-
copy_submission_to_workdir()
|
|
187
|
-
|
|
188
|
-
try:
|
|
189
|
-
nb = read_notebook(c.submission_file_path)
|
|
190
|
-
except Exception as e:
|
|
191
|
-
send_feedback(
|
|
192
|
-
0.0,
|
|
193
|
-
f"There was a problem reading your notebook. Details:\\n{str(e)}",
|
|
194
|
-
err=True,
|
|
195
|
-
)
|
|
196
|
-
|
|
197
|
-
if not notebook_is_up_to_date(nb):
|
|
198
|
-
msg = f"You are submitting a version of the assignment that is behind the latest version.\\nThe latest version is {c.latest_version} and you are on version {notebook_version(nb)}."
|
|
199
|
-
|
|
200
|
-
send_feedback(0.0, msg)
|
|
201
|
-
|
|
202
|
-
transformations = [cut_notebook(), keep_tagged_cells()]
|
|
203
|
-
|
|
204
|
-
for t in transformations:
|
|
205
|
-
nb = t(nb)
|
|
206
|
-
|
|
207
|
-
try:
|
|
208
|
-
learner_mod = compile_partial_module(nb, "learner_mod", verbose=False)
|
|
209
|
-
except Exception as e:
|
|
210
|
-
send_feedback(
|
|
211
|
-
0.0,
|
|
212
|
-
f"There was a problem compiling the code from your notebook, please check that you saved before submitting. Details:\\n{str(e)}",
|
|
213
|
-
err=True,
|
|
214
|
-
)
|
|
215
|
-
|
|
216
|
-
solution_nb = read_notebook(c.solution_file_path)
|
|
217
|
-
|
|
218
|
-
for t in transformations:
|
|
219
|
-
solution_nb = t(solution_nb)
|
|
220
|
-
|
|
221
|
-
solution_mod = compile_partial_module(solution_nb, "solution_mod", verbose=False)
|
|
222
|
-
|
|
223
|
-
g_func = handle_part_id(c.part_id)(learner_mod)
|
|
224
|
-
|
|
225
|
-
try:
|
|
226
|
-
cases = g_func()
|
|
227
|
-
except Exception as e:
|
|
228
|
-
send_feedback(
|
|
229
|
-
0.0,
|
|
230
|
-
f"There was an error grading your submission. Details:\\n{str(e)}",
|
|
231
|
-
err=True,
|
|
232
|
-
)
|
|
233
|
-
|
|
234
|
-
if graded_obj_missing(cases):
|
|
235
|
-
additional_msg = ""
|
|
236
|
-
if partial_grading_enabled(nb):
|
|
237
|
-
additional_msg = "The # grade-up-to-here comment in the notebook might be causing the problem."
|
|
238
|
-
|
|
239
|
-
send_feedback(
|
|
240
|
-
0.0,
|
|
241
|
-
f"Object required for grading not found. If you haven't completed the exercise this is expected. Otherwise, check your solution as grader omits cells that throw errors.\\n{additional_msg}",
|
|
242
|
-
err=True,
|
|
243
|
-
)
|
|
244
|
-
|
|
245
|
-
score, feedback = compute_grading_score(cases)
|
|
246
|
-
|
|
247
|
-
send_feedback(score, feedback)
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
if __name__ == "__main__":
|
|
251
|
-
main()
|
|
252
|
-
"""
|
|
253
428
|
|
|
254
429
|
template_dict = {
|
|
255
|
-
"dockerfile": dedent(dockerfile
|
|
430
|
+
"dockerfile": dedent(dockerfile),
|
|
256
431
|
"makefile": dedent(makefile[1:]),
|
|
257
432
|
"conf": dedent(conf[1:]),
|
|
258
433
|
"grader_py": dedent(grader_py[1:]),
|
|
259
|
-
"entry_py": dedent(entry_py
|
|
434
|
+
"entry_py": dedent(entry_py),
|
|
435
|
+
"copy_assignment_to_submission_sh": dedent(copy_assignment_to_submission_sh),
|
|
260
436
|
}
|
|
261
437
|
|
|
262
438
|
return template_dict
|
dlai_grader/types.py
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
+
from collections.abc import Callable
|
|
1
2
|
from types import ModuleType
|
|
2
|
-
from typing import Any, Callable, List, Optional, Union
|
|
3
|
-
from .grading import test_case, LearnerSubmission
|
|
4
3
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
4
|
+
from .grading import LearnerSubmission, test_case
|
|
5
|
+
|
|
6
|
+
grading_function = Callable[..., list[test_case]]
|
|
7
|
+
learner_submission = ModuleType | LearnerSubmission
|
|
8
|
+
grading_wrapper = Callable[[learner_submission, ModuleType | None], grading_function]
|
|
@@ -1,18 +1,23 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: dlai-grader
|
|
3
|
-
Version:
|
|
3
|
+
Version: 2.0b1
|
|
4
4
|
Summary: Grading utilities for DLAI courses
|
|
5
5
|
Home-page: https://github.com/https-deeplearning-ai/grader
|
|
6
6
|
Author: Andres Zarta
|
|
7
|
-
Author-email: andrezb5@gmail.com
|
|
7
|
+
Author-email: Andres Zarta <andrezb5@gmail.com>
|
|
8
8
|
License: MIT
|
|
9
|
+
Project-URL: Homepage, https://github.com/https-deeplearning-ai/grader
|
|
9
10
|
Classifier: Programming Language :: Python :: 3
|
|
10
11
|
Classifier: License :: OSI Approved :: MIT License
|
|
11
12
|
Classifier: Operating System :: OS Independent
|
|
13
|
+
Requires-Python: >=3.10
|
|
12
14
|
Description-Content-Type: text/markdown
|
|
13
15
|
License-File: LICENSE
|
|
14
|
-
Requires-Dist: nbformat
|
|
15
|
-
Requires-Dist: jupytext
|
|
16
|
+
Requires-Dist: nbformat>=5.1.3
|
|
17
|
+
Requires-Dist: jupytext>=1.13.0
|
|
18
|
+
Dynamic: author
|
|
19
|
+
Dynamic: home-page
|
|
20
|
+
Dynamic: license-file
|
|
16
21
|
|
|
17
22
|
# grader
|
|
18
23
|
Automatic grading for DLAI courses. Designed to be compatible with Coursera's grading requirements.
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
dlai_grader/__init__.py,sha256=V0DW6eFVqfyWdxFvJavFxD2QV63rZCpT400IzY1QBkk,210
|
|
2
|
+
dlai_grader/cli.py,sha256=NIwboE-AFn1LXOFmF4O70Ow0fkRxgclG_eMwmWiua38,4917
|
|
3
|
+
dlai_grader/compiler.py,sha256=elbHNUCqBCoOOoNmMRXbgeNL0nt0RM57eZi0-6AqycA,3036
|
|
4
|
+
dlai_grader/config.py,sha256=DokK1tVF_r7v0p9tWpBN-7lOAlPmHSpFXDZiI8cGw7s,1821
|
|
5
|
+
dlai_grader/grading.py,sha256=BMIoZ_loQDuNCEk1Dj3on4IWz-FGgIbnhbDyk8HmQ7c,5041
|
|
6
|
+
dlai_grader/io.py,sha256=TEp5R8014EL9e-_RID5qp9INOujAGi7KLuy-SWWsPkM,8654
|
|
7
|
+
dlai_grader/notebook.py,sha256=MgxZFuetTXwwZ-HXSB5ItLVD_9LP45E0xHAngS0g4EU,12101
|
|
8
|
+
dlai_grader/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
9
|
+
dlai_grader/templates.py,sha256=SEMv4KFlq_2jy4UORMzHStOOex9P6LXr48Ie-r1JZcc,14627
|
|
10
|
+
dlai_grader/types.py,sha256=5uiFaF3aDn-vjxTp9ec-ND-PRqeeV2_NfPHS2ngGsRo,306
|
|
11
|
+
dlai_grader-2.0b1.dist-info/licenses/LICENSE,sha256=a_kch_UqdJPtyxk35QJr9O84K_koPixqWPYW9On4-io,1072
|
|
12
|
+
dlai_grader-2.0b1.dist-info/METADATA,sha256=0-7wdm6Otf4nHWASbjMHMBt8ruXvl3RrDakgAVPCMlA,8776
|
|
13
|
+
dlai_grader-2.0b1.dist-info/WHEEL,sha256=lTU6B6eIfYoiQJTZNc-fyaR6BpL6ehTzU3xGYxn2n8k,91
|
|
14
|
+
dlai_grader-2.0b1.dist-info/entry_points.txt,sha256=4OcSAUIluONXa3ymViQ7CBQ2Lk52nb6xZnfph1rlMnk,71
|
|
15
|
+
dlai_grader-2.0b1.dist-info/top_level.txt,sha256=4YKtA3ztisFtx_g4hsGivy3J2NHnXxFziIMqawC8HWg,12
|
|
16
|
+
dlai_grader-2.0b1.dist-info/RECORD,,
|
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
dlai_grader/__init__.py,sha256=NJssrhk1f1ZuaTeZOoOhn2cgxdwJV9v6bXD-7WdoS7M,211
|
|
2
|
-
dlai_grader/cli.py,sha256=NIwboE-AFn1LXOFmF4O70Ow0fkRxgclG_eMwmWiua38,4917
|
|
3
|
-
dlai_grader/compiler.py,sha256=elbHNUCqBCoOOoNmMRXbgeNL0nt0RM57eZi0-6AqycA,3036
|
|
4
|
-
dlai_grader/config.py,sha256=HQ3dzaFpRswIA_7EC8XdP8DdJH-XePsbMQMHG8Esblc,1638
|
|
5
|
-
dlai_grader/grading.py,sha256=Gmft9b7M8At_y_WZDatYdW6tinZMfqQoT7bDXp6uz2I,4606
|
|
6
|
-
dlai_grader/io.py,sha256=HybOy-0aPbJfxzQDjYOE2qo1myXKt0rlX90fzV1PXJo,8264
|
|
7
|
-
dlai_grader/notebook.py,sha256=66-08JUF5l5AS3UETYyTOn0mDjSPJtEetP7guiz4cYQ,12125
|
|
8
|
-
dlai_grader/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
9
|
-
dlai_grader/templates.py,sha256=u4BrPuDKefiyjKovxHPF0MbG73FvUIkLhgGpEWRYpL8,7843
|
|
10
|
-
dlai_grader/types.py,sha256=_IIVbYL9cMmwA6in0aI5fEWCIaAMNcQbxG64X1P1CkE,335
|
|
11
|
-
dlai_grader-1.22.2.dist-info/LICENSE,sha256=a_kch_UqdJPtyxk35QJr9O84K_koPixqWPYW9On4-io,1072
|
|
12
|
-
dlai_grader-1.22.2.dist-info/METADATA,sha256=oQls7F3kEzBalJF1ao-tP5cvRaLOk_lD0VXA6lXyalc,8612
|
|
13
|
-
dlai_grader-1.22.2.dist-info/WHEEL,sha256=R0nc6qTxuoLk7ShA2_Y-UWkN8ZdfDBG2B6Eqpz2WXbs,91
|
|
14
|
-
dlai_grader-1.22.2.dist-info/entry_points.txt,sha256=4OcSAUIluONXa3ymViQ7CBQ2Lk52nb6xZnfph1rlMnk,71
|
|
15
|
-
dlai_grader-1.22.2.dist-info/top_level.txt,sha256=4YKtA3ztisFtx_g4hsGivy3J2NHnXxFziIMqawC8HWg,12
|
|
16
|
-
dlai_grader-1.22.2.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|