check-pfda 0.0.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- check_pfda-0.0.1/PKG-INFO +27 -0
- check_pfda-0.0.1/README.md +16 -0
- check_pfda-0.0.1/pyproject.toml +26 -0
- check_pfda-0.0.1/setup.cfg +4 -0
- check_pfda-0.0.1/src/check_pfda/__init__.py +0 -0
- check_pfda-0.0.1/src/check_pfda/cli.py +29 -0
- check_pfda-0.0.1/src/check_pfda/core.py +54 -0
- check_pfda-0.0.1/src/check_pfda/utils.py +348 -0
- check_pfda-0.0.1/src/check_pfda.egg-info/PKG-INFO +27 -0
- check_pfda-0.0.1/src/check_pfda.egg-info/SOURCES.txt +12 -0
- check_pfda-0.0.1/src/check_pfda.egg-info/dependency_links.txt +1 -0
- check_pfda-0.0.1/src/check_pfda.egg-info/entry_points.txt +2 -0
- check_pfda-0.0.1/src/check_pfda.egg-info/requires.txt +4 -0
- check_pfda-0.0.1/src/check_pfda.egg-info/top_level.txt +1 -0
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: check-pfda
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: PFDA test running package.
|
|
5
|
+
Requires-Python: >=3.8
|
|
6
|
+
Description-Content-Type: text/markdown
|
|
7
|
+
Requires-Dist: click
|
|
8
|
+
Requires-Dist: pytest
|
|
9
|
+
Requires-Dist: requests
|
|
10
|
+
Requires-Dist: PyYAML
|
|
11
|
+
|
|
12
|
+
# check-pfda
|
|
13
|
+
|
|
14
|
+
> Compatibility goes here
|
|
15
|
+
|
|
16
|
+
Check-pfda is a package that tests student code and provides helpful feedback.
|
|
17
|
+
|
|
18
|
+
## Installation
|
|
19
|
+
`pip install check-pfda`
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
### Usage
|
|
23
|
+
1. Navigate to the root directory of a cloned assignment.
|
|
24
|
+
2. Run `pfda check`.
|
|
25
|
+
|
|
26
|
+
## Documentation
|
|
27
|
+
> Read The Docs page goes here
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# check-pfda
|
|
2
|
+
|
|
3
|
+
> Compatibility goes here
|
|
4
|
+
|
|
5
|
+
Check-pfda is a package that tests student code and provides helpful feedback.
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
`pip install check-pfda`
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
### Usage
|
|
12
|
+
1. Navigate to the root directory of a cloned assignment.
|
|
13
|
+
2. Run `pfda check`.
|
|
14
|
+
|
|
15
|
+
## Documentation
|
|
16
|
+
> Read The Docs page goes here
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61.0"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "check-pfda"
|
|
7
|
+
version = "0.0.1"
|
|
8
|
+
description = "PFDA test running package."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.8"
|
|
11
|
+
dependencies = [
|
|
12
|
+
"click",
|
|
13
|
+
"pytest",
|
|
14
|
+
"requests",
|
|
15
|
+
"PyYAML"
|
|
16
|
+
]
|
|
17
|
+
|
|
18
|
+
[project.scripts]
|
|
19
|
+
pfda = "check_pfda.cli:cli"
|
|
20
|
+
|
|
21
|
+
[tool.setuptools]
|
|
22
|
+
include-package-data = true
|
|
23
|
+
|
|
24
|
+
[tool.pydoclint]
|
|
25
|
+
style = 'sphinx'
|
|
26
|
+
exclude = '\.git|\.venv'
|
|
File without changes
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"""Command-line interface for package usage."""
|
|
2
|
+
|
|
3
|
+
import click
|
|
4
|
+
|
|
5
|
+
from .core import check_student_code
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@click.group()
|
|
9
|
+
def cli():
|
|
10
|
+
"""Group for CLI entrypoint."""
|
|
11
|
+
pass
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@cli.command()
|
|
15
|
+
@click.option(
|
|
16
|
+
'-v',
|
|
17
|
+
'--verbosity',
|
|
18
|
+
count=True,
|
|
19
|
+
help='Verbosity of test output'
|
|
20
|
+
)
|
|
21
|
+
@click.option(
|
|
22
|
+
'-d',
|
|
23
|
+
'--debug',
|
|
24
|
+
is_flag=True,
|
|
25
|
+
help='Swap to offline tests'
|
|
26
|
+
)
|
|
27
|
+
def check(verbosity: int, debug: bool) -> None:
|
|
28
|
+
"""Run student checks."""
|
|
29
|
+
check_student_code(verbosity, debug)
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
"""Collect tests and run them on supplied code."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
print(f"PID: {os.getpid()}")
|
|
5
|
+
|
|
6
|
+
from tempfile import NamedTemporaryFile
|
|
7
|
+
import sys
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
from check_pfda.utils import get_current_assignment, get_tests
|
|
11
|
+
|
|
12
|
+
from click import echo, secho
|
|
13
|
+
|
|
14
|
+
import pytest
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def check_student_code(verbosity: int, debug: bool = False) -> None:
|
|
18
|
+
"""Check student code."""
|
|
19
|
+
chapter, assignment = get_current_assignment()
|
|
20
|
+
echo(f"Checking assignment {assignment} at verbosity {verbosity}...")
|
|
21
|
+
cwd_src = os.path.join(os.getcwd(), "src")
|
|
22
|
+
if cwd_src not in sys.path:
|
|
23
|
+
sys.path.insert(0, cwd_src)
|
|
24
|
+
if debug:
|
|
25
|
+
secho("\nIN DEBUG MODE\n", fg="blue", bold=True)
|
|
26
|
+
base_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
|
27
|
+
test_path = os.path.join(base_dir, "check_pfda",
|
|
28
|
+
".test_static_imports",
|
|
29
|
+
f"test_{assignment}.py")
|
|
30
|
+
args = [test_path]
|
|
31
|
+
if verbosity > 0:
|
|
32
|
+
args.append(f"-{'v' * verbosity}")
|
|
33
|
+
exit_code = pytest.main(args)
|
|
34
|
+
echo(f"Pytest finished with exit code {exit_code}")
|
|
35
|
+
return
|
|
36
|
+
tests = get_tests(chapter, assignment)
|
|
37
|
+
|
|
38
|
+
temp_file = NamedTemporaryFile(suffix=".py", delete=False)
|
|
39
|
+
try:
|
|
40
|
+
temp_file.write(tests.encode("utf-8"))
|
|
41
|
+
temp_file.flush()
|
|
42
|
+
temp_file.close()
|
|
43
|
+
|
|
44
|
+
echo(f"Temp test file at: {temp_file.name}")
|
|
45
|
+
args = [temp_file.name]
|
|
46
|
+
if verbosity > 0:
|
|
47
|
+
args.append(f"-{'v' * verbosity}")
|
|
48
|
+
exit_code = pytest.main(args)
|
|
49
|
+
echo(f"Pytest finished with exit code {exit_code}")
|
|
50
|
+
finally:
|
|
51
|
+
os.remove(temp_file.name)
|
|
52
|
+
echo("Removed temp test file.")
|
|
53
|
+
if cwd_src in sys.path:
|
|
54
|
+
sys.path.remove(cwd_src)
|
|
@@ -0,0 +1,348 @@
|
|
|
1
|
+
"""Public modules."""
|
|
2
|
+
import os
|
|
3
|
+
import sys
|
|
4
|
+
from importlib import import_module
|
|
5
|
+
from io import StringIO
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
import re
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
import click
|
|
11
|
+
|
|
12
|
+
import py.path
|
|
13
|
+
|
|
14
|
+
import pytest
|
|
15
|
+
|
|
16
|
+
import requests
|
|
17
|
+
|
|
18
|
+
import yaml
|
|
19
|
+
|
|
20
|
+
# Constants.
|
|
21
|
+
STRING_LEN_LIMIT = 1000
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
"""
|
|
25
|
+
Public functions. These are intended for direct implementation in unit tests.
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def assert_script_exists(module_name: str, accepted_dirs: list) -> None:
|
|
30
|
+
"""Check accepted subfolders for the module script.
|
|
31
|
+
|
|
32
|
+
:param module_name: The name of the module to check.
|
|
33
|
+
:type module_name: str
|
|
34
|
+
:param accepted_dirs: The accepted subfolders for the script.
|
|
35
|
+
:type accepted_dirs: list
|
|
36
|
+
:return: None
|
|
37
|
+
:rtype: None
|
|
38
|
+
"""
|
|
39
|
+
curr_dir = os.getcwd()
|
|
40
|
+
for subfolder in accepted_dirs:
|
|
41
|
+
filename = os.path.join(curr_dir, subfolder, f"{module_name}.py")
|
|
42
|
+
print(filename)
|
|
43
|
+
if os.path.exists(filename):
|
|
44
|
+
return None
|
|
45
|
+
pytest.fail(reason=f"The script '{module_name}.py' does not exist in "
|
|
46
|
+
f"the accepted directories: {accepted_dirs}.")
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def build_user_friendly_err(actual: Any, expected: Any) -> str:
|
|
50
|
+
"""Build a user-friendly error to accompany a pytest AssertionError.
|
|
51
|
+
|
|
52
|
+
:param actual: The actual output of the tested program.
|
|
53
|
+
:type actual: Any
|
|
54
|
+
:param expected: The expected output of the tested program.
|
|
55
|
+
:type expected: Any
|
|
56
|
+
:return: A user-friendly error message.
|
|
57
|
+
:rtype: str
|
|
58
|
+
"""
|
|
59
|
+
errors = []
|
|
60
|
+
|
|
61
|
+
if actual is None and expected is not None:
|
|
62
|
+
errors.append("Your function/program did not produce any output.")
|
|
63
|
+
elif actual is not None and expected is None:
|
|
64
|
+
errors.append("Your function/program produced output when it was "
|
|
65
|
+
"not expected.")
|
|
66
|
+
|
|
67
|
+
if _is_different_type(expected, actual):
|
|
68
|
+
errors.append(
|
|
69
|
+
f"The expected data type is {_format_type(type(expected))}, "
|
|
70
|
+
f"but your actual output data type is "
|
|
71
|
+
f"{_format_type(type(actual))}.")
|
|
72
|
+
elif isinstance(expected, str):
|
|
73
|
+
for error in _find_string_comparison_errors(expected, actual):
|
|
74
|
+
errors.append(error)
|
|
75
|
+
else:
|
|
76
|
+
errors.append("Your output does not match the expected "
|
|
77
|
+
"format or values.")
|
|
78
|
+
|
|
79
|
+
errors_formatted = "\n- ".join(errors)
|
|
80
|
+
error_msg = (
|
|
81
|
+
f"ANGM2305 Autograder User-friendly Message:"
|
|
82
|
+
f"\n--------------------------------------------------------"
|
|
83
|
+
f"\nThe Test Failed."
|
|
84
|
+
f"\n\nWhat the Test Expected:"
|
|
85
|
+
f"\n{expected}"
|
|
86
|
+
f"\n\nWhat your Function/Program output:"
|
|
87
|
+
f"\n{actual}"
|
|
88
|
+
f"\n\nIssues Found:"
|
|
89
|
+
f"\n- {errors_formatted}"
|
|
90
|
+
f"\n\nPytest Error Message:"
|
|
91
|
+
f"\n---------------------")
|
|
92
|
+
return error_msg
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def generate_temp_file(filename: str,
|
|
96
|
+
tmpdir: py.path.local,
|
|
97
|
+
contents: Any) -> str:
|
|
98
|
+
"""Generate a temporary file to test with.
|
|
99
|
+
|
|
100
|
+
:param filename: The name of the temporary file.
|
|
101
|
+
:type filename: str
|
|
102
|
+
:param tmpdir: Pytest's tmpdir fixture.
|
|
103
|
+
:type tmpdir: py.path.local
|
|
104
|
+
:param contents: The contents to write to the temporary file.
|
|
105
|
+
:type contents: Any
|
|
106
|
+
:return: The path to the temporary file.
|
|
107
|
+
:rtype: str
|
|
108
|
+
"""
|
|
109
|
+
filepath = os.path.join(tmpdir, filename)
|
|
110
|
+
with open(filepath, "w") as f:
|
|
111
|
+
f.write(contents)
|
|
112
|
+
return filepath
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def get_tests(chapter, assignment: str) -> str:
|
|
116
|
+
"""Get tests for a given assignment."""
|
|
117
|
+
tests_repo_url = _construct_test_url(chapter, assignment)
|
|
118
|
+
try:
|
|
119
|
+
r = requests.get(tests_repo_url, timeout=10)
|
|
120
|
+
r.raise_for_status()
|
|
121
|
+
except requests.exceptions.RequestException as e:
|
|
122
|
+
click.secho(f"Error fetching test file for assignment '"
|
|
123
|
+
f"{assignment}': {e}", fg="red", bold=True)
|
|
124
|
+
sys.exit(1)
|
|
125
|
+
|
|
126
|
+
if not r.text.strip():
|
|
127
|
+
click.secho("Error: Received empty test file. Contact your "
|
|
128
|
+
"instructor", fg="red", bold=True)
|
|
129
|
+
sys.exit(1)
|
|
130
|
+
|
|
131
|
+
if "def test_" not in r.text:
|
|
132
|
+
click.secho(
|
|
133
|
+
"Warning: This may not be a valid test file.",
|
|
134
|
+
fg="yellow"
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
return r.text
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def reload_module(module_name: str) -> None:
|
|
141
|
+
"""Reload the module. Ensures it is reloaded if previously loaded.
|
|
142
|
+
|
|
143
|
+
:param module_name: The name of the module to reload.
|
|
144
|
+
:type module_name: str
|
|
145
|
+
"""
|
|
146
|
+
sys.modules.pop(module_name, None)
|
|
147
|
+
import_module(name=module_name)
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def patch_input_output(monkeypatch: Any,
|
|
151
|
+
test_inputs: list,
|
|
152
|
+
module_name: str) -> StringIO:
|
|
153
|
+
"""Patch input() and standard out.
|
|
154
|
+
|
|
155
|
+
:param monkeypatch: Pytest's monkeypatch fixture.
|
|
156
|
+
:type monkeypatch: Any
|
|
157
|
+
:param test_inputs: The inputs to test known outputs against.
|
|
158
|
+
:type test_inputs: list
|
|
159
|
+
:param module_name: The name of the module to test.
|
|
160
|
+
:type module_name: str
|
|
161
|
+
:return: The patched standard out.
|
|
162
|
+
:rtype: StringIO
|
|
163
|
+
"""
|
|
164
|
+
# patches the standard output to catch the output of print()
|
|
165
|
+
patch_stdout = StringIO()
|
|
166
|
+
# Returns a new mock object which undoes any patching done inside
|
|
167
|
+
# the with block on exit to avoid breaking pytest itself.
|
|
168
|
+
with monkeypatch.context() as m:
|
|
169
|
+
# patches the input()
|
|
170
|
+
m.setattr("builtins.input", lambda _: test_inputs.pop(0))
|
|
171
|
+
m.setattr("sys.stdout", patch_stdout)
|
|
172
|
+
reload_module(module_name)
|
|
173
|
+
return patch_stdout
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
"""
|
|
177
|
+
Private functions. Do not implement these directly in any unit tests.
|
|
178
|
+
"""
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def _format_type(var_type: str) -> str:
|
|
182
|
+
"""Format repr class type.
|
|
183
|
+
|
|
184
|
+
:param var_type: The name of a type.
|
|
185
|
+
:type var_type: str
|
|
186
|
+
:return: The formatted type.
|
|
187
|
+
:rtype: str
|
|
188
|
+
"""
|
|
189
|
+
return var_type.split("'")[1::2][0]
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def _is_different_type(expected: Any, actual: Any) -> bool:
|
|
193
|
+
"""Evaluate if the two arguments are the same type.
|
|
194
|
+
|
|
195
|
+
:param expected: The expected object.
|
|
196
|
+
:type expected: Any
|
|
197
|
+
:param actual: The actual object.
|
|
198
|
+
:type actual: Any
|
|
199
|
+
:return: If the two objects are the same type.
|
|
200
|
+
:rtype: bool
|
|
201
|
+
"""
|
|
202
|
+
return not isinstance(actual, type(expected))
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def _find_string_comparison_errors(expected: str, actual: str) -> list:
|
|
206
|
+
"""Handle string comparison for asserting equivalency.
|
|
207
|
+
|
|
208
|
+
:param expected: The expected string.
|
|
209
|
+
:type expected: str
|
|
210
|
+
:param actual: The actual string.
|
|
211
|
+
:type actual: str
|
|
212
|
+
:return: An error message.
|
|
213
|
+
:rtype: list
|
|
214
|
+
"""
|
|
215
|
+
errors = []
|
|
216
|
+
expected_len = len(expected)
|
|
217
|
+
actual_len = len(actual)
|
|
218
|
+
# Enforce a length limit in case a student accidentally makes
|
|
219
|
+
# an enormous string.
|
|
220
|
+
check_length_error_msg = _check_length_limit(actual, STRING_LEN_LIMIT)
|
|
221
|
+
if check_length_error_msg:
|
|
222
|
+
errors.append(check_length_error_msg)
|
|
223
|
+
return errors
|
|
224
|
+
check_functions = [_check_trailing_newline, _check_double_spaces]
|
|
225
|
+
for f in check_functions:
|
|
226
|
+
if f(expected, actual):
|
|
227
|
+
errors.append(f(expected, actual))
|
|
228
|
+
# Highlight which character differs.
|
|
229
|
+
if expected_len == actual_len:
|
|
230
|
+
errors.append(_find_incorrect_char(expected, actual))
|
|
231
|
+
# Else highlight the length difference.
|
|
232
|
+
else:
|
|
233
|
+
errors.append(f"The expected and actual string lengths are "
|
|
234
|
+
f"different. Expected length: {expected_len}, but "
|
|
235
|
+
f"got length: {actual_len}.")
|
|
236
|
+
return errors
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
def _find_incorrect_char(expected: str, actual: str) -> str:
|
|
240
|
+
"""Find the index of the first actual char that doesn't match expected.
|
|
241
|
+
|
|
242
|
+
:param expected: The expected string.
|
|
243
|
+
:type expected: str
|
|
244
|
+
:param actual: The actual string.
|
|
245
|
+
:type actual: str
|
|
246
|
+
:return: A string containing the incorrect character and its index.
|
|
247
|
+
:rtype: str
|
|
248
|
+
"""
|
|
249
|
+
for idx, expected_char in enumerate(expected):
|
|
250
|
+
actual_char = actual[idx]
|
|
251
|
+
if expected_char != actual_char:
|
|
252
|
+
return (f"Character '{actual[idx]}' at index {idx} does "
|
|
253
|
+
f"not match with the expect output. "
|
|
254
|
+
f"This is the first mismatched character. There "
|
|
255
|
+
f"may be others.")
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
def _check_trailing_newline(expected: str, actual: str) -> str | None:
|
|
259
|
+
"""Check the actual string for common errors.
|
|
260
|
+
|
|
261
|
+
:param expected: The expected string.
|
|
262
|
+
:type expected: str
|
|
263
|
+
:param actual: The actual string.
|
|
264
|
+
:type actual: str
|
|
265
|
+
:return: A string to concatenate to the error if there are
|
|
266
|
+
common errors, otherwise None.
|
|
267
|
+
:rtype: str | None
|
|
268
|
+
"""
|
|
269
|
+
if actual.endswith("\n") and not expected.endswith("\n"):
|
|
270
|
+
return ("Your program/function's output has an extra newline "
|
|
271
|
+
"character \'\\n\' at the end.")
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
def _check_double_spaces(expected: str, actual: str) -> str | None:
|
|
275
|
+
"""Check the actual string for double spaces.
|
|
276
|
+
|
|
277
|
+
:param expected: The expected string.
|
|
278
|
+
:type expected: str
|
|
279
|
+
:param actual: The actual string.
|
|
280
|
+
:type actual: str
|
|
281
|
+
:return: A string to concatenate to the error if there are
|
|
282
|
+
common errors, otherwise None.
|
|
283
|
+
:rtype: str | None
|
|
284
|
+
"""
|
|
285
|
+
if " " in actual and " " not in expected:
|
|
286
|
+
return (f"There are two spaces at index {actual.index(' ')} "
|
|
287
|
+
f"of your program/function's output.")
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
def _check_length_limit(actual: str, limit: int) -> str | None:
|
|
291
|
+
"""Enforce a length limit on the actual string.
|
|
292
|
+
|
|
293
|
+
:param actual: The actual string.
|
|
294
|
+
:type actual: str
|
|
295
|
+
:param limit: The expected length.
|
|
296
|
+
:type limit: int
|
|
297
|
+
:return: A string to concatenate to the error if there are
|
|
298
|
+
common errors, otherwise None.
|
|
299
|
+
:rtype: str | None
|
|
300
|
+
"""
|
|
301
|
+
actual_len = len(actual)
|
|
302
|
+
if actual_len > limit:
|
|
303
|
+
return (f"The actual string exceeds the maximum allowed "
|
|
304
|
+
f"length.\n Actual length is: {actual_len}\n"
|
|
305
|
+
f"Limit is: {limit}")
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
def _construct_test_url(chapter, assignment: str) -> str:
|
|
309
|
+
base_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
|
310
|
+
config_path = os.path.join(base_dir, "check_pfda", "config.yaml")
|
|
311
|
+
|
|
312
|
+
with open(config_path, "r") as f:
|
|
313
|
+
config = yaml.safe_load(f)
|
|
314
|
+
|
|
315
|
+
base_url = config["tests"]["tests_repo_url"]
|
|
316
|
+
|
|
317
|
+
# query at the end forces browser to flush cache
|
|
318
|
+
return f"{base_url}/c{chapter}/test_{assignment}.py?now=0423"
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
def get_module_in_src() -> str:
|
|
322
|
+
"""Get the name of the assignment the student is working on."""
|
|
323
|
+
src_dir = Path.cwd() / "src"
|
|
324
|
+
py_files = list(src_dir.glob("*.py"))
|
|
325
|
+
if not py_files:
|
|
326
|
+
raise FileNotFoundError("No Python module found in the src/"
|
|
327
|
+
" directory.")
|
|
328
|
+
if len(py_files) > 1:
|
|
329
|
+
raise RuntimeError("Multiple Python modules found in src/."
|
|
330
|
+
" Expected only one.")
|
|
331
|
+
return py_files[0].stem
|
|
332
|
+
|
|
333
|
+
|
|
334
|
+
def get_current_assignment():
|
|
335
|
+
"""
|
|
336
|
+
Parses a path string to extract chapter and assignment information.
|
|
337
|
+
return:
|
|
338
|
+
A dictionary with 'chapter' and 'assignment' if found, otherwise an empty dictionary.
|
|
339
|
+
"""
|
|
340
|
+
path = str(Path.cwd())
|
|
341
|
+
match = re.search(r"c(\d+)-lab-(.*?)-bencres-demo", path)
|
|
342
|
+
if match:
|
|
343
|
+
chapter = match.group(1).replace('-', '_')
|
|
344
|
+
assignment = match.group(2).replace('-', '_')
|
|
345
|
+
return (chapter, assignment)
|
|
346
|
+
# return {"chapter": chapter, "assignment": assignment}
|
|
347
|
+
else:
|
|
348
|
+
return {}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: check-pfda
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: PFDA test running package.
|
|
5
|
+
Requires-Python: >=3.8
|
|
6
|
+
Description-Content-Type: text/markdown
|
|
7
|
+
Requires-Dist: click
|
|
8
|
+
Requires-Dist: pytest
|
|
9
|
+
Requires-Dist: requests
|
|
10
|
+
Requires-Dist: PyYAML
|
|
11
|
+
|
|
12
|
+
# check-pfda
|
|
13
|
+
|
|
14
|
+
> Compatibility goes here
|
|
15
|
+
|
|
16
|
+
Check-pfda is a package that tests student code and provides helpful feedback.
|
|
17
|
+
|
|
18
|
+
## Installation
|
|
19
|
+
`pip install check-pfda`
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
### Usage
|
|
23
|
+
1. Navigate to the root directory of a cloned assignment.
|
|
24
|
+
2. Run `pfda check`.
|
|
25
|
+
|
|
26
|
+
## Documentation
|
|
27
|
+
> Read The Docs page goes here
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
README.md
|
|
2
|
+
pyproject.toml
|
|
3
|
+
src/check_pfda/__init__.py
|
|
4
|
+
src/check_pfda/cli.py
|
|
5
|
+
src/check_pfda/core.py
|
|
6
|
+
src/check_pfda/utils.py
|
|
7
|
+
src/check_pfda.egg-info/PKG-INFO
|
|
8
|
+
src/check_pfda.egg-info/SOURCES.txt
|
|
9
|
+
src/check_pfda.egg-info/dependency_links.txt
|
|
10
|
+
src/check_pfda.egg-info/entry_points.txt
|
|
11
|
+
src/check_pfda.egg-info/requires.txt
|
|
12
|
+
src/check_pfda.egg-info/top_level.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
check_pfda
|