Skip to content

Commit a4f23e6

Browse files
committed
feat: add util functions, fix pypi deployment
Add sanity checks.
1 parent b20b338 commit a4f23e6

File tree

6 files changed

+84
-12
lines changed

6 files changed

+84
-12
lines changed

CHANGELOG.md

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,19 @@
22

33
## Unreleased
44

5+
- Add `ensure_lf_line_ending`.
6+
- Account for the different of `tempfile.TemporaryDirectory` between 3.12 and 3.10.
7+
- Add `BaseTestCase.assertArchiveFileIsGzip`
8+
- Add sanity checks to `Repository` wheh working with `.tar.gz` file.
9+
- Add `Repository.run_executable` where `cwd` is always set to working directory of the repository.
10+
- Remove extra dependencies `doc` since we cannot list Git repository as a dependency and upload to PyPI.
11+
512
## v0.1.1a1
613

714
- (docs) Switch to manual API listing.
815
- (docs) Pin `sphinx-autodoc2` to our own fork before its [#17](https://github.com/sphinx-extensions2/sphinx-autodoc2/issues/17) is fixed.
916
- Add timeout to `run_executable` since some commands may require human's input and get stuck.
10-
- Add `grading.repository.RepositoryBaseTestCase`.
17+
- Add `grading_lib.repository.RepositoryBaseTestCase`.
1118
- Add `Repository.get_all_tag_refs`.
1219
- Add `Repository.get_tag_refs_at`.
1320
- Fix `Repository` from an archive file is using the archive's filename instead of the "repo".

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ python -m pip install cs3560-grading-lib
1515

1616
## Documentation
1717

18-
View the [latest documentation](https://grading-lib.readthedocs.io/en/latest/)
18+
View the [latest documentation](https://grading-lib.readthedocs.io/en/latest/) or the documentation for the [stable version](https://grading-lib.readthedocs.io/en/stable/).
1919

2020
## Similar Projects / More Mature Projects
2121

docs/README.md

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
# Documentation
22

3-
Ensure that the package is installed with `.[doc]` extra dependencies. To build
4-
this documentation, run
5-
63
```console
4+
python -m pip install -r ./requirements.txt
75
make html
86
```
7+
8+
## About our sphinx-autodoc2
9+
10+
The sphinx-autodoc2 does not fix [issue #17](https://github.com/sphinx-extensions2/sphinx-autodoc2/issues/17) yet, but PyPI does not allow direct Github repository as a dependencies.

grading_lib/common.py

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import datetime
2-
import functools
32
import os
43
import subprocess
4+
import sys
55
import tempfile
66
import time
77
import unittest
@@ -98,6 +98,16 @@ def run_executable(
9898
return CommandResult(False, " ".join(args), e.output.decode())
9999

100100

101+
def ensure_lf_line_ending(path: Path | str) -> CommandResult:
102+
"""Run dos2unix on the file at path.
103+
104+
Students who are using Windows OS may submit answer.sh that has CRLF as its line
105+
ending. The CR will ofen get mixed into shell commands in the file and produces
106+
strange result / error.
107+
"""
108+
return run_executable(["dos2unix", str(path)])
109+
110+
101111
class MinimalistTestResult(unittest.TextTestResult):
102112
"""TextTestResult without the traceback.
103113
@@ -150,12 +160,31 @@ def setUp(self) -> None:
150160

151161
self.temporary_dir = None
152162
if self.with_temporary_dir:
153-
self.temporary_dir = tempfile.TemporaryDirectory(dir=Path("."))
163+
if sys.version_info < (3, 12, 0):
164+
self.temporary_dir = tempfile.TemporaryDirectory(dir=Path("."))
165+
else:
166+
self.temporary_dir = tempfile.TemporaryDirectory(
167+
dir=Path("."), delete=False
168+
)
154169
self.temporary_dir_path = Path(self.temporary_dir.name)
155170

156171
def tearDown(self) -> None:
157172
if not self.is_debug_mode and self.temporary_dir is not None:
158173
self.temporary_dir.cleanup()
174+
elif self.is_debug_mode and self.temporary_dir is not None:
175+
print(
176+
f"[info]: The temporary directory at '{self.temporary_dir_path}' is not deleted since the DEBUG is set to True."
177+
)
178+
179+
def assertArchiveFileIsGzip(self, path: Path):
180+
cmd_result = run_executable(["gunzip", "-t", str(path)])
181+
self.assertTrue(
182+
"not in gzip format" not in cmd_result.output,
183+
msg="Hint: Did you forget to use '-z' flag when creating the archive?",
184+
)
185+
self.assertCommandSuccessful(
186+
cmd_result,
187+
)
159188

160189
def assertFileExists(
161190
self, path: Path, msg_template: str = FILE_NOT_EXIST_TEXT_TEMPLATE

grading_lib/repository.py

Lines changed: 39 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import shutil
22
import subprocess
3+
import sys
34
import tempfile
45
import uuid
56
from pathlib import Path
@@ -9,7 +10,7 @@
910
from git.repo.fun import is_git_dir
1011
from git.util import IterableList
1112

12-
from .common import BaseTestCase
13+
from .common import BaseTestCase, CommandResult, run_executable
1314

1415

1516
class Repository:
@@ -30,7 +31,10 @@ def __init__(self, path: str | Path, *args, **kwargs):
3031
path = Path(path)
3132

3233
if "".join(path.suffixes) == ".tar.gz":
33-
self.temp_dir = tempfile.TemporaryDirectory(delete=False)
34+
if sys.version_info < (3, 12, 0):
35+
self.temp_dir = tempfile.TemporaryDirectory()
36+
else:
37+
self.temp_dir = tempfile.TemporaryDirectory(delete=False)
3438
temp_dir_path = Path(self.temp_dir.name)
3539
shutil.copy(path, temp_dir_path / path.name)
3640
subprocess.run(
@@ -40,6 +44,15 @@ def __init__(self, path: str | Path, *args, **kwargs):
4044
# We are not trying to find out what the root folder in the archive file is.
4145
# We do not create an archive file that does not have the root folder because
4246
# whens student extract the archive file, files will be everywhere.
47+
if not (temp_dir_path / "repo").exists():
48+
raise FileNotFoundError(
49+
f"Expect the archive to have folder 'repo', but this 'repo' folder cannot be found after extracting '{path.name}'"
50+
)
51+
if not (temp_dir_path / "repo" / ".git").exists():
52+
raise FileNotFoundError(
53+
f"Expect the 'repo' to be a Git repository (it must have .git folder), but '.git' is missing from the 'repo' extracted from '{path.name}'"
54+
)
55+
4356
self.repo = Repo(temp_dir_path / "repo", *args, **kwargs)
4457
else:
4558
if is_git_dir(path):
@@ -87,6 +100,12 @@ def to_gzip_archive(self, path: Path) -> None:
87100
]
88101
)
89102

103+
def run_executable(self, args: list[str], timeout: float = 15.0) -> CommandResult:
104+
"""
105+
Run a command using repostiory's working directory as cwd.
106+
"""
107+
return run_executable(args, cwd=self.repo.working_tree_dir, timeout=timeout)
108+
90109
def create_and_add_random_file(
91110
self, name: str | None = None, content: str | None = None
92111
) -> str:
@@ -139,6 +158,13 @@ def get_tag_refs_at(self, commit_hash: str) -> list[Tag]:
139158
matched_tag_refs.append(tag_ref)
140159
return matched_tag_refs
141160

161+
def visualize(self) -> str:
162+
"""
163+
Run 'git log --oneline --all --graph --decorate' and returns the output.
164+
"""
165+
res = self.repo.git.log("--graph", "--all", "--decorate", "--oneline")
166+
return res
167+
142168

143169
class RepositoryBaseTestCase(BaseTestCase):
144170
def assertHasTagWithNameAt(self, repo: Repository, name: str, commit_hash: str):
@@ -148,8 +174,9 @@ def assertHasTagWithNameAt(self, repo: Repository, name: str, commit_hash: str):
148174
if tag_ref.path == tag_path:
149175
return
150176

177+
tags_text = "\n".join(tag_ref.path for tag_ref in tag_refs)
151178
raise self.failureException(
152-
f"Expect to see a tag '{name}' at commit '{commit_hash}', but found none."
179+
f"Expect to see a tag '{name}' at commit '{commit_hash}', but found none. Tags at commit {commit_hash}:\n{tags_text}"
153180
)
154181

155182
def assertHasTagWithNameAndMessageAt(
@@ -166,6 +193,14 @@ def assertHasTagWithNameAndMessageAt(
166193
):
167194
return
168195

196+
tags_texts = []
197+
for tag_ref in tag_refs:
198+
if tag_ref.tag is None:
199+
text = f"{tag_ref.path}"
200+
else:
201+
text = f"{tag_ref.path}: {tag_ref.tag.message}"
202+
tags_texts.append(text)
203+
tags_text = "\n".join(tags_texts)
169204
raise self.failureException(
170-
f"Expect to see a tag '{name}' with message '{message}' at commit '{commit_hash}', but found none."
205+
f"Expect to see a tag '{name}' with message '{message}' at commit '{commit_hash}', but found none. Tags at commit {commit_hash}:\n{tags_text}"
171206
)

pyproject.toml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,6 @@ dependencies = [
2626

2727
[project.optional-dependencies]
2828
dev = ["pytest", "pytest-cov", "mypy", "ruff", "tox"]
29-
doc = ["sphinx", "myst-parser", "sphinx-book-theme", "sphinx-autodoc2@git+https://github.com/krerkkiat/sphinx-autodoc2.git@krerkkiat/fix-17"]
3029

3130
[project.urls]
3231
Documentation = "https://grading-lib.readthedocs.io/en/latest/"

0 commit comments

Comments
 (0)