phigrade 3.4.0__tar.gz → 3.4.2__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 (57) hide show
  1. {phigrade-3.4.0 → phigrade-3.4.2}/PKG-INFO +52 -28
  2. {phigrade-3.4.0 → phigrade-3.4.2}/README.md +51 -27
  3. {phigrade-3.4.0 → phigrade-3.4.2}/examples/colab/README.md +15 -5
  4. {phigrade-3.4.0 → phigrade-3.4.2}/examples/colab/lab1_instructor.ipynb +9 -4
  5. {phigrade-3.4.0 → phigrade-3.4.2}/examples/colab/lab1_student.ipynb +7 -6
  6. phigrade-3.4.2/examples/group/README.md +71 -0
  7. phigrade-3.4.2/examples/group/phigrade.yaml +61 -0
  8. {phigrade-3.4.0 → phigrade-3.4.2}/phigrade/phigrade.py +203 -9
  9. phigrade-3.4.2/phigrade/pytest_plugin.py +90 -0
  10. {phigrade-3.4.0 → phigrade-3.4.2}/pyproject.toml +1 -1
  11. {phigrade-3.4.0 → phigrade-3.4.2}/tests/test_notebook_ipytest.py +25 -0
  12. {phigrade-3.4.0 → phigrade-3.4.2}/tests/test_phigrade_config.py +125 -0
  13. {phigrade-3.4.0 → phigrade-3.4.2}/tests/test_submit_colab_notebook.py +83 -2
  14. {phigrade-3.4.0 → phigrade-3.4.2}/tests/test_teacher_mode_endpoints.py +159 -1
  15. phigrade-3.4.0/phigrade/pytest_plugin.py +0 -46
  16. {phigrade-3.4.0 → phigrade-3.4.2}/.gitignore +0 -0
  17. {phigrade-3.4.0 → phigrade-3.4.2}/LICENSE +0 -0
  18. {phigrade-3.4.0 → phigrade-3.4.2}/examples/colab/phigrade.yaml +0 -0
  19. {phigrade-3.4.0 → phigrade-3.4.2}/examples/phigrade.yaml +0 -0
  20. {phigrade-3.4.0 → phigrade-3.4.2}/examples/phigrade_db.json +0 -0
  21. {phigrade-3.4.0 → phigrade-3.4.2}/examples/run_teacher_mode.py +0 -0
  22. {phigrade-3.4.0 → phigrade-3.4.2}/examples/simplefns.py +0 -0
  23. {phigrade-3.4.0 → phigrade-3.4.2}/examples/test_simplefns.py +0 -0
  24. {phigrade-3.4.0 → phigrade-3.4.2}/phigrade/__init__.py +0 -0
  25. {phigrade-3.4.0 → phigrade-3.4.2}/phigrade/app.py +0 -0
  26. {phigrade-3.4.0 → phigrade-3.4.2}/phigrade/cli.py +0 -0
  27. {phigrade-3.4.0 → phigrade-3.4.2}/phigrade/colab.py +0 -0
  28. {phigrade-3.4.0 → phigrade-3.4.2}/phigrade/compare.py +0 -0
  29. {phigrade-3.4.0 → phigrade-3.4.2}/phigrade/credentials.py +0 -0
  30. {phigrade-3.4.0 → phigrade-3.4.2}/phigrade/errors.py +0 -0
  31. {phigrade-3.4.0 → phigrade-3.4.2}/phigrade/gradescope_output.py +0 -0
  32. {phigrade-3.4.0 → phigrade-3.4.2}/phigrade/messages.py +0 -0
  33. {phigrade-3.4.0 → phigrade-3.4.2}/phigrade/server.py +0 -0
  34. {phigrade-3.4.0 → phigrade-3.4.2}/tests/__init__.py +0 -0
  35. {phigrade-3.4.0 → phigrade-3.4.2}/tests/conftest.py +0 -0
  36. {phigrade-3.4.0 → phigrade-3.4.2}/tests/notebook_harness.py +0 -0
  37. {phigrade-3.4.0 → phigrade-3.4.2}/tests/test_all_close_local.py +0 -0
  38. {phigrade-3.4.0 → phigrade-3.4.2}/tests/test_cli.py +0 -0
  39. {phigrade-3.4.0 → phigrade-3.4.2}/tests/test_client_setup_errors.py +0 -0
  40. {phigrade-3.4.0 → phigrade-3.4.2}/tests/test_colab.py +0 -0
  41. {phigrade-3.4.0 → phigrade-3.4.2}/tests/test_colab_api_key.py +0 -0
  42. {phigrade-3.4.0 → phigrade-3.4.2}/tests/test_compare_keyvalue.py +0 -0
  43. {phigrade-3.4.0 → phigrade-3.4.2}/tests/test_compare_rowmatch.py +0 -0
  44. {phigrade-3.4.0 → phigrade-3.4.2}/tests/test_comparison_failure_message.py +0 -0
  45. {phigrade-3.4.0 → phigrade-3.4.2}/tests/test_credentials.py +0 -0
  46. {phigrade-3.4.0 → phigrade-3.4.2}/tests/test_errors.py +0 -0
  47. {phigrade-3.4.0 → phigrade-3.4.2}/tests/test_failure_message_local.py +0 -0
  48. {phigrade-3.4.0 → phigrade-3.4.2}/tests/test_gradescope_output.py +0 -0
  49. {phigrade-3.4.0 → phigrade-3.4.2}/tests/test_group_submission.py +0 -0
  50. {phigrade-3.4.0 → phigrade-3.4.2}/tests/test_is_equal_local.py +0 -0
  51. {phigrade-3.4.0 → phigrade-3.4.2}/tests/test_local_server.py +0 -0
  52. {phigrade-3.4.0 → phigrade-3.4.2}/tests/test_messages.py +0 -0
  53. {phigrade-3.4.0 → phigrade-3.4.2}/tests/test_notebook_config_errors.py +0 -0
  54. {phigrade-3.4.0 → phigrade-3.4.2}/tests/test_phigrade_aggregation_local.py +0 -0
  55. {phigrade-3.4.0 → phigrade-3.4.2}/tests/test_public_api_names.py +0 -0
  56. {phigrade-3.4.0 → phigrade-3.4.2}/tests/test_real_backend_integration.py +0 -0
  57. {phigrade-3.4.0 → phigrade-3.4.2}/tests/test_student_mode_no_persistence.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: phigrade
3
- Version: 3.4.0
3
+ Version: 3.4.2
4
4
  Summary: A lightweight autograder where tests run locally and data is stored remotely
5
5
  Project-URL: Repository, https://github.com/mld-instructors/phigrade
6
6
  Author-email: Matt Gormley <mgormley@cs.cmu.edu>, Jacob Rast <jrast@andrew.cmu.edu>
@@ -533,16 +533,29 @@ but a Colab notebook raises `PhiGradeUsageError`, naming the config key and sayi
533
533
  set it `false` for local runs, rather than silently submitting without the notebook the
534
534
  student believes they sent.
535
535
 
536
- > **Restart the session before the run you want recorded.** phigrade caches
537
- > the notebook it attaches, the submission it creates, and `phigrade.yaml`
538
- > itself for the life of the kernel — a plain `pytest` process is fresh every
539
- > time, but a notebook kernel is not. The first `%%ipytest` cell you run in a
540
- > session captures the notebook and the submission at that moment; editing
541
- > code and re-running `%%ipytest` afterwards submits again, but the attached
542
- > `.ipynb` and the config are still the ones from that first run, not your
543
- > latest edits. Before the run you actually want graded, use **Runtime →
544
- > Restart session and run all** so the notebook, the submission, and
545
- > `phigrade.yaml` are all captured fresh.
536
+ > **Each `%%ipytest` run starts fresh, even without restarting the
537
+ > kernel — with one caveat for the notebook itself.** phigrade caches the
538
+ > notebook it attaches, the submission it creates, and `phigrade.yaml` at
539
+ > module level — a plain `pytest` process is fresh every time, but a notebook
540
+ > kernel is not, and this used to mean a second `%%ipytest` run reused the
541
+ > first run's submission and never noticed later edits. A
542
+ > `pytest_sessionstart` hook now clears that cache at the start of every
543
+ > pytest session, so editing code or `phigrade.yaml` and re-running
544
+ > `%%ipytest` submits your latest edits, not the first run's. What this
545
+ > guarantees for the notebook itself is narrower: each run *re-asks Colab*
546
+ > for it (`google.colab._message`'s `get_ipynb`) rather than replaying a
547
+ > cached copy, but not that Colab answers with unsaved editor state. **Save
548
+ > the notebook (Ctrl/Cmd+S) before the run you want graded.**
549
+ >
550
+ > **Exception: switching between teacher and student mode against the local
551
+ > server needs a restart.** `use_local_server: true` starts one server
552
+ > process for the life of the kernel, and that process is locked to whichever
553
+ > mode — and `local_server_db_file` — it first saw: `app._setup_db` uses
554
+ > `teacher_mode` at that moment to decide between the real reference file and
555
+ > a throwaway temp copy, so a later, disagreeing config could otherwise read
556
+ > or write the wrong one. phigrade refuses instead of doing that silently. If
557
+ > you flip `PHIGRADE_TEACHER_MODE` or `teacher_mode` mid-kernel and hit that
558
+ > error, use **Runtime → Restart session and run all**.
546
559
 
547
560
  > **Publish the answer key from the notebook, not from a `.py` file.** A
548
561
  > test defined in a cell belongs to module `__main__`, and the server
@@ -561,15 +574,20 @@ student believes they sent.
561
574
  > ```
562
575
  >
563
576
  > or set `teacher_mode: true` in the `%%writefile phigrade.yaml` cell.
564
- > Either way, use **Runtime → Restart session and run all** (see the
565
- > stale-notebook warning above) so the setting is in effect before the
566
- > tests run.
577
+ > Either way, re-run the `%%ipytest` cell after setting it — the next pytest
578
+ > session reloads the environment and `phigrade.yaml` fresh (see above). If
579
+ > you are running against the local server and this switches modes from an
580
+ > earlier run in the same kernel, restart the session first (see the
581
+ > exception above), or the run raises rather than publishing.
567
582
 
568
583
  ## Configuration
569
584
 
570
585
  The only configuration source is a YAML file named `phigrade.yaml`, located **in the
571
586
  directory containing the test module** that defines the `@weight` function — not the
572
- current working directory. It is loaded once and cached for the life of the process.
587
+ current working directory. It is loaded once per pytest session and cached for
588
+ the rest of that session — one session per `pytest` process outside a notebook,
589
+ and, since the [Colab notebooks](#colab-notebooks) section above, one session per
590
+ `%%ipytest` run in a single kernel.
573
591
 
574
592
  Five keys can be overridden from the environment — see
575
593
  [Environment overrides](#environment-overrides) below. No other key can.
@@ -628,7 +646,8 @@ Local answer key: slot_a_key.json (PHIGRADE_LOCAL_DB_FILE)
628
646
 
629
647
  That line is the whole point of announcing: a variable exported once in a shell profile
630
648
  must not silently republish an answer key, or redirect a submission, weeks later. The
631
- config is loaded once per process, so each line appears at most once per `pytest` run.
649
+ config is loaded once per pytest session, so each line appears at most once per
650
+ session (see [Configuration](#configuration) above).
632
651
 
633
652
  ### Submission Slots
634
653
 
@@ -667,9 +686,10 @@ Either way the resolved slot is announced — `Submitting to: Human only` — so
667
686
  submissions. On the environment-variable path that line goes to stdout, which
668
687
  pytest captures and prints on failure (or always, under `-s`).
669
688
 
670
- The prompt reads and writes `/dev/tty` directly rather than stdin/stdout, so it
671
- works under a plain `pytest` run **with no `-s` flag** — pytest's output capture
672
- never sees it.
689
+ The prompt reads and writes the terminal device directly rather than stdin/stdout
690
+ — `/dev/tty` on macOS and Linux, and `CONIN$`/`CONOUT$` on Windows, which has no
691
+ single duplex device — so it works under a plain `pytest` run **with no `-s`
692
+ flag**: pytest's output capture never sees it.
673
693
 
674
694
  Some runs have no terminal that can be prompted on, and they **must** set
675
695
  `PHIGRADE_SLOT` to one of the slot names:
@@ -678,14 +698,18 @@ Some runs have no terminal that can be prompted on, and they **must** set
678
698
  export PHIGRADE_SLOT="Human only"
679
699
  ```
680
700
 
681
- That covers non-interactive environments — a Gradescope autograder, CI — and two
682
- cases that would otherwise hang: `pytest -n auto` (each xdist worker is its own
683
- process, so N workers would interleave N prompts on one terminal) and any
684
- background job (`nohup pytest &` under a job-control shell, where reading the
685
- terminal raises `SIGTTIN` and suspends the process). Both are detected and turned
686
- into the same error naming the valid slot names.
687
-
688
- Because the config is loaded once and cached for the life of the process (see
701
+ That covers non-interactive environments — a Gradescope autograder, CI, or a
702
+ Windows process with no console at all (a `pythonw` run, or native Windows
703
+ Python under Git Bash/mintty) — and three cases that would otherwise hang:
704
+ `pytest -n auto` (each xdist worker is its own process, so N workers would
705
+ interleave N prompts on one terminal), any background job (`nohup pytest &`
706
+ under a job-control shell, where reading the terminal raises `SIGTTIN` and
707
+ suspends the process — POSIX only), and a notebook kernel, which on Windows
708
+ can inherit the console of whatever launched it and would prompt into a window
709
+ the student is not looking at. Each is detected and turned into the same error
710
+ naming the valid slot names.
711
+
712
+ Because the config is loaded once and cached for the rest of the session (see
689
713
  above), a `pytest` run resolves the slot — and therefore prompts, if it prompts at
690
714
  all — at most once, no matter how many tests run.
691
715
 
@@ -698,7 +722,7 @@ gets a `Sub-unit-check reference not found` error on every checkpoint, naming th
698
722
  assessment, module, function and checkpoint it looked for.
699
723
 
700
724
  So an instructor must publish once per slot. Because the config is cached per
701
- process, that means one process per slot:
725
+ session, that means one `pytest` process (or one `%%ipytest` run) per slot:
702
726
 
703
727
  ```bash
704
728
  PHIGRADE_TEACHER_MODE=true PHIGRADE_SLOT="Human only" pytest test_mysolution.py
@@ -508,16 +508,29 @@ but a Colab notebook raises `PhiGradeUsageError`, naming the config key and sayi
508
508
  set it `false` for local runs, rather than silently submitting without the notebook the
509
509
  student believes they sent.
510
510
 
511
- > **Restart the session before the run you want recorded.** phigrade caches
512
- > the notebook it attaches, the submission it creates, and `phigrade.yaml`
513
- > itself for the life of the kernel — a plain `pytest` process is fresh every
514
- > time, but a notebook kernel is not. The first `%%ipytest` cell you run in a
515
- > session captures the notebook and the submission at that moment; editing
516
- > code and re-running `%%ipytest` afterwards submits again, but the attached
517
- > `.ipynb` and the config are still the ones from that first run, not your
518
- > latest edits. Before the run you actually want graded, use **Runtime →
519
- > Restart session and run all** so the notebook, the submission, and
520
- > `phigrade.yaml` are all captured fresh.
511
+ > **Each `%%ipytest` run starts fresh, even without restarting the
512
+ > kernel — with one caveat for the notebook itself.** phigrade caches the
513
+ > notebook it attaches, the submission it creates, and `phigrade.yaml` at
514
+ > module level — a plain `pytest` process is fresh every time, but a notebook
515
+ > kernel is not, and this used to mean a second `%%ipytest` run reused the
516
+ > first run's submission and never noticed later edits. A
517
+ > `pytest_sessionstart` hook now clears that cache at the start of every
518
+ > pytest session, so editing code or `phigrade.yaml` and re-running
519
+ > `%%ipytest` submits your latest edits, not the first run's. What this
520
+ > guarantees for the notebook itself is narrower: each run *re-asks Colab*
521
+ > for it (`google.colab._message`'s `get_ipynb`) rather than replaying a
522
+ > cached copy, but not that Colab answers with unsaved editor state. **Save
523
+ > the notebook (Ctrl/Cmd+S) before the run you want graded.**
524
+ >
525
+ > **Exception: switching between teacher and student mode against the local
526
+ > server needs a restart.** `use_local_server: true` starts one server
527
+ > process for the life of the kernel, and that process is locked to whichever
528
+ > mode — and `local_server_db_file` — it first saw: `app._setup_db` uses
529
+ > `teacher_mode` at that moment to decide between the real reference file and
530
+ > a throwaway temp copy, so a later, disagreeing config could otherwise read
531
+ > or write the wrong one. phigrade refuses instead of doing that silently. If
532
+ > you flip `PHIGRADE_TEACHER_MODE` or `teacher_mode` mid-kernel and hit that
533
+ > error, use **Runtime → Restart session and run all**.
521
534
 
522
535
  > **Publish the answer key from the notebook, not from a `.py` file.** A
523
536
  > test defined in a cell belongs to module `__main__`, and the server
@@ -536,15 +549,20 @@ student believes they sent.
536
549
  > ```
537
550
  >
538
551
  > or set `teacher_mode: true` in the `%%writefile phigrade.yaml` cell.
539
- > Either way, use **Runtime → Restart session and run all** (see the
540
- > stale-notebook warning above) so the setting is in effect before the
541
- > tests run.
552
+ > Either way, re-run the `%%ipytest` cell after setting it — the next pytest
553
+ > session reloads the environment and `phigrade.yaml` fresh (see above). If
554
+ > you are running against the local server and this switches modes from an
555
+ > earlier run in the same kernel, restart the session first (see the
556
+ > exception above), or the run raises rather than publishing.
542
557
 
543
558
  ## Configuration
544
559
 
545
560
  The only configuration source is a YAML file named `phigrade.yaml`, located **in the
546
561
  directory containing the test module** that defines the `@weight` function — not the
547
- current working directory. It is loaded once and cached for the life of the process.
562
+ current working directory. It is loaded once per pytest session and cached for
563
+ the rest of that session — one session per `pytest` process outside a notebook,
564
+ and, since the [Colab notebooks](#colab-notebooks) section above, one session per
565
+ `%%ipytest` run in a single kernel.
548
566
 
549
567
  Five keys can be overridden from the environment — see
550
568
  [Environment overrides](#environment-overrides) below. No other key can.
@@ -603,7 +621,8 @@ Local answer key: slot_a_key.json (PHIGRADE_LOCAL_DB_FILE)
603
621
 
604
622
  That line is the whole point of announcing: a variable exported once in a shell profile
605
623
  must not silently republish an answer key, or redirect a submission, weeks later. The
606
- config is loaded once per process, so each line appears at most once per `pytest` run.
624
+ config is loaded once per pytest session, so each line appears at most once per
625
+ session (see [Configuration](#configuration) above).
607
626
 
608
627
  ### Submission Slots
609
628
 
@@ -642,9 +661,10 @@ Either way the resolved slot is announced — `Submitting to: Human only` — so
642
661
  submissions. On the environment-variable path that line goes to stdout, which
643
662
  pytest captures and prints on failure (or always, under `-s`).
644
663
 
645
- The prompt reads and writes `/dev/tty` directly rather than stdin/stdout, so it
646
- works under a plain `pytest` run **with no `-s` flag** — pytest's output capture
647
- never sees it.
664
+ The prompt reads and writes the terminal device directly rather than stdin/stdout
665
+ — `/dev/tty` on macOS and Linux, and `CONIN$`/`CONOUT$` on Windows, which has no
666
+ single duplex device — so it works under a plain `pytest` run **with no `-s`
667
+ flag**: pytest's output capture never sees it.
648
668
 
649
669
  Some runs have no terminal that can be prompted on, and they **must** set
650
670
  `PHIGRADE_SLOT` to one of the slot names:
@@ -653,14 +673,18 @@ Some runs have no terminal that can be prompted on, and they **must** set
653
673
  export PHIGRADE_SLOT="Human only"
654
674
  ```
655
675
 
656
- That covers non-interactive environments — a Gradescope autograder, CI — and two
657
- cases that would otherwise hang: `pytest -n auto` (each xdist worker is its own
658
- process, so N workers would interleave N prompts on one terminal) and any
659
- background job (`nohup pytest &` under a job-control shell, where reading the
660
- terminal raises `SIGTTIN` and suspends the process). Both are detected and turned
661
- into the same error naming the valid slot names.
662
-
663
- Because the config is loaded once and cached for the life of the process (see
676
+ That covers non-interactive environments — a Gradescope autograder, CI, or a
677
+ Windows process with no console at all (a `pythonw` run, or native Windows
678
+ Python under Git Bash/mintty) — and three cases that would otherwise hang:
679
+ `pytest -n auto` (each xdist worker is its own process, so N workers would
680
+ interleave N prompts on one terminal), any background job (`nohup pytest &`
681
+ under a job-control shell, where reading the terminal raises `SIGTTIN` and
682
+ suspends the process — POSIX only), and a notebook kernel, which on Windows
683
+ can inherit the console of whatever launched it and would prompt into a window
684
+ the student is not looking at. Each is detected and turned into the same error
685
+ naming the valid slot names.
686
+
687
+ Because the config is loaded once and cached for the rest of the session (see
664
688
  above), a `pytest` run resolves the slot — and therefore prompts, if it prompts at
665
689
  all — at most once, no matter how many tests run.
666
690
 
@@ -673,7 +697,7 @@ gets a `Sub-unit-check reference not found` error on every checkpoint, naming th
673
697
  assessment, module, function and checkpoint it looked for.
674
698
 
675
699
  So an instructor must publish once per slot. Because the config is cached per
676
- process, that means one process per slot:
700
+ session, that means one `pytest` process (or one `%%ipytest` run) per slot:
677
701
 
678
702
  ```bash
679
703
  PHIGRADE_TEACHER_MODE=true PHIGRADE_SLOT="Human only" pytest test_mysolution.py
@@ -42,11 +42,21 @@ are half of each unit check's identity, so the two must not drift. The notebooks
42
42
  generated from one shared definition for exactly this reason — if you edit the tests,
43
43
  edit both.
44
44
 
45
- **Restart the session before the run that counts.** phigrade captures the attached
46
- notebook, the submission, and `phigrade.yaml` on the first `%%ipytest` cell of a
47
- kernel session and reuses them until the kernel restarts. Edit-and-re-run still
48
- re-grades, but the attached `.ipynb` remains the one from that first run. Both
49
- notebooks say so at the point where it matters.
45
+ **Each `%%ipytest` run starts fresh — save before the run that counts.** A
46
+ `pytest_sessionstart` hook resets phigrade's cached submission and config at the
47
+ start of every pytest session, so editing code or `phigrade.yaml` and re-running
48
+ `%%ipytest` submits your latest edits, not the first run's — no restart needed for
49
+ that. The guarantee is narrower for the notebook itself: each run re-asks Colab for
50
+ it rather than replaying a cached copy, not that Colab returns unsaved editor
51
+ state, so save the notebook (Ctrl/Cmd+S) before the run you want graded. A restart
52
+ is still needed if you run this example against a local server
53
+ (`use_local_server: true`, see "Trying it without Colab" below): the server
54
+ process is locked to whichever mode it first saw, so phigrade now refuses a
55
+ mid-kernel switch between teacher and student mode rather than misroute a
56
+ submission, and re-publishing the answer key from `lab1_instructor.ipynb` in the
57
+ same kernel still needs a restart too, since the local server's answer-key file
58
+ accumulates duplicate rows across sessions otherwise. Both notebooks say so at the
59
+ point where it matters.
50
60
 
51
61
  ## Trying it without Colab
52
62
 
@@ -176,10 +176,15 @@
176
176
  "cell_type": "markdown",
177
177
  "metadata": {},
178
178
  "source": [
179
- "> **Re-publishing?** Use **Runtime -> Restart session and run all**.\n",
180
- "> phigrade caches the config and the reference attempt for the life of\n",
181
- "> the kernel, so a second `%%ipytest` run in the same session does not\n",
182
- "> start a clean publish."
179
+ "> **Re-publishing?** Use **Runtime -> Restart session and run all** if\n",
180
+ "> you're running against a local server (`use_local_server: true`).\n",
181
+ "> phigrade's cached config and reference attempt now reset automatically\n",
182
+ "> at the start of every `%%ipytest` run, so a second run in the same\n",
183
+ "> *hosted* session does start a clean publish. Against a local server,\n",
184
+ "> restart anyway: the server process is locked to whichever mode it first\n",
185
+ "> saw, so phigrade now refuses a mode switch mid-kernel rather than\n",
186
+ "> misroute a submission, and a second publish in the same kernel still\n",
187
+ "> appends to the local answer-key file instead of replacing it."
183
188
  ]
184
189
  }
185
190
  ],
@@ -150,13 +150,14 @@
150
150
  "cell_type": "markdown",
151
151
  "metadata": {},
152
152
  "source": [
153
- "> **Before the run you want graded: Runtime -> Restart session and run all.**\n",
153
+ "> **Save before the run you want graded.**\n",
154
154
  ">\n",
155
- "> phigrade captures the notebook it attaches, the submission it creates, and\n",
156
- "> `phigrade.yaml` on the *first* `%%ipytest` cell of a session, and reuses them\n",
157
- "> for the life of the kernel. If you edit your code and re-run, the grade\n",
158
- "> updates but the attached notebook is still the one from that first run.\n",
159
- "> Restarting makes all three fresh."
155
+ "> phigrade resets its cached submission and config at the start of every\n",
156
+ "> `%%ipytest` run, so editing your code and re-running submits your latest\n",
157
+ "> work -- no restart needed for that. The guarantee is narrower for the\n",
158
+ "> notebook itself: each run re-asks Colab for it rather than reusing an old\n",
159
+ "> copy, not that Colab returns unsaved editor state. Save the notebook\n",
160
+ "> (Ctrl/Cmd+S) before the run you want graded."
160
161
  ]
161
162
  }
162
163
  ],
@@ -0,0 +1,71 @@
1
+ # A group submission
2
+
3
+ A worked example of the `group_members` key from
4
+ [the client README's configuration table](../../README.md#configuration): one
5
+ student runs the tests, and every student named in the config gets the grade.
6
+
7
+ | File | What it is |
8
+ | --- | --- |
9
+ | `phigrade.yaml` | A complete group configuration, commented. The example *is* the config — there is no test file here, because nothing about writing tests changes when a submission is shared. |
10
+
11
+ Copy it beside the test module that defines your `@weight` functions —
12
+ `phigrade.yaml` is looked up next to that file, not in the working directory —
13
+ fill in `course_id`, `assessment_id` and `server_url`, and put both group members'
14
+ addresses in `group_members`.
15
+
16
+ Your instructor has to allow it first: **Group Size** on the assessment settings
17
+ page is `1` by default, which means students submit individually, and a submission
18
+ carrying a group is refused with a message saying so.
19
+
20
+ ## Three things this example exists to show
21
+
22
+ **List the whole group, including yourself.** Not "your partners" — everyone. Both
23
+ students work in the same repository, so both commit the same file, and both
24
+ machines tag every attempt with the same group. That symmetry is what makes the
25
+ resolution agree for both of you: your grade comes from the most recently
26
+ submitted active attempt among the attempts you are on, and if the two of you
27
+ described the group differently, that sentence would resolve differently for each
28
+ of you. A list that omits your own address is refused rather than quietly guessed
29
+ at.
30
+
31
+ **Read the line it prints.** A group submission announces itself:
32
+
33
+ ```
34
+ Created submission 8f2c1a4e-…
35
+ Submitting as a group: alice@andrew.cmu.edu, bob@andrew.cmu.edu
36
+ ```
37
+
38
+ That is the only guard against a silent solo submission, and the failure it
39
+ guards against — believing you submitted together and discovering otherwise when
40
+ grades come out — has no undo. In Colab, use `%%ipytest -s`; plain `%%ipytest`
41
+ captures stdout and the line never reaches you. The client README's
42
+ [Colab notebooks](../../README.md#colab-notebooks) section says the same thing
43
+ about submission slots, for the same reason.
44
+
45
+ **Only one of you needs to run the tests.** The partner who never ran anything
46
+ still sees the submission and its per-unit-test breakdown in the web app, on both
47
+ the Attempts page and the assessment Overview, labelled with who submitted it.
48
+ That is the whole feature: the work is graded once and counts for everyone on it.
49
+
50
+ ## What this does not do
51
+
52
+ There are no persistent teams. The group is named at submission time and restated
53
+ by submitting again — change `group_members` and re-run, and the new list is what
54
+ counts. There is no invitation or acceptance step either, so a submission can name
55
+ you without asking; it is visible to you and to staff the moment it lands, and
56
+ submitting your own work makes yours the one that counts again.
57
+
58
+ Group submissions also work on the rubric-graded PDF side, the same rule
59
+ applying to a scanned upload as to a code submission. The group is named in a
60
+ different place on each side: `group_members` in `phigrade.yaml` here, versus
61
+ a "Partners" field on the PDF upload form in the web application.
62
+
63
+ One PDF-specific wrinkle to know before you rely on it: if your partner
64
+ uploads a PDF alone after you two submitted together, that solo upload
65
+ replaces your shared submission for **both** of you, and you are left with
66
+ no active submission at all until you submit again yourself. On the coding
67
+ side this does not happen — a partner's solo re-submission only replaces
68
+ their own attempt, and yours keeps counting. So on the PDF side, treat a
69
+ group submission as something either partner can end for both of you by
70
+ submitting alone; if that happens, re-upload to have your own work count
71
+ again.
@@ -0,0 +1,61 @@
1
+ # A group submission: one student runs the tests, the whole group gets the grade.
2
+ #
3
+ # Drop this beside the test module that defines your `@weight` functions --
4
+ # `phigrade.yaml` is looked up next to that file, not in the working directory.
5
+ # Replace course_id, assessment_id and server_url with your own.
6
+
7
+ course_id: "your-course-uuid"
8
+ assessment_id: "your-assessment-uuid"
9
+
10
+ use_local_server: false
11
+ server_url: "https://phigrade.example.org"
12
+
13
+ # Every student this submission is for, INCLUDING YOURSELF.
14
+ #
15
+ # Not "your partners" -- everyone. Both of you work in the same repository, so
16
+ # both of you commit this same file, and both machines then tag every attempt
17
+ # with the same group. That symmetry is the point: the server resolves your
18
+ # grade to the most recently submitted active attempt among the attempts you
19
+ # are on, so listing the group identically on both sides is what makes it give
20
+ # both of you the same answer.
21
+ #
22
+ # Leaving this out entirely is an ordinary solo submission.
23
+ group_members:
24
+ - alice@andrew.cmu.edu
25
+ - bob@andrew.cmu.edu
26
+
27
+ teacher_mode: false
28
+ submission_files:
29
+ - solution.py
30
+
31
+ # ---------------------------------------------------------------------------
32
+ # What to expect when you run this
33
+ #
34
+ # Every group submission prints its group, right after the submission id:
35
+ #
36
+ # Created submission 8f2c1a4e-...
37
+ # Submitting as a group: alice@andrew.cmu.edu, bob@andrew.cmu.edu
38
+ #
39
+ # Read that line. It is the only thing standing between you and a silent solo
40
+ # submission -- believing you submitted as a group and finding out at grade
41
+ # release has no undo. In a Colab notebook use `%%ipytest -s`: plain
42
+ # `%%ipytest` captures stdout and swallows the line.
43
+ #
44
+ # Four things are refused, each with 422 and a message naming the problem:
45
+ #
46
+ # * the assessment does not allow groups -- "Group submissions are not
47
+ # enabled for this assessment." Your instructor sets Group Size in the
48
+ # assessment settings; it is 1 by default, which means submit individually.
49
+ # * the group is larger than that setting.
50
+ # * an address is not an active student on this course. Staff are refused
51
+ # here too, by their own address: group submissions are for students.
52
+ # * the list does not contain your own address (see above -- list everyone).
53
+ #
54
+ # Addresses are stripped, lower-cased and de-duplicated before they are sent,
55
+ # so spacing and capitalisation here do not matter.
56
+ #
57
+ # `PHIGRADE_GROUP_MEMBERS="alice@andrew.cmu.edu,bob@andrew.cmu.edu"` overrides
58
+ # this key for one run, which is what a CI job or a scripted submission wants.
59
+ # It announces itself on stdout when it takes effect. The file is the normal
60
+ # path -- in Colab as much as on a laptop, since a Colab session writes this
61
+ # file from a `%%writefile phigrade.yaml` cell.