Skip to content
Merged

Dev #74

Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 50 additions & 2 deletions .github/workflows/cicd.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,61 @@ permissions:
id-token: write

jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- name: "Set up Python"
uses: actions/setup-python@v5
with:
python-version-file: ".python-version"

- name: Install uv
uses: astral-sh/setup-uv@v6

- name: Install the project
run: uv sync --locked --all-extras --dev

- name: pylint
run: uv run pylint grader desktop tests pygrader.py --fail-under 9

- name: mypy
run: uv run mypy grader desktop tests pygrader.py --ignore-missing-imports

- name: flake8
run: uv run flake8 grader desktop tests pygrader.py

- name: complexipy
run: uv run complexipy .
test:
runs-on: ubuntu-latest
needs: lint
steps:
- uses: actions/checkout@v4
- uses: ./.github/actions/tests

- name: "Set up Python"
uses: actions/setup-python@v5
with:
python-version-file: ".python-version"

- name: Install uv
uses: astral-sh/setup-uv@v6

- name: Install the project
run: uv sync --locked --all-extras --dev

- name: Run the unit tests
run: |
find tests -type f -name "test_*.py" -not -name "test_functional.py" -not -path "*sample_project*" | xargs uv run -m unittest -v
shell: bash

- name: Run the functional tests
run: |
uv run -m unittest discover -s tests -p "test_functional.py"
shell: bash
docker:
# needs: test
needs: test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
Expand Down
49 changes: 44 additions & 5 deletions .github/workflows/pr.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,3 @@
# This workflow will install Python dependencies, run tests and lint with a single version of Python
# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python

name: pygrader pull request checks

on:
Expand All @@ -16,10 +13,52 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: ./.github/actions/lint

- name: "Set up Python"
uses: actions/setup-python@v5
with:
python-version-file: ".python-version"

- name: Install uv
uses: astral-sh/setup-uv@v6

- name: Install the project
run: uv sync --locked --all-extras --dev

- name: pylint
run: uv run pylint grader desktop tests pygrader.py --fail-under 9

- name: mypy
run: uv run mypy grader desktop tests pygrader.py --ignore-missing-imports

- name: flake8
run: uv run flake8 grader desktop tests pygrader.py

- name: complexipy
run: uv run complexipy .
test:
runs-on: ubuntu-latest
needs: lint
steps:
- uses: actions/checkout@v4
- uses: ./.github/actions/tests

- name: "Set up Python"
uses: actions/setup-python@v5
with:
python-version-file: ".python-version"

- name: Install uv
uses: astral-sh/setup-uv@v6

- name: Install the project
run: uv sync --locked --all-extras --dev

- name: Run the unit tests
run: |
find tests -type f -name "test_*.py" -not -name "test_functional.py" -not -path "*sample_project*" | xargs uv run -m unittest -v
shell: bash

- name: Run the functional tests
run: |
uv run -m unittest discover -s tests -p "test_functional.py"
shell: bash
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -181,4 +181,6 @@ docs/diagrams/out

*.env
.private
*.zip
*.zip

**/.DS_Store
1 change: 1 addition & 0 deletions .python-version
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
3.13
15 changes: 14 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,19 @@
# pygrader

## 1.7.0

- Pylintrc can now be part of the config
- Add total score and max score to JSON, CSV, and PlainText result displays
- Environment variables can now be passed from the config

## 1.6.0

- Pygrader now accepts zip archieves as input

## 1.5.3

- Add pyproject.toml install support

## 1.5.2

- Float comparison caused issues when there are failing tests
Expand All @@ -14,7 +28,6 @@
- Structure check files are now in JSON format.
- Configs accept template parameters


## 1.4.0

- Pygrader now works with config from the web.
Expand Down
12 changes: 8 additions & 4 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,14 +1,18 @@
FROM python:3.12-slim
FROM ghcr.io/astral-sh/uv:python3.13-slim

RUN mkdir /app
RUN mkdir /assets
WORKDIR /app

COPY requirements-prod.txt .
RUN pip install -r requirements-prod.txt
# COPY uv.lock .
# COPY pyproject.toml .
# RUN uv sync --locked --no-install-project --no-dev

COPY . .

RUN uv sync --locked --no-dev

VOLUME ["/project"]

ENTRYPOINT ["python", "pygrader.py", "--config", "https://api.github.com/repos/fmipython/PythonCourse2025/contents/homeworks/homework3/pygrader_config_public_web.json", "/project"]
ENTRYPOINT ["uv", "run", "--no-dev", "pygrader.py", "--config", "https://api.github.com/repos/fmipython/PythonCourse2025/contents/homeworks/homework3/pygrader_config_public_web.json", "/project"]

20 changes: 0 additions & 20 deletions Dockerfile.web

This file was deleted.

2 changes: 1 addition & 1 deletion config/2024.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
{
"name": "type-hints",
"max_points": 3,
"is_venv_required": false
"is_venv_required": true
},
{
"name": "coverage",
Expand Down
31 changes: 31 additions & 0 deletions config/environment.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
{
"environment": {
"variables": {
"GLOBAL_VAR": "global_value",
"API_KEY": "global_api_key"
}
},
"checks": [
{
"name": "tests",
"max_points": 13.5,
"is_venv_required": true,
"tests_path": [
"https://raw.githubusercontent.com/fmipython/pygrader-sample-project/refs/heads/main/tests/test_sample_code.py"
],
"default_test_score": 1,
"test_score_mapping": {
"TestCalculator": 0.5,
"test_add": 2,
"test_divide": 2,
"test_main_invalid_choice": 2.5
},
"environment": {
"variables": {
"TEST_SPECIFIC_VAR": "test_value",
"API_KEY": "test_override_key"
}
}
}
]
}
4 changes: 2 additions & 2 deletions config/full.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,12 @@
{
"name": "type-hints",
"max_points": 10,
"is_venv_required": false
"is_venv_required": true
},
{
"name": "coverage",
"max_points": 10,
"is_venv_required": true
}
]
}
}
4 changes: 2 additions & 2 deletions config/full_single_point.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,12 @@
{
"name": "type-hints",
"max_points": 1,
"is_venv_required": false
"is_venv_required": true
},
{
"name": "coverage",
"max_points": 1,
"is_venv_required": true
}
]
}
}
5 changes: 3 additions & 2 deletions config/only_pylint.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@
{
"name": "pylint",
"max_points": 1,
"is_venv_required": true
"is_venv_required": true,
"pylintrc_path": "${{config_dir}}/2024.pylintrc"
}
]
}
}
16 changes: 15 additions & 1 deletion desktop/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,11 @@
Calls all the checks, and stores their results
"""

import os
import shutil

import grader.utils.constants as const

from desktop.cli import get_args
from desktop.results_reporter import (
JSONResultsReporter,
Expand All @@ -11,6 +16,7 @@
ResultsReporter,
)
from grader.utils.logger import setup_logger
from grader.utils.files import is_path_zip, unzip_archive
from grader.grader import Grader


Expand Down Expand Up @@ -40,8 +46,13 @@ def run_grader() -> None:
is_suppressing_info = args["report_format"] == "json" or args["report_format"] == "csv" or args["suppress_info"]
log = setup_logger(args["student_id"], verbosity=args["verbosity"], suppress_info=is_suppressing_info)

if is_path_zip(args["project_root"]):
project_root = unzip_archive(args["project_root"])
else:
project_root = str(args["project_root"]) # type safety

grader = Grader(
args["student_id"], args["project_root"], args["config"], log, args["keep_venv"], args["skip_venv_creation"]
args["student_id"], project_root, args["config"], log, args["keep_venv"], args["skip_venv_creation"]
)

checks_results = grader.grade()
Expand All @@ -50,3 +61,6 @@ def run_grader() -> None:

# TODO - Add output to a file
reporter.display(checks_results)

if os.path.exists(const.WORK_DIR):
shutil.rmtree(const.WORK_DIR)
18 changes: 17 additions & 1 deletion desktop/results_reporter.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,11 +43,17 @@ class JSONResultsReporter(ResultsReporter):
"""

def display(self, results: list[CheckResult], file_descriptor: TextIO = sys.stdout) -> None:
scored_results = [result for result in results if isinstance(result, ScoredCheckResult)]
total_score = sum(scored_result.result for scored_result in scored_results)
total_max_score = sum(result.max_score for result in scored_results)

content = {
"scored_checks": [result_to_json(result) for result in results if isinstance(result, ScoredCheckResult)],
"scored_checks": [result_to_json(result) for result in scored_results],
"non_scored_checks": [
result_to_json(result) for result in results if isinstance(result, NonScoredCheckResult)
],
"total_score": total_score,
"total_max_score": total_max_score,
}

output = json.dumps(content, indent=4)
Expand Down Expand Up @@ -88,8 +94,13 @@ class CSVResultsReporter(ResultsReporter):
"""

def display(self, results: list[CheckResult], file_descriptor: TextIO = sys.stdout) -> None:
scored_results = [result for result in results if isinstance(result, ScoredCheckResult)]
total_score = sum(scored_result.result for scored_result in scored_results)
total_max_score = sum(result.max_score for result in scored_results)

output = ["Check,Score,Max Score"]
output += [result_to_csv(check_result) for check_result in results]
output.append(f"Total,{total_score},{total_max_score}")

self._to_file_descriptor("\n".join(output) + "\n", file_descriptor)

Expand Down Expand Up @@ -120,7 +131,12 @@ class PlainTextResultsReporter(ResultsReporter):
"""

def display(self, results: list[CheckResult], file_descriptor: TextIO = sys.stdout) -> None:
scored_results = [result for result in results if isinstance(result, ScoredCheckResult)]
total_score = sum(scored_result.result for scored_result in scored_results)
total_max_score = sum(result.max_score for result in scored_results)

output = [result_to_plain_text(check_result) for check_result in results]
output.append(f"Total Score: {total_score}/{total_max_score}")
self._to_file_descriptor("\n".join(output) + "\n", file_descriptor)


Expand Down
2 changes: 1 addition & 1 deletion docs/source/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
# -- Project information -----------------------------------------------------
# https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information

project = "PythonProjectGrader"
project = "pygrader"
copyright = "2025, Lyuboslav Karev"
author = "Lyuboslav Karev"
release = "1.0.0"
Expand Down
Loading
Loading