Skip to content

Commit a8a4b84

Browse files
committed
add check_function and has_equal_ast w/tests
1 parent 4724e1e commit a8a4b84

4 files changed

Lines changed: 183 additions & 35 deletions

File tree

pythonwhat/check_funcs.py

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -268,7 +268,8 @@ def check_args(name, missing_msg='FMT:Are you sure it is defined?', state=None):
268268
if name in ['*args', '**kwargs']:
269269
return check_part(name, name, state=state, missing_msg = missing_msg)
270270
else:
271-
return check_part_index('args', name, "argument `%s`"%name, state=state, missing_msg = missing_msg)
271+
arg_str = "%s argument"%get_ord(name+1) if isinstance(name, int) else "argument `%s`"%name
272+
return check_part_index('args', name, arg_str, state=state, missing_msg = missing_msg)
272273

273274

274275
# CALL CHECK ==================================================================
@@ -370,6 +371,18 @@ def call(args,
370371
# Expression tests ------------------------------------------------------------
371372
from pythonwhat.tasks import ReprFail, UndefinedValue
372373
from pythonwhat import utils
374+
375+
def has_equal_ast(incorrect_msg="FMT: Your code does not seem to match the solution.", state=None):
376+
rep = Reporter.active_reporter
377+
378+
stu_rep = ast.dump(state.student_tree)
379+
sol_rep = ast.dump(state.solution_tree)
380+
381+
_msg = state.build_message(incorrect_msg)
382+
rep.do_test(EqualTest(stu_rep, sol_rep, Feedback(_msg, state.highlight)))
383+
384+
return state
385+
373386
def has_expr(incorrect_msg="FMT:Unexpected expression {test}: expected `{sol_eval}`, got `{stu_eval}` with values{extra_env}.",
374387
error_msg="Running an expression in the student process caused an issue",
375388
undefined_msg="FMT:Have you defined `{name}` without errors?",

pythonwhat/check_function.py

Lines changed: 58 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,45 +1,73 @@
11
from pythonwhat.Reporter import Reporter
2-
from pythonwhat.check_funcs import check_node, check_part_index
2+
from pythonwhat.check_funcs import part_to_child
33
from pythonwhat.test_funcs.test_function import bind_args
44
from pythonwhat.tasks import getSignatureInProcess
5+
from pythonwhat.utils import get_ord
6+
from pythonwhat.Test import Test
7+
from pythonwhat.Feedback import Feedback
8+
from pythonwhat.parsing import IndexedDict
59
from functools import partial
610

7-
def check_function(name, index=0,
8-
missing_msg = "Did you define {typestr}?",
9-
expand_msg = "In your definition of {sol_part[name]}, ",
10-
params_not_matched_msg = "Something went wrong in figuring out how you specified the "
11-
"arguments for `{sol_part[name]}`; have another look at your code and its output.",
11+
def bind_args(signature, args_part):
12+
pos_args = []; kw_args = {}
13+
for k, arg in args_part.items():
14+
if isinstance(k, int): pos_args.append(arg)
15+
else: kw_args[k] = arg
16+
17+
bound_args = signature.bind(*pos_args, **kw_args)
18+
19+
return (IndexedDict(bound_args.arguments), signature)
20+
21+
MSG_PREPEND = "__JINJA__:Check your code in the {{child['part']+ ' of the' if child['part']}} {{typestr}}. "
22+
def check_function(name, index,
23+
missing_msg = "FMT:Did you define {typestr}?",
24+
params_not_matched_msg = "FMT:Something went wrong in figuring out how you specified the "
25+
"arguments for `{name}`; have another look at your code and its output.",
26+
expand_msg = MSG_PREPEND,
27+
signature=None,
28+
typestr = "{ordinal} function call",
1229
state=None):
1330
rep = Reporter.active_reporter
1431
stu_out = state.student_function_calls
1532
sol_out = state.solution_function_calls
1633

17-
import pdb; pdb.set_trace()
18-
# get function state
19-
func_list = check_node('function_calls', name, '{ordinal} function call to {name}', missing_msg, expand_msg, state)
20-
# grab specific function call
21-
# TODO NoneType not subscriptable, alter parsing so func part is dict
22-
child_func = check_part_index(None, index, "", func_list, state=func_list)
23-
stu_parts, sol_parts = child_func.student_parts, child_func.solution_parts
24-
# Signatures
25-
get_sig = partial(getSignatureInProcess, name=name, signature=signature,
26-
manual_sigs = state.get_manual_sigs())
34+
fmt_kwargs = {'ordinal': get_ord(index+1),
35+
'index': index,
36+
'name': name}
37+
fmt_kwargs['typestr'] = typestr.format(**fmt_kwargs)
2738

39+
# Get Parts ----
2840
try:
29-
sol_sig = get_sig(mapped_name=sol_parts['name'], process=solution_process)
30-
sol_parts['args'], _ = bind_args(sol_sig, sol_parts['pos_args'], sol_parts['keywords'])
31-
except:
32-
raise ValueError("Something went wrong in matching call index {index} of {name} to its signature. "
33-
"You might have to manually specify or correct the signature."
34-
.format(index=index, name=name))
35-
36-
# TODO if can't parse sig, send failed test msg
37-
try:
38-
stu_sig = get_sig(mapped_name=stu_parts['name'], process=child_func.student_process)
39-
stu_parts['args'], _ = bind_args(stu_sig, stu_parts['pos_args'], stu_parts['keywords'])
40-
except:
41-
_msg = state.build_message(params_not_matched_msg)
41+
stu_parts = stu_out[name][index]
42+
except (KeyError, IndexError):
43+
_msg = state.build_message(missing_msg, fmt_kwargs)
4244
rep.do_test(Test(Feedback(_msg, state.highlight)))
4345

46+
sol_parts = sol_out[name][index]
47+
48+
# Signatures -----
49+
if signature:
50+
signature = None if isinstance(signature, bool) else signature
51+
get_sig = partial(getSignatureInProcess, name=name, signature=signature,
52+
manual_sigs = state.get_manual_sigs())
53+
54+
try:
55+
sol_sig = get_sig(mapped_name=sol_parts['name'], process=state.solution_process)
56+
sol_parts['args'], _ = bind_args(sol_sig, sol_parts['args'])
57+
except:
58+
raise ValueError("Something went wrong in matching call index {index} of {name} to its signature. "
59+
"You might have to manually specify or correct the signature."
60+
.format(index=index, name=name))
61+
62+
# TODO if can't parse sig, send failed test msg
63+
try:
64+
stu_sig = get_sig(mapped_name=stu_parts['name'], process=state.student_process)
65+
stu_parts['args'], _ = bind_args(stu_sig, stu_parts['args'])
66+
except Exception as e:
67+
_msg = state.build_message(params_not_matched_msg, fmt_kwargs)
68+
rep.do_test(Test(Feedback(_msg, state.highlight)))
69+
4470
# three types of parts: pos_args, keywords, args (e.g. these are bound to sig)
45-
return child_func
71+
append_message = {'msg': expand_msg, 'kwargs': fmt_kwargs}
72+
child = part_to_child(stu_parts, sol_parts, append_message, state, node_name='function_calls')
73+
return child

pythonwhat/check_wrappers.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@
5757
scts['check_function'] = check_function
5858

5959
for k in ['set_context',
60-
'has_equal_value', 'has_equal_output', 'has_equal_error', 'call',
60+
'has_equal_value', 'has_equal_output', 'has_equal_error', 'has_equal_ast', 'call',
6161
'extend', 'multi', 'test_not', 'fail', 'quiet',
6262
'with_context',
6363
'check_args',

tests/test_test_function.py

Lines changed: 110 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,13 @@ def test_Pass(self):
3434
self.assertTrue(sct_payload['correct'])
3535
self.assertEqual(sct_payload['message'], "Great!")
3636

37+
def test_Pass_spec2(self):
38+
self.data['DC_SCT'] = """
39+
Ex().check_function('print', 0).check_args(0).has_equal_ast()
40+
"""
41+
sct_payload = helper.run(self.data)
42+
self.assertTrue(sct_payload['correct'])
43+
3744
class TestFunctionExerciseNumpy(unittest.TestCase):
3845

3946
def setUp(self):
@@ -599,9 +606,7 @@ def setUp(self):
599606
test_function("print", index = 3, highlight=True)
600607
'''
601608
}
602-
self.DC_SCT_SPEC2 = '''
603-
Ex().check_function("print", 0).check_arg(0).has_equal_value()
604-
'''
609+
605610
def test_multiple_1(self):
606611
self.data["DC_CODE"] = 'print("abc")'
607612
sct_payload = helper.run(self.data)
@@ -659,7 +664,109 @@ def test_nohighlight_too_few_calls(self):
659664
self.assertFalse(sct_payload['correct'])
660665
self.assertEqual(sct_payload.get('line_start'), None)
661666

667+
class TestCheckFunction(unittest.TestCase):
668+
def setUp(self):
669+
self.data = {
670+
"DC_PEC": "import numpy as np",
671+
"DC_CODE": "np.array([1,2,3])",
672+
"DC_SOLUTION": "np.array([1,2,3])",
673+
"DC_SCT": "Ex().check_function('numpy.array', 0)"
674+
}
675+
676+
def run_append(self, sct):
677+
self.data["DC_SCT"] += sct
678+
return helper.run(self.data)
679+
680+
def run_pass(self, sct):
681+
sct_payload = self.run_append(sct)
682+
print(sct_payload)
683+
self.assertTrue(sct_payload['correct'])
684+
return sct_payload
685+
686+
def run_fail(self, sct):
687+
self.assertFalse(self.run_append(sct)['correct'])
688+
689+
def test_pass_np_call_exists(self):
690+
sct_payload = helper.run(self.data)
691+
self.assertTrue(sct_payload['correct'])
692+
693+
def test_pass_test_student_typed(self):
694+
self.run_pass(".test_student_typed(r'np\.array\(\[1,2,3\]\)')")
695+
696+
def test_fail_test_student_typed(self):
697+
self.data["DC_CODE"] = "np.array([1,2])"
698+
self.run_fail(".test_student_typed(r'np\.array\(\[1,2,3\]\)')")
699+
700+
def test_pass_func_has_equal_ast(self):
701+
self.run_pass(".has_equal_ast()")
702+
703+
def test_fail_func_has_equal_ast(self):
704+
self.data["DC_CODE"] = "np.array([1,2])"
705+
self.run_fail(".has_equal_ast()")
706+
707+
def test_pass_check_args_pos_0(self):
708+
self.run_pass(".check_args(0)")
709+
710+
def test_fail_check_args_pos_0(self):
711+
self.data["DC_CODE"] = "np.array()"
712+
self.run_fail(".check_args(0)")
662713

714+
def test_pass_pos_0_test_student_typed(self):
715+
self.run_pass(".check_args(0).test_student_typed(r'\[1,2,3\]')")
716+
717+
def test_fail_pos_0_test_student_typed(self):
718+
self.data["DC_CODE"] = "np.array([1,2])"
719+
self.run_fail(".check_args(0).test_student_typed(r'\[1,2,3\]')")
720+
721+
def test_pass_pos_0_has_equal_ast(self):
722+
self.run_pass(".check_args(0).has_equal_ast()")
723+
724+
def test_fail_pos_0_has_equal_ast(self):
725+
self.data["DC_CODE"] = "np.array([1,2])"
726+
self.run_fail(".check_args(0).has_equal_ast()")
727+
728+
def test_pass_pos_0_has_equal_value(self):
729+
self.run_pass(".check_args(0).has_equal_value()")
730+
731+
def test_fail_pos_0_has_equal_value(self):
732+
self.data["DC_CODE"] = "np.array([1,2])"
733+
self.run_fail(".check_args(0).has_equal_value()")
734+
735+
def test_pass_pos_0_inline_if_body(self):
736+
self.data["DC_CODE"] = "np.array([1,2,3] if True else [1])"
737+
self.data["DC_SOLUTION"] = "np.array([1,2,3] if False else [1])"
738+
self.run_pass(".check_args(0).check_if_exp(0).check_body().has_equal_ast()")
739+
740+
def test_fail_pos_0_inline_if_body(self):
741+
self.data["DC_CODE"] = "np.array([1,2,3] if True else [1])"
742+
self.data["DC_SOLUTION"] = "np.array([1,2] if False else [1])"
743+
self.run_fail(".check_args(0).check_if_exp(0).check_body().has_equal_ast()")
744+
745+
class TestCheckFunctionCases(unittest.TestCase):
746+
def setup_color(self):
747+
self.data = {
748+
'DC_PEC': "def f(*args, **kwargs): pass",
749+
'DC_CODE': "f(color = 'blue')"
750+
}
751+
self.data["DC_SOLUTION"] = self.data["DC_CODE"]
752+
753+
def test_pass_sig_false(self):
754+
self.setup_color()
755+
self.data['DC_SCT'] = "Ex().check_function('f', 0, signature=False).check_args('color').has_equal_ast()"
756+
757+
sct_payload = helper.run(self.data)
758+
self.assertTrue(sct_payload['correct'])
759+
760+
@unittest.skip("TODO: implement override")
761+
def test_pass_sig_false_override(self):
762+
self.setup_color()
763+
self.data["DC_SCT"].replace('color', 'c')
764+
self.data['DC_SCT'] = """
765+
Ex().check_function('f', 0, signature=False).override("f(c = 'blue')").check_args('c').has_equal_ast()
766+
"""
767+
768+
sct_payload = helper.run(self.data)
769+
self.assertTrue(sct_payload['correct'])
663770

664771
if __name__ == "__main__":
665772
unittest.main()

0 commit comments

Comments
 (0)