Skip to content

Commit a62c247

Browse files
author
Filip Schouwenaars
committed
switch from thonny ast tokenization to asttokens lib
- requires a 'module wrapper' that inherits first and last tokens from children - some more tests for added robustness - simplify some tests + switch to pytest for some
1 parent cf55def commit a62c247

17 files changed

+207
-826
lines changed

.gitignore

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,4 +73,7 @@ target/
7373
tests/.DS_Store
7474

7575
# pytest
76-
.pytest_cache/
76+
.pytest_cache/
77+
78+
# datasets
79+
*.csv

pythonwhat/Feedback.py

Lines changed: 7 additions & 101 deletions
Original file line numberDiff line numberDiff line change
@@ -1,112 +1,18 @@
11
import re
2-
from pythonwhat import utils
3-
from pythonwhat import utils_ast
4-
import _ast
52

63
class Feedback(object):
74

85
def __init__(self, message, astobj = None):
96
self.message = message
107
self.line_info = {}
118
try:
12-
if astobj is not None:
13-
if issubclass(type(astobj), (_ast.Module, _ast.Expression)):
14-
astobj = astobj.body
15-
if isinstance(astobj, list) and len(astobj) > 0:
16-
start = astobj[0]
17-
end = astobj[-1]
18-
else:
19-
start = astobj
20-
end = astobj
21-
if hasattr(start, "lineno") and \
22-
hasattr(start, "col_offset") and \
23-
hasattr(end, "end_lineno") and \
24-
hasattr(end, "end_col_offset"):
25-
self.line_info["line_start"] = start.lineno
26-
self.line_info["column_start"] = start.col_offset
27-
self.line_info["line_end"] = end.end_lineno
28-
self.line_info["column_end"] = end.end_col_offset
9+
if astobj is not None and \
10+
hasattr(astobj, "first_token") and \
11+
hasattr(astobj, "last_token"):
12+
self.line_info["line_start"] = astobj.first_token.start[0]
13+
self.line_info["column_start"] = astobj.first_token.start[1]
14+
self.line_info["line_end"] = astobj.last_token.end[0]
15+
self.line_info["column_end"] = astobj.last_token.end[1]
2916
except:
3017
pass
3118

32-
# TODO FILIP: No used for now, come back to this later.
33-
class FeedbackMessage(object):
34-
"""Generate feedback.
35-
36-
Don't use this yet!
37-
38-
This class will hold all functionality which is related to feedback messaging.
39-
At the moment it is NOT used, feedback generation is still HIGLY interwoven with
40-
test_... files. Should be decoupled.
41-
42-
Class should be refactored to use .format() instead.
43-
44-
Will be documented when it's refactored.
45-
"""
46-
def __init__(self, message_string):
47-
self.set(message_string)
48-
self.information = {}
49-
50-
def add_information(self, key, value):
51-
if (not(key in self.information)):
52-
self.set_information(key, value)
53-
54-
def set_information(self, key, value):
55-
self.information[key] = utils.shorten_str(str(value))
56-
57-
def remove_information(self, key):
58-
if (key in self.information):
59-
self.information.pop(key)
60-
61-
def set(self, message_string):
62-
self.message_string = str(message_string)
63-
64-
def append(self, message_string):
65-
self.message_string += str(message_string)
66-
67-
def cond_append(self, cond, message_string):
68-
self.message_string += "${{" + \
69-
str(cond) + " ? " + str(message_string) + "}}"
70-
71-
def generateString(self):
72-
generated_string = FeedbackMessage.replaceRegularTags(
73-
self.message_string, self.information)
74-
generated_string = FeedbackMessage.replaceConditionalTags(
75-
generated_string, self.information)
76-
return(generated_string)
77-
78-
def replaceRegularTags(message_string, information):
79-
generated_string = message_string
80-
81-
pattern = "\${([a-zA-Z]*?)}"
82-
83-
keywords = re.findall(pattern, generated_string)
84-
for keyword in keywords:
85-
replace = "\${" + keyword + "}"
86-
if (keyword in information):
87-
generated_string = re.sub(
88-
replace, information[keyword], generated_string)
89-
else:
90-
generated_string = re.sub(replace, "", generated_string)
91-
92-
return(generated_string)
93-
94-
def replaceConditionalTags(message_string, information):
95-
generated_string = message_string.replace("\n", "\\\\n")
96-
pattern = "\${{([a-zA-Z]*?) \? (.*?)}}"
97-
98-
cond_keywords = re.findall(pattern, generated_string)
99-
for (keyword, k_string) in cond_keywords:
100-
replace = "\${{" + keyword + " \? " + re.escape(k_string) + "}}"
101-
if (keyword in information):
102-
generated_string = re.sub(
103-
replace,
104-
" " +
105-
FeedbackMessage.replaceRegularTags(
106-
k_string,
107-
information),
108-
generated_string)
109-
else:
110-
generated_string = re.sub(replace, "", generated_string)
111-
112-
return(generated_string)

pythonwhat/Reporter.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,6 @@ def do_test(self, testobj, prepend_on_fail="", fallback_ast=None):
2929
Execute a given test, unless some previous test has failed. If the test has failed,
3030
the state of the reporter changes and the feedback is kept.
3131
"""
32-
3332
if prepend_on_fail: self.failure_msg = prepend_on_fail
3433
if fallback_ast: self.fallback_ast = fallback_ast
3534

@@ -40,7 +39,7 @@ def do_test(self, testobj, prepend_on_fail="", fallback_ast=None):
4039
self.failed_test = True
4140
self.feedback = testobj.get_feedback()
4241
self.feedback.message = self.failure_msg + self.feedback.message
43-
if not self.feedback.line_info and self.fallback_ast:
42+
if not self.feedback.line_info and self.fallback_ast:
4443
self.feedback = Feedback(self.feedback.message, self.fallback_ast)
4544
raise TestFail
4645

pythonwhat/State.py

Lines changed: 17 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,13 @@
66
from pythonwhat.parsing import TargetVars, FunctionParser, ObjectAccessParser, parser_dict
77
from pythonwhat.Reporter import Reporter
88
from pythonwhat.Feedback import Feedback
9-
from pythonwhat import utils_ast
109
from pythonwhat import signatures
1110
from pythonwhat.converters import get_manual_converters
1211
from collections.abc import Mapping
1312
from itertools import chain
1413
from jinja2 import Template
14+
import asttokens
15+
from pythonwhat.utils_ast import wrap_in_module
1516

1617
class Context(Mapping):
1718
def __init__(self, context=None, prev=None):
@@ -88,13 +89,13 @@ def __init__(self,
8889

8990
# parse code if didn't happen yet
9091
if not hasattr(self, 'student_tree'):
91-
self.student_tree = State.parse_ext(self.student_code)
92+
self.student_tree_tokens, self.student_tree = State.parse_external(self.student_code)
9293

9394
if not hasattr(self, 'solution_tree'):
94-
self.solution_tree = State.parse_int(self.solution_code)
95+
_, self.solution_tree = State.parse_internal(self.solution_code)
9596

9697
if not hasattr(self, 'pre_exercise_tree'):
97-
self.pre_exercise_tree = State.parse_int(self.pre_exercise_code)
98+
_, self.pre_exercise_tree = State.parse_internal(self.pre_exercise_code)
9899

99100
if not hasattr(self, 'parent_state'):
100101
self.parent_state = None
@@ -170,9 +171,9 @@ def to_child_state(self, student_subtree, solution_subtree,
170171
"""
171172

172173
if isinstance(student_subtree, list):
173-
student_subtree = ast.Module(student_subtree)
174+
student_subtree = wrap_in_module(student_subtree)
174175
if isinstance(solution_subtree, list):
175-
solution_subtree = ast.Module(solution_subtree)
176+
solution_subtree = wrap_in_module(solution_subtree)
176177

177178
# get new contexts
178179
if solution_context is not None:
@@ -196,8 +197,9 @@ def to_child_state(self, student_subtree, solution_subtree,
196197
highlight = highlight, messages = messages)
197198

198199
klass = State if not node_name else self.SUBCLASSES[node_name]
199-
child = klass(student_code = utils_ast.extract_text_from_node(self.full_student_code, student_subtree),
200+
child = klass(student_code = self.student_tree_tokens.get_text(student_subtree),
200201
full_student_code = self.full_student_code,
202+
student_tree_tokens = self.student_tree_tokens,
201203
pre_exercise_code = self.pre_exercise_code,
202204
student_context = student_context,
203205
solution_context = solution_context,
@@ -222,14 +224,13 @@ def update(self, **kwargs):
222224
return child
223225

224226
@staticmethod
225-
def parse_ext(x):
227+
def parse_external(x):
226228
rep = Reporter.active_reporter
227229

228-
res = None
230+
res = (None, None)
229231
try:
230-
res = ast.parse(x)
231-
# enrich tree with end lines and end columns
232-
utils_ast.mark_text_ranges(res, x + '\n')
232+
res = asttokens.ASTTokens(x, parse = True)
233+
return(res, res._tree)
233234

234235
except IndentationError as e:
235236
e.filename = "script.py"
@@ -249,26 +250,19 @@ def parse_ext(x):
249250
rep.feedback.message = "Something went wrong while parsing your code."
250251
rep.failed_test = True
251252

252-
finally:
253-
if (res is None):
254-
res = False
255-
256253
return(res)
257254

258255
@staticmethod
259-
def parse_int(x):
260-
res = None
256+
def parse_internal(x):
257+
res = (None, None)
261258
try:
262-
res = ast.parse(x)
263-
utils_ast.mark_text_ranges(res, x + '\n')
259+
res = asttokens.ASTTokens(x, parse = True)
260+
return(res, res._tree)
264261

265262
except SyntaxError as e:
266263
raise SyntaxError(str(e))
267264
except TypeError as e:
268265
raise TypeError(str(e))
269-
finally:
270-
if (res is None):
271-
res = False
272266

273267
return(res)
274268

pythonwhat/check_funcs.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,6 @@ def check_part(name, part_msg, state=None, missing_msg="Are you sure it's define
3030
append_message = {'msg': expand_msg, 'kwargs': {'part': part_msg,}}
3131

3232
has_part(name, missing_msg, state, append_message['kwargs'])
33-
3433
stu_part = state.student_parts[name]
3534
sol_part = state.solution_parts[name]
3635

@@ -231,7 +230,7 @@ def with_context(*args, state=None):
231230
solution_res = setUpNewEnvInProcess(process = state.solution_process,
232231
context = state.solution_parts['with_items'])
233232
if isinstance(solution_res, Exception):
234-
raise Exception("error in the solution, running test_with() on with %d: %s" % (index - 1, str(solution_res)))
233+
raise Exception("error in the solution, running test_with(): %s" % str(solution_res))
235234

236235
student_res = setUpNewEnvInProcess(process = state.student_process,
237236
context = state.student_parts['with_items'])

pythonwhat/parsing.py

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import ast
2+
from pythonwhat.utils_ast import wrap_in_module
23
from collections.abc import Sequence, Mapping
34
from collections import OrderedDict
45
from contextlib import ExitStack
@@ -472,7 +473,6 @@ def get_part(name_node, ass_node=None):
472473
name = getattr(name_node, 'id', name_node)
473474
load_name = ast.Name(id=name, ctx=ast.Load())
474475
ast.fix_missing_locations(load_name)
475-
#
476476
return {'name': name,
477477
'node': load_name,
478478
'highlight': ass_node or name_node,
@@ -610,7 +610,9 @@ def parse_node(cls, node):
610610
all_args = [*args, varargs, *kw_args, kwargs]
611611

612612
if isinstance(node, ast.Lambda): body_node = node.body
613-
else: body_node = FunctionBodyTransformer().visit(ast.Module(node.body))
613+
else:
614+
bodyMod = wrap_in_module(node.body)
615+
body_node = FunctionBodyTransformer().visit(bodyMod)
614616

615617
return {
616618
"node": node,
@@ -757,13 +759,10 @@ def visit_Return(self, node):
757759
new_node = ast.copy_location(ast.Expr(value = node.value), node)
758760
return FunctionBodyTransformer.decorate(new_node, node)
759761

762+
@staticmethod
760763
def decorate(new_node, node):
761-
try:
762-
# only possible on the student side!
763-
new_node.end_lineno = node.end_lineno
764-
new_node.end_col_offset = node.end_col_offset
765-
except:
766-
pass
764+
new_node.first_token = node.first_token
765+
new_node.last_token = node.last_token
767766
return new_node
768767

769768
class WithParser(Parser):

pythonwhat/test_funcs/test_for_loop.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,6 @@ def test_for_loop(index=1,
6262
:code:`test_exression_output()` will pass on the body code.
6363
"""
6464
rep = Reporter.active_reporter
65-
6665
state = check_node('for_loops', index-1, "`for` loops", MSG_MISSING, MSG_PREPEND, state=state)
6766

6867
# TODO for_iter is a level up, so shouldn't have targets set, but this is done is check_node

pythonwhat/test_funcs/test_function.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55
from pythonwhat.Reporter import Reporter
66
from pythonwhat.Feedback import Feedback
77
from pythonwhat.utils import get_ord, get_num
8-
from pythonwhat.utils_ast import extract_text_from_node
98
from pythonwhat.tasks import getResultInProcess, getSignatureInProcess, ReprFail
109
from .test_or import test_or
1110

pythonwhat/test_funcs/test_if_else.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,6 @@ def test_if_else(index=1,
7070
"""
7171
rep = Reporter.active_reporter
7272

73-
7473
# get state with specific if block
7574
node_name = 'if_exps' if use_if_exp else 'if_elses'
7675
# TODO original typestr for check_node used if rather than `if`

0 commit comments

Comments
 (0)