git-autograder reference

git-autograder is the grading library that loads a Git-Mastery exercise attempt and turns repository or answer checks into structured verification results.

Installation

pip install git-autograder

Basic structure of a verify.py

from git_autograder import (
    GitAutograderExercise,
    GitAutograderOutput,
    GitAutograderStatus,
)

SOME_ERROR = "You haven't done X yet."


def verify(exercise: GitAutograderExercise) -> GitAutograderOutput:
    main = exercise.repo.branches.branch("main")

    if not main.user_commits:
        raise exercise.wrong_answer([SOME_ERROR])

    return exercise.to_output(["Well done!"], GitAutograderStatus.SUCCESSFUL)

The app calls verify(exercise) directly. All exception handling and output formatting is done by the app.


GitAutograderExercise

The main object passed to every verify(exercise) function.

Attribute / Method Description
exercise.exercise_name Exercise identifier (from config)
exercise.exercise_path Path to the exercise root directory
exercise.config Parsed .gitmastery-exercise.json as ExerciseConfig
exercise.repo GitAutograderRepo (or NullGitAutograderRepo for ignore exercises)
exercise.answers Parsed answers.txt as GitAutograderAnswers
exercise.to_output(comments, status) Build the return value
exercise.wrong_answer(comments) Raise a grading failure
exercise.read_config(key) Read a key from .gitmastery-exercise.json
exercise.write_config(key, value) Write a key to .gitmastery-exercise.json

Exception types

Exception When to use
GitAutograderWrongAnswerException The student’s attempt is incorrect → UNSUCCESSFUL
GitAutograderInvalidStateException The exercise is in an invalid state → ERROR

Use raise exercise.wrong_answer([...]) for grading failures. Reserve bare exceptions for unexpected errors only.


exercise.repo — repository helpers

exercise.repo is a GitAutograderRepo for most exercises. For ignore and local-ignore exercises it is a NullGitAutograderRepo that raises on any Git access.

exercise.repo.branchesBranchHelper

branch = exercise.repo.branches.branch("main")       # raises if missing
branch = exercise.repo.branches.branch_or_none("main")  # returns None if missing
exists = exercise.repo.branches.has_branch("feature/login")

GitAutograderBranch

Property / Method Description
branch.name Branch name
branch.commits All commits (newest first)
branch.user_commits Commits after the start tag (student’s work)
branch.latest_commit Most recent commit
branch.latest_user_commit Most recent student commit
branch.start_commit The Git-Mastery start tag commit
branch.reflog List of GitAutograderReflogEntry
branch.has_non_empty_commits() True if any student commit changed files
branch.has_edited_file(path) True if the file was modified since the start tag
branch.has_added_file(path) True if the file was added since the start tag
branch.checkout() Checkout this branch

Example — check that the student committed on main:

main = exercise.repo.branches.branch("main")
if not main.user_commits:
    raise exercise.wrong_answer(["You have no commits on main yet."])

exercise.repo.commitsCommitHelper

commit = exercise.repo.commits.commit("HEAD")
commit = exercise.repo.commits.commit_or_none("abc1234")

GitAutograderCommit

Property / Method Description
commit.hexsha Full commit SHA
commit.stats GitPython Stats object (files changed)
commit.parents List of GitAutograderCommit
commit.branches Branch names that contain this commit
commit.is_child(parent) True if this commit descends from parent
commit.file_change_type(file) "A", "M", "D", or None
commit.file(path) Context manager yielding the file contents at this commit
commit.checkout() Detach HEAD to this commit

exercise.repo.remotesRemoteHelper

remote = exercise.repo.remotes.remote("origin")          # raises if missing
remote = exercise.repo.remotes.remote_or_none("origin")  # returns None
exists = exercise.repo.remotes.has_remote("origin")

GitAutograderRemote

Wraps a GitPython Remote.

Method Description
remote.remote The underlying GitPython Remote object
remote.is_for_repo(owner, repo_name) True if the remote URL points to owner/repo_name on GitHub (supports both HTTPS and SSH URLs)
remote.track_branches(branches) Check out remote-tracking branches locally

Example — verify the remote points to the right repository:

origin = exercise.repo.remotes.remote("origin")
if not origin.is_for_repo("git-mastery", "exercises"):
    raise exercise.wrong_answer(["Your remote 'origin' does not point to the correct repository."])

exercise.repo.filesFileHelper

# Open a file if it exists
with exercise.repo.files.file_or_none("notes.txt") as f:
    content = f.read() if f else None

# Open a file (raises if missing)
with exercise.repo.files.file("notes.txt") as f:
    content = f.read()

# List untracked files
untracked = exercise.repo.files.untracked_files()

Raw GitPython access

If no helper covers your use case, access the underlying GitPython repo directly:

exercise.repo.repo   # GitPython Repo object

exercise.answers — answer file support

For exercises where students fill in an answers.txt file.

answers.txt format

Q: What does git add do?
A: Stages changes for the next commit

Q: What does git commit do?
A: Records staged changes to the repository

Usage

answers.question(q) and answers.question_or_none(q) return a GitAutograderAnswersRecord with two attributes:

Attribute Description
record.question The question string
record.answer The student’s answer string
answers = exercise.answers

# Get a specific answer (raises GitAutograderInvalidStateException if missing)
record = answers.question("What does git add do?")
print(record.answer)

# Safe access — returns None if the question is not present
record = answers.question_or_none("What does git add do?")
if record is None:
    raise exercise.wrong_answer(["Missing answer for 'What does git add do?'"])

# Validate answers with rules, then call validate() to apply them all at once
from git_autograder.answers.rules.not_empty_rule import NotEmptyRule
from git_autograder.answers.rules.has_exact_value_rule import HasExactValueRule
from git_autograder.answers.rules.contains_value_rule import ContainsValueRule

answers.add_validation("What does git add do?", NotEmptyRule())
answers.add_validation("Name a git command", HasExactValueRule("git commit"))
answers.validate()  # raises GitAutograderWrongAnswerException if any rule fails

Available answer rules

Rule Description
NotEmptyRule Answer must not be blank
HasExactValueRule(value) Answer must equal value exactly (case-insensitive)
ContainsValueRule(value) Answer must contain value (case-insensitive)
ContainsListRule(values) Answer must contain all values in the list
HasExactListRule(values) Answer must match all values in the list exactly

All rules are in git_autograder.answers.rules.

In tests

Mock answers.txt content via GitAutograderTestLoader:

with loader.start(mock_answers={"What does git add do?": "Stages changes"}) as (test, rs):
    output = test.run()
    assert_output(output, GitAutograderStatus.SUCCESSFUL)

GitAutograderStatus

Value Meaning
SUCCESSFUL Student completed the exercise correctly
UNSUCCESSFUL Student’s attempt is incorrect or incomplete
ERROR Exercise is in an invalid or unexpected state

GitAutograderOutput

Built by exercise.to_output(comments, status). Contains:

Field Type Description
exercise_name str Exercise identifier
started_at datetime When GitAutograderExercise was constructed
completed_at datetime When to_output was called
comments List[str] Feedback shown to the student
status GitAutograderStatus Final result

Full example — branch check

from git_autograder import (
    GitAutograderExercise,
    GitAutograderOutput,
    GitAutograderStatus,
)

NOT_ON_MAIN = "You aren't on the main branch. Run 'git checkout main'."
NO_COMMITS = "You haven't committed your changes yet."
SUCCESS = "Great work! Your changes are committed to main."


def verify(exercise: GitAutograderExercise) -> GitAutograderOutput:
    try:
        active = exercise.repo.repo.active_branch.name
    except TypeError:
        raise exercise.wrong_answer(["You are in a detached HEAD state."])

    if active != "main":
        raise exercise.wrong_answer([NOT_ON_MAIN])

    main = exercise.repo.branches.branch("main")
    if not main.user_commits:
        raise exercise.wrong_answer([NO_COMMITS])

    return exercise.to_output([SUCCESS], GitAutograderStatus.SUCCESSFUL)

See How to add an exercise and Verification flow for how this fits into the full exercise lifecycle.