diff --git a/pyproject.toml b/pyproject.toml index 7271e7a..0e4cd6f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "hatchling.build" [project] name = "git-autograder" -version = "3.0.0" +version = "4.0.0" authors = [{ name = "Jiahao, Woo", email = "woojiahao1234@gmail.com" }] description = "Library for autograding Git repositories" readme = "README.md" @@ -16,7 +16,13 @@ classifiers = [ "Programming Language :: Python :: 3.13", ] license.file = "LICENSE" -dependencies = ["pytz", "types-pytz", "difflib_parser", "GitPython"] +dependencies = [ + "pytz", + "types-pytz", + "difflib_parser", + "GitPython", + "repo-smith", +] [project.urls] Homepage = "https://github.com/git-mastery/git-autograder.git" diff --git a/src/git_autograder/__init__.py b/src/git_autograder/__init__.py index 6863efb..d031c56 100644 --- a/src/git_autograder/__init__.py +++ b/src/git_autograder/__init__.py @@ -1,8 +1,10 @@ __all__ = [ - "autograder", "setup_autograder", "set_env", "assert_output", + "GitAutograderException", + "GitAutograderInvalidStateException", + "GitAutograderWrongAnswerException", "GitAutograderTestLoader", "GitAutograderRepo", "GitAutograderStatus", @@ -14,11 +16,15 @@ from .status import GitAutograderStatus from .output import GitAutograderOutput +from .exception import ( + GitAutograderException, + GitAutograderInvalidStateException, + GitAutograderWrongAnswerException, +) from .repo import GitAutograderRepo from .commit import GitAutograderCommit from .branch import GitAutograderBranch from .remote import GitAutograderRemote -from .decorators import autograder from .test_utils import ( setup_autograder, set_env, diff --git a/src/git_autograder/answers/rules/not_empty_rule.py b/src/git_autograder/answers/rules/not_empty_rule.py index e805ef8..af58e8c 100644 --- a/src/git_autograder/answers/rules/not_empty_rule.py +++ b/src/git_autograder/answers/rules/not_empty_rule.py @@ -6,5 +6,5 @@ class NotEmptyRule(AnswerRule): EMPTY = "Answer for {question} is empty." def apply(self, answer: GitAutograderAnswersRecord) -> None: - if answer.answer.strip() != "": + if answer.answer.strip() == "": raise Exception(self.EMPTY.format(question=answer.question)) diff --git a/src/git_autograder/decorators.py b/src/git_autograder/decorators.py deleted file mode 100644 index 2d908a3..0000000 --- a/src/git_autograder/decorators.py +++ /dev/null @@ -1,88 +0,0 @@ -import functools -import os -from datetime import datetime -from typing import Callable - -import pytz - -from git_autograder.exception import ( - GitAutograderInvalidStateException, - GitAutograderWrongAnswerException, -) -from git_autograder.output import GitAutograderOutput -from git_autograder.repo import GitAutograderRepo -from git_autograder.status import GitAutograderStatus - - -def autograder() -> Callable[ - [Callable[..., GitAutograderOutput]], Callable[..., GitAutograderOutput] -]: - """ - Decorator to denote that a function is an autograder function. - - Initializes the GitAutograderRepo and provides it as an argument to the function. - - Handles the Git Autograder exceptions thrown by the GitAutograderRepo or from wrong - answers. - - All outputs are saved directly to disk. - """ - - def inner( - func: Callable[..., GitAutograderOutput], - ) -> Callable[..., GitAutograderOutput]: - @functools.wraps(func) - def wrapper(*args, **kwargs) -> GitAutograderOutput: - output = None - is_local = os.environ.get("is_local", "false") == "true" - - exercise_name = os.environ.get("exercise_name") - if exercise_name is None: - output = GitAutograderOutput( - exercise_name=None, - started_at=None, - completed_at=None, - is_local=is_local, - comments=["Missing exercise_name in environment"], - status=GitAutograderStatus.ERROR, - ) - output.save() - return output - - repo = GitAutograderRepo(is_local=is_local, exercise_name=exercise_name) - try: - output = func(repo, *args, **kwargs) - except ( - GitAutograderInvalidStateException, - GitAutograderWrongAnswerException, - ) as e: - output = GitAutograderOutput( - exercise_name=repo.exercise_name, - started_at=repo.started_at, - completed_at=datetime.now(tz=pytz.UTC), - is_local=repo.is_local, - comments=[e.message] if isinstance(e.message, str) else e.message, - status=( - GitAutograderStatus.ERROR - if isinstance(e, GitAutograderInvalidStateException) - else GitAutograderStatus.UNSUCCESSFUL - ), - ) - except Exception as e: - # Unexpected exception - output = GitAutograderOutput( - exercise_name=exercise_name, - started_at=None, - completed_at=None, - is_local=is_local, - comments=[str(e)], - status=GitAutograderStatus.ERROR, - ) - - assert output is not None - output.save() - return output - - return wrapper - - return inner diff --git a/src/git_autograder/output.py b/src/git_autograder/output.py index b208546..3c8cdde 100644 --- a/src/git_autograder/output.py +++ b/src/git_autograder/output.py @@ -14,7 +14,6 @@ class GitAutograderOutput: started_at: Optional[datetime] completed_at: Optional[datetime] - is_local: Optional[bool] comments: Optional[List[str]] = None exercise_name: Optional[str] = None diff --git a/src/git_autograder/repo.py b/src/git_autograder/repo.py index 5a77e9a..bfa9a07 100644 --- a/src/git_autograder/repo.py +++ b/src/git_autograder/repo.py @@ -23,22 +23,14 @@ class GitAutograderRepo: def __init__( self, - is_local: bool, exercise_name: str, - repo_path: Optional[str | os.PathLike] = None, + repo_path: str | os.PathLike, ) -> None: # TODO: We should not be starting the grading at the point of initializing, but # we're keeping this because of the exception system self.started_at = self.__now() - self.is_local: bool = is_local self.exercise_name = exercise_name - self.repo_path = ( - repo_path - if repo_path is not None - else Path.cwd().parent / "main" - if not is_local - else Path.cwd().parent / "exercises" / exercise_name - ) + self.repo_path = repo_path self.repo: Repo = Repo(self.repo_path) @@ -85,7 +77,6 @@ def to_output( exercise_name=self.exercise_name, started_at=self.started_at, completed_at=self.__now(), - is_local=self.is_local, comments=comments, status=( GitAutograderStatus.SUCCESSFUL diff --git a/src/git_autograder/test_utils.py b/src/git_autograder/test_utils.py index 9aa3393..c63921b 100644 --- a/src/git_autograder/test_utils.py +++ b/src/git_autograder/test_utils.py @@ -35,9 +35,11 @@ def set_env(**kwargs) -> mock._patch_dict: class GitAutograderTestLoader: def __init__( self, + test_path: str, exercise_name: str, grade_func: Callable[[GitAutograderRepo], GitAutograderOutput], ) -> None: + self.test_path = test_path self.exercise_name = exercise_name self.grade_func = grade_func @@ -48,6 +50,10 @@ def load( step_id: str, setup: Optional[Callable[[Repo], None]] = None, ) -> Iterator[GitAutograderOutput]: + # This is done to work around the limitation of running tests not within the exercise/tests/ folder + test_dir = os.path.dirname(self.test_path) + spec_path = os.path.join(test_dir, spec_path) + repo_initializer = initialize_repo(spec_path) attach_start_tag(repo_initializer, step_id) with repo_initializer.initialize() as r: @@ -57,7 +63,6 @@ def load( started_at = datetime.now(tz=pytz.UTC) try: autograder = GitAutograderRepo( - is_local=True, exercise_name=self.exercise_name, repo_path=r.working_dir, ) @@ -70,7 +75,6 @@ def load( exercise_name=self.exercise_name, started_at=started_at, completed_at=datetime.now(tz=pytz.UTC), - is_local=True, comments=[e.message] if isinstance(e.message, str) else e.message, status=( GitAutograderStatus.ERROR @@ -84,7 +88,6 @@ def load( exercise_name=self.exercise_name, started_at=None, completed_at=None, - is_local=True, comments=[str(e)], status=GitAutograderStatus.ERROR, ) @@ -101,6 +104,10 @@ def setup_autograder( grade_func: Callable[[GitAutograderRepo], GitAutograderOutput], setup: Optional[Callable[[Repo], None]] = None, ) -> Iterator[GitAutograderOutput]: + # This is done to work around the limitation of running tests not within the exercise/tests/ folder + cur_dir = os.path.dirname(__file__) + spec_path = os.path.join(cur_dir, spec_path) + repo_initializer = initialize_repo(spec_path) attach_start_tag(repo_initializer, step_id) with repo_initializer.initialize() as r: @@ -110,7 +117,7 @@ def setup_autograder( started_at = datetime.now(tz=pytz.UTC) try: autograder = GitAutograderRepo( - is_local=True, exercise_name=exercise_name, repo_path=r.working_dir + exercise_name=exercise_name, repo_path=r.working_dir ) output = grade_func(autograder) except ( @@ -121,7 +128,6 @@ def setup_autograder( exercise_name=exercise_name, started_at=started_at, completed_at=datetime.now(tz=pytz.UTC), - is_local=True, comments=[e.message] if isinstance(e.message, str) else e.message, status=( GitAutograderStatus.ERROR @@ -135,7 +141,6 @@ def setup_autograder( exercise_name=exercise_name, started_at=None, completed_at=None, - is_local=True, comments=[str(e)], status=GitAutograderStatus.ERROR, )