assignment-codeval 0.0.29__py3-none-any.whl → 0.0.30__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.
@@ -98,7 +98,7 @@ def get_assignment(course, assignment_name):
98
98
  error(fr" {a.name}")
99
99
  sys.exit(2)
100
100
  elif len(assignments) > 1:
101
- strict_name_assignments = [a for a in assignments if a.name == assignment_name]
101
+ strict_name_assignments = [a for a in assignments if despace(a.name) == assignment_name]
102
102
  if len(strict_name_assignments) == 1:
103
103
  assignments = strict_name_assignments
104
104
  else:
@@ -38,11 +38,23 @@ num_failed = 0
38
38
  is_hidden_testcase = False
39
39
  is_verbose = False
40
40
  compilelog = []
41
+ last_compile_command = ""
42
+ temp_files = []
43
+ _active_temp_files = []
41
44
 
42
45
  ###########################################################
43
46
  # Specification Tags to Function Mapping
44
47
  ###########################################################
45
48
 
49
+ # Shell commands that likely indicate a missing CMD prefix when found untagged
50
+ _BARE_SHELL_COMMANDS = {
51
+ 'echo', 'rm', 'cp', 'mv', 'mkdir', 'rmdir', 'cat', 'ls', 'grep',
52
+ 'sed', 'awk', 'find', 'chmod', 'chown', 'touch', 'ln', 'diff',
53
+ 'sort', 'head', 'tail', 'cut', 'tr', 'wc', 'bash', 'sh', 'python',
54
+ 'python3', 'make', 'export', 'source', 'kill', 'pkill', 'sleep',
55
+ 'printf', 'read', 'unzip', 'tar', 'curl', 'wget',
56
+ }
57
+
46
58
 
47
59
  def compile_code(compile_command):
48
60
  """Specifies the command to compile the submission code
@@ -53,6 +65,9 @@ def compile_code(compile_command):
53
65
  Returns:
54
66
  None
55
67
  """
68
+ global last_compile_command
69
+ last_compile_command = compile_command
70
+
56
71
  if test_case_count != 0:
57
72
  check_test()
58
73
 
@@ -112,6 +127,44 @@ def _find_compiled_artifact(source_file):
112
127
  return None
113
128
 
114
129
 
130
+ def _extract_executable_from_compile(compile_command):
131
+ """Extract the output executable name from a compile command (looks for -o <name>).
132
+
133
+ Returns the executable name, or None if not found.
134
+ """
135
+ parts = compile_command.split()
136
+ for i, part in enumerate(parts):
137
+ if part == '-o' and i + 1 < len(parts):
138
+ return parts[i + 1]
139
+ return None
140
+
141
+
142
+ def _extract_source_files_from_compile(compile_command):
143
+ """Extract source file arguments from a compile command.
144
+
145
+ Recognises common source file extensions (.c, .cpp, .cc, .cxx, .java, .py)
146
+ and skips option arguments that consume the following token as a value
147
+ (e.g. -o, -I, -L, -l, -include, -isystem, -MF, -MT, -MQ).
148
+
149
+ Returns a list of source file paths found in the command.
150
+ """
151
+ source_exts = ('.c', '.cpp', '.cc', '.cxx', '.java', '.py')
152
+ value_flags = {'-o', '-I', '-L', '-l', '-include', '-isystem', '-MF', '-MT', '-MQ'}
153
+ parts = compile_command.split()
154
+ files = []
155
+ skip_next = False
156
+ for part in parts:
157
+ if skip_next:
158
+ skip_next = False
159
+ continue
160
+ if part in value_flags:
161
+ skip_next = True
162
+ continue
163
+ if not part.startswith('-') and any(part.endswith(ext) for ext in source_exts):
164
+ files.append(part)
165
+ return files
166
+
167
+
115
168
  def _function_used_in_c_cpp(function_name, files):
116
169
  """Use objdump on compiled artifacts to detect C/C++ function usage.
117
170
 
@@ -209,14 +262,20 @@ def _function_used_regex(function_name, files):
209
262
  return result.returncode == 0
210
263
 
211
264
 
212
- def _is_function_used(function_name, files):
265
+ def _is_function_used(function_name, files, allow_regex_fallback=True):
213
266
  """Detect whether function_name is used in the given files.
214
267
 
215
268
  Dispatches to the appropriate tool based on file language:
216
- - C/C++: objdump on compiled artifact (falls back to regex if none found)
217
- - Java: javap on .class file (falls back to regex if none found)
269
+ - C/C++: objdump on compiled artifact (falls back to regex if none found
270
+ and allow_regex_fallback is True)
271
+ - Java: javap on .class file (falls back to regex if none found
272
+ and allow_regex_fallback is True)
218
273
  - Python: ast module on source
219
- - Other: regex on source
274
+ - Other: regex on source (only when allow_regex_fallback is True)
275
+
276
+ When allow_regex_fallback is False (used when no filename was given in the
277
+ CF tag), the function relies solely on compiled-artifact inspection and
278
+ returns False rather than falling back to regex.
220
279
  """
221
280
  lang = _detect_language(files)
222
281
  if lang == 'python':
@@ -224,36 +283,56 @@ def _is_function_used(function_name, files):
224
283
  if lang == 'java':
225
284
  result = _function_used_in_java(function_name, files)
226
285
  if result is None:
227
- return _function_used_regex(function_name, files)
286
+ return _function_used_regex(function_name, files) if allow_regex_fallback else False
228
287
  return result
229
288
  if lang == 'c_cpp':
230
289
  result = _function_used_in_c_cpp(function_name, files)
231
290
  if result is None:
232
- return _function_used_regex(function_name, files)
291
+ return _function_used_regex(function_name, files) if allow_regex_fallback else False
233
292
  return result
234
- return _function_used_regex(function_name, files)
293
+ if allow_regex_fallback:
294
+ return _function_used_regex(function_name, files)
295
+ return False
235
296
 
236
297
 
237
298
  def check_function(args):
238
- """Will be followed by a function name and a list of files to check to ensure that the function
239
- is used by one of those files.
299
+ """Will be followed by a function name and an optional list of files to check to ensure that
300
+ the function is used by one of those files.
301
+
302
+ When no filename is provided (preferred usage), the source files are derived from the most
303
+ recent C (compile) tag. The check then relies solely on compiled-artifact inspection
304
+ (objdump for C/C++, javap for Java, ast for Python) with no regex fallback.
305
+
306
+ When a filename is provided (legacy usage), the same compiled-artifact inspection is used
307
+ first, but a regex scan of the source file is available as a fallback when no compiled
308
+ artifact can be found.
240
309
 
241
310
  Arguments:
242
311
  function_name: the function name to check files for usage of
243
- *files: the files to check for the function name
312
+ *files: (optional) the files to check for the function name. If omitted, source
313
+ files are inferred from the most recent C tag.
244
314
 
245
315
  Returns:
246
316
  None
247
317
  """
248
318
  check_test()
249
- args = args.split()
250
- function_name = args[0]
251
- files = args[1:]
252
-
253
- if _is_function_used(function_name, files):
254
- print(f"Used {function_name} PASSED")
319
+ args_list = args.split()
320
+ function_name = args_list[0]
321
+ files = args_list[1:]
322
+
323
+ if not files:
324
+ # No filename provided — derive source files from the compile command and
325
+ # disable the regex fallback (compiled-artifact check only).
326
+ files = _extract_source_files_from_compile(last_compile_command)
327
+ if _is_function_used(function_name, files, allow_regex_fallback=False):
328
+ print(f"Used {function_name} PASSED")
329
+ else:
330
+ print(f"Not using {function_name} FAILED")
255
331
  else:
256
- print(f"Not using {function_name} FAILED")
332
+ if _is_function_used(function_name, files):
333
+ print(f"Used {function_name} PASSED")
334
+ else:
335
+ print(f"Not using {function_name} FAILED")
257
336
 
258
337
  def check_object(args):
259
338
  """Will be followed by an object and a list of files to ensure that the function is
@@ -352,22 +431,38 @@ def check_container(args):
352
431
 
353
432
 
354
433
  def check_not_function(args):
355
- """Will be followed by a function name and a list of files to check to ensure that the function
356
- is not used by any of those files.
434
+ """Will be followed by a function name and an optional list of files to check to ensure that
435
+ the function is not used by any of those files.
436
+
437
+ When no filename is provided (preferred usage), the source files are derived from the most
438
+ recent C (compile) tag. The check relies solely on compiled-artifact inspection with no
439
+ regex fallback.
440
+
441
+ When a filename is provided (legacy usage), compiled-artifact inspection is used first with
442
+ a regex fallback when no compiled artifact is found.
357
443
 
358
444
  Arguments:
359
445
  function_name: the function name to check files for usage of
360
- *files: the files to check for the function name
446
+ *files: (optional) the files to check for the function name. If omitted, source
447
+ files are inferred from the most recent C tag.
361
448
 
362
449
  Returns:
363
450
  None
364
451
  """
365
452
  check_test()
366
- args = args.split()
367
- function_name = args[0]
368
- files = args[1:]
453
+ args_list = args.split()
454
+ function_name = args_list[0]
455
+ files = args_list[1:]
456
+
457
+ if not files:
458
+ # No filename provided — derive source files from the compile command and
459
+ # disable the regex fallback (compiled-artifact check only).
460
+ files = _extract_source_files_from_compile(last_compile_command)
461
+ used = _is_function_used(function_name, files, allow_regex_fallback=False)
462
+ else:
463
+ used = _is_function_used(function_name, files)
369
464
 
370
- if _is_function_used(function_name, files):
465
+ if used:
371
466
  print(f"Used {function_name} FAILED")
372
467
  else:
373
468
  print(f"Not using {function_name} PASSED")
@@ -412,6 +507,7 @@ def run_command_noerror(command):
412
507
  None
413
508
  """
414
509
  check_test()
510
+ _pre_test_temp_cleanup()
415
511
 
416
512
  # Run as test case
417
513
  global test_case_count
@@ -460,6 +556,7 @@ def test_case(test_case_command):
460
556
  None
461
557
  """
462
558
  check_test()
559
+ _pre_test_temp_cleanup()
463
560
 
464
561
  # Clear hint
465
562
  global test_case_hint
@@ -488,6 +585,7 @@ def test_case_hidden(test_case_command):
488
585
  None
489
586
  """
490
587
  check_test()
588
+ _pre_test_temp_cleanup()
491
589
 
492
590
  # Clear hint
493
591
  global test_case_hint
@@ -585,9 +683,10 @@ def check_output_file(output_file):
585
683
  Returns:
586
684
  None
587
685
  """
686
+ output_length(os.path.getsize(output_file))
588
687
  with open(output_file, "r") as infile:
589
688
  output_lines = infile.readlines()
590
-
689
+
591
690
  with open(get_testing_path("expectedoutput"), "a") as outfile:
592
691
  outfile.writelines(output_lines)
593
692
 
@@ -631,6 +730,48 @@ def hint(hints):
631
730
  test_case_hint = hints
632
731
 
633
732
 
733
+ def register_temp_file(filename):
734
+ """Registers a file to be deleted before and after the next T, HT, or TCMD test.
735
+
736
+ Arguments:
737
+ filename: the file to delete before and after the next test
738
+
739
+ Returns:
740
+ None
741
+ """
742
+ global temp_files
743
+ temp_files.append(filename.strip())
744
+
745
+
746
+ def _cleanup_temp_files():
747
+ """Delete all registered temp files and reset the list."""
748
+ global temp_files
749
+ for f in temp_files:
750
+ try:
751
+ os.remove(f)
752
+ except FileNotFoundError:
753
+ pass
754
+ temp_files = []
755
+
756
+
757
+ def _pre_test_temp_cleanup():
758
+ """Before a test: delete registered temp files and save the list for post-test cleanup."""
759
+ global temp_files, _active_temp_files
760
+ _active_temp_files = list(temp_files)
761
+ _cleanup_temp_files()
762
+
763
+
764
+ def _post_test_temp_cleanup():
765
+ """After a test: delete the files that were registered when the test started."""
766
+ global _active_temp_files
767
+ for f in _active_temp_files:
768
+ try:
769
+ os.remove(f)
770
+ except FileNotFoundError:
771
+ pass
772
+ _active_temp_files = []
773
+
774
+
634
775
  def timeout(timeout_sec):
635
776
  """Specifies the time limit in seconds for a test case to run. Defaults to 20 seconds.
636
777
 
@@ -743,6 +884,7 @@ tag_func_map = {
743
884
  "OLEN": output_length,
744
885
  "X": exit_code,
745
886
  "SS": start_server,
887
+ "TEMP": register_temp_file,
746
888
  }
747
889
 
748
890
 
@@ -840,6 +982,13 @@ def parse_tags(tags: list[str]):
840
982
  print(f" {line_num}: {tag_line.rstrip()}")
841
983
  print(f" Valid tags are: {', '.join(sorted(valid_tags))}")
842
984
  sys.exit(1)
985
+ else:
986
+ # Check if this looks like a bare shell command missing a CMD prefix
987
+ first_word = tag_line.split()[0] if tag_line.split() else ""
988
+ if first_word.lower() in _BARE_SHELL_COMMANDS:
989
+ print(f"Warning on line {line_num}: bare shell command without CMD prefix, shell commands will be ignored")
990
+ print(f" {line_num}: {tag_line.rstrip()}")
991
+ print(f" Did you mean: CMD {tag_line.rstrip()}")
843
992
  continue
844
993
 
845
994
  tag = tag_match.group(1)
@@ -1138,10 +1287,14 @@ def check_test():
1138
1287
  if len(lines) > 22:
1139
1288
  print(f" ... ({len(lines) - 22} more lines)")
1140
1289
 
1290
+ _post_test_temp_cleanup()
1291
+ cleanup()
1292
+
1141
1293
  # Exit program after failed test case
1142
1294
  sys.exit(2)
1143
1295
 
1144
1296
  # reinitialize test variables and files here
1297
+ _post_test_temp_cleanup()
1145
1298
  setup()
1146
1299
 
1147
1300
 
@@ -605,7 +605,8 @@ def get_submissions_by_id(assignment):
605
605
 
606
606
  @click.command()
607
607
  @click.argument("course_name", metavar="COURSE", required=False)
608
- def list_codeval_assignments(course_name):
608
+ @click.option("--path", is_flag=True, help="Show the path to the codeval file")
609
+ def list_codeval_assignments(course_name, path):
609
610
  """
610
611
  List all assignments that have corresponding codeval files.
611
612
 
@@ -646,10 +647,18 @@ def list_codeval_assignments(course_name):
646
647
  assignment_key = despace(assignment.name).lower()
647
648
  if assignment_key in codeval_files:
648
649
  matched_codeval_keys.add(assignment_key)
649
- info(f"{course.name}: {assignment.name}")
650
+ codeval_path = os.path.join(codeval_dir, f"{codeval_files[assignment_key]}.codeval")
651
+ if path:
652
+ info(f"{course.name}: {assignment.name} [{codeval_path}]")
653
+ else:
654
+ info(f"{course.name}: {assignment.name}")
650
655
  elif assignment_key in codeval_titles:
651
656
  matched_codeval_keys.add(codeval_titles[assignment_key].lower())
652
- info(f"{course.name}: {assignment.name}")
657
+ codeval_path = os.path.join(codeval_dir, f"{codeval_titles[assignment_key]}.codeval")
658
+ if path:
659
+ info(f"{course.name}: {assignment.name} [{codeval_path}]")
660
+ else:
661
+ info(f"{course.name}: {assignment.name}")
653
662
 
654
663
  # Report codeval files that don't match any assignment
655
664
  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.29
3
+ Version: 0.0.30
4
4
  Summary: CodEval for evaluating programming assignments
5
5
  Requires-Python: >=3.12
6
6
  Description-Content-Type: text/markdown
@@ -69,7 +69,8 @@ Tags used in a spec file (\<course name>.codeval)
69
69
  | CTO | Compile Timeout | Timeout in seconds for the compile command to run |
70
70
  | RUN | Run Script | Specifies the script to use to evaluate the specification file. Defaults to evaluate.sh. |
71
71
  | Z | Download Zip | Will be followed by zip files to download from Canvas to use when running the test cases. |
72
- | CF | Check Function | Will be followed by a function name and a list of files to check to ensure that the function is used by one of those files. |
72
+ | CF | Check Function | `CF <function_name> [filename]` — checks that the function is used. The filename is **optional** and omitting it (e.g. `CF strtol`) is the preferred usage; source files are inferred from the most recent `C` tag and the check uses compiled-artifact inspection only (objdump/javap/ast). Providing a filename (e.g. `CF strtol mycalc.c`) is supported for backwards compatibility and additionally enables a regex fallback when no compiled artifact is found. |
73
+ | NCF | Check Not Function | `NCF <function_name> [filename]` — checks that the function is **not** used. The filename is **optional** (same behaviour as CF). |
73
74
  | CC | Check Container | Will be followed by a function name and a list of files to check to ensure that a container is used by one of those files. Primarily supports C++ containers such as std::vector |
74
75
  | CO | Check Object | Will be followed by a function name and a list of files to check to ensure that an object is used by one of those files. Primarily support C++ stream operations |
75
76
  | PRINT | Print Label | Prints a label/message to stdout. Cleaner alternative to `CMD echo "..."` for section labels. |
@@ -82,6 +83,7 @@ Tags used in a spec file (\<course name>.codeval)
82
83
  | TO | Timeout | Specifies the time limit in seconds for a test case to run. Defaults to 20 seconds. |
83
84
  | X | Exit Code | Specifies the expected exit code for a test case. Defaults to zero. |
84
85
  | SS | Start Server | Command containing timeout (wait until server starts), kill timeout (wait to kill the server), and the command to start a server |
86
+ | TEMP | Temp File | Registers a file to be deleted before the next T, HT, or TCMD test runs (clean state) and again after it completes (cleanup). Only applies to the immediately following test — use a new TEMP tag for each test that needs it. |
85
87
 
86
88
  Refer to a sample spec file [here](samples/assignment-name.codeval)
87
89
 
@@ -1,21 +1,21 @@
1
1
  assignment_codeval/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
2
  assignment_codeval/ai_benchmark.py,sha256=ZGWWrBeUcaArkuQqS9wY42gxP8g7J2nivJMedYq9RFs,18045
3
- assignment_codeval/canvas_utils.py,sha256=MNtGXc93tHlqudEZsb_6r-5QzV9tSqf3AVxIVGEmk-U,5989
3
+ assignment_codeval/canvas_utils.py,sha256=NF2xw10A1okVtDnP98YRzEiSo7QlqYF-IVAih4-WeLQ,5998
4
4
  assignment_codeval/check_grading.py,sha256=rnNdp8T_XWckDfF1zUi3r70BMe9CPNTD-RfjfarI-CM,5889
5
5
  assignment_codeval/cli.py,sha256=Vl27zNI7kI0QEZoXTIE2aTrraEJWj9vgJiyVWou3vgU,1357
6
6
  assignment_codeval/commons.py,sha256=rr09qHacHoLmmU8S9sBss4_utb0DH8-IyL9Dsj0ycE0,1412
7
7
  assignment_codeval/convertMD2Html.py,sha256=cCfTWwxH-0SKZ7Br-Qn54OabPuEAhHpx3q4Aeqp_NAo,7682
8
8
  assignment_codeval/create_assignment.py,sha256=duGI8wZRpeEnmYPAAth_QXcFHWd185zkjkSJsaeH-GY,11999
9
- assignment_codeval/evaluate.py,sha256=yGkWYjYtXGiR0OF-7Y6Aou9B5TtXUf1pykDmmGCAS0k,36205
9
+ assignment_codeval/evaluate.py,sha256=PlpWGD-aHi_OrZzKmmmDOHN14D1p1BN-9AwWt0YZ4ts,42362
10
10
  assignment_codeval/export_tests.py,sha256=IDAlGAp3zk-K5AFwLr85v4w9_WcYr6HXVF0joi4eYTg,5467
11
11
  assignment_codeval/file_utils.py,sha256=RDgAgmyIOLKvRlxbAr-zILz2A24zSr-r3--p-dxl4G8,2132
12
12
  assignment_codeval/github_connect.py,sha256=AP7mZPhzKKK8fxS-3JMfG6AQ-1wUutfsq-Bt-KG9WUs,4270
13
13
  assignment_codeval/install_assignment.py,sha256=HsXzTex4G-U3t4tb-HyQ2AobBDBPcKeR6oAUDFqBP-A,4599
14
14
  assignment_codeval/recent_comments.py,sha256=lmGjr5rcpdP6Q69AjG8kSsgOFQgdeyL8XcbNvwqGk-Q,9102
15
- assignment_codeval/submissions.py,sha256=Qn9YRbsid47azmg3Ev_aRJHprUOHHuL1I38vibgmfXY,29882
15
+ assignment_codeval/submissions.py,sha256=93lLooN3eI0hTc3ZdUGoMwIQkWragjkuyePuvcU52tk,30431
16
16
  assignment_codeval/test_template.html,sha256=lgWsin5sYE_Pwjp3xqa7_UT-yZ4dWrVqAvNlSBBiNMI,2584
17
- assignment_codeval-0.0.29.dist-info/METADATA,sha256=I7x-a47l5flYi-8VN7jHkEsXM2ZL8Xv9fM7rMrEJYXs,15988
18
- assignment_codeval-0.0.29.dist-info/WHEEL,sha256=YCfwYGOYMi5Jhw2fU4yNgwErybb2IX5PEwBKV4ZbdBo,91
19
- assignment_codeval-0.0.29.dist-info/entry_points.txt,sha256=Pk0eAhsXBpCsgkv6HYLSHTRf5x_zUf-Lg8KK9KsK2B8,66
20
- assignment_codeval-0.0.29.dist-info/top_level.txt,sha256=a_P8W2IWjAELxKlTV2t4rLwWVoTxElxUMvWgWjR_1n4,19
21
- assignment_codeval-0.0.29.dist-info/RECORD,,
17
+ assignment_codeval-0.0.30.dist-info/METADATA,sha256=GeGmxlA9BNfAo_s-4VnpfKKy90LKau0SRPAYIAjfzLY,16730
18
+ assignment_codeval-0.0.30.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
19
+ assignment_codeval-0.0.30.dist-info/entry_points.txt,sha256=Pk0eAhsXBpCsgkv6HYLSHTRf5x_zUf-Lg8KK9KsK2B8,66
20
+ assignment_codeval-0.0.30.dist-info/top_level.txt,sha256=a_P8W2IWjAELxKlTV2t4rLwWVoTxElxUMvWgWjR_1n4,19
21
+ assignment_codeval-0.0.30.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (82.0.0)
2
+ Generator: setuptools (82.0.1)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5