Skip to content

Commit 2ed1b6f

Browse files
author
Filip Schouwenaars
authored
Merge pull request #164 from datacamp/spec-multi-with-gen
Spec multi with gen, add test_not and fail SCTs
2 parents 67895c1 + b89f4a8 commit 2ed1b6f

5 files changed

Lines changed: 125 additions & 16 deletions

File tree

pythonwhat/check_funcs.py

Lines changed: 51 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
from pythonwhat.Test import Test, EqualTest
33
from pythonwhat.Feedback import Feedback
44
from pythonwhat.utils import get_ord
5+
from types import GeneratorType
56
from functools import partial
67
import copy
78

@@ -136,6 +137,8 @@ def has_equal_part_len(name, insufficient_msg, state=None):
136137
return state
137138

138139

140+
# functions for running multiple sub-tests ------------------------------------
141+
139142
def extend(*args, state=None):
140143
"""Run multiple subtests in sequence, each using the output state of the previous."""
141144

@@ -151,30 +154,51 @@ def multi(*args, state=None):
151154
rep = Reporter.active_reporter
152155

153156
# when input is a single list of subtests
154-
args = args[0] if len(args) == 1 and isinstance(args[0], (list, tuple)) else args
157+
if len(args) == 1 and isinstance(args[0], (list, tuple, GeneratorType)):
158+
args = args[0]
155159

156160
for test in args:
157161
# assume test is function needing a state argument
158162
# partial state so reporter can test
159-
# TODO: it seems clear the reporter doesn't need to hold state anymore
160163
closure = partial(test, state=state)
161-
# message from parent checks
162-
#prefix = state.build_message()
163-
## resetting reporter message until it can be refactored
164-
#prev_msg = rep.failure_msg
165164
rep.do_test(closure, "", state.highlight)
166-
#rep.failure_msg = prev_msg
167165

168166
# return original state, so can be chained
169167
return state
170168

169+
from pythonwhat.Test import TestFail
170+
171+
def test_not(*args, msg, state=None):
172+
"""Pass if all of the subtests fail"""
173+
rep = Reporter.active_reporter
174+
175+
try: multi(*args, state=state)
176+
except TestFail as e:
177+
rep.failed_test = False # protect against old behavior
178+
return state
179+
180+
_msg = state.build_message(msg)
181+
return rep.do_test(Test(msg))
182+
183+
# utility functions -----------------------------------------------------------
184+
171185
def quiet(n = 0, state=None):
172186
"""Turn off prepended messages. Defaults to turning all off."""
173187
cpy = copy.copy(state)
174188
hushed = [{**m, 'msg': ""} for m in cpy.messages]
175189
cpy.messages = hushed
176190
return cpy
177191

192+
def fail(msg="", state=None):
193+
"""Fail test with message"""
194+
rep = Reporter.active_reporter
195+
_msg = state.build_message(msg)
196+
rep.do_test(Test(Feedback(msg, state.highlight)))
197+
198+
return state
199+
200+
# context functions -----------------------------------------------------------
201+
178202
from pythonwhat.tasks import setUpNewEnvInProcess, breakDownNewEnvInProcess
179203
def with_context(*args, state=None):
180204
# set up context in processes
@@ -209,18 +233,31 @@ def with_context(*args, state=None):
209233
return state
210234

211235
def set_context(*args, state=None, **kwargs):
236+
"""Update context values for student and solution environments.
237+
238+
Note that excess args and unmatched kwargs will be unused in the student environment.
239+
If an argument is specified both by name and position args, will use named arg.
240+
"""
212241
stu_crnt = state.student_context.context
213242
sol_crnt = state.solution_context.context
214-
# set args specified by pos ----
215-
upd_stu = stu_crnt.update(dict(zip(sol_crnt.keys(), args)))
243+
# set args specified by pos -----------------------------------------------
244+
# stop if too many pos args for solution
245+
if len(args) > len(sol_crnt):
246+
raise IndexError("Too many positional args. There are {} context vals, but tried to set {}"
247+
.format(len(sol_crnt), len(args)))
248+
# set pos args
216249
upd_sol = sol_crnt.update(dict(zip(stu_crnt.keys(), args)))
250+
upd_stu = stu_crnt.update(dict(zip(sol_crnt.keys(), args)))
217251

218-
# set args specified by keyword ----
252+
# set args specified by keyword -------------------------------------------
253+
if set(kwargs) - set(upd_sol):
254+
raise KeyError("Context val names are {}, but tried to set {}"
255+
.format(upd_sol or "none", kwargs.keys()))
219256
out_sol = upd_sol.update(kwargs)
220257
# need to match keys in kwargs with corresponding keys in stu context
221258
# in case they used, e.g., different loop variable names
222259
match_keys = dict(zip(sol_crnt.keys(), stu_crnt.keys()))
223-
out_stu = upd_stu.update({match_keys[k]: v for k,v in kwargs.items()})
260+
out_stu = upd_stu.update({match_keys[k]: v for k,v in kwargs.items() if k in match_keys})
224261

225262
return state.to_child_state(student_subtree = None, solution_subtree = None,
226263
student_context = out_stu, solution_context = out_sol)
@@ -304,8 +341,8 @@ def call(args,
304341
eval_sol, str_sol = run_call(args, state.solution_parts['node'], state.solution_process, get_func, **kwargs)
305342

306343
if (test == 'error') ^ isinstance(str_sol, Exception):
307-
_msg = state.build_message("FMT:Calling for arguments {args} resulted in an error (or not an error if testing for one). Error message: {str_sol}",
308-
dict(args=args, str_sol=str_sol))
344+
_msg = state.build_message("FMT:Calling for arguments {args} resulted in an error (or not an error if testing for one). Error message: {type_err} {str_sol}",
345+
dict(args=args, type_err=type(str_sol), str_sol=str_sol))
309346
raise ValueError(_msg)
310347

311348
if isinstance(eval_sol, ReprFail):
@@ -367,7 +404,7 @@ def has_expr(incorrect_msg="FMT:Unexpected expression {test}: expected `{sol_eva
367404

368405
if (test == 'error') ^ isinstance(str_sol, Exception):
369406
raise ValueError("evaluating expression raised error in solution process (or not an error if testing for one). "
370-
"Error message: %s"%str_sol)
407+
"Error: %s - %s"%(type(str_sol), str_sol))
371408
if isinstance(eval_sol, ReprFail):
372409
raise ValueError("Couldn't figure out the value of a default argument: " + eval_sol.info)
373410

pythonwhat/check_syntax.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,24 +45,31 @@ class Chain:
4545
def __init__(self, state):
4646
self._state = state
4747
self._crnt_sct = None
48+
self._waiting_on_call = False
4849

4950
def __getattr__(self, attr):
5051
if attr not in ATTR_SCTS: raise AttributeError("No SCT named %s"%attr)
52+
elif self._waiting_on_call:
53+
raise AttributeError("Did you forget to call a statement? "
54+
"e.g. Ex().check_list_comp.check_body()")
5155
else:
5256
# make a copy to return,
5357
# in case someone does: a = chain.a; b = chain.b
5458
chain = copy.copy(self)
5559
chain._crnt_sct = ATTR_SCTS[attr]
60+
chain._waiting_on_call = True
5661
return chain
5762

5863
def __call__(self, *args, **kwargs):
5964
self._state = self._crnt_sct(state=self._state, *args, **kwargs)
65+
self._waiting_on_call = False
6066
return self
6167

6268
class F(Chain):
6369
def __init__(self, stack = None):
6470
self._crnt_sct = None
6571
self._stack = [] if stack is None else stack
72+
self._waiting_on_call = False
6673

6774
def __call__(self, *args, **kwargs):
6875
if not self._crnt_sct:
@@ -84,7 +91,7 @@ def Ex():
8491
# Prepare SCTs that may be chained attributes ----------------------
8592
# decorate functions that may try to run test_* function nodes as subtests
8693
# so they remove those nodes from the tree
87-
for k in ['multi', 'with_context']:
94+
for k in ['multi', 'with_context', 'test_not']:
8895
ATTR_SCTS[k] = multi_dec(ATTR_SCTS[k])
8996
# allow test_* functions as chained attributes
9097
for k in TEST_NAMES:

pythonwhat/check_wrappers.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@
5656

5757
for k in ['set_context',
5858
'has_equal_value', 'has_equal_output', 'has_equal_error', 'call',
59-
'extend', 'multi',
59+
'extend', 'multi', 'test_not', 'fail', 'quiet',
6060
'with_context',
6161
'check_args',
6262
'has_equal_part']:

tests/test_instructor_warnings.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,5 +10,27 @@ def test_converter_err(self):
1010
data['DC_CODE'] = data['DC_SOLUTION']
1111
self.assertRaises(ValueError, lambda: helper.run(data))
1212

13+
def test_check_syntax_double_getattr(self):
14+
data = {
15+
"DC_SOLUTION": "",
16+
"DC_CODE": "",
17+
"DC_SCT": """Ex().check_list_comp.check_body()"""
18+
}
19+
self.assertRaises(AttributeError, lambda: helper.run(data))
20+
21+
def test_check_syntax_check_index_no_index(self):
22+
data = {
23+
"DC_SOLUTION": "[i for i in range(1)]",
24+
"DC_CODE": "[i for i in range(1)]",
25+
"DC_SCT": """Ex().check_list_comp()"""
26+
}
27+
self.assertRaises(TypeError, lambda: helper.run(data))
28+
29+
def test_context_vals_wrong_place_in_chain(self):
30+
data = {"DC_SOLUTION": "[(i,j) for i,j in enumerate(range(10))]"}
31+
data["DC_CODE"] = data["DC_SOLUTION"]
32+
data["DC_SCT"] = """Ex().check_list_comp(0).set_context(i=1,j=2).check_iter()"""
33+
self.assertRaises(KeyError, lambda: helper.run(data))
34+
1335
if __name__ == "__main__":
1436
unittest.main()

tests/test_spec.py

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,7 +135,50 @@ def test_multi_splits_node_and_check(self):
135135
sct_payload = helper.run(self.data)
136136
self.assertTrue(sct_payload['correct'])
137137

138+
def test_multi_generator(self):
139+
self.data["DC_SCT"] = """
140+
Ex().check_list_comp(0).check_body()\
141+
.multi(set_context(aa=i).has_equal_value('wrong') for i in range(2))
142+
"""
143+
sct_payload = helper.run(self.data)
144+
self.assertTrue(sct_payload['correct'])
145+
146+
class TestTestNot(unittest.TestCase):
147+
def setUp(self):
148+
self.data = {
149+
"DC_SOLUTION": "x = 1",
150+
"DC_CODE": "x = 1"
151+
}
152+
153+
def test_pass(self):
154+
self.data["DC_SCT"] = """Ex().test_not(check_list_comp(0), msg="no")"""
155+
sct_payload = helper.run(self.data)
156+
self.assertTrue(sct_payload['correct'])
157+
158+
def test_fail(self):
159+
# obviously this would be a terrible sct...
160+
self.data["DC_SCT"] = """Ex().test_not(test_object('x'), msg="no")"""
161+
sct_payload = helper.run(self.data)
162+
self.assertFalse(sct_payload['correct'])
163+
164+
class TestTestFail(unittest.TestCase):
165+
def setUp(self):
166+
self.data = {
167+
"DC_SOLUTION": "", "DC_CODE": ""
168+
}
169+
170+
def test_fail(self):
171+
self.data["DC_SCT"] = """Ex().fail()"""
172+
sct_payload = helper.run(self.data)
173+
self.assertFalse(sct_payload['correct'])
174+
138175

176+
#class TestSetContext(unittest.TestCase):
177+
# def setUp(self):
178+
# self.data = {
179+
# "DC_PEC": "",
180+
# "DC_SOLUTION": "[(i,j) for i,j in enumerate(range(10))]"
181+
# }
139182

140183
if __name__ == "__main__":
141184
unittest.main()

0 commit comments

Comments
 (0)