Skip to content

Commit ad0a52a

Browse files
author
Filip Schouwenaars
authored
Merge pull request #151 from datacamp/spec-consolidate-tasks
Spec consolidate tasks
2 parents 9341919 + 40e995a commit ad0a52a

29 files changed

Lines changed: 870 additions & 691 deletions

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
# Vim swapfiles
2+
.*.swp
3+
14
# Byte-compiled / optimized / DLL files
25
__pycache__/
36
*.py[cod]

pythonwhat/Test.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -210,6 +210,9 @@ def is_equal(x, y):
210210
elif objs_are(x, y, [pd.Series]):
211211
pd.util.testing.assert_series_equal(x, y)
212212
return True
213+
elif objs_are(x, y, [Exception]):
214+
assert type(x) == type(y) and str(x) == str(y)
215+
return True
213216
else:
214217
return x == y
215218

pythonwhat/check_funcs.py

Lines changed: 183 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
from pythonwhat.Reporter import Reporter
2-
from pythonwhat.Test import Test
2+
from pythonwhat.Test import Test, EqualTest
33
from pythonwhat.Feedback import Feedback
44
from pythonwhat.utils import get_ord
55
from functools import partial
@@ -96,7 +96,10 @@ def has_part(name, msg, state=None, fmt_kwargs=None):
9696
**fmt_kwargs
9797
}
9898

99-
if not d['stu_part'][name] is not None:
99+
try:
100+
part = state.student_parts[name]
101+
if part is None: raise KeyError
102+
except (KeyError, IndexError):
100103
_msg = state.build_message(msg, d)
101104
rep.do_test(Test(Feedback(_msg, state.highlight)))
102105

@@ -127,30 +130,6 @@ def has_equal_part_len(name, insufficient_msg, state=None):
127130

128131
return state
129132

130-
def has_equal_value(msg, state=None):
131-
from pythonwhat.tasks import getResultInProcess, ReprFail
132-
from pythonwhat.Test import EqualTest
133-
rep = Reporter.active_reporter
134-
eval_solution, str_solution = getResultInProcess(tree = state.solution_tree,
135-
context = state.solution_context,
136-
process = state.solution_process)
137-
if str_solution is None:
138-
raise ValueError("Evaluating a default argument in the solution environment raised an error")
139-
if isinstance(eval_solution, ReprFail):
140-
raise ValueError("Couldn't figure out the value of a default argument: " + eval_solution.info)
141-
142-
eval_student, str_student = getResultInProcess(tree = state.student_tree,
143-
context = state.student_context,
144-
process = state.student_process)
145-
146-
_msg = state.build_message(msg, {'stu_part': state.student_parts, 'sol_part': state.solution_parts})
147-
feedback = Feedback(_msg, state.highlight)
148-
if str_student is None:
149-
rep.do_test(Test(feedback))
150-
else:
151-
rep.do_test(EqualTest(eval_student, eval_solution, feedback))
152-
153-
return state
154133

155134
def extend(*args, state=None):
156135
"""Run multiple subtests in sequence, each using the output state of the previous."""
@@ -175,11 +154,11 @@ def multi(*args, state=None):
175154
# TODO: it seems clear the reporter doesn't need to hold state anymore
176155
closure = partial(test, state=state)
177156
# message from parent checks
178-
prefix = state.build_message()
179-
# resetting reporter message until it can be refactored
180-
prev_msg = rep.failure_msg
181-
rep.do_test(closure, prefix, state.highlight)
182-
rep.failure_msg = prev_msg
157+
#prefix = state.build_message()
158+
## resetting reporter message until it can be refactored
159+
#prev_msg = rep.failure_msg
160+
rep.do_test(closure, "", state.highlight)
161+
#rep.failure_msg = prev_msg
183162

184163
# return original state, so can be chained
185164
return state
@@ -241,3 +220,176 @@ def set_context(*args, state=None, **kwargs):
241220
return state.to_child_state(student_subtree = None, solution_subtree = None,
242221
student_context = out_stu, solution_context = out_sol)
243222

223+
224+
def check_arg(name, missing_msg='check the argument `{part}`, ', state=None):
225+
if name in ['*args', '**kwargs']:
226+
return check_part(name, name, state=state, missing_msg = missing_msg)
227+
else:
228+
return check_part_index('args', name, name, state=state, missing_msg = missing_msg)
229+
230+
231+
# CALL CHECK ==================================================================
232+
233+
from pythonwhat.tasks import getResultInProcess, getOutputInProcess, getErrorInProcess, ReprFail
234+
import ast
235+
236+
evalCalls = {'value': getResultInProcess,
237+
'output': getOutputInProcess,
238+
'error': getErrorInProcess}
239+
240+
call_warnings = {
241+
'value': 'in the solution process resulted in an error',
242+
'error': 'did not generate an error in the solution environment',
243+
'output': 'in the solution process resulted in an error'
244+
}
245+
246+
def fix_format(arguments):
247+
if isinstance(arguments, str):
248+
arguments = (arguments, )
249+
if isinstance(arguments, tuple):
250+
arguments = list(arguments)
251+
252+
if isinstance(arguments, list):
253+
arguments = {'args': arguments, 'kwargs': {}}
254+
255+
if not isinstance(arguments, dict) or 'args' not in arguments or 'kwargs' not in arguments:
256+
raise ValueError("Wrong format of arguments in 'results', 'outputs' or 'errors'; either a list, or a dictionary with names args (a list) and kwargs (a dict)")
257+
258+
return(arguments)
259+
260+
# TODO: test string syntax with check_function_def
261+
# test argument syntax with check_lambda
262+
# implement for error and output
263+
def run_call(args, node, process, get_func, **kwargs):
264+
# Get function expression
265+
if isinstance(node, ast.FunctionDef): # function name
266+
func_expr = ast.Name(id=node.name, ctx=ast.Load())
267+
elif isinstance(node, ast.Lambda): # lambda body expr
268+
func_expr = node
269+
else: raise TypeError("Only function definition or lambda may be called")
270+
271+
# args is a call string or argument list/dict
272+
if isinstance(args, str):
273+
parsed = ast.parse(args).body[0].value
274+
parsed.func = func_expr
275+
ast.fix_missing_locations(parsed)
276+
return get_func(process = process, tree = parsed, **kwargs)
277+
else:
278+
# e.g. list -> {args: [...], kwargs: {}}
279+
fmt_args = fix_format(args)
280+
ast.fix_missing_locations(func_expr)
281+
return get_func(process = process, tree=func_expr, call = fmt_args, **kwargs)
282+
283+
284+
MSG_CALL_INCORRECT = "Calling it should result in {str_sol}, instead got {str_sol}"
285+
MSG_CALL_ERROR = "Calling it should result in {str_sol}, instead got an error"
286+
def call(args,
287+
test='value',
288+
incorrect_msg=MSG_CALL_INCORRECT,
289+
error_msg=MSG_CALL_ERROR,
290+
# TODO hardcoded lambda description for now
291+
argstr='the Xth lambda function',
292+
state=None, **kwargs):
293+
rep = Reporter.active_reporter
294+
test_type = ('value', 'output', 'error')
295+
296+
get_func = evalCalls[test]
297+
298+
# Run for Solution --------------------------------------------------------
299+
eval_sol, str_sol = run_call(args, state.solution_parts['node'], state.solution_process, get_func, **kwargs)
300+
301+
if (test == 'error') ^ isinstance(str_sol, Exception):
302+
_msg_prefix = "Calling %s for arguments %s " % (argstr, args)
303+
raise ValueError(_msg_prefix + call_warnings[test])
304+
305+
if isinstance(eval_sol, ReprFail):
306+
raise ValueError("Can't get the result of calling %s for arguments %s: %s" % (argstr, args, eval_sol.info))
307+
308+
# Run for Submission ------------------------------------------------------
309+
eval_stu, str_stu = run_call(args, state.student_parts['node'], state.student_process, get_func, **kwargs)
310+
fmt_kwargs = {'part': argstr, 'argstr': argstr, 'str_sol': str_sol, 'str_stu': str_stu}
311+
312+
# either error test and no error, or vice-versa
313+
stu_node = state.student_parts['node']
314+
if (test == 'error') ^ isinstance(str_stu, Exception):
315+
_msg = state.build_message(error_msg, fmt_kwargs)
316+
rep.do_test(Test(Feedback(_msg, stu_node)))
317+
318+
# incorrect result
319+
_msg = state.build_message(incorrect_msg, fmt_kwargs)
320+
rep.do_test(EqualTest(eval_sol, eval_stu, Feedback(_msg, stu_node)))
321+
322+
return state
323+
324+
# Expression tests ------------------------------------------------------------
325+
from pythonwhat.tasks import ReprFail, UndefinedValue
326+
from pythonwhat.Test import EqualTest
327+
from pythonwhat import utils
328+
def has_expr(incorrect_msg,
329+
error_msg="Running an expression in the student process caused an issue",
330+
undefined_msg="Have you defined `{name}` without errors?",
331+
extra_env=None,
332+
context_vals=None,
333+
expr_code=None,
334+
pre_code=None,
335+
keep_objs_in_env=None,
336+
name=None,
337+
highlight=None,
338+
state=None,
339+
test=None):
340+
rep = Reporter.active_reporter
341+
342+
# run function to highlight a block of code
343+
if callable(highlight):
344+
try: highlight = highlight(state=state).student_tree
345+
except: pass
346+
highlight = highlight or state.highlight
347+
348+
get_func = partial(evalCalls[test],
349+
extra_env = extra_env,
350+
context_vals = context_vals,
351+
pre_code = pre_code,
352+
expr_code = expr_code,
353+
keep_objs_in_env = keep_objs_in_env,
354+
name=name,
355+
do_exec = True if test == 'output' else False)
356+
357+
eval_sol, str_sol = get_func(tree = state.solution_tree,
358+
process = state.solution_process,
359+
context = state.solution_context)
360+
361+
if (test == 'error') ^ isinstance(str_sol, Exception):
362+
raise ValueError("evaluating expression raised error in solution process")
363+
if isinstance(eval_sol, ReprFail):
364+
raise ValueError("Couldn't figure out the value of a default argument: " + eval_sol.info)
365+
366+
eval_stu, str_stu = get_func(tree = state.student_tree,
367+
process = state.student_process,
368+
context = state.student_context)
369+
370+
# kwargs ---
371+
fmt_kwargs = {'stu_part': state.student_parts, 'sol_part': state.solution_parts, 'name': name}
372+
fmt_kwargs['stu_eval'] = utils.shorten_str(str(eval_stu))
373+
fmt_kwargs['sol_eval'] = utils.shorten_str(str(eval_sol))
374+
375+
# tests ---
376+
# error in process
377+
if (test == 'error') ^ isinstance(str_stu, Exception):
378+
_msg = state.build_message(error_msg, fmt_kwargs)
379+
feedback = Feedback(_msg, highlight)
380+
rep.do_test(Test(feedback))
381+
382+
# name is undefined after running expression
383+
if isinstance(str_stu, UndefinedValue):
384+
_msg = state.build_message(undefined_msg, fmt_kwargs)
385+
rep.do_test(Test(Feedback(_msg, highlight)))
386+
387+
# test equality of results
388+
_msg = state.build_message(incorrect_msg, fmt_kwargs)
389+
rep.do_test(EqualTest(eval_stu, eval_sol, Feedback(_msg, highlight)))
390+
391+
return state
392+
393+
has_equal_value = partial(has_expr, test = 'value')
394+
has_equal_output = partial(has_expr, test = 'output')
395+
has_equal_error = partial(has_expr, test = 'error')

pythonwhat/check_function.py

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
from pythonwhat.check_funcs import check_node
2+
from pythonwhat.test_funcs.test_function import mapped_name
3+
from pythonwhat.tasks import getSignatureInProcess
4+
from functools import partial
5+
6+
def check_function(name, index=0,
7+
missing_msg = "Did you define {sol_part[name]}?",
8+
expand_msg = "In your definition of {sol_part[name]}, ",
9+
state=None):
10+
rep = Reporter.active_reporter
11+
stu_out = state.student_function_calls
12+
sol_out = state.solution_function_calls
13+
14+
# test if function exists
15+
stud_name = get_mapped_name(name, state.student_mappings)
16+
17+
func_list = check_node('function_calls', name, 'function call', missing_msg, expand_msg, state)
18+
# get function state
19+
if index is None:
20+
return func_list
21+
else:
22+
# TODO make has_part more robust
23+
# grab specific function call
24+
child_func = check_part(index, "FUNCTION MSG", func_list, "not enough func calls")
25+
stu_parts, sol_parts = child_func.student_parts, child_func.solution_parts
26+
# Signatures
27+
get_sig = partial(getSignatureInProcess, name=name, signature=signature,
28+
manual_sigs = state.get_manual_sigs())
29+
30+
# TODO if can't parse, raise warnings
31+
sol_sig = get_sig(mapped_name=sol_parts['name'], process=solution_process)
32+
sol_parts['args'], _ = bind_ards(sol_sig, sol_parts['pos_args'], sol_parts['keywords'])
33+
34+
# TODO if can't parse sig, send failed test msg
35+
stu_sig = get_sig(mapped_name=stu_parts['name'], process=student_process)
36+
stu_parts['args'], _ = bind_ards(stu_sig, stu_parts['pos_args'], stu_parts['keywords'])
37+
38+
# three types of parts: pos_args, keywords, args (e.g. these are bound to sig)
39+
return child_func

pythonwhat/check_wrappers.py

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,8 @@
99
'key' : 'key part',
1010
'value': 'value part',
1111
'orelse': 'else part',
12-
'vararg': 'vararg part',
13-
'kwarg': ' kwarg part',
12+
#'vararg': 'vararg part',
13+
#'kwarg': ' kwarg part',
1414
'test': 'condition'
1515
}
1616

@@ -46,6 +46,11 @@
4646
for k, v in __NODE_WRAPPERS__.items():
4747
scts['check_'+k] = partial(check_node, k+'s', typestr=v)
4848

49-
for k in ['set_context', 'has_equal_value', 'extend', 'multi', 'with_context']:
49+
for k in ['set_context',
50+
'has_equal_value', 'has_equal_output', 'has_equal_error', 'call',
51+
'extend', 'multi',
52+
'with_context',
53+
'check_arg',
54+
'has_equal_part']:
5055
scts[k] = getattr(check_funcs, k)
5156

0 commit comments

Comments
 (0)