dlai-grader 1.22.1__tar.gz → 2.0b1__tar.gz

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.

Files changed (24) hide show
  1. {dlai_grader-1.22.1 → dlai_grader-2.0b1}/PKG-INFO +7 -11
  2. {dlai_grader-1.22.1 → dlai_grader-2.0b1}/dlai_grader/__init__.py +1 -1
  3. {dlai_grader-1.22.1 → dlai_grader-2.0b1}/dlai_grader/config.py +12 -5
  4. {dlai_grader-1.22.1 → dlai_grader-2.0b1}/dlai_grader/grading.py +58 -30
  5. {dlai_grader-1.22.1 → dlai_grader-2.0b1}/dlai_grader/io.py +46 -18
  6. {dlai_grader-1.22.1 → dlai_grader-2.0b1}/dlai_grader/notebook.py +21 -17
  7. dlai_grader-2.0b1/dlai_grader/templates.py +438 -0
  8. dlai_grader-2.0b1/dlai_grader/types.py +8 -0
  9. {dlai_grader-1.22.1 → dlai_grader-2.0b1}/dlai_grader.egg-info/PKG-INFO +7 -11
  10. {dlai_grader-1.22.1 → dlai_grader-2.0b1}/dlai_grader.egg-info/SOURCES.txt +1 -0
  11. dlai_grader-2.0b1/pyproject.toml +34 -0
  12. {dlai_grader-1.22.1 → dlai_grader-2.0b1}/setup.py +3 -2
  13. dlai_grader-1.22.1/dlai_grader/templates.py +0 -262
  14. dlai_grader-1.22.1/dlai_grader/types.py +0 -7
  15. {dlai_grader-1.22.1 → dlai_grader-2.0b1}/LICENSE +0 -0
  16. {dlai_grader-1.22.1 → dlai_grader-2.0b1}/README.md +0 -0
  17. {dlai_grader-1.22.1 → dlai_grader-2.0b1}/dlai_grader/cli.py +0 -0
  18. {dlai_grader-1.22.1 → dlai_grader-2.0b1}/dlai_grader/compiler.py +0 -0
  19. {dlai_grader-1.22.1 → dlai_grader-2.0b1}/dlai_grader/py.typed +0 -0
  20. {dlai_grader-1.22.1 → dlai_grader-2.0b1}/dlai_grader.egg-info/dependency_links.txt +0 -0
  21. {dlai_grader-1.22.1 → dlai_grader-2.0b1}/dlai_grader.egg-info/entry_points.txt +0 -0
  22. {dlai_grader-1.22.1 → dlai_grader-2.0b1}/dlai_grader.egg-info/requires.txt +0 -0
  23. {dlai_grader-1.22.1 → dlai_grader-2.0b1}/dlai_grader.egg-info/top_level.txt +0 -0
  24. {dlai_grader-1.22.1 → dlai_grader-2.0b1}/setup.cfg +0 -0
@@ -1,27 +1,23 @@
1
- Metadata-Version: 2.2
1
+ Metadata-Version: 2.4
2
2
  Name: dlai-grader
3
- Version: 1.22.1
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
8
- License: MIT License
7
+ Author-email: Andres Zarta <andrezb5@gmail.com>
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
16
  Requires-Dist: nbformat>=5.1.3
15
17
  Requires-Dist: jupytext>=1.13.0
16
18
  Dynamic: author
17
- Dynamic: author-email
18
- Dynamic: classifier
19
- Dynamic: description
20
- Dynamic: description-content-type
21
19
  Dynamic: home-page
22
- Dynamic: license
23
- Dynamic: requires-dist
24
- Dynamic: summary
20
+ Dynamic: license-file
25
21
 
26
22
  # grader
27
23
  Automatic grading for DLAI courses. Designed to be compatible with Coursera's grading requirements.
@@ -6,6 +6,6 @@ from . import grading
6
6
  from . import types
7
7
 
8
8
 
9
- __version__ = "1.22.1"
9
+ __version__ = "2.0b1"
10
10
  __author__ = "Andres Zarta"
11
11
  __credits__ = "DeepLearning.AI"
@@ -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
- ) -> Dict[str, str]:
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 = os.path.join(solution_workdir, solution_file)
47
+ solution_file_path: str = ""
49
48
  submission_file: str = "submission.ipynb"
50
- submission_file_path: str = os.path.join(submission_workdir, submission_file)
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
+ )
@@ -1,75 +1,96 @@
1
1
  from dataclasses import dataclass
2
2
  from functools import wraps
3
- from typing import Any, Callable, List, Tuple, Union
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 = Union[ModuleType, LearnerSubmission]
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: List[test_case]
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
- return (
48
- 0.0,
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
- feedback_msg = "All tests passed! Congratulations!"
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"Failed test case: {failed_case.msg}.\nExpected:\n{failed_case.want},\nbut got:\n{failed_case.got}.\n\n"
60
- return round(score, 2), feedback_msg
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: List[aggregated_test_case],
67
- ) -> Tuple[float, str]:
68
- """Computes the score based on the number of failed and total cases.
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], List[test_case]]], Callable[[Any], List[test_case]]]:
102
- """Used as a parameterized decorator to get any number of attributes from a module.
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: List[test_case]) -> None:
124
- """Prints feedback of public unit tests within notebook.
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: List[test_case]) -> bool:
141
- """Check if the object to grade was found in the learned module.
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 == type(None):
177
+ if len(test_cases) == 1 and test_cases[0].got is type(None):
150
178
  return True
151
179
 
152
180
  return False
@@ -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
- from contextlib import contextmanager, redirect_stderr, redirect_stdout
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
- """Reads a notebook found in the given path and returns a serialized version.
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
- """Adds 'graded' tag to all code cells of a notebook.
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
- """Makes all code cells of a notebook non-deletable.
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
- """Makes all non-graded code cells of a notebook non-editable.
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(nb, {"editable": False})
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
- """Extracts a tar file unto the desired destination.
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
- """Sends feedback to the learner.
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 (not "TAG_ID" in l):
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
- os.makedirs("data")
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,
@@ -72,8 +72,8 @@ def keep_tagged_cells(
72
72
  filtered_cells = []
73
73
 
74
74
  for cell in notebook["cells"]:
75
- if (not "tags" in cell["metadata"]) or (
76
- not tag in cell["metadata"].get("tags")
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 not "name" in metadata:
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 not "tags" in cell["metadata"]:
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 tag in tags:
177
+ if tag not in tags:
178
178
  tags.append(tag)
179
179
  cell["metadata"]["tags"] = tags
180
180
 
@@ -238,27 +238,31 @@ def add_metadata_code_cells_with_pattern(
238
238
 
239
239
 
240
240
  def add_metadata_code_cells_without_pattern(
241
- notebook: NotebookNode, metadata: dict, regex_pattern: str = "^# GRADED "
241
+ notebook: NotebookNode,
242
+ metadata: dict,
243
+ match_pattern: str = "^# GRADED",
244
+ ignore_pattern: str = None,
242
245
  ) -> NotebookNode:
243
- """Adds metadata to code cells of a notebook that don't match a regexp pattern.
246
+ """Adds metadata to code cells of a notebook that don't match a regexp pattern and aren't ignored by another pattern.
247
+
244
248
  Args:
245
249
  notebook (NotebookNode): Notebook to filter.
246
250
  metadata (dict): The metadata which should be a key-value pair.
247
- regex_pattern (str, optional): Pattern to check. Defaults to "w[1-9]_unittest".
251
+ match_pattern (str, optional): Pattern to check which cells to add metadata to. Defaults to "^# GRADED ".
252
+ ignore_pattern (str, optional): Pattern to check which cells to ignore. If a cell matches this pattern, it will be skipped.
253
+
248
254
  Returns:
249
255
  NotebookNode: The notebook with the new metadata.
250
256
  """
251
- filtered_cells = []
252
-
253
257
  for cell in notebook["cells"]:
254
- if cell["cell_type"] == "code" and not re.search(regex_pattern, cell["source"]):
255
- current_metadata = cell["metadata"]
256
- current_metadata.update(metadata)
257
- cell["metadata"] = current_metadata
258
-
259
- filtered_cells.append(cell)
258
+ if cell["cell_type"] == "code":
259
+ if ignore_pattern and re.search(ignore_pattern, cell["source"]):
260
+ continue
260
261
 
261
- notebook["cells"] = filtered_cells
262
+ if not re.search(match_pattern, cell["source"]):
263
+ current_metadata = cell.get("metadata", {})
264
+ current_metadata.update(metadata)
265
+ cell["metadata"] = current_metadata
262
266
 
263
267
  return notebook
264
268