Skip to content

Commit d01d2a3

Browse files
authored
Merge pull request #389 from datacamp/jh/lazychain
[CX-1182] Feat: lazychain
2 parents a2805eb + 223ec8a commit d01d2a3

34 files changed

+404
-374
lines changed

pythonwhat/State.py

Lines changed: 57 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,22 @@
1+
import asttokens
2+
13
from functools import partialmethod
4+
from collections.abc import Mapping
5+
6+
from protowhat.failure import debugger
7+
from protowhat.Feedback import FeedbackComponent
8+
from protowhat.selectors import DispatcherInterface
9+
from protowhat.State import State as ProtoState
10+
from protowhat.utils import parameters_attr
11+
from pythonwhat import signatures
12+
from pythonwhat.converters import get_manual_converters
13+
from pythonwhat.feedback import Feedback
214
from pythonwhat.parsing import (
315
TargetVars,
416
FunctionParser,
517
ObjectAccessParser,
618
parser_dict,
719
)
8-
from protowhat.State import State as ProtoState
9-
from protowhat.selectors import DispatcherInterface
10-
from protowhat.Feedback import InstructorError
11-
from pythonwhat import signatures
12-
from pythonwhat.converters import get_manual_converters
13-
from collections.abc import Mapping
14-
import asttokens
1520
from pythonwhat.utils_ast import wrap_in_module
1621

1722

@@ -35,6 +40,7 @@ def __len__(self):
3540
return len(self._items)
3641

3742

43+
@parameters_attr
3844
class State(ProtoState):
3945
"""State of the SCT environment.
4046
@@ -48,6 +54,8 @@ class State(ProtoState):
4854
4955
"""
5056

57+
feedback_cls = Feedback
58+
5159
def __init__(
5260
self,
5361
student_code,
@@ -60,8 +68,9 @@ def __init__(
6068
reporter,
6169
force_diagnose=False,
6270
highlight=None,
71+
highlight_offset=None,
6372
highlighting_disabled=None,
64-
messages=None,
73+
feedback_context=None,
6574
creator=None,
6675
student_ast=None,
6776
solution_ast=None,
@@ -75,23 +84,21 @@ def __init__(
7584
solution_env=Context(),
7685
):
7786
args = locals().copy()
78-
self.params = list()
87+
self.debug = False
7988

8089
for k, v in args.items():
8190
if k != "self":
82-
self.params.append(k)
8391
setattr(self, k, v)
8492

85-
self.messages = messages if messages else []
86-
8793
self.ast_dispatcher = self.get_dispatcher()
8894

8995
# Parse solution and student code
9096
# if possible, not done yet and wanted (ast arguments not False)
9197
if isinstance(self.student_code, str) and student_ast is None:
9298
self.student_ast = self.parse(student_code)
9399
if isinstance(self.solution_code, str) and solution_ast is None:
94-
self.solution_ast = self.parse(solution_code, test=False)
100+
with debugger(self):
101+
self.solution_ast = self.parse(solution_code)
95102

96103
if highlight is None: # todo: check parent_state? (move check to reporting?)
97104
self.highlight = self.student_ast
@@ -106,26 +113,29 @@ def get_manual_sigs(self):
106113

107114
return self.manual_sigs
108115

109-
def to_child(self, append_message="", node_name="", **kwargs):
116+
def to_child(self, append_message=None, node_name="", **kwargs):
110117
"""Dive into nested tree.
111118
112119
Set the current state as a state with a subtree of this syntax tree as
113120
student tree and solution tree. This is necessary when testing if statements or
114121
for loops for example.
115122
"""
116-
bad_pars = set(kwargs) - set(self.params)
117-
if bad_pars:
118-
raise ValueError("Invalid init params for State: %s" % ", ".join(bad_pars))
123+
bad_parameters = set(kwargs) - set(self.parameters)
124+
if bad_parameters:
125+
raise ValueError(
126+
"Invalid init parameters for State: %s" % ", ".join(bad_parameters)
127+
)
119128

120129
base_kwargs = {
121130
attr: getattr(self, attr)
122-
for attr in self.params
123-
if attr not in ["highlight"]
131+
for attr in self.parameters
132+
if hasattr(self, attr) and attr not in ["ast_dispatcher", "highlight"]
124133
}
125134

126-
if not isinstance(append_message, dict):
127-
append_message = {"msg": append_message, "kwargs": {}}
128-
kwargs["messages"] = [*self.messages, append_message]
135+
if append_message and not isinstance(append_message, FeedbackComponent):
136+
append_message = FeedbackComponent(append_message)
137+
kwargs["feedback_context"] = append_message
138+
kwargs["creator"] = {"type": "to_child", "args": {"state": self}}
129139

130140
def update_kwarg(name, func):
131141
kwargs[name] = func(kwargs[name])
@@ -162,11 +172,11 @@ def update_context(name):
162172
init_kwargs = {**base_kwargs, **kwargs}
163173
child = klass(**init_kwargs)
164174

165-
extra_attrs = set(vars(self)) - set(self.params)
175+
extra_attrs = set(vars(self)) - set(self.parameters)
166176
for attr in extra_attrs:
167177
# don't copy attrs set on new instances in init
168178
# the cached manual_sigs is passed
169-
if attr not in {"params", "ast_dispatcher", "converters"}:
179+
if attr not in {"ast_dispatcher", "converters"}:
170180
setattr(child, attr, getattr(self, attr))
171181

172182
return child
@@ -183,27 +193,30 @@ def has_different_processes(self):
183193

184194
def assert_execution_root(self, fun, extra_msg=""):
185195
if not (self.is_root or self.is_creator_type("run")):
186-
raise InstructorError(
187-
"`%s()` should only be called focusing on a full script, following `Ex()` or `run()`. %s"
188-
% (fun, extra_msg)
189-
)
196+
with debugger(self):
197+
self.report(
198+
"`%s()` should only be called focusing on a full script, following `Ex()` or `run()`. %s"
199+
% (fun, extra_msg)
200+
)
190201

191202
def is_creator_type(self, type):
192203
return self.creator and self.creator.get("type") == type
193204

194205
def assert_is(self, klasses, fun, prev_fun):
195206
if self.__class__.__name__ not in klasses:
196-
raise InstructorError(
197-
"`%s()` can only be called on %s."
198-
% (fun, " or ".join(["`%s()`" % pf for pf in prev_fun]))
199-
)
207+
with debugger(self):
208+
self.report(
209+
"`%s()` can only be called on %s."
210+
% (fun, " or ".join(["`%s()`" % pf for pf in prev_fun]))
211+
)
200212

201213
def assert_is_not(self, klasses, fun, prev_fun):
202214
if self.__class__.__name__ in klasses:
203-
raise InstructorError(
204-
"`%s()` should not be called on %s."
205-
% (fun, " or ".join(["`%s()`" % pf for pf in prev_fun]))
206-
)
215+
with debugger(self):
216+
self.report(
217+
"`%s()` should not be called on %s."
218+
% (fun, " or ".join(["`%s()`" % pf for pf in prev_fun]))
219+
)
207220

208221
def parse_external(self, code):
209222
res = (None, None)
@@ -235,17 +248,17 @@ def parse_internal(self, code):
235248
try:
236249
return self.ast_dispatcher.parse(code)
237250
except Exception as e:
238-
raise InstructorError(
251+
self.report(
239252
"Something went wrong when parsing the solution code: %s" % str(e)
240253
)
241254

242-
def parse(self, text, test=True):
243-
if test:
244-
parse_method = self.parse_external
245-
token_attr = "student_ast_tokens"
246-
else:
255+
def parse(self, text):
256+
if self.debug:
247257
parse_method = self.parse_internal
248258
token_attr = "solution_ast_tokens"
259+
else:
260+
parse_method = self.parse_external
261+
token_attr = "student_ast_tokens"
249262

250263
tokens, ast = parse_method(text)
251264
setattr(self, token_attr, tokens)
@@ -256,9 +269,8 @@ def get_dispatcher(self):
256269
try:
257270
return Dispatcher(self.pre_exercise_code)
258271
except Exception as e:
259-
raise InstructorError(
260-
"Something went wrong when parsing the PEC: %s" % str(e)
261-
)
272+
with debugger(self):
273+
self.report("Something went wrong when parsing the PEC: %s" % str(e))
262274

263275

264276
class Dispatcher(DispatcherInterface):

pythonwhat/checks/check_funcs.py

Lines changed: 21 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
1+
from protowhat.Feedback import FeedbackComponent
12
from pythonwhat.checks.check_logic import multi
23
from pythonwhat.checks.has_funcs import has_part
3-
from protowhat.Feedback import InstructorError
4+
from protowhat.failure import debugger
45
from pythonwhat.tasks import setUpNewEnvInProcess, breakDownNewEnvInProcess
5-
from pythonwhat.utils import get_ord
6+
from protowhat.utils_messaging import get_ord
67
from pythonwhat.utils_ast import assert_ast
78
import ast
89
from jinja2 import Template
@@ -14,7 +15,7 @@ def render(template, kwargs):
1415

1516
def part_to_child(stu_part, sol_part, append_message, state, node_name=None):
1617
# stu_part and sol_part will be accessible on all templates
17-
append_message["kwargs"].update({"stu_part": stu_part, "sol_part": sol_part})
18+
append_message.kwargs.update({"stu_part": stu_part, "sol_part": sol_part})
1819

1920
# if the parts are dictionaries, use to deck out child state
2021
if all(isinstance(p, dict) for p in [stu_part, sol_part]):
@@ -51,14 +52,14 @@ def check_part(state, name, part_msg, missing_msg=None, expand_msg=None):
5152

5253
if not part_msg:
5354
part_msg = name
54-
append_message = {"msg": expand_msg, "kwargs": {"part": part_msg}}
55+
append_message = FeedbackComponent(expand_msg, {"part": part_msg})
5556

56-
has_part(state, name, missing_msg, append_message["kwargs"])
57+
has_part(state, name, missing_msg, append_message.kwargs)
5758

5859
stu_part = state.student_parts[name]
5960
sol_part = state.solution_parts[name]
6061

61-
assert_ast(state, sol_part, append_message["kwargs"])
62+
assert_ast(state, sol_part, append_message.kwargs)
6263

6364
return part_to_child(stu_part, sol_part, append_message, state)
6465

@@ -83,7 +84,7 @@ def check_part_index(state, name, index, part_msg, missing_msg=None, expand_msg=
8384
fmt_kwargs = {"index": index, "ordinal": ordinal}
8485
fmt_kwargs.update(part=render(part_msg, fmt_kwargs))
8586

86-
append_message = {"msg": expand_msg, "kwargs": fmt_kwargs}
87+
append_message = FeedbackComponent(expand_msg, fmt_kwargs)
8788

8889
# check there are enough parts for index
8990
has_part(state, name, missing_msg, fmt_kwargs, index)
@@ -130,14 +131,13 @@ def check_node(
130131
try:
131132
stu_out[index]
132133
except (KeyError, IndexError): # TODO comment errors
133-
_msg = state.build_message(missing_msg, fmt_kwargs)
134-
state.report(_msg)
134+
state.report(missing_msg, fmt_kwargs)
135135

136136
# get node at index
137137
stu_part = stu_out[index]
138138
sol_part = sol_out[index]
139139

140-
append_message = {"msg": expand_msg, "kwargs": fmt_kwargs}
140+
append_message = FeedbackComponent(expand_msg, fmt_kwargs)
141141

142142
return part_to_child(stu_part, sol_part, append_message, state, node_name=name)
143143

@@ -151,9 +151,10 @@ def with_context(state, *args, child=None):
151151
process=state.solution_process, context=state.solution_parts["with_items"]
152152
)
153153
if isinstance(solution_res, Exception):
154-
raise InstructorError(
155-
"error in the solution, running test_with(): %s" % str(solution_res)
156-
)
154+
with debugger(state):
155+
state.report(
156+
"error in the solution, running test_with(): %s" % str(solution_res)
157+
)
157158

158159
student_res = setUpNewEnvInProcess(
159160
process=state.student_process, context=state.student_parts["with_items"]
@@ -178,10 +179,11 @@ def with_context(state, *args, child=None):
178179
process=state.solution_process
179180
)
180181
if isinstance(close_solution_context, Exception):
181-
raise InstructorError(
182-
"error in the solution, closing the `with` fails with: %s"
183-
% close_solution_context
184-
)
182+
with debugger(state):
183+
state.report(
184+
"error in the solution, closing the `with` fails with: %s"
185+
% close_solution_context
186+
)
185187

186188
close_student_context = breakDownNewEnvInProcess(process=state.student_process)
187189
if isinstance(close_student_context, Exception):
@@ -205,7 +207,7 @@ def check_args(state, name, missing_msg=None):
205207
Args:
206208
name (str): the name of the argument for which you want to check if it is specified. This can also be
207209
a number, in which case it refers to the positional arguments. Named arguments take precedence.
208-
missing_msg (str): If specified, this overrides an automatically generated feedback message in case
210+
missing_msg (str): If specified, this overrides the automatically generated feedback message in case
209211
the student did specify the argument.
210212
state (State): State object that is passed from the SCT Chain (don't specify this).
211213
@@ -321,7 +323,7 @@ def my_power(x):
321323
stu_part, _argstr = build_call(callstr, state.student_parts["node"])
322324
sol_part, _ = build_call(callstr, state.solution_parts["node"])
323325

324-
append_message = {"msg": expand_msg, "kwargs": {"argstr": argstr or _argstr}}
326+
append_message = FeedbackComponent(expand_msg, {"argstr": argstr or _argstr})
325327
child = part_to_child(stu_part, sol_part, append_message, state)
326328

327329
return child

0 commit comments

Comments
 (0)