Skip to content

Commit 5134e8a

Browse files
Update formatting and CI (#21)
- Completely migrate to `pre-commit` and `pre-commit.ci` - Add `ruff`, `codespell`, and some built-in pre-commit hooks - Add Pipfile verification - Deprecate `check.sh` and `format.sh` - Apply new formatting to the entire codebase
1 parent 84d500a commit 5134e8a

25 files changed

+469
-547
lines changed

.github/workflows/ci.yml

+6-3
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,10 @@ jobs:
3030
run: |
3131
set -e
3232
pip install pipenv
33-
pipenv install --dev --deploy
3433
35-
- name: Check formatting with format.sh
36-
run: pipenv run ./scripts/format.sh && test -z "$(git status --porcelain=v1 .)"
34+
- name: Check Pipfile.lock
35+
run: pipenv verify
36+
37+
# Since we don't install dependencies, we have to have this
38+
- name: Make dir for virtualenv
39+
run: mkdir -p $HOME/.local/share/virtualenvs

.pre-commit-config.yaml

+36-8
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,37 @@
1+
default_stages: [commit, push]
2+
fail_fast: false
3+
exclude: ^tin/(static/.*vendor|.*migrations)
4+
15
repos:
2-
- repo: local
3-
hooks:
4-
- id: format
5-
name: format
6-
entry: ./scripts/format.sh && test -z "$(git status --porcelain=v1 .)"
7-
language: system
8-
types: [python]
9-
pass_filenames: false
6+
- repo: https://github.com/pre-commit/pre-commit-hooks
7+
rev: v4.4.0
8+
hooks:
9+
- id: check-ast
10+
name: validate python
11+
- id: trailing-whitespace
12+
- id: mixed-line-ending
13+
- id: check-toml
14+
- id: detect-private-key
15+
- id: check-yaml
16+
- repo: https://github.com/codespell-project/codespell
17+
rev: v2.2.5
18+
hooks:
19+
- id: codespell
20+
files: ^.*\.(py|md|rst)$
21+
# TODO: Remove after python version >= 3.11
22+
additional_dependencies:
23+
- tomli
24+
- repo: https://github.com/astral-sh/ruff-pre-commit
25+
rev: v0.3.5
26+
hooks:
27+
- id: ruff
28+
args: [ "--fix", "--exit-non-zero-on-fix" ]
29+
files: ^tin/.*
30+
name: ruff lint
31+
- id: ruff-format
32+
files: ^tin/.*
33+
- repo: https://github.com/pycqa/isort
34+
rev: 5.12.0
35+
hooks:
36+
- id: isort
37+
name: isort (python)

Pipfile

-6
Original file line numberDiff line numberDiff line change
@@ -24,12 +24,6 @@ mosspy = "~=1.0"
2424
django-debug-toolbar = "~=4.3"
2525

2626
[dev-packages]
27-
flake8 = "*"
28-
pylint = "*"
29-
isort = "*"
30-
black = "*"
31-
autopep8 = "*"
32-
pylint-django = "*"
3327
django-stubs = "*"
3428
pre-commit = "*"
3529

Pipfile.lock

+197-375
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

create_debug_users.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@
3131
# fmt: on
3232

3333
for user_info in users:
34-
print("Creating user {}".format(user_info[0]))
34+
print(f"Creating user {user_info[0]}")
3535
if user_info[1] is None:
3636
user = User.objects.get_or_create(username=user_info[0])[0]
3737
else:

pyproject.toml

+74
Original file line numberDiff line numberDiff line change
@@ -18,3 +18,77 @@ exclude = '''
1818
)/
1919
'''
2020

21+
[tool.ruff]
22+
exclude = [
23+
".eggs",
24+
".git",
25+
".github",
26+
".hg",
27+
".mypy_cache",
28+
".tox",
29+
".venv",
30+
".env",
31+
"venv",
32+
"env",
33+
"secret",
34+
"build",
35+
"_build",
36+
"buck-out",
37+
"dist",
38+
"media",
39+
"migrations",
40+
]
41+
42+
# show fixes made in stdout
43+
# show-fixes = true
44+
45+
line-length = 100
46+
47+
target-version = "py38"
48+
49+
[tool.ruff.lint]
50+
select = [
51+
# flake8-bugbear
52+
"B",
53+
# flake8-comprehensions
54+
"C4",
55+
# flake8-django
56+
"DJ",
57+
# pycodestyle
58+
"E",
59+
# Pyflakes
60+
"F",
61+
# flake8-no-pep420
62+
"INP",
63+
# Pylint
64+
"PL",
65+
# pygrep hooks
66+
"PGH",
67+
# ruff
68+
"RUF",
69+
# pyupgrade
70+
"UP",
71+
]
72+
ignore = [
73+
# null=True on charfields
74+
"DJ001",
75+
# branching
76+
"PLR09",
77+
# magic number comparison
78+
"PLR2004",
79+
# mutable class attrs annotated as typing.ClassVar
80+
"RUF012",
81+
# as recommended by https://docs.astral.sh/ruff/formatter/#conflicting-lint-rules
82+
"E111",
83+
"E114",
84+
"E117",
85+
"E501",
86+
]
87+
88+
[tool.ruff.format]
89+
docstring-code-format = true
90+
line-ending = "lf"
91+
92+
[tool.codespell]
93+
write-changes = true
94+
ignore-words-list = ["num", "ans"]

scripts/check.sh

+3
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
#!/bin/bash
22
cd "$(dirname -- "$(dirname -- "$(readlink -f "$0")")")"
33

4+
echo "This script is now deprecated, please use pre-commit instead."
5+
echo "To run pre-commit before commiting, do 'pre-commit install'"
6+
47
for cmd in flake8 isort pylint; do
58
if [[ ! -x "$(which "$cmd")" ]]; then
69
echo "Could not find $cmd. Please make sure that flake8, isort, and pylint are all installed."

scripts/format.sh

+3
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
#!/bin/bash
22
cd "$(dirname -- "$(dirname -- "$(readlink -f "$0")")")"
33

4+
echo "This script is now deprecated, please use pre-commit instead"
5+
echo "To run pre-commit before commiting, do 'pre-commit install'"
6+
47
for cmd in black autopep8 isort; do
58
if [[ ! -x "$(which "$cmd")" ]]; then
69
echo "Could not find $cmd. Please make sure that black, autopep8, and isort are all installed."

tin/apps/assignments/forms.py

+9-9
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
from __future__ import annotations
22

33
from logging import getLogger
4-
from typing import Dict, Iterable, Tuple
4+
from typing import Iterable
55

66
from django import forms
77
from django.conf import settings
@@ -33,20 +33,20 @@ def __init__(self, course, *args, **kwargs):
3333
# prevent description from getting too big
3434
self.fields["description"].widget.attrs.update({"id": "description"})
3535

36-
def get_sections(self) -> Iterable[Dict[str, str | Tuple[str, ...] | bool]]:
36+
def get_sections(self) -> Iterable[dict[str, str | tuple[str, ...] | bool]]:
3737
for section in self.Meta.sections:
3838
if section["name"]:
3939
# operate on copy so errors on refresh don't happen
40-
section = section.copy()
41-
section["fields"] = tuple(self[field] for field in section["fields"])
42-
yield section
40+
new_section = section.copy()
41+
new_section["fields"] = tuple(self[field] for field in new_section["fields"])
42+
yield new_section
4343

44-
def get_main_section(self) -> Dict[str, str | Tuple[str, ...]]:
44+
def get_main_section(self) -> dict[str, str | tuple[str, ...]]:
4545
for section in self.Meta.sections:
4646
if section["name"] == "":
47-
section = section.copy()
48-
section["fields"] = tuple(self[field] for field in section["fields"])
49-
return section
47+
new_section = section.copy()
48+
new_section["fields"] = tuple(self[field] for field in new_section["fields"])
49+
return new_section
5050
logger.error(f"Could not find main section for assignment {self}")
5151
return {"fields": ()}
5252

tin/apps/assignments/models.py

+23-23
Original file line numberDiff line numberDiff line change
@@ -28,12 +28,12 @@ class Folder(models.Model):
2828
def __str__(self):
2929
return self.name
3030

31-
def __repr__(self):
32-
return self.name
33-
3431
def get_absolute_url(self):
3532
return reverse("assignments:show_folder", args=[self.course.id, self.id])
3633

34+
def __repr__(self):
35+
return self.name
36+
3737

3838
class AssignmentQuerySet(models.query.QuerySet):
3939
def filter_visible(self, user):
@@ -54,14 +54,12 @@ def filter_editable(self, user):
5454
def upload_grader_file_path(assignment, _): # pylint: disable=unused-argument
5555
assert assignment.id is not None
5656
if assignment.language == "P":
57-
return "assignment-{}/grader.py".format(assignment.id)
57+
return f"assignment-{assignment.id}/grader.py"
5858
else:
59-
return "assignment-{}/Grader.java".format(assignment.id)
59+
return f"assignment-{assignment.id}/Grader.java"
6060

6161

6262
class Assignment(models.Model):
63-
objects = AssignmentQuerySet.as_manager()
64-
6563
name = models.CharField(max_length=50)
6664
folder = models.ForeignKey(
6765
Folder,
@@ -126,15 +124,17 @@ class Assignment(models.Model):
126124

127125
last_action_output = models.CharField(max_length=16 * 1024, default="", null=False, blank=True)
128126

129-
def __str__(self):
130-
return self.name
127+
objects = AssignmentQuerySet.as_manager()
131128

132-
def __repr__(self):
129+
def __str__(self):
133130
return self.name
134131

135132
def get_absolute_url(self):
136133
return reverse("assignments:show", args=(self.id,))
137134

135+
def __repr__(self):
136+
return self.name
137+
138138
def make_assignment_dir(self) -> None:
139139
assignment_path = os.path.join(settings.MEDIA_ROOT, f"assignment-{self.id}")
140140
os.makedirs(assignment_path, exist_ok=True)
@@ -169,7 +169,7 @@ def save_grader_file(self, grader_text: str) -> None:
169169
stdout=subprocess.DEVNULL,
170170
stderr=subprocess.PIPE,
171171
encoding="utf-8",
172-
universal_newlines=True,
172+
text=True,
173173
check=True,
174174
)
175175
except FileNotFoundError as e:
@@ -203,7 +203,7 @@ def list_files(self) -> List[Tuple[int, str, str, int, datetime.datetime]]:
203203
def save_file(self, file_text: str, file_name: str) -> None:
204204
self.make_assignment_dir()
205205

206-
fpath = os.path.join(settings.MEDIA_ROOT, "assignment-{}".format(self.id), file_name)
206+
fpath = os.path.join(settings.MEDIA_ROOT, f"assignment-{self.id}", file_name)
207207

208208
os.makedirs(os.path.dirname(fpath), exist_ok=True)
209209

@@ -220,7 +220,7 @@ def save_file(self, file_text: str, file_name: str) -> None:
220220
stdout=subprocess.DEVNULL,
221221
stderr=subprocess.PIPE,
222222
encoding="utf-8",
223-
universal_newlines=True,
223+
text=True,
224224
check=True,
225225
)
226226
except FileNotFoundError as e:
@@ -349,12 +349,12 @@ class Meta:
349349
def __str__(self):
350350
return f"Quiz for {self.assignment}"
351351

352-
def __repr__(self):
353-
return f"Quiz for {self.assignment}"
354-
355352
def get_absolute_url(self):
356353
return reverse("assignments:show", args=(self.assignment.id,))
357354

355+
def __repr__(self):
356+
return f"Quiz for {self.assignment}"
357+
358358
def issues_for_student(self, student):
359359
return (
360360
sum(lm.severity for lm in self.log_messages.filter(student=student))
@@ -387,14 +387,14 @@ class LogMessage(models.Model):
387387
def __str__(self):
388388
return f"{self.content} for {self.quiz}"
389389

390-
def __repr__(self):
391-
return f"{self.content} for {self.quiz}"
392-
393390
def get_absolute_url(self):
394391
return reverse(
395392
"assignments:student_submission", args=(self.quiz.assignment.id, self.student.id)
396393
)
397394

395+
def __repr__(self):
396+
return f"{self.content} for {self.quiz}"
397+
398398

399399
def moss_base_file_path(obj, _): # pylint: disable=unused-argument
400400
assert obj.assignment.id is not None
@@ -452,6 +452,9 @@ class MossResult(models.Model):
452452
url = models.URLField(max_length=200, null=True, blank=True)
453453
status = models.CharField(max_length=1024, default="", null=False, blank=True)
454454

455+
def __str__(self):
456+
return f"Moss result for {self.assignment}"
457+
455458
@property
456459
def extension(self):
457460
return "java" if self.language == "java" else "py"
@@ -460,9 +463,6 @@ def extension(self):
460463
def download_folder(self):
461464
return os.path.join(settings.MEDIA_ROOT, "moss-runs", f"moss-{self.id}")
462465

463-
def __str__(self):
464-
return f"Moss result for {self.assignment}"
465-
466466
def __repr__(self):
467467
return f"Moss result for {self.assignment}"
468468

@@ -476,7 +476,7 @@ def run_action(command: List[str]) -> str:
476476
stdout=subprocess.PIPE,
477477
stderr=subprocess.STDOUT,
478478
encoding="utf-8",
479-
universal_newlines=True,
479+
text=True,
480480
)
481481
except FileNotFoundError as e:
482482
logger.error("File not found: %s", e)

0 commit comments

Comments
 (0)