assignment-codeval 0.0.27__tar.gz → 0.0.29__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.
Files changed (33) hide show
  1. {assignment_codeval-0.0.27 → assignment_codeval-0.0.29}/PKG-INFO +4 -1
  2. {assignment_codeval-0.0.27 → assignment_codeval-0.0.29}/README.md +3 -0
  3. {assignment_codeval-0.0.27 → assignment_codeval-0.0.29}/pyproject.toml +1 -1
  4. {assignment_codeval-0.0.27 → assignment_codeval-0.0.29}/src/assignment_codeval/check_grading.py +1 -1
  5. {assignment_codeval-0.0.27 → assignment_codeval-0.0.29}/src/assignment_codeval/convertMD2Html.py +49 -15
  6. {assignment_codeval-0.0.27 → assignment_codeval-0.0.29}/src/assignment_codeval/evaluate.py +0 -5
  7. {assignment_codeval-0.0.27 → assignment_codeval-0.0.29}/src/assignment_codeval/recent_comments.py +2 -2
  8. {assignment_codeval-0.0.27 → assignment_codeval-0.0.29}/src/assignment_codeval/submissions.py +40 -1
  9. {assignment_codeval-0.0.27 → assignment_codeval-0.0.29}/src/assignment_codeval.egg-info/PKG-INFO +4 -1
  10. {assignment_codeval-0.0.27 → assignment_codeval-0.0.29}/setup.cfg +0 -0
  11. {assignment_codeval-0.0.27 → assignment_codeval-0.0.29}/src/assignment_codeval/__init__.py +0 -0
  12. {assignment_codeval-0.0.27 → assignment_codeval-0.0.29}/src/assignment_codeval/ai_benchmark.py +0 -0
  13. {assignment_codeval-0.0.27 → assignment_codeval-0.0.29}/src/assignment_codeval/canvas_utils.py +0 -0
  14. {assignment_codeval-0.0.27 → assignment_codeval-0.0.29}/src/assignment_codeval/cli.py +0 -0
  15. {assignment_codeval-0.0.27 → assignment_codeval-0.0.29}/src/assignment_codeval/commons.py +0 -0
  16. {assignment_codeval-0.0.27 → assignment_codeval-0.0.29}/src/assignment_codeval/create_assignment.py +0 -0
  17. {assignment_codeval-0.0.27 → assignment_codeval-0.0.29}/src/assignment_codeval/export_tests.py +0 -0
  18. {assignment_codeval-0.0.27 → assignment_codeval-0.0.29}/src/assignment_codeval/file_utils.py +0 -0
  19. {assignment_codeval-0.0.27 → assignment_codeval-0.0.29}/src/assignment_codeval/github_connect.py +0 -0
  20. {assignment_codeval-0.0.27 → assignment_codeval-0.0.29}/src/assignment_codeval/install_assignment.py +0 -0
  21. {assignment_codeval-0.0.27 → assignment_codeval-0.0.29}/src/assignment_codeval/test_template.html +0 -0
  22. {assignment_codeval-0.0.27 → assignment_codeval-0.0.29}/src/assignment_codeval.egg-info/SOURCES.txt +0 -0
  23. {assignment_codeval-0.0.27 → assignment_codeval-0.0.29}/src/assignment_codeval.egg-info/dependency_links.txt +0 -0
  24. {assignment_codeval-0.0.27 → assignment_codeval-0.0.29}/src/assignment_codeval.egg-info/entry_points.txt +0 -0
  25. {assignment_codeval-0.0.27 → assignment_codeval-0.0.29}/src/assignment_codeval.egg-info/requires.txt +0 -0
  26. {assignment_codeval-0.0.27 → assignment_codeval-0.0.29}/src/assignment_codeval.egg-info/top_level.txt +0 -0
  27. {assignment_codeval-0.0.27 → assignment_codeval-0.0.29}/tests/test_check_grading.py +0 -0
  28. {assignment_codeval-0.0.27 → assignment_codeval-0.0.29}/tests/test_codeval.py +0 -0
  29. {assignment_codeval-0.0.27 → assignment_codeval-0.0.29}/tests/test_create_assignment.py +0 -0
  30. {assignment_codeval-0.0.27 → assignment_codeval-0.0.29}/tests/test_evaluate_submissions.py +0 -0
  31. {assignment_codeval-0.0.27 → assignment_codeval-0.0.29}/tests/test_export_tests.py +0 -0
  32. {assignment_codeval-0.0.27 → assignment_codeval-0.0.29}/tests/test_function_detection.py +0 -0
  33. {assignment_codeval-0.0.27 → assignment_codeval-0.0.29}/tests/test_install_assignment.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: assignment-codeval
3
- Version: 0.0.27
3
+ Version: 0.0.29
4
4
  Summary: CodEval for evaluating programming assignments
5
5
  Requires-Python: >=3.12
6
6
  Description-Content-Type: text/markdown
@@ -57,6 +57,9 @@ Use a part of the course name that can uniquely identify the course on Canvas.
57
57
  - Default: --no-copytmpdir
58
58
  - Copy temporary directory content to current directory for debugging
59
59
 
60
+ ### Codeval File Matching
61
+ When looking up the codeval file for an assignment, CodEval first tries to match by filename (case-insensitive). If no filename match is found, it falls back to checking the `CRT_HW START` title inside each `.codeval` file in the codeval directory. This allows the codeval filename to differ from the assignment name on Canvas as long as the `CRT_HW START` title matches.
62
+
60
63
  ### Specification Tags
61
64
  Tags used in a spec file (\<course name>.codeval)
62
65
 
@@ -37,6 +37,9 @@ Use a part of the course name that can uniquely identify the course on Canvas.
37
37
  - Default: --no-copytmpdir
38
38
  - Copy temporary directory content to current directory for debugging
39
39
 
40
+ ### Codeval File Matching
41
+ When looking up the codeval file for an assignment, CodEval first tries to match by filename (case-insensitive). If no filename match is found, it falls back to checking the `CRT_HW START` title inside each `.codeval` file in the codeval directory. This allows the codeval filename to differ from the assignment name on Canvas as long as the `CRT_HW START` title matches.
42
+
40
43
  ### Specification Tags
41
44
  Tags used in a spec file (\<course name>.codeval)
42
45
 
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "assignment-codeval"
7
- version = "0.0.27"
7
+ version = "0.0.29"
8
8
  description = "CodEval for evaluating programming assignments"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.12"
@@ -62,7 +62,7 @@ def _has_codeval_comment_after_submission(submission_node, codeval_prefix):
62
62
  comments = submission_node.get("commentsConnection", {}).get("nodes", [])
63
63
  for comment in comments:
64
64
  text = comment.get("comment", "")
65
- if not text.startswith(codeval_prefix):
65
+ if not text.startswith(codeval_prefix.rstrip()):
66
66
  continue
67
67
  created_at = comment.get("createdAt")
68
68
  if created_at and created_at > submitted_at:
@@ -1,5 +1,6 @@
1
1
  import os
2
2
  import re
3
+ import zipfile
3
4
 
4
5
  import markdown
5
6
 
@@ -41,19 +42,28 @@ def ansi_to_html(text):
41
42
  return ''.join(result)
42
43
 
43
44
 
44
- def _read_file_content(filename, spec_dir):
45
- """Read the content of a file referenced by an OF or IF tag."""
46
- if not spec_dir:
47
- return None
48
- filepath = os.path.join(spec_dir, filename)
49
- try:
50
- with open(filepath, 'r') as f:
51
- return f.read()
52
- except (FileNotFoundError, OSError):
53
- return None
45
+ def _read_file_content(filename, spec_dir, zip_archives=None):
46
+ """Read the content of a file referenced by an IF/OF/EF tag.
54
47
 
48
+ Searches local spec_dir first, then zip archives. Returns content as string.
49
+ """
50
+ if spec_dir:
51
+ filepath = os.path.join(spec_dir, filename)
52
+ try:
53
+ with open(filepath, 'r') as f:
54
+ return f.read()
55
+ except (FileNotFoundError, OSError):
56
+ pass
57
+ if zip_archives:
58
+ for zf in zip_archives:
59
+ try:
60
+ return zf.read(filename).decode('utf-8')
61
+ except KeyError:
62
+ continue
63
+ return None
55
64
 
56
- def sampleTestCases(listOfTC, numOfTC, spec_dir=None):
65
+
66
+ def sampleTestCases(listOfTC, numOfTC, spec_dir=None, zip_archives=None):
57
67
  counter = 0
58
68
  samples = "<pre><code>"
59
69
  for line in listOfTC:
@@ -64,7 +74,7 @@ def sampleTestCases(listOfTC, numOfTC, spec_dir=None):
64
74
  samples = samples + "\n" + "Command to RUN: " + line[2:]
65
75
  elif line.startswith('IF '):
66
76
  filename = line[3:].strip()
67
- content = _read_file_content(filename, spec_dir)
77
+ content = _read_file_content(filename, spec_dir, zip_archives)
68
78
  if content is not None:
69
79
  samples = samples + "<span style=\"color:green\">" + ansi_to_html(content) + "</span>"
70
80
  else:
@@ -75,7 +85,7 @@ def sampleTestCases(listOfTC, numOfTC, spec_dir=None):
75
85
  samples = samples + "<span style=\"color:green\">" + line[2:] + "</span>"
76
86
  elif line.startswith('OF '):
77
87
  filename = line[3:].strip()
78
- content = _read_file_content(filename, spec_dir)
88
+ content = _read_file_content(filename, spec_dir, zip_archives)
79
89
  if content is not None:
80
90
  samples = samples + "<span style=\"color:blue\">" + ansi_to_html(content) + "</span>"
81
91
  else:
@@ -86,6 +96,13 @@ def sampleTestCases(listOfTC, numOfTC, spec_dir=None):
86
96
  samples = samples + "<span style=\"color:blue\">" + line[2:] + "</span>"
87
97
  elif line.startswith('X '):
88
98
  samples = samples + "Expected Exit Code: " + line[2:]
99
+ elif line.startswith('EF '):
100
+ filename = line[3:].strip()
101
+ content = _read_file_content(filename, spec_dir, zip_archives)
102
+ if content is not None:
103
+ samples = samples + "<span style=\"color:Tomato\">" + ansi_to_html(content) + "</span>"
104
+ else:
105
+ samples = samples + "<span style=\"color:Tomato\">Expected error from file: " + filename + "\n</span>"
89
106
  elif line.startswith('EB '):
90
107
  samples = samples + "<span style=\"color:Tomato\">" + line[3:] + "</span>"
91
108
  elif line.startswith('E '):
@@ -104,24 +121,41 @@ def mdToHtml(file_name, files_resolver=None):
104
121
  compile_command = ""
105
122
  past_crt_hw = False
106
123
  numOfSampleTC = 1
124
+ zip_paths = []
107
125
  for line in f:
108
126
  if 'CRT_HW START' in line:
109
127
  assignment_name = line[13:].strip()
110
128
  elif 'CRT_HW END' in line:
111
129
  assignment = text
112
130
  past_crt_hw = True
113
- elif line.startswith(('T ', 'I ', 'IB ', 'IF ', 'O ', 'OB ', 'OF ', 'X ', 'E ', 'EB ')):
131
+ elif line.startswith(('T ', 'I ', 'IB ', 'IF ', 'O ', 'OB ', 'OF ',
132
+ 'EF ', 'X ', 'E ', 'EB ')):
114
133
  examples.append(line)
115
134
  elif line.startswith('HT '):
116
135
  break
117
136
  else:
118
137
  if past_crt_hw and line.startswith('C '):
119
138
  compile_command = line[2:].strip()
139
+ if past_crt_hw and line.startswith('Z '):
140
+ zip_paths.append(line[2:].strip())
120
141
  if 'EXMPLS ' in line:
121
142
  numOfSampleTC = int(line[7:])
122
143
  text = text + line
123
144
  spec_dir = os.path.dirname(os.path.abspath(file_name))
124
- samples = sampleTestCases(examples, numOfSampleTC, spec_dir)
145
+
146
+ # Open zip archives for IF/OF/EF file resolution
147
+ zip_archives = []
148
+ try:
149
+ for zp in zip_paths:
150
+ full_path = os.path.join(spec_dir, zp)
151
+ if os.path.exists(full_path):
152
+ zip_archives.append(zipfile.ZipFile(full_path, 'r'))
153
+ samples = sampleTestCases(examples, numOfSampleTC, spec_dir,
154
+ zip_archives)
155
+ finally:
156
+ for zf in zip_archives:
157
+ zf.close()
158
+
125
159
  assignment = re.sub('EXMPLS [0-9]+', samples, assignment)
126
160
  if compile_command:
127
161
  assignment = assignment.replace('COMPILE', compile_command)
@@ -1138,8 +1138,6 @@ def check_test():
1138
1138
  if len(lines) > 22:
1139
1139
  print(f" ... ({len(lines) - 22} more lines)")
1140
1140
 
1141
- cleanup()
1142
-
1143
1141
  # Exit program after failed test case
1144
1142
  sys.exit(2)
1145
1143
 
@@ -1215,8 +1213,5 @@ def run_evaluation(codeval_file):
1215
1213
 
1216
1214
  check_test()
1217
1215
 
1218
- # cleanup
1219
- cleanup()
1220
-
1221
1216
  end_time_seconds = time.time()
1222
1217
  print(f"took {end_time_seconds - start_time_seconds} seconds")
@@ -12,6 +12,7 @@ from assignment_codeval.canvas_utils import (
12
12
  get_canvas_credentials, graphql_request, fetch_all_submissions,
13
13
  )
14
14
  from assignment_codeval.commons import despace
15
+ from assignment_codeval.submissions import find_codeval_file
15
16
 
16
17
  COURSE_ASSIGNMENTS_QUERY = """
17
18
  query CourseAssignmentsQuery($courseId: ID!) {
@@ -164,8 +165,7 @@ def recent_comments(course_name, active, time_period, codeval_prefix, verbose, s
164
165
  # Check if this assignment has a codeval file
165
166
  has_codeval = True
166
167
  if codeval_dir:
167
- codeval_file = os.path.join(codeval_dir, f"{despace(assignment_name)}.codeval")
168
- has_codeval = os.path.exists(codeval_file)
168
+ has_codeval = find_codeval_file(codeval_dir, despace(assignment_name)) is not None
169
169
  if not has_codeval:
170
170
  if verbose:
171
171
  click.echo(f" (no codeval file)", err=True)
@@ -17,13 +17,41 @@ from assignment_codeval.canvas_utils import connect_to_canvas, get_course, get_c
17
17
  from assignment_codeval.commons import debug, error, info, warn, despace
18
18
 
19
19
 
20
+ def _extract_codeval_title(filepath):
21
+ """Extract the assignment title from the CRT_HW START line of a codeval file.
22
+ Returns None if no CRT_HW START line is found."""
23
+ try:
24
+ with open(filepath, 'r') as f:
25
+ for line in f:
26
+ if 'CRT_HW START' in line:
27
+ idx = line.index('CRT_HW START') + len('CRT_HW START')
28
+ title = line[idx:].strip()
29
+ return title if title else None
30
+ except (OSError, UnicodeDecodeError):
31
+ pass
32
+ return None
33
+
34
+
20
35
  def find_codeval_file(codeval_dir, assignment_name):
21
36
  """Find a codeval file by assignment name, case-insensitive.
37
+ First tries matching by filename, then falls back to checking
38
+ CRT_HW START titles inside each .codeval file.
22
39
  Returns the full path if found, or None if not found."""
23
40
  target = f"{assignment_name}.codeval".lower()
41
+ codeval_files = []
24
42
  for f in os.listdir(codeval_dir):
25
43
  if f.lower() == target:
26
44
  return os.path.join(codeval_dir, f)
45
+ if f.endswith('.codeval'):
46
+ codeval_files.append(f)
47
+
48
+ # Fallback: check CRT_HW START titles
49
+ target_name = assignment_name.lower()
50
+ for f in codeval_files:
51
+ filepath = os.path.join(codeval_dir, f)
52
+ title = _extract_codeval_title(filepath)
53
+ if title and despace(title).lower() == target_name:
54
+ return filepath
27
55
  return None
28
56
 
29
57
 
@@ -596,7 +624,15 @@ def list_codeval_assignments(course_name):
596
624
  raise click.UsageError(f"CODEVAL directory does not exist: {codeval_dir}")
597
625
 
598
626
  # Get all codeval files in the directory (map lowercase name -> original name)
599
- codeval_files = {f[:-8].lower(): f[:-8] for f in os.listdir(codeval_dir) if f.endswith('.codeval')}
627
+ codeval_files = {}
628
+ codeval_titles = {} # map despaced-lowercase CRT_HW title -> original filename stem
629
+ for f in os.listdir(codeval_dir):
630
+ if f.endswith('.codeval'):
631
+ stem = f[:-8]
632
+ codeval_files[stem.lower()] = stem
633
+ title = _extract_codeval_title(os.path.join(codeval_dir, f))
634
+ if title:
635
+ codeval_titles[despace(title).lower()] = stem
600
636
  matched_codeval_keys = set()
601
637
 
602
638
  (canvas, user) = connect_to_canvas()
@@ -611,6 +647,9 @@ def list_codeval_assignments(course_name):
611
647
  if assignment_key in codeval_files:
612
648
  matched_codeval_keys.add(assignment_key)
613
649
  info(f"{course.name}: {assignment.name}")
650
+ elif assignment_key in codeval_titles:
651
+ matched_codeval_keys.add(codeval_titles[assignment_key].lower())
652
+ info(f"{course.name}: {assignment.name}")
614
653
 
615
654
  # Report codeval files that don't match any assignment
616
655
  unmatched_keys = set(codeval_files.keys()) - matched_codeval_keys
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: assignment-codeval
3
- Version: 0.0.27
3
+ Version: 0.0.29
4
4
  Summary: CodEval for evaluating programming assignments
5
5
  Requires-Python: >=3.12
6
6
  Description-Content-Type: text/markdown
@@ -57,6 +57,9 @@ Use a part of the course name that can uniquely identify the course on Canvas.
57
57
  - Default: --no-copytmpdir
58
58
  - Copy temporary directory content to current directory for debugging
59
59
 
60
+ ### Codeval File Matching
61
+ When looking up the codeval file for an assignment, CodEval first tries to match by filename (case-insensitive). If no filename match is found, it falls back to checking the `CRT_HW START` title inside each `.codeval` file in the codeval directory. This allows the codeval filename to differ from the assignment name on Canvas as long as the `CRT_HW START` title matches.
62
+
60
63
  ### Specification Tags
61
64
  Tags used in a spec file (\<course name>.codeval)
62
65