ygrader 2.6.4__tar.gz → 2.6.6__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 (24) hide show
  1. {ygrader-2.6.4/ygrader.egg-info → ygrader-2.6.6}/PKG-INFO +1 -1
  2. {ygrader-2.6.4 → ygrader-2.6.6}/setup.py +1 -1
  3. {ygrader-2.6.4 → ygrader-2.6.6}/ygrader/feedback.py +18 -1
  4. {ygrader-2.6.4 → ygrader-2.6.6}/ygrader/grader.py +21 -2
  5. {ygrader-2.6.4 → ygrader-2.6.6}/ygrader/grading_item.py +57 -42
  6. {ygrader-2.6.4 → ygrader-2.6.6/ygrader.egg-info}/PKG-INFO +1 -1
  7. {ygrader-2.6.4 → ygrader-2.6.6}/LICENSE +0 -0
  8. {ygrader-2.6.4 → ygrader-2.6.6}/setup.cfg +0 -0
  9. {ygrader-2.6.4 → ygrader-2.6.6}/test/test_interactive.py +0 -0
  10. {ygrader-2.6.4 → ygrader-2.6.6}/test/test_unittest.py +0 -0
  11. {ygrader-2.6.4 → ygrader-2.6.6}/ygrader/__init__.py +0 -0
  12. {ygrader-2.6.4 → ygrader-2.6.6}/ygrader/deductions.py +0 -0
  13. {ygrader-2.6.4 → ygrader-2.6.6}/ygrader/grades_csv.py +0 -0
  14. {ygrader-2.6.4 → ygrader-2.6.6}/ygrader/grading_item_config.py +0 -0
  15. {ygrader-2.6.4 → ygrader-2.6.6}/ygrader/remote.py +0 -0
  16. {ygrader-2.6.4 → ygrader-2.6.6}/ygrader/score_input.py +0 -0
  17. {ygrader-2.6.4 → ygrader-2.6.6}/ygrader/send_ctrl_backtick.ahk +0 -0
  18. {ygrader-2.6.4 → ygrader-2.6.6}/ygrader/student_repos.py +0 -0
  19. {ygrader-2.6.4 → ygrader-2.6.6}/ygrader/upstream_merger.py +0 -0
  20. {ygrader-2.6.4 → ygrader-2.6.6}/ygrader/utils.py +0 -0
  21. {ygrader-2.6.4 → ygrader-2.6.6}/ygrader.egg-info/SOURCES.txt +0 -0
  22. {ygrader-2.6.4 → ygrader-2.6.6}/ygrader.egg-info/dependency_links.txt +0 -0
  23. {ygrader-2.6.4 → ygrader-2.6.6}/ygrader.egg-info/requires.txt +0 -0
  24. {ygrader-2.6.4 → ygrader-2.6.6}/ygrader.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ygrader
3
- Version: 2.6.4
3
+ Version: 2.6.6
4
4
  Summary: Grading scripts used in BYU's Electrical and Computer Engineering Department
5
5
  Home-page: https://github.com/byu-cpe/ygrader
6
6
  Author: Jeff Goeders
@@ -4,7 +4,7 @@ setup(
4
4
  name="ygrader",
5
5
  packages=["ygrader"],
6
6
  package_data={"ygrader": ["*.ahk"]},
7
- version="2.6.4",
7
+ version="2.6.6",
8
8
  description="Grading scripts used in BYU's Electrical and Computer Engineering Department",
9
9
  author="Jeff Goeders",
10
10
  author_email="jeff.goeders@gmail.com",
@@ -293,11 +293,19 @@ def assemble_grades(
293
293
 
294
294
  if items_graded and items_not_graded:
295
295
  print_color(
296
- TermColors.YELLOW,
296
+ TermColors.RED,
297
297
  f"Partial grade: {net_id} graded for [{', '.join(items_graded)}] "
298
298
  f"but NOT [{', '.join(items_not_graded)}]",
299
299
  )
300
300
 
301
+ # Check if student has no grades at all
302
+ student_graded = len(items_graded) > 0
303
+ if not student_graded:
304
+ print_color(
305
+ TermColors.YELLOW,
306
+ f"No grades: {net_id} has no grades, receiving 0",
307
+ )
308
+
301
309
  # Get submit info for this student
302
310
  _, effective_due_date, submitted_datetime = (
303
311
  _get_student_key_and_submit_info(
@@ -353,6 +361,7 @@ def assemble_grades(
353
361
  late_penalty_callback=late_penalty_callback,
354
362
  due_date=due_date,
355
363
  due_date_exceptions=due_date_exceptions,
364
+ student_graded=student_graded,
356
365
  )
357
366
 
358
367
  filename = (
@@ -391,6 +400,7 @@ def _generate_student_feedback(
391
400
  late_penalty_callback: Optional[LatePenaltyCallback] = None,
392
401
  due_date: Optional[datetime.datetime] = None,
393
402
  due_date_exceptions: Optional[Dict[str, datetime.datetime]] = None,
403
+ student_graded: bool = True,
394
404
  ) -> str:
395
405
  """Generate the feedback text content for a single student.
396
406
 
@@ -401,6 +411,7 @@ def _generate_student_feedback(
401
411
  late_penalty_callback: Optional callback for calculating late penalty.
402
412
  due_date: The default due date for the assignment.
403
413
  due_date_exceptions: Mapping from net_id to exception due date.
414
+ student_graded: Whether the student was graded at all (False if no submission).
404
415
 
405
416
  Returns:
406
417
  The formatted feedback text.
@@ -517,6 +528,12 @@ def _generate_student_feedback(
517
528
  lines.append(
518
529
  f"{late_label:<{item_col_width}} {-late_penalty_points:>{score_col_width}.1f}"
519
530
  )
531
+ elif not student_graded:
532
+ # Student was never graded (no submission)
533
+ final_score = score_before_late
534
+ lines.append(
535
+ f"{'Late Penalty:':<{item_col_width}} {'No Submission':>{score_col_width}}"
536
+ )
520
537
  else:
521
538
  final_score = score_before_late
522
539
  lines.append(
@@ -357,7 +357,9 @@ class Grader:
357
357
  run_only: bool
358
358
  Whether you only want to run/grade and not build the students code. This will be passed to your
359
359
  callback function, and is useful for labs that take a while to build. You can build all the code
360
- in one pass, then return and grade the code later.
360
+ in one pass, then return and grade the code later. When using GitHub submissions, this will skip
361
+ fetching/cloning repositories and assume they already exist in a good state. If a student's
362
+ repository does not exist, an error will be reported and that student will be skipped.
361
363
  allow_rebuild: bool
362
364
  By default, the program will pass build=True and run=True to your callback on the first invocation,
363
365
  and then allow the grader the option to "re-run" the student's code, where build=False and run=True
@@ -875,10 +877,27 @@ class Grader:
875
877
  def _get_student_code_github(self, row, student_work_path, output=None):
876
878
  if output is None:
877
879
  output = sys.stdout
880
+
881
+ # If run_only mode, skip fetching and just verify the repo exists
882
+ if self.run_only:
883
+ if student_work_path.is_dir() and list(student_work_path.iterdir()):
884
+ print(
885
+ f"run_only mode: Using existing repo at {student_work_path}",
886
+ file=output,
887
+ )
888
+ return True
889
+ msg = f"run_only mode: Repo does not exist at {student_work_path}"
890
+ if output is sys.stdout:
891
+ print_color(TermColors.RED, msg)
892
+ else:
893
+ print(msg, file=output)
894
+ return False
895
+
878
896
  student_work_path.mkdir(parents=True, exist_ok=True)
879
897
 
880
898
  # Clone student repo
881
- print("Student repo url: " + row["github_url"], file=output)
899
+ https_url = student_repos.convert_github_url_format(row["github_url"], to_https=True)
900
+ print("Student repo url: " + https_url, file=output)
882
901
  if not student_repos.clone_repo(
883
902
  row["github_url"], self.github_tag, student_work_path, output=output
884
903
  ):
@@ -144,43 +144,20 @@ class GradeItem:
144
144
  )
145
145
 
146
146
  # Verify workflow hash if configured
147
- if self.grader.workflow_hash is not None:
148
- student_code_path = callback_args.get("student_code_path")
149
- if student_code_path:
150
- workflow_file_path = (
151
- student_code_path / ".github" / "workflows" / "submission.yml"
147
+ workflow_errors = []
148
+ student_code_path = callback_args.get("student_code_path")
149
+ if self.grader.workflow_hash is not None and student_code_path:
150
+ workflow_file_path = (
151
+ student_code_path / ".github" / "workflows" / "submission.yml"
152
+ )
153
+ try:
154
+ verify_workflow_hash(
155
+ workflow_file_path, self.grader.workflow_hash
152
156
  )
153
- try:
154
- verify_workflow_hash(
155
- workflow_file_path, self.grader.workflow_hash
156
- )
157
- except WorkflowHashError as e:
158
- print("")
159
- print_color(TermColors.RED, "=" * 70)
160
- print_color(
161
- TermColors.RED,
162
- "WARNING: WORKFLOW FILE VERIFICATION FAILED!",
163
- )
164
- print_color(TermColors.RED, "=" * 70)
165
- print_color(TermColors.RED, str(e))
166
- print_color(TermColors.RED, "")
167
- print_color(
168
- TermColors.RED,
169
- "This student may have modified the GitHub workflow system.",
170
- )
171
- print_color(
172
- TermColors.RED, "The submission date CANNOT be guaranteed."
173
- )
174
- print_color(TermColors.RED, "")
175
- print_color(
176
- TermColors.RED,
177
- "Please contact the instructor before grading this student.",
178
- )
179
- print_color(TermColors.RED, "=" * 70)
180
- print("")
157
+ except WorkflowHashError as e:
158
+ workflow_errors.append(f"Workflow hash mismatch: {e}")
181
159
 
182
160
  # Display submission date if available and store for later late calculation
183
- student_code_path = callback_args.get("student_code_path")
184
161
  # Store submission time but don't save yet - will be saved only after successful grading
185
162
  pending_submit_time = None
186
163
  if student_code_path:
@@ -188,10 +165,12 @@ class GradeItem:
188
165
  if submission_date_path.is_file():
189
166
  try:
190
167
  with open(submission_date_path, encoding="utf-8") as f:
191
- submission_time = datetime.datetime.strptime(
192
- f.read().strip(),
193
- "%a %b %d %H:%M:%S %Z %Y",
194
- )
168
+ date_str = f.read().strip()
169
+ # Expected format from workflow: "Sun Jan 12 21:51:14 MST 2026"
170
+ submission_time = datetime.datetime.strptime(
171
+ date_str,
172
+ "%a %b %d %H:%M:%S %Z %Y",
173
+ )
195
174
  print_color(
196
175
  TermColors.BLUE,
197
176
  f"Submitted: {submission_time.strftime('%Y-%m-%d %H:%M:%S')}",
@@ -199,10 +178,46 @@ class GradeItem:
199
178
  # Store as ISO format for later late calculation
200
179
  pending_submit_time = submission_time.isoformat()
201
180
  except (ValueError, IOError) as e:
202
- print_color(
203
- TermColors.YELLOW,
204
- f"Could not parse submission date: {e}",
205
- )
181
+ workflow_errors.append(f"Submission date parsing failed: {e}")
182
+
183
+ # Print unified warning if any workflow-related errors occurred
184
+ if workflow_errors:
185
+ print("")
186
+ print_color(TermColors.RED, "=" * 70)
187
+ print_color(
188
+ TermColors.RED,
189
+ "WARNING: WORKFLOW VERIFICATION FAILED!",
190
+ )
191
+ print_color(TermColors.RED, "=" * 70)
192
+ for err in workflow_errors:
193
+ print_color(TermColors.RED, f" - {err}")
194
+ print_color(TermColors.RED, "")
195
+ print_color(
196
+ TermColors.RED,
197
+ "This student may have modified the GitHub workflow system.",
198
+ )
199
+ print_color(
200
+ TermColors.RED, "The submission date CANNOT be guaranteed."
201
+ )
202
+ print_color(TermColors.RED, "")
203
+ print_color(
204
+ TermColors.RED,
205
+ "Please contact the instructor before grading this student.",
206
+ )
207
+ print_color(TermColors.RED, "=" * 70)
208
+ print("")
209
+
210
+ # Ask for confirmation before grading
211
+ while True:
212
+ response = input(
213
+ "Do you want to grade this student anyway? [y/n]: "
214
+ ).strip().lower()
215
+ if response in ("y", "yes"):
216
+ break
217
+ if response in ("n", "no"):
218
+ print_color(TermColors.BLUE, "Skipping student")
219
+ return False
220
+ print("Please enter 'y' or 'n'")
206
221
 
207
222
  # Process callback result:
208
223
  # - None: interactive mode (prompt for deductions)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ygrader
3
- Version: 2.6.4
3
+ Version: 2.6.6
4
4
  Summary: Grading scripts used in BYU's Electrical and Computer Engineering Department
5
5
  Home-page: https://github.com/byu-cpe/ygrader
6
6
  Author: Jeff Goeders
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes