Skip to content

Commit 0d96047

Browse files
authored
Merge pull request #198 from datacamp/feature-has-context
Feature has context
2 parents 263112a + 25ebe56 commit 0d96047

11 files changed

Lines changed: 185 additions & 51 deletions

.travis.yml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,4 +8,5 @@ before_install:
88
- "git clone https://$GH_TOKEN@github.com/datacamp/pythonbackend.git && pip install ./pythonbackend && rm -rf pythonbackend"
99
install: pip install -r requirements.txt
1010

11-
script: pytest tests -m "not dep_matplotlib" -s
11+
script: pytest tests -s -m "not dep_matplotlib"
12+
after_script: "export PYTHONWHAT_DEBUG_FEEDBACK='True' && pytest tests -s -m 'feedback'"

pythonwhat/check_funcs.py

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -52,11 +52,7 @@ def check_part_index(name, index, part_msg,
5252
'kwargs': fmt_kwargs}
5353

5454
# check there are enough parts for index
55-
stu_parts = state.student_parts[name]
56-
try: stu_parts[index]
57-
except (KeyError, IndexError):
58-
_msg = state.build_message(missing_msg, append_message['kwargs'])
59-
rep.do_test(Test(Feedback(_msg, state.highlight)))
55+
has_part(name, missing_msg, state, append_message['kwargs'], index)
6056

6157
# get part at index
6258
stu_part = state.student_parts[name][index]
@@ -66,7 +62,7 @@ def check_part_index(name, index, part_msg,
6662
return part_to_child(stu_part, sol_part, append_message, state)
6763

6864
MSG_MISSING = "FMT:The system wants to check the {typestr} you defined but hasn't found it."
69-
MSG_PREPEND = "__JINJA__:Check your code in the {{child['part']+ ' of the' if child['part']}} {{typestr}}. "
65+
MSG_PREPEND = "__JINJA__:Check your code in the{{' ' + child['part']+ ' of the' if child['part']}} {{typestr}}. "
7066
def check_node(name, index, typestr, missing_msg=MSG_MISSING, expand_msg=MSG_PREPEND, state=None):
7167
rep = Reporter.active_reporter
7268
stu_out = getattr(state, 'student_'+name)
@@ -97,7 +93,7 @@ def check_node(name, index, typestr, missing_msg=MSG_MISSING, expand_msg=MSG_PRE
9793

9894
# Part tests ------------------------------------------------------------------
9995

100-
def has_part(name, msg, state=None, fmt_kwargs=None):
96+
def has_part(name, msg, state=None, fmt_kwargs=None, index=None):
10197
rep = Reporter.active_reporter
10298
d = {'sol_part': state.solution_parts,
10399
'stu_part': state.student_parts,
@@ -106,6 +102,7 @@ def has_part(name, msg, state=None, fmt_kwargs=None):
106102

107103
try:
108104
part = state.student_parts[name]
105+
if index is not None: part = part[index]
109106
if part is None: raise KeyError
110107
except (KeyError, IndexError):
111108
_msg = state.build_message(msg, d)
@@ -137,7 +134,6 @@ def has_equal_part_len(name, insufficient_msg, state=None):
137134

138135
return state
139136

140-
141137
# functions for running multiple sub-tests ------------------------------------
142138

143139
def extend(*args, state=None):

pythonwhat/check_has_context.py

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
from pythonwhat.Reporter import Reporter
2+
from pythonwhat.Test import Test, EqualTest
3+
from pythonwhat.Feedback import Feedback
4+
from pythonwhat.State import State
5+
from functools import singledispatch
6+
from pythonwhat.check_funcs import check_part_index
7+
8+
MSG_INCORRECT_LOOP = "FMT:Have you used the correct iterator variable names? Was expecting `{sol_vars}` but got `{stu_vars}`."
9+
MSG_INCORRECT_WITH = "FMT:Make sure to use the correct context variable names. Was expecting `{sol_vars}` but got `{stu_vars}`."
10+
11+
def has_context(incorrect_msg=None, exact_names=False, state=None):
12+
# call _has_context, since the built-in singledispatch can only use 1st pos arg
13+
return _has_context(state, incorrect_msg, exact_names)
14+
15+
def _test(state, incorrect_msg, exact_names, tv_name, highlight_name):
16+
rep = Reporter.active_reporter
17+
# get parts for testing from state
18+
# TODO: this could be rewritten to use check_part_index -> has_equal_part, etc..
19+
stu_vars = state.student_parts[tv_name]
20+
sol_vars = state.solution_parts[tv_name]
21+
stu_target = state.student_parts.get(highlight_name) # TODO should be node?
22+
23+
# variables exposed to messages
24+
d = { 'stu_vars': stu_vars,
25+
'sol_vars': sol_vars,
26+
'num_vars': len(sol_vars)}
27+
28+
if exact_names:
29+
# message for wrong iter var names
30+
_msg = state.build_message(incorrect_msg, d)
31+
# test
32+
rep.do_test(EqualTest(stu_vars, sol_vars, Feedback(_msg, stu_target)))
33+
else:
34+
# message for wrong number of iter vars
35+
_msg = state.build_message(incorrect_msg, d)
36+
# test
37+
rep.do_test(EqualTest(len(stu_vars), len(sol_vars), Feedback(_msg, stu_target)))
38+
39+
return state
40+
41+
42+
@singledispatch
43+
def _has_context(state, incorrect_msg, exact_names):
44+
raise BaseException("first argument to _has_context must be a State instance or subclass")
45+
46+
@_has_context.register(State)
47+
def has_context_state(*args, **kwargs):
48+
return _test(*args, tv_name='target_vars', highlight_name='highlight', **kwargs)
49+
50+
@_has_context.register(State.SUBCLASSES['for_loops'])
51+
@_has_context.register(State.SUBCLASSES['whiles'])
52+
@_has_context.register(State.SUBCLASSES['dict_comps'])
53+
@_has_context.register(State.SUBCLASSES['generator_exps'])
54+
@_has_context.register(State.SUBCLASSES['list_comps'])
55+
def has_context_loop(state, incorrect_msg, exact_names):
56+
"""When dispatched on loops, has_context the target vars are the attribute _target_vars.
57+
58+
Note: This is to allow people to call has_context on a node (e.g. for_loop) rather than
59+
one of its attributes (e.g. body). Purely for convenience.
60+
"""
61+
return _test(state, incorrect_msg or MSG_INCORRECT_LOOP, exact_names,
62+
tv_name='_target_vars', highlight_name='target')
63+
64+
@_has_context.register(State.SUBCLASSES['withs'])
65+
def has_context_with(state, incorrect_msg, exact_names):
66+
"""When dispatched on with statements, has_context loops over each context manager.
67+
68+
Note: This is to allow people to call has_context on the with statement, rather than
69+
having to manually loop over each context manager.
70+
71+
e.g. Ex().check_with(0).has_context() vs Ex().check_with(0).check_context(0).has_context()
72+
"""
73+
74+
for i in range(len(state.solution_parts['context'])):
75+
ctxt_state = check_part_index('context', i, '{ordinal} context', state=state)
76+
_has_context(ctxt_state, incorrect_msg or MSG_INCORRECT_WITH, exact_names)
77+
78+
return state

pythonwhat/check_wrappers.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from pythonwhat.check_funcs import check_part, check_part_index, check_node, has_equal_part
22
from pythonwhat import check_funcs, check_object
33
from pythonwhat.check_function import check_function
4+
from pythonwhat.check_has_context import has_context
45
from pythonwhat.test_funcs.test_data_frame import check_df
56
from pythonwhat.test_funcs.test_dictionary import check_dict
67
from pythonwhat import test_funcs
@@ -70,5 +71,4 @@
7071

7172
scts['check_df'] = check_df
7273
scts['check_dict'] = check_dict
73-
74-
74+
scts['has_context'] = has_context

pythonwhat/parsing.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -574,6 +574,7 @@ def visit_For(self, node):
574574
'body': {'node': node.body, 'target_vars': tv},
575575
'orelse': {'node': node.orelse, 'target_vars': tv},
576576
'target': node.target,
577+
'_target_vars': tv
577578
})
578579

579580

@@ -779,6 +780,7 @@ def visit_With(self, node):
779780
}
780781
for item in items]
781782

783+
tv_all = TargetVars(sum([list(c['target_vars'].items()) for c in context], []))
782784
#body_tv = []
783785
#for c in context: body_tv.extend(c['target_vars'])
784786

pythonwhat/test_funcs/test_comp.py

Lines changed: 6 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
from pythonwhat.Test import EqualTest
44
from pythonwhat.utils import get_ord
55
from pythonwhat.check_funcs import check_node, check_part, check_part_index, multi, has_equal_part_len
6+
from pythonwhat.check_has_context import has_context
7+
68

79
MSG_NOT_CALLED = "FMT:The system wants to check the {ordinal} {typestr} you defined but hasn't found it."
810
MSG_PREPEND = "FMT:Check your code in the {child[part]} of the {ordinal} {typestr}. "
@@ -86,7 +88,10 @@ def test_comp(typestr, comptype, index, iter_vars_names,
8688

8789
# test comprehension iter and its variable names (or number of variables)
8890
if comp_iter: multi(comp_iter, state=check_part("iter", "iterable part", state))
89-
has_iter_vars(incorrect_iter_vars_msg, iter_vars_names, state=quiet_state)
91+
92+
# test iterator variables
93+
default_msg = MSG_INCORRECT_ITER_VARS if iter_vars_names else MSG_INCORRECT_NUM_ITER_VARS
94+
has_context(incorrect_iter_vars_msg or default_msg, iter_vars_names, state=quiet_state)
9095

9196
# test the main expressions.
9297
if body: multi(body, state=check_part("body", "body", state)) # list and gen comp
@@ -100,30 +105,3 @@ def test_comp(typestr, comptype, index, iter_vars_names,
100105
# test individual ifs
101106
multi(if_test, state=check_part_index("ifs", i, get_ord(i+1) + " if", state=state))
102107

103-
104-
def has_iter_vars(incorrect_iter_vars_msg, exact_names=False, state=None):
105-
rep = Reporter.active_reporter
106-
# get parts for testing from state
107-
# TODO: this could be rewritten to use check_part_index -> has_equal_part, etc..
108-
stu_vars = state.student_parts['_target_vars']
109-
sol_vars = state.solution_parts['_target_vars']
110-
stu_target = state.student_parts['target']
111-
112-
# variables exposed to messages
113-
d = { 'stu_vars': stu_vars,
114-
'sol_vars': sol_vars,
115-
'num_vars': len(sol_vars)}
116-
117-
if exact_names:
118-
# message for wrong iter var names
119-
_msg = state.build_message(incorrect_iter_vars_msg or MSG_INCORRECT_ITER_VARS, d)
120-
# test
121-
rep.do_test(EqualTest(stu_vars, sol_vars, Feedback(_msg, stu_target)))
122-
else:
123-
# message for wrong number of iter vars
124-
_msg = state.build_message(incorrect_iter_vars_msg or MSG_INCORRECT_NUM_ITER_VARS, d)
125-
# test
126-
rep.do_test(EqualTest(len(stu_vars), len(sol_vars), Feedback(_msg, stu_target)))
127-
128-
return state
129-

pythonwhat/test_funcs/test_with.py

Lines changed: 7 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@
33
from pythonwhat.Test import EqualTest, Test
44
from pythonwhat import utils
55
from pythonwhat.tasks import setUpNewEnvInProcess, breakDownNewEnvInProcess
6-
from pythonwhat.check_funcs import check_node, check_part, check_part_index, multi, quiet, has_equal_part, with_context
6+
from pythonwhat.check_funcs import check_node, check_part, check_part_index, multi, quiet, has_equal_part, with_context, has_equal_part_len
7+
from pythonwhat.check_has_context import has_context
78

89
from functools import partial
910

@@ -12,7 +13,7 @@
1213
MSG_PREPEND2 = "FMT:Check the {child[part]} of the {ordinal} `with` statement. "
1314
MSG_NUM_CTXT = "make sure to use the correct number of context variables. It seems you defined too many."
1415
MSG_NUM_CTXT2 = "make sure to use the correct number of context variables. It seems you defined too little."
15-
MSG_CTXT_NAMES = "FMT:make sure to use the correct context variable names. Was expecting `{sol_part[target_vars]}` but got `{stu_part[target_vars]}`."
16+
MSG_CTXT_NAMES = "FMT:make sure to use the correct context variable names. Was expecting `{sol_vars}` but got `{stu_vars}`."
1617

1718

1819
def test_with(index,
@@ -44,17 +45,12 @@ def test_with(index,
4445
quiet_child = quiet(1, child)
4546

4647
if context_vals:
47-
# test num context vars ----
48-
too_many = len(child.student_parts['context']) > len(child.solution_parts['context'])
49-
if too_many:
50-
_msg = child.build_message(MSG_NUM_CTXT)
51-
rep.do_test(Test(Feedback(_msg, child.student_tree)))
52-
5348
# test context var names ----
54-
for i in range(len(child.solution_parts['context'])):
55-
ctxt_state = check_part_index('context', i, "", state=child)
56-
has_equal_part('target_vars', MSG_CTXT_NAMES, state=ctxt_state)
49+
has_context(incorrect_msg=context_vals_msg or MSG_CTXT_NAMES, exact_names = True, state=child)
5750

51+
# test num context vars ----
52+
has_equal_part_len('context', MSG_NUM_CTXT, state=child)
53+
5854

5955
# Context sub tests ----
6056
if context_tests and not isinstance(context_tests, list): context_tests = [context_tests]

tests/helper.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from pythonbackend.Exercise import Exercise
22
from pythonbackend import utils
33
import re
4+
import os
45

56
def get_sct_payload(output):
67
sct_output = [out for out in output if out['type'] == 'sct']
@@ -17,7 +18,10 @@ def run(data):
1718
print(output)
1819
raise(ValueError("Backend error"))
1920
output = exercise.runSubmit(data)
20-
return(get_sct_payload(output))
21+
sct_payload = get_sct_payload(output)
22+
if os.environ.get('PYTHONWHAT_DEBUG_FEEDBACK'):
23+
print('message: %s'%sct_payload.get('message'))
24+
return(sct_payload)
2125

2226
def test_lines(test, sct_payload, ls, le, cs, ce):
2327
test.assertEqual(sct_payload['line_start'], ls)

tests/test_test_comp.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -259,6 +259,22 @@ def test_pass(self):
259259
sct_payload = helper.run(self.data)
260260
self.assertTrue(sct_payload['correct'])
261261

262+
def test_fail_spec2(self):
263+
self.data["DC_SCT"] = "Ex().check_list_comp(0).has_context()"
264+
self.data["DC_CODE"] = "[a for a in x.items()]"
265+
sct_payload = helper.run(self.data)
266+
self.assertFalse(sct_payload['correct'])
267+
268+
def test_pass_spec2(self):
269+
self.data["DC_SCT"] = "Ex().check_list_comp(0).has_context()"
270+
self.test_pass()
271+
272+
def test_fail_spec2_exact_names(self):
273+
self.data["DC_CODE"] = "[a for a,b in x.items()]"
274+
self.data["DC_SCT"] = "Ex().check_list_comp(0).has_context(exact_names=True)"
275+
sct_payload = helper.run(self.data)
276+
self.assertFalse(sct_payload['correct'])
277+
262278

263279
class TestListDestructuring(unittest.TestCase):
264280
def setUp(self):

tests/test_test_for_loop.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import unittest
22
import helper
3+
import pytest
34

45
class TestForLoop(unittest.TestCase):
56

@@ -75,6 +76,33 @@ def test_Pass_exchain(self):
7576
self.data["DC_SCT"] = "Ex().\\" + helper.remove_lambdas(self.data["DC_SCT"])
7677
self.test_Pass()
7778

79+
def test_has_context_pass(self):
80+
self.data["DC_CODE"] = "for i in range(10): pass"
81+
self.data["DC_SCT"] = "Ex().check_for_loop(0).has_context()"
82+
sct_payload = helper.run(self.data)
83+
self.assertTrue(sct_payload['correct'])
84+
85+
def test_has_context_mult_pass(self):
86+
self.data["DC_SOLUTION"] = "for x,y in zip(range(10), range(10)): pass"
87+
self.data["DC_CODE"] = "for i,j in zip(range(10), range(10)): pass"
88+
self.data["DC_SCT"] = "Ex().check_for_loop(0).has_context()"
89+
sct_payload = helper.run(self.data)
90+
self.assertTrue(sct_payload['correct'])
91+
92+
@pytest.mark.feedback
93+
def test_has_context_fail(self):
94+
self.data["DC_CODE"] = "for i in range(10): pass"
95+
self.data["DC_SCT"] = "Ex().check_for_loop(0).has_context(exact_names=True)"
96+
sct_payload = helper.run(self.data)
97+
self.assertFalse(sct_payload['correct'])
98+
99+
@pytest.mark.feedback
100+
def test_has_context_mult_fail(self):
101+
self.data["DC_SOLUTION"] = "for x,y in zip(range(10), range(10)): pass"
102+
self.data["DC_CODE"] = "for i,j in zip(range(10), range(10)): pass"
103+
self.data["DC_SCT"] = "Ex().check_for_loop(0).has_context(exact_names=True)"
104+
sct_payload = helper.run(self.data)
105+
self.assertFalse(sct_payload['correct'])
78106

79107
class TestForLoop2(unittest.TestCase):
80108

0 commit comments

Comments
 (0)