Skip to content

Commit f04191d

Browse files
Nathan SpragueNathan Sprague
Nathan Sprague
authored and
Nathan Sprague
committed
add docstrings
1 parent c5c8493 commit f04191d

File tree

2 files changed

+146
-6
lines changed

2 files changed

+146
-6
lines changed

jmu_gradescope_utils/jmu_test_case.py

Lines changed: 141 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,13 @@
1+
"""JMUTestCase class and related code.
2+
3+
The ``JMUTestCase`` is a subclass of ``unittest.TestCase`` with the following
4+
features:
5+
6+
* Some useful assertions for autograding.
7+
* Guaranteed test method execution order based on definition order.
8+
* ``@required`` annotation that can be used to make a particular test a requirement for all subsequent tests.
9+
10+
"""
111
import types
212
import unittest
313
import tempfile
@@ -80,14 +90,37 @@ def __new__(cls, name, bases, local):
8090

8191

8292
class _JmuTestCase(unittest.TestCase):
83-
""" Additional useful assertions for grading. """
93+
"""Additional useful assertions for grading.
94+
95+
This is the superclass for JmuTestCase. Users should subclass
96+
``JmuTestCase`` in their test code. """
8497

8598
# counts the number of dynamic modules created
8699
module_count = 0
87100

88101
def assertScriptOutputEqual(self, filename, string_in, expected,
89102
variables=None, args="", msg=None,
90103
processor=None):
104+
"""Assert correct output for the provided Python script.
105+
106+
Args:
107+
filename (str): The name of the Python file to test
108+
string_in (str): A string that will be fed to stdin for the script
109+
expected (str): Expected stdout
110+
variables (dict): A dictionary mapping from variable names to
111+
values. The script will be edited with these
112+
substitutions before it is executed.
113+
args (str): Command line arguments that will be passed to the script.
114+
msg (str): Error message that will be printed if the assertion fails.
115+
processor (func): A function mapping from string to string that will
116+
process the script output before it is compared
117+
to the expected output.
118+
119+
Raises:
120+
AssertionError: If the expected output doesn't match the actual
121+
output.
122+
123+
"""
91124
tmpdir = None
92125
try:
93126
tmpdir, new_file_name = utils.replace_variables(filename,
@@ -136,6 +169,18 @@ def assertScriptOutputEqual(self, filename, string_in, expected,
136169
shutil.rmtree(tmpdir)
137170

138171
def assertNoLoops(self, filename, msg=None):
172+
""" Assert that the provided script has no for or while loops.
173+
174+
Comments will be ignored.
175+
176+
Args:
177+
filename (str): The name of the Python file to test
178+
msg (str): Error message that will be printed if the assertion fails.
179+
180+
Raises:
181+
AssertionError: If the file contains a loop.
182+
183+
"""
139184
loop_regex = "(^|(\r\n?|\n))\s*(for|while).*:\s*(#.*)*($|(\r\n?|\n))"
140185
count = utils.count_regex_matches(loop_regex, filename)
141186
message = f"It looks like the file {filename} contains at least one loop."
@@ -145,6 +190,18 @@ def assertNoLoops(self, filename, msg=None):
145190
self.fail(message)
146191

147192
def assertNoForLoops(self, filename, msg=None):
193+
""" Assert that the provided script has no for loops.
194+
195+
Comments will be ignored.
196+
197+
Args:
198+
filename (str): The name of the Python file to test
199+
msg (str): Error message that will be printed if the assertion fails.
200+
201+
Raises:
202+
AssertionError: If the file contains a for loop.
203+
204+
"""
148205
loop_regex = "(^|(\r\n?|\n))\s*(for).*:\s*(#.*)*($|(\r\n?|\n))"
149206
count = utils.count_regex_matches(loop_regex, filename)
150207
message = f"It looks like the file {filename} contains at least one for loop."
@@ -154,6 +211,18 @@ def assertNoForLoops(self, filename, msg=None):
154211
self.fail(message)
155212

156213
def assertNoWhileLoops(self, filename, msg=None):
214+
""" Assert that the provided script has no while loops.
215+
216+
Comments will be ignored.
217+
218+
Args:
219+
filename (str): The name of the Python file to test
220+
msg (str): Error message that will be printed if the assertion fails.
221+
222+
Raises:
223+
AssertionError: If the file contains a while loop.
224+
225+
"""
157226
loop_regex = "(^|(\r\n?|\n))\s*(while).*:\s*(#.*)*($|(\r\n?|\n))"
158227
count = utils.count_regex_matches(loop_regex, filename)
159228
message = f"It looks like the file {filename} contains at least one while loop."
@@ -163,6 +232,18 @@ def assertNoWhileLoops(self, filename, msg=None):
163232
self.fail(message)
164233

165234
def assertNoConditionals(self, filename, msg=None):
235+
""" Assert that the provided script has no conditional statements.
236+
237+
Comments will be ignored. ``if __name__ == "__main__":`` will be ignored.
238+
239+
Args:
240+
filename (str): The name of the Python file to test
241+
msg (str): Error message that will be printed if the assertion fails.
242+
243+
Raises:
244+
AssertionError: If the file contains an if.
245+
246+
"""
166247
if_regex = "(^|(\r\n?|\n))\s*if.*:\s*(#.*)*($|(\r\n?|\n))"
167248
main_regex = "(^|(\r\n?|\n))\s*if\s*__name__.*:\s*(#.*)*($|(\r\n?|\n))"
168249
count = utils.count_regex_matches(if_regex, filename)
@@ -174,18 +255,55 @@ def assertNoConditionals(self, filename, msg=None):
174255
self.fail(message)
175256

176257
def assertPassesPep8(self, filename):
258+
"""Assert that there are no formatting errors as discovered by flake8.
259+
260+
This will use the config file flake8.cfg included in the autograder
261+
folder.
262+
263+
Args:
264+
filename (str): The name of the Python file to test
265+
266+
Raises:
267+
AssertionError: If flake8 produces any output.
268+
269+
"""
177270
output = utils.run_flake8(filename)
178271
if len(output) != 0:
179272
self.fail("Submission does not pass pep8 checks:\n" + output)
180273
print('Submission passes all formatting checks!')
181274

182275
def assertDocstringsCorrect(self, filename):
276+
"""Assert that there are no formatting errors as discovered by flake8.
277+
278+
This will use the config file docstring.cfg included in the autograder
279+
folder.
280+
281+
Args:
282+
filename (str): The name of the Python file to test
283+
284+
Raises:
285+
AssertionError: If flake8 produces any output.
286+
287+
"""
183288
output = utils.run_flake8_docstring(filename)
184289
if len(output) != 0:
185290
self.fail("Submission does not pass docstring checks:\n" + output)
186291
print('Submission passes all docstring checks!')
187292

188293
def assertRequiredFilesPresent(self, required_files):
294+
"""Assert that all files in the provided list were submitted.
295+
296+
Note that this assertion won't get a chance to run if the test file
297+
attempts to import a missing file. One workaround is to do the
298+
imports inside the test methods.
299+
300+
Args:
301+
required_files (list): A list of Python file names.
302+
303+
Raises:
304+
AssertionError: If any of the indicated files are missing.
305+
306+
"""
189307
missing_files = utils.check_submitted_files(required_files)
190308
for path in missing_files:
191309
print('Missing {0}'.format(path))
@@ -194,6 +312,11 @@ def assertRequiredFilesPresent(self, required_files):
194312

195313
def assertOutputCorrect(self, filename, string_in, expected,
196314
variables=None, processor=None):
315+
""" Wrapper for assertScriptOutputEqual.
316+
317+
I'm not sure why this exists. -NRS
318+
319+
"""
197320
self.assertScriptOutputEqual(filename, string_in, expected,
198321
variables=variables, processor=processor)
199322
print('Correct output:\n' + expected)
@@ -213,6 +336,23 @@ def run_with_substitution(self, filename, variables, func):
213336
func(dynamic_module)
214337

215338
def assertMatchCount(self, filename, regex, num_matches, msg=None):
339+
"""Assert that the regex matches exactly the correct number of times.
340+
341+
Ignores comments and docstrings.
342+
343+
Could be used if the problem instructions say something like:
344+
"Your program must use exactly one while loop."
345+
346+
Args:
347+
filename (str): The name of the Python file to test
348+
regex (str): A Python regular expression.
349+
num_matches (str): The expected number of matches.
350+
msg (str): Error message that will be printed if the assertion fails.
351+
352+
Raises:
353+
AssertionError: If the count doesn't match.
354+
355+
"""
216356
count = utils.count_regex_matches(regex, filename)
217357
self.assertEqual(num_matches, count, msg=msg)
218358

jmu_gradescope_utils/utils.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -141,12 +141,12 @@ class IOContext:
141141
"""
142142
Context manager that allows specifying simulated keyboard input, and captures text output.
143143
144-
Use like so:
144+
Use like so::
145145
146-
context = IOContext("program input")
147-
with context:
148-
# my code block
149-
output = context.output
146+
context = IOContext("program input")
147+
with context:
148+
# my code block
149+
output = context.output
150150
"""
151151
def __init__(self, in_string):
152152
self.text_in = io.StringIO(in_string)

0 commit comments

Comments
 (0)