assignment-codeval 0.0.25__py3-none-any.whl → 0.0.26__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.
@@ -28,6 +28,7 @@ def get_testing_path(filename: str) -> str:
28
28
  test_args = ""
29
29
  cmps = []
30
30
  timeout_val = 10
31
+ output_length_limit = 4096
31
32
  expected_exit_code = -1
32
33
  test_case_count = 0
33
34
  test_case_hint = ""
@@ -643,6 +644,19 @@ def timeout(timeout_sec):
643
644
  timeout_val = float(timeout_sec)
644
645
 
645
646
 
647
+ def output_length(length):
648
+ """Specifies the maximum number of bytes of diff output to render. Defaults to 4096.
649
+
650
+ Arguments:
651
+ length: maximum output length in bytes
652
+
653
+ Returns:
654
+ None
655
+ """
656
+ global output_length_limit
657
+ output_length_limit = int(length)
658
+
659
+
646
660
  def exit_code(test_case_exit_code):
647
661
  """Specifies the expected exit code for a test case. Defaults to zero.
648
662
 
@@ -726,6 +740,7 @@ tag_func_map = {
726
740
  "EB": check_error_bare,
727
741
  "HINT": hint,
728
742
  "TO": timeout,
743
+ "OLEN": output_length,
729
744
  "X": exit_code,
730
745
  "SS": start_server,
731
746
  }
@@ -907,6 +922,72 @@ def parse_diff(diff_lines: list[str], testing_dir: str):
907
922
  outfile.write(line)
908
923
 
909
924
 
925
+ def _render_diff_output(raw_bytes: bytes) -> str:
926
+ """Render raw bytes for diff output, making unprintable characters visible.
927
+
928
+ Replaces the shell pipeline `cat -te | head -22` with pure Python.
929
+ """
930
+ result = []
931
+ i = 0
932
+ n = len(raw_bytes)
933
+
934
+ while i < n:
935
+ b = raw_bytes[i]
936
+
937
+ if b == 0x0A: # newline
938
+ result.append("$\n")
939
+ i += 1
940
+ elif b == 0x20 or (0x21 <= b <= 0x7E): # space or printable ASCII
941
+ result.append(chr(b))
942
+ i += 1
943
+ elif b <= 0x1F: # control chars (except newline handled above)
944
+ result.append("^" + chr(b + 0x40))
945
+ i += 1
946
+ elif b == 0x7F: # DEL
947
+ result.append("^?")
948
+ i += 1
949
+ elif b >= 0x80:
950
+ # Try to decode as UTF-8 multi-byte sequence
951
+ seq_len = 0
952
+ if (b & 0xE0) == 0xC0:
953
+ seq_len = 2
954
+ elif (b & 0xF0) == 0xE0:
955
+ seq_len = 3
956
+ elif (b & 0xF8) == 0xF0:
957
+ seq_len = 4
958
+
959
+ decoded_char = None
960
+ if seq_len >= 2 and i + seq_len <= n:
961
+ try:
962
+ decoded_char = raw_bytes[i:i + seq_len].decode("utf-8")
963
+ except UnicodeDecodeError:
964
+ pass
965
+
966
+ if decoded_char and len(decoded_char) == 1 and decoded_char.isprintable():
967
+ result.append(decoded_char)
968
+ i += seq_len
969
+ elif decoded_char and len(decoded_char) == 1:
970
+ # Valid UTF-8 but non-printable: render each byte as \xCC
971
+ for j in range(seq_len):
972
+ result.append(f"\\x{raw_bytes[i + j]:02X}")
973
+ i += seq_len
974
+ else:
975
+ # Invalid UTF-8 byte
976
+ result.append(f"\\x{b:02X}")
977
+ i += 1
978
+ else:
979
+ result.append(chr(b))
980
+ i += 1
981
+
982
+ output = "".join(result)
983
+
984
+ # If output doesn't end with \n, append $ at the very end
985
+ if output and not output.endswith("\n"):
986
+ output += "$"
987
+
988
+ return output
989
+
990
+
910
991
  def _find_touched_files(pre_run_timestamp):
911
992
  """Find files touched (modified or accessed) since pre_run_timestamp.
912
993
 
@@ -960,24 +1041,22 @@ def check_test():
960
1041
  # Difflog handling
961
1042
  with open(get_testing_path("difflog"), "w") as outfile:
962
1043
  diff_popen = subprocess.Popen(
963
- f"diff -U1 -a ./{TESTING_DIR}/youroutput ./{TESTING_DIR}/expectedoutput | cat -te | head -22",
964
- shell=True,
965
- stdout=outfile,
966
- stderr=outfile,
967
- text=True,
1044
+ ["diff", "-U1", "-a",
1045
+ f"./{TESTING_DIR}/youroutput", f"./{TESTING_DIR}/expectedoutput"],
1046
+ stdout=subprocess.PIPE, stderr=subprocess.PIPE,
968
1047
  )
969
- diff_popen.communicate()
1048
+ raw_output, _ = diff_popen.communicate()
1049
+ outfile.write(_render_diff_output(raw_output[:output_length_limit]))
970
1050
 
971
1051
  # Append to difflog second time around
972
1052
  with open(get_testing_path("difflog"), "a") as outfile:
973
1053
  diff_popen = subprocess.Popen(
974
- f"diff -U1 -a ./{TESTING_DIR}/yourerror ./{TESTING_DIR}/expectederror | cat -te | head -22",
975
- shell=True,
976
- stdout=outfile,
977
- stderr=outfile,
978
- text=True,
1054
+ ["diff", "-U1", "-a",
1055
+ f"./{TESTING_DIR}/yourerror", f"./{TESTING_DIR}/expectederror"],
1056
+ stdout=subprocess.PIPE, stderr=subprocess.PIPE,
979
1057
  )
980
- diff_popen.communicate()
1058
+ raw_output, _ = diff_popen.communicate()
1059
+ outfile.write(_render_diff_output(raw_output[:output_length_limit]))
981
1060
 
982
1061
  # Now read all the lines to accumulate both diffs
983
1062
  with open(get_testing_path("difflog"), "r") as infile:
@@ -180,6 +180,37 @@ def delete_submission_comment(canvas, course_id, assignment_id, user_id, comment
180
180
  resp.raise_for_status()
181
181
 
182
182
 
183
+ def _parse_substitutions_file(filepath):
184
+ """Parse a substitutions file where each line has the form /pattern/replacement/.
185
+
186
+ The first character of each line defines the delimiter. The delimiter must appear
187
+ exactly once in-between (separating pattern from replacement) and once at the end.
188
+ Returns a list of (pattern, replacement) tuples.
189
+ Raises click.ClickException on parse errors.
190
+ """
191
+ substitutions = []
192
+ with open(filepath, "r") as fd:
193
+ for lineno, line in enumerate(fd, start=1):
194
+ line = line.rstrip("\n")
195
+ if not line:
196
+ continue
197
+ delim = line[0]
198
+ parts = line[1:].split(delim)
199
+ if len(parts) != 3 or parts[2] != "":
200
+ raise click.ClickException(
201
+ f"{filepath}:{lineno}: invalid substitution line: {line}"
202
+ )
203
+ substitutions.append((parts[0], parts[1]))
204
+ return substitutions
205
+
206
+
207
+ def _apply_substitutions(comment, substitutions):
208
+ """Apply a list of (pattern, replacement) substitutions to comment text."""
209
+ for pattern, replacement in substitutions:
210
+ comment = comment.replace(pattern, replacement)
211
+ return comment
212
+
213
+
183
214
  @click.command()
184
215
  @click.argument("submissions_dir", metavar="SUBMISSIONS_DIR")
185
216
  @click.option("--codeval-prefix", help="prefix for codeval comments", default="codeval: ", show_default=True)
@@ -214,8 +245,12 @@ def upload_submission_comments(submissions_dir, codeval_prefix, delete):
214
245
  write_html_file(dirpath)
215
246
  file_id = upload_file_for_comment(canvas, course.id, assignment.id, student_id, f"{dirpath}/results.html")
216
247
  with open(f"{dirpath}/comments.txt", "r") as fd:
217
- comment = fd.read()
218
- comment = comment.replace("\0", "\\0").strip()
248
+ comment = fd.read(4096)
249
+ comment = comment.replace("\0", "\\0").strip().replace("<", "&lt;")
250
+ subs_file = f"{dirpath}/SUBSTITUTIONS.txt"
251
+ if os.path.isfile(subs_file):
252
+ substitutions = _parse_substitutions_file(subs_file)
253
+ comment = _apply_substitutions(comment, substitutions)
219
254
  if delete:
220
255
  all_comments = sorted(submission.submission_comments, key=lambda c: c['created_at'])
221
256
  if all_comments:
@@ -226,7 +261,7 @@ def upload_submission_comments(submissions_dir, codeval_prefix, delete):
226
261
  f'{canvas_url}/api/v1/courses/{course.id}/assignments/{assignment.id}/submissions/{student_id}',
227
262
  headers={'Authorization': f'Bearer {canvas_token}'},
228
263
  data={
229
- 'comment[text_comment]': f'{codeval_prefix.rstrip()}\n{comment}',
264
+ 'comment[text_comment]': f'{codeval_prefix.rstrip()}\n<pre>{comment}</pre>',
230
265
  'comment[file_ids][]': file_id,
231
266
  }
232
267
  ).raise_for_status()
@@ -312,6 +347,9 @@ def evaluate_submissions(codeval_dir, submissions_dir):
312
347
  if not move_to_next_submission:
313
348
  for zf_name in zip_files:
314
349
  with ZipFile(os.path.join(codeval_dir, zf_name)) as zf:
350
+ if "SUBSTITUTIONS.txt" in zf.namelist():
351
+ with open(os.path.join(dirpath, "SUBSTITUTIONS.txt"), "wb") as out_f:
352
+ out_f.write(zf.read("SUBSTITUTIONS.txt"))
315
353
  for f in zf.infolist():
316
354
  dest_dir = os.path.join(submission_dir, assignment_working_dir)
317
355
  zf.extract(f, dest_dir)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: assignment-codeval
3
- Version: 0.0.25
3
+ Version: 0.0.26
4
4
  Summary: CodEval for evaluating programming assignments
5
5
  Requires-Python: >=3.12
6
6
  Description-Content-Type: text/markdown
@@ -6,16 +6,16 @@ assignment_codeval/cli.py,sha256=Vl27zNI7kI0QEZoXTIE2aTrraEJWj9vgJiyVWou3vgU,135
6
6
  assignment_codeval/commons.py,sha256=rr09qHacHoLmmU8S9sBss4_utb0DH8-IyL9Dsj0ycE0,1412
7
7
  assignment_codeval/convertMD2Html.py,sha256=4yu41U4Up1oGI5vDZGelmuogwFmEZrRS55M4ZHbRfaE,6243
8
8
  assignment_codeval/create_assignment.py,sha256=oZ-0LadVb6R-rXVg0Lz5itziaalIvFoxd71cIxgMfow,10815
9
- assignment_codeval/evaluate.py,sha256=knUWxOmnQNeod1wKSoMBYIhqOp5q60BgOxAzZbp2xrs,33720
9
+ assignment_codeval/evaluate.py,sha256=er21lA-eohP1BfHPYPWRw9m0a_IOeljMNtcidOYacJY,36253
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=e8UQL2_9dsPlf-NYFrGSaM6h6XYnTf_MW2jWhtEsb38,9097
15
- assignment_codeval/submissions.py,sha256=fIhov4fdRVNDw5xjbRHs4zqBtyNM1f8cy220-G1Vqnk,26598
15
+ assignment_codeval/submissions.py,sha256=YbwKynZwWsmcyytwRi3Kuxq3WN-V-vvshO9JrNJoxAs,28333
16
16
  assignment_codeval/test_template.html,sha256=lgWsin5sYE_Pwjp3xqa7_UT-yZ4dWrVqAvNlSBBiNMI,2584
17
- assignment_codeval-0.0.25.dist-info/METADATA,sha256=8Gn38IZ3P6JhWRjkPRG0keYNjq9GfgMBkv1u27Ha7iQ,15589
18
- assignment_codeval-0.0.25.dist-info/WHEEL,sha256=YCfwYGOYMi5Jhw2fU4yNgwErybb2IX5PEwBKV4ZbdBo,91
19
- assignment_codeval-0.0.25.dist-info/entry_points.txt,sha256=Pk0eAhsXBpCsgkv6HYLSHTRf5x_zUf-Lg8KK9KsK2B8,66
20
- assignment_codeval-0.0.25.dist-info/top_level.txt,sha256=a_P8W2IWjAELxKlTV2t4rLwWVoTxElxUMvWgWjR_1n4,19
21
- assignment_codeval-0.0.25.dist-info/RECORD,,
17
+ assignment_codeval-0.0.26.dist-info/METADATA,sha256=CvKtHeb1kR3x4Sz0ojGIKui1v_Q1ejiColTIShfMOfg,15589
18
+ assignment_codeval-0.0.26.dist-info/WHEEL,sha256=YCfwYGOYMi5Jhw2fU4yNgwErybb2IX5PEwBKV4ZbdBo,91
19
+ assignment_codeval-0.0.26.dist-info/entry_points.txt,sha256=Pk0eAhsXBpCsgkv6HYLSHTRf5x_zUf-Lg8KK9KsK2B8,66
20
+ assignment_codeval-0.0.26.dist-info/top_level.txt,sha256=a_P8W2IWjAELxKlTV2t4rLwWVoTxElxUMvWgWjR_1n4,19
21
+ assignment_codeval-0.0.26.dist-info/RECORD,,