Skip to content

Commit 074cd40

Browse files
committed
Merge branch 'fix-safe-templating' into spec-messages
2 parents 2e81b0c + 7e89786 commit 074cd40

18 files changed

Lines changed: 136 additions & 77 deletions

pythonwhat/State.py

Lines changed: 44 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import ast
22
import inspect
3+
import string
34
from copy import copy
45
from functools import partial
56
from pythonwhat.parsing import TargetVars, FunctionParser, ObjectAccessParser, parser_dict
@@ -10,6 +11,7 @@
1011
from pythonwhat.converters import get_manual_converters
1112
from collections.abc import Mapping
1213
from itertools import chain
14+
from jinja2 import Template
1315

1416
class Context(Mapping):
1517
def __init__(self, context=None, prev=None):
@@ -32,6 +34,37 @@ def __len__(self):
3234
return len(self._items)
3335

3436

37+
#class MsgFormatter(string.Formatter):
38+
# def vformat(self, format_string, args, kwargs):
39+
# """Restricted vformat, which does not format entries with converters or format specs"""
40+
# used_args = set()
41+
# result = []
42+
# for chunk in string._string.formatter_parser(format_string):
43+
# orig = self._orig_from_chunk(*chunk)
44+
# # return original string if there are converters or format specs,
45+
# # otherwise, parse as normal
46+
# if chunk[1] and any(chunk[2:]):
47+
# result.append(orig)
48+
# elif chunk[0] and not any(chunk[1:]):
49+
# result.append(chunk[0])
50+
# else:
51+
# res, _ = self._vformat(orig, args, kwargs, used_args, 1)
52+
# result.append(res)
53+
# return "".join(result)
54+
#
55+
# def get_field(self, field_name, args, kwargs):
56+
# try:
57+
# return super().get_field(field_name, args, kwargs)
58+
# except (KeyError, AttributeError):
59+
# return "{"+field_name+"}", "NA"
60+
#
61+
# @staticmethod
62+
# def _orig_from_chunk(literal_text, field_name, format_spec, conversion):
63+
# # of form, literal_str {var_name!conversion:format_spec}
64+
# conversion = '!' + conversion if conversion else ""
65+
# format_spec = ":" + format_spec if format_spec else ""
66+
# return "%s{%s%s%s}"%(literal_text, field_name, conversion, format_spec)
67+
3568
class State(object):
3669
"""State of the SCT environment.
3770
@@ -109,10 +142,17 @@ def build_message(self, tail="", fmt_kwargs=None):
109142
msgs = self.messages[:] + [{'msg': tail or "", 'kwargs':fmt_kwargs}]
110143
# format messages in list, by iterating over previous, current, and next message
111144
for prev_d, d, next_d in zip([{}, *msgs[:-1]], msgs, [*msgs[1:], {}]):
112-
out = d['msg'].format(parent = prev_d.get('kwargs'),
113-
child = next_d.get('kwargs'),
114-
this = d['kwargs'],
115-
**d['kwargs'])
145+
tmp_kwargs = {'parent': prev_d.get('kwargs'),
146+
'child': next_d.get('kwargs'),
147+
'this': d['kwargs'],
148+
**d['kwargs']}
149+
if d['msg'].startswith('FMT:'):
150+
out = d['msg'].replace('FMT:', "").format(**tmp_kwargs)
151+
elif d['msg'].startswith('__JINJA__:'):
152+
out = Template(d['msg'].replace('__JINJA__:', "")).render(**tmp_kwargs)
153+
else:
154+
out = d['msg']
155+
116156
out_list.append(out)
117157

118158
return "".join(out_list)

pythonwhat/check_funcs.py

Lines changed: 15 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -64,8 +64,8 @@ def check_part_index(name, index, part_msg,
6464
# return child state from part
6565
return part_to_child(stu_part, sol_part, append_message, state)
6666

67-
MSG_MISSING = "The system wants to check the {typestr} you defined but hasn't found it."
68-
MSG_PREPEND = "Check your code in the {child[part]} of the {typestr}. "
67+
MSG_MISSING = "FMT:The system wants to check the {typestr} you defined but hasn't found it."
68+
MSG_PREPEND = "__JINJA__:Check your code in the {{child['part']+ ' of the' if child['part']}} {{typestr}}. "
6969
def check_node(name, index, typestr, missing_msg=MSG_MISSING, expand_msg=MSG_PREPEND, state=None):
7070
rep = Reporter.active_reporter
7171
stu_out = getattr(state, 'student_'+name)
@@ -226,7 +226,7 @@ def set_context(*args, state=None, **kwargs):
226226
student_context = out_stu, solution_context = out_sol)
227227

228228

229-
def check_args(name, missing_msg='check the argument `{part}`, ', state=None):
229+
def check_args(name, missing_msg='FMT:check the argument `{part}`, ', state=None):
230230
if name in ['*args', '**kwargs']:
231231
return check_part(name, name, state=state, missing_msg = missing_msg)
232232
else:
@@ -286,8 +286,8 @@ def run_call(args, node, process, get_func, **kwargs):
286286
return get_func(process = process, tree=func_expr, call = fmt_args, **kwargs)
287287

288288

289-
MSG_CALL_INCORRECT = "Calling it should result in {str_sol}, instead got {str_sol}"
290-
MSG_CALL_ERROR = "Calling it should result in {str_sol}, instead got an error"
289+
MSG_CALL_INCORRECT = "FMT:Calling it should result in {str_sol}, instead got {str_sol}"
290+
MSG_CALL_ERROR = "FMT:Calling it should result in {str_sol}, instead got an error"
291291
def call(args,
292292
test='value',
293293
incorrect_msg=MSG_CALL_INCORRECT,
@@ -304,11 +304,14 @@ def call(args,
304304
eval_sol, str_sol = run_call(args, state.solution_parts['node'], state.solution_process, get_func, **kwargs)
305305

306306
if (test == 'error') ^ isinstance(str_sol, Exception):
307-
_msg_prefix = "Calling %s for arguments %s " % (argstr, args)
308-
raise ValueError(_msg_prefix + call_warnings[test])
307+
_msg = state.build_message("FMT:Calling for arguments {args} resulted in an error (or not an error if testing for one).",
308+
dict(args=args))
309+
raise ValueError(_msg)
309310

310311
if isinstance(eval_sol, ReprFail):
311-
raise ValueError("Can't get the result of calling %s for arguments %s: %s" % (argstr, args, eval_sol.info))
312+
_msg = state.build_message("FMT:Can't get the result of calling it for arguments {args}: {eval_sol.info}",
313+
dict(args = args, eval_sol=eval_sol))
314+
raise ValueError(_msg)
312315

313316
# Run for Submission ------------------------------------------------------
314317
eval_stu, str_stu = run_call(args, state.student_parts['node'], state.student_process, get_func, **kwargs)
@@ -329,9 +332,9 @@ def call(args,
329332
# Expression tests ------------------------------------------------------------
330333
from pythonwhat.tasks import ReprFail, UndefinedValue
331334
from pythonwhat import utils
332-
def has_expr(incorrect_msg="Unexpected expression: expected `{sol_eval}`, got `{stu_eval}` with values{extra_env}.",
335+
def has_expr(incorrect_msg="FMT:Unexpected expression {test}: expected `{sol_eval}`, got `{stu_eval}` with values{extra_env}.",
333336
error_msg="Running an expression in the student process caused an issue",
334-
undefined_msg="Have you defined `{name}` without errors?",
337+
undefined_msg="FMT:Have you defined `{name}` without errors?",
335338
extra_env=None,
336339
context_vals=None,
337340
expr_code=None,
@@ -372,7 +375,8 @@ def has_expr(incorrect_msg="Unexpected expression: expected `{sol_eval}`, got `{
372375
context = state.student_context)
373376

374377
# kwargs ---
375-
fmt_kwargs = {'stu_part': state.student_parts, 'sol_part': state.solution_parts, 'name': name,
378+
fmt_kwargs = {'stu_part': state.student_parts, 'sol_part': state.solution_parts,
379+
'name': name, 'test': test,
376380
'extra_env': " "+str(extra_env or ""), 'context_vals': context_vals}
377381
fmt_kwargs['stu_eval'] = utils.shorten_str(str(eval_stu))
378382
fmt_kwargs['sol_eval'] = utils.shorten_str(str(eval_sol))

pythonwhat/check_wrappers.py

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
1-
from pythonwhat.check_funcs import check_part, check_part_index, check_node
1+
from pythonwhat.check_funcs import check_part, check_part_index, check_node, has_equal_part
22
from pythonwhat import check_funcs
33
from functools import partial
44
import inspect
5+
#from jinja2 import Template
56

67
__PART_WRAPPERS__ = {
78
'iter': 'iterable part',
@@ -15,7 +16,7 @@
1516
__PART_INDEX_WRAPPERS__ = {
1617
'ifs': '{ordinal} if',
1718
'handlers': '{index} `except` block',
18-
'context': 'context'
19+
'context': '{ordinal} context'
1920
}
2021

2122
__NODE_WRAPPERS__ = {
@@ -34,6 +35,11 @@
3435

3536
scts = {}
3637

38+
# make has_equal_part wrappers
39+
40+
scts['has_equal_name'] = partial(has_equal_part, 'name', msg='Make sure to use the correct {name}, was expecting {sol_part[name]}, instead got {stu_part[name]}.')
41+
scts['is_default'] = partial(has_equal_part, 'is_default', msg="__JINJA__:Make sure it {{ 'has' if sol_part.is_default else 'does not have'}} a default argument.")
42+
3743
for k, v in __PART_WRAPPERS__.items():
3844
scts['check_'+k] = partial(check_part, k, v)
3945

@@ -51,4 +57,3 @@
5157
'check_args',
5258
'has_equal_part']:
5359
scts[k] = getattr(check_funcs, k)
54-

pythonwhat/test_funcs/test_comp.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,12 @@
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
66

7-
MSG_NOT_CALLED = "The system wants to check the {ordinal} {typestr} you defined but hasn't found it."
8-
MSG_PREPEND = "Check your code in the {child[part]} of the {ordinal} {typestr}. "
7+
MSG_NOT_CALLED = "FMT:The system wants to check the {ordinal} {typestr} you defined but hasn't found it."
8+
MSG_PREPEND = "FMT:Check your code in the {child[part]} of the {ordinal} {typestr}. "
99

10-
MSG_INCORRECT_ITER_VARS = "Have you used the correct iterator variables in the {parent[ordinal]} {parent[typestr]}? Make sure you use the correct names!"
11-
MSG_INCORRECT_NUM_ITER_VARS = "Have you used {num_vars} iterator variables in the {parent[ordinal]} {parent[typestr]}?"
12-
MSG_INSUFFICIENT_IFS = "Have you used {sol_len} ifs inside the {parent[ordinal]} {parent[typestr]}?"
10+
MSG_INCORRECT_ITER_VARS = "FMT:Have you used the correct iterator variables in the {parent[ordinal]} {parent[typestr]}? Make sure you use the correct names!"
11+
MSG_INCORRECT_NUM_ITER_VARS = "FMT:Have you used {num_vars} iterator variables in the {parent[ordinal]} {parent[typestr]}?"
12+
MSG_INSUFFICIENT_IFS = "FMT:Have you used {sol_len} ifs inside the {parent[ordinal]} {parent[typestr]}?"
1313

1414
def test_list_comp(index=1,
1515
not_called_msg=None,

pythonwhat/test_funcs/test_data_frame.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,10 @@
55

66
import pandas as pd
77

8-
MSG_UNDEFINED = "Are you sure you defined the pandas DataFrame: `{parent[sol_part][name]}`?"
9-
MSG_NOT_INSTANCE = "`{parent[sol_part][name]}` is not a pandas DataFrame."
10-
MSG_KEY_MISSING = "There is no column `{key}` inside `{parent[sol_part][name]}`."
11-
MSG_INCORRECT_VAL = "Column `{key}` of your pandas DataFrame, `{parent[sol_part][name]}`, is not correct."
8+
MSG_UNDEFINED = "FMT:Are you sure you defined the pandas DataFrame: `{parent[sol_part][name]}`?"
9+
MSG_NOT_INSTANCE = "FMT:`{parent[sol_part][name]}` is not a pandas DataFrame."
10+
MSG_KEY_MISSING = "FMT:There is no column `{key}` inside `{parent[sol_part][name]}`."
11+
MSG_INCORRECT_VAL = "FMT:Column `{key}` of your pandas DataFrame, `{parent[sol_part][name]}`, is not correct."
1212

1313
def test_data_frame(name,
1414
columns=None,

pythonwhat/test_funcs/test_dictionary.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,10 @@
44
from pythonwhat.tasks import isInstanceInProcess, getKeysInProcess, getValueInProcess, isDefinedCollInProcess, ReprFail
55
from .test_object import check_object
66

7-
MSG_UNDEFINED = "Are you sure you defined the dictionary `{parent[sol_part][name]}`?"
8-
MSG_NOT_INSTANCE = "`{parent[sol_part][name]}` is not a dictionary."
9-
MSG_KEY_MISSING = "Have you specified a key `{key}` inside `{parent[sol_part][name]}`?"
10-
MSG_INCORRECT_VAL = "Have you specified the correct value for the key `{key}` inside `{parent[sol_part][name]}`?"
7+
MSG_UNDEFINED = "FMT:Are you sure you defined the dictionary `{parent[sol_part][name]}`?"
8+
MSG_NOT_INSTANCE = "FMT:`{parent[sol_part][name]}` is not a dictionary."
9+
MSG_KEY_MISSING = "FMT:Have you specified a key `{key}` inside `{parent[sol_part][name]}`?"
10+
MSG_INCORRECT_VAL = "FMT:Have you specified the correct value for the key `{key}` inside `{parent[sol_part][name]}`?"
1111

1212
def test_dictionary(name,
1313
keys=None,

pythonwhat/test_funcs/test_expression_output.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -66,9 +66,9 @@ def test_expression_output(extra_env=None,
6666
feedback_msg = incorrect_msg
6767
else:
6868
if expr_code is not None:
69-
prestring = "When running %s e" % expr_code
69+
prestring = "FMT:When running %s e" % expr_code
7070
else:
71-
prestring = "E"
71+
prestring = "FMT:E"
7272
feedback_msg = "%sxpected output `{sol_eval}`, instead got `{stu_eval}`" % (prestring)
7373
if extra_env:
7474
# need double brackets to not screw up string formatting

pythonwhat/test_funcs/test_expression_result.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ def test_expression_result(extra_env=None,
8080
else:
8181
# need to double bracket extra_env, so doesn't mess up str templating
8282
feedback_msg = (
83-
"Unexpected expression: expected `{sol_eval}`, got `{stu_eval}` with values{extra_env}."
83+
"FMT:Unexpected expression: expected `{sol_eval}`, got `{stu_eval}` with values{extra_env}."
8484
)
8585

8686
has_equal_value(feedback_msg,

pythonwhat/test_funcs/test_for_loop.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@
33

44
from functools import partial
55

6-
MSG_MISSING = "Define more {typestr}."
7-
MSG_PREPEND = "Check your code in the {child[part]} of the {ordinal} for loop. "
6+
MSG_MISSING = "FMT:Define more {typestr}."
7+
MSG_PREPEND = "FMT:Check your code in the {child[part]} of the {ordinal} for loop. "
88

99
def test_for_loop(index=1,
1010
for_iter=None,

pythonwhat/test_funcs/test_function_definition.py

Lines changed: 17 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -3,28 +3,28 @@
33

44
from functools import partial
55

6-
MSG_MISSING = "You didn't define the following function: {typestr}."
7-
MSG_PREPEND = "Check your definition of {typestr}. "
6+
MSG_MISSING = "FMT:You didn't define the following function: {typestr}."
7+
MSG_PREPEND = "FMT:Check your definition of {typestr}. "
88

9-
MSG_NUM_ARGS = "You should define {parent[typestr]} with {sol_len} arguments, instead got {stu_len}."
9+
MSG_NUM_ARGS = "FMT:You should define {parent[typestr]} with {sol_len} arguments, instead got {stu_len}."
1010

11-
MSG_PREPEND_ARG = "In your definition of {typestr}, "
12-
MSG_BAD_ARG_NAME = "the {parent[ordinal]} {parent[part]} should be called `{sol_part[name]}`, instead got `{stu_part[name]}`."
13-
MSG_BAD_DEFAULT = "the {parent[part]} `{stu_part[name]}` should have no default."
14-
MSG_INC_DEFAULT = "the {parent[part]} `{stu_part[name]}` does not have the correct default."
11+
MSG_PREPEND_ARG = "FMT:In your definition of {typestr}, "
12+
MSG_BAD_ARG_NAME = "FMT:the {parent[ordinal]} {parent[part]} should be called `{sol_part[name]}`, instead got `{stu_part[name]}`."
13+
MSG_BAD_DEFAULT = "FMT:the {parent[part]} `{stu_part[name]}` should have no default."
14+
MSG_INC_DEFAULT = "FMT:the {parent[part]} `{stu_part[name]}` does not have the correct default."
1515

16-
MSG_NO_VARARG = "have you specified an argument to take a `*` argument and named it `{sol_part[*args][name]}`?"
17-
MSG_NO_KWARGS = "have you specified an argument to take a `**` argument and named it `{sol_part[**kwargs][name]}`?"
18-
MSG_VARARG_NAME = "have you specified an argument to take a `*` argument and named it `{sol_part[name]}`?"
19-
MSG_KWARG_NAME = "have you specified an argument to take a `**` argument and named it `{sol_part[name]}`?"
16+
MSG_NO_VARARG = "FMT:have you specified an argument to take a `*` argument and named it `{sol_part[*args][name]}`?"
17+
MSG_NO_KWARGS = "FMT:have you specified an argument to take a `**` argument and named it `{sol_part[**kwargs][name]}`?"
18+
MSG_VARARG_NAME = "FMT:have you specified an argument to take a `*` argument and named it `{sol_part[name]}`?"
19+
MSG_KWARG_NAME = "FMT:have you specified an argument to take a `**` argument and named it `{sol_part[name]}`?"
2020

2121
# TODO some need to reference the eval rather than str
22-
MSG_RES_ERROR = "Calling `{argstr}` should result in `{str_sol}`, instead got an error."
23-
MSG_RES_INCORRECT = "Calling `{argstr}` should result in `{str_sol}`, instead got `{str_stu}`."
24-
MSG_ERR_NONE = "Calling `{argstr}` doesn't result in an error, but it should!"
25-
MSG_ERR_INCORRECT = "Calling `{argstr}` should result in a `{str_sol.__class__.__name__}`, instead got a `{str_stu.__class__.__name__}`."
26-
MSG_OUT_ERROR = "Calling `{argstr}` should output {str_sol}, instead got an error."
27-
MSG_OUT_INCORRECT = "Calling `{argstr}` should output `{str_sol}`, instead got {str_stu}."
22+
MSG_RES_ERROR = "FMT:Calling `{argstr}` should result in `{str_sol}`, instead got an error."
23+
MSG_RES_INCORRECT = "FMT:Calling `{argstr}` should result in `{str_sol}`, instead got `{str_stu}`."
24+
MSG_ERR_NONE = "FMT:Calling `{argstr}` doesn't result in an error, but it should!"
25+
MSG_ERR_INCORRECT = "FMT:Calling `{argstr}` should result in a `{str_sol.__class__.__name__}`, instead got a `{str_stu.__class__.__name__}`."
26+
MSG_OUT_ERROR = "FMT:Calling `{argstr}` should output {str_sol}, instead got an error."
27+
MSG_OUT_INCORRECT = "FMT:Calling `{argstr}` should output `{str_sol}`, instead got {str_stu}."
2828

2929
def test_function_definition(name,
3030
arg_names=True,

0 commit comments

Comments
 (0)