Skip to content

Commit a566f14

Browse files
committed
Add quiz module with checks for docstring and return types
1 parent 64caa90 commit a566f14

File tree

3 files changed

+90
-0
lines changed

3 files changed

+90
-0
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1313
- `get_caller()` function in coverage.py
1414
- `half_credit()` function in coverage.py
1515
- `full_credit()` function in coverage.py
16+
- `check_docstring()` function in quiz.py
17+
- `check_return_types()` function in quiz.py
1618

1719
### Changed
1820

jmu_pytest_utils/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
* `jmu_pytest_utils.audit` – analyze the student's source code
1515
* `jmu_pytest_utils.coverage` – analyze the student's unit tests
1616
* `jmu_pytest_utils.meta` – analyze the submission metadata
17+
* `jmu_pytest_utils.quiz` – analyze docstring and return types
1718
1819
And of course `pytest`:
1920

jmu_pytest_utils/quiz.py

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
"""Helper functions used during in-class quizzes."""
2+
3+
from types import ModuleType
4+
from typing import Any, Callable
5+
6+
import pytest
7+
from coverage import get_caller
8+
9+
10+
def check_docstring(module: ModuleType, min_len: int = 15) -> None:
11+
"""Verify that a module's docstring exists.
12+
13+
If the calling test function does not already have a docstring, the
14+
docstring is automatically set to "Check for docstring."
15+
16+
If the check passes, the `output` attribute of the test function is
17+
set to report the docstring's length.
18+
19+
Args:
20+
module: The module to examine.
21+
min_len: Minimum length of the docstring.
22+
"""
23+
24+
# Set the test function's docstring
25+
test_func = get_caller()
26+
if not test_func.__doc__:
27+
test_func.__doc__ = "Check for docstring"
28+
29+
# Check for the module's docstring
30+
assert module.__doc__, "Missing module docstring"
31+
length = len(module.__doc__)
32+
assert length >= min_len, "❌ Docstring is too short"
33+
test_func.output = f"✅ Docstring is {length} characters"
34+
35+
36+
def _type_name(value: Any) -> str:
37+
"""Format the name of an object's type name for output.
38+
39+
Args:
40+
value: The value returned from a function.
41+
42+
Returns:
43+
str: A phrase like "None", "an int", or "a str".
44+
"""
45+
name = type(value).__name__
46+
if name == "NoneType":
47+
return "None"
48+
elif name.startswith(("a", "e", "i", "o", "u")):
49+
return "an " + name
50+
else:
51+
return "a " + name
52+
53+
54+
def check_return_types(*calls: tuple[type, Callable[..., Any], *tuple[Any, ...]]) -> None:
55+
"""Verify the return type of one or more function calls.
56+
57+
If the calling test function does not already have a docstring, the
58+
docstring is automatically set to "Check return types."
59+
60+
This function builds a result string with a ✅ or ❌ for each call.
61+
If any return type is incorrect, pytest.fail() is called. Otherwise,
62+
the `output` attribute of the test function is set.
63+
64+
Args:
65+
calls: Tuples of (expected_type, function, *args).
66+
"""
67+
output = ""
68+
for expected_type, function, *args in calls:
69+
try:
70+
result = function(*args)
71+
if isinstance(result, expected_type):
72+
output += f"✅ {function.__name__}() returned {_type_name(result)}\n"
73+
else:
74+
output += f"❌ {function.__name__}() returned {_type_name(result)}\n"
75+
except Exception as e:
76+
output += f"❌ {function.__name__}() raised {type(e).__name__}: {e}\n"
77+
78+
# Set the test function's docstring
79+
test_func = get_caller()
80+
if not test_func.__doc__:
81+
test_func.__doc__ = "Check return types"
82+
83+
# Set the test function's output
84+
if "❌" in output:
85+
pytest.fail("\n" + output)
86+
else:
87+
test_func.output = output

0 commit comments

Comments
 (0)