Below we walk through an example of how to setup and use pygrade to automatically grade student assignments using GitHub and Python unittests.

1. Create assignment skeletons.

First, we will create a GitHub repository containing starter code for each assignment.

In this tutorial, we will use: https://github.com/tapilab/pygrade-example-assignment

2. Create a unittest.

test_asg0.py contains the unit tests for this assignment, as well as indications of the points to be deducted for each failing test case.

3. Create students.tsv

Next, we create a tab-separated file containing student information. The only required fields are:

4. Initialize repositories

Using students.tsv, we will create private repositories for each student and initialize them with the starter code from https://github.com/tapilab/pygrade-example-assignment . (Note, I've made this repo public for demonstration purposes).

$ pygrade init -h
Initialize student repositories. Create one repo per student. Also create one team per student consisting of that student. Each repo is made private to that team.

usage:
    pygrade init --org <name> --user <username> --pass <passwd> --remote <uri> [--students <file>] [--workdir <file>]

Options
    -h, --help
    -o, --org <string>          Name of the GitHub Organization for the course.
    -p, --pass <file>           GitHub password
    -r, --remote <uri>          URL of remote github repo used for starter code.
    -s, --students <file>       Students JSON file [default: students.tsv]
    -u, --user <file>           GitHub username
    -w, --workdir <file>        Temporary directory for storing assignments [default: students]
$ pygrade init --org tapilab --user aronwc --pass XXX --remote https://github.com/tapilab/pygrade-example-assignment --students students.tsv --workdir students
found org https://api.github.com/orgs/tapilab
initializing repo https://github.com/tapilab/pygrade-example-student for aronwc
  found existing team pygrade-example-student
  found existing remote repo pygrade-example-student
  cloning https://github.com/tapilab/pygrade-example-student to students/pygrade-example-student ...
  pushed Info.md
  pushed template from remote https://github.com/tapilab/pygrade-example-assignment

A new student repo has been created and cloned to the local working directory (students by default).

$ ls students/*/*
students/pygrade-example-student/Info.md   students/pygrade-example-student/README.md

students/pygrade-example-student/asg0:
asg0.py

5. Grade assignments

After the students have pushed their solutions to their private repositories, we can then grade the assignments using pygrade grade:

$ pygrade grade -h
Grade a Python assignment.

usage:
    pygrade grade --test <file> [--students <file>] [--output <file>] [--workdir <file>]

Options
    -h, --help
    -o, --output <file>             Output file [default: grades.json]
    -s, --students <file>           Students JSON file [default: students.tsv]
    -t, --test <file>               File containing python tests for grading
    -w, --workdir <file>            Temporary directory for storing assignments [default: students]
$ pygrade grade -t test_asg0.py -s students.tsv 
working directory=students
read 1 students
saved results in grades.json
$ cat grades.json  | json
{
  "student": {
    "github_repo": "https://github.com/tapilab/pygrade-example-student",
    "github_id": "aronwc",
    "Name": "Test Student",
    "Student ID": "1234567"
  },
  "possible_points": 20,
  "grade": 8,
  "assignment": [
    "asg0/asg0.py"
  ],
  "deductions": [
    {
      "summary": "test_add:  @points=10 ",
      "points": 10,
      "trace": "Traceback (most recent call last):\n  File \"test_asg0.py\", line 39, in test_add\n    self.assertEqual(add(2, 2), 4)\nAssertionError: 0 != 4\n\nsource:\n    def test_add(self):\n         \"\"\" @points=10 \"\"\"\n         self.assertEqual(add(2, 2), 4)\n"
    },
    {
      "summary": "test_hard:  @points=2 ",
      "points": 2,
      "trace": "Traceback (most recent call last):\n  File \"test_asg0.py\", line 35, in test_hard\n    self.assertTrue(is_mammal('dolphin'))\nAssertionError: False is not true\n\nsource:\n    def test_hard(self):\n         \"\"\" @points=2 \"\"\"\n         self.assertTrue(is_mammal('dolphin'))\n"
    }
  ],
  "time_graded": "Mon Jan 18 10:28:27 2016"
}

grades.json has one entry per student. In addition to storing the student info, the most important keys are "grade" (the final grade), and "deductions" (a list of test failures and the points deducted for each).

6. Push the grades to each students private repo.

Finally, we will create a grade.txt file for each student and push it to their private repo. This file will list the deductions for the assignment.

$ pygrade push -h
Push grades to student repositories.

usage:
    pygrade push [--grades <file>] [--workdir <file>]

Options
    -h, --help
    -g, --grades <file>             JSON grades output by the grade command [default: grades.json]
    -w, --workdir <file>            Temporary directory for storing assignments [default: students]
$ pygrade push -h
Push grades to student repositories.

usage:
    pygrade push [--grades <file>] [--workdir <file>]

Options
    -h, --help
    -g, --grades <file>             JSON grades output by the grade command [default: grades.json]
    -w, --workdir <file>            Temporary directory for storing assignments [default: students]
bronze:tmp awculott$ pygrade push 
pushing 1 grades
pushing grade students/pygrade-example-student/asg0/grade.txt to https://github.com/tapilab/pygrade-example-student

This file is now here: https://github.com/tapilab/pygrade-example-student/blob/master/asg0/grade.txt:

Grade: 8.00/20.00

2 Deduction(s):

--------------
#1: 10.00 points
Failing test: test_add:  @points=10 
Traceback (most recent call last):
  File "test_asg0.py", line 39, in test_add
    self.assertEqual(add(2, 2), 4)
AssertionError: 0 != 4

source:
    def test_add(self):
         """ @points=10 """
         self.assertEqual(add(2, 2), 4)
--------------

--------------
#2: 2.00 points
Failing test: test_hard:  @points=2 
Traceback (most recent call last):
  File "test_asg0.py", line 35, in test_hard
    self.assertTrue(is_mammal('dolphin'))
AssertionError: False is not true

source:
    def test_hard(self):
         """ @points=2 """
         self.assertTrue(is_mammal('dolphin'))
--------------

That's it!