Skip to content

Commit 882ebd4

Browse files
committed
Handle parsing in dispatcher
- similar to protowhat - easier to find parsing in code by searching for ast_dispatcher
1 parent 2e268c6 commit 882ebd4

8 files changed

Lines changed: 68 additions & 63 deletions

File tree

pythonwhat/State.py

Lines changed: 54 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
import inspect
33
import string
44
from copy import copy
5-
from functools import partial
5+
from functools import partialmethod
66
from pythonwhat.parsing import (
77
TargetVars,
88
FunctionParser,
@@ -89,6 +89,8 @@ def __init__(
8989
if not hasattr(self, "pre_exercise_tree"):
9090
_, self.pre_exercise_tree = self.parse(self.pre_exercise_code, test=False)
9191

92+
self.ast_dispatcher = Dispatcher(self.pre_exercise_tree)
93+
9294
if not hasattr(self, "parent_state"):
9395
self.parent_state = None
9496

@@ -286,12 +288,10 @@ def assert_is_not(self, klasses, fun, prev_fun):
286288
% (fun, " or ".join(["`%s()`" % pf for pf in prev_fun]))
287289
)
288290

289-
def parse_external(self, x):
291+
def parse_external(self, code):
290292
res = (None, None)
291293
try:
292-
res = asttokens.ASTTokens(x, parse=True)
293-
return (res, res.tree)
294-
294+
return Dispatcher.parse(code)
295295
except IndentationError as e:
296296
e.filename = "script.py"
297297
# no line info for now
@@ -326,10 +326,9 @@ def parse_external(self, x):
326326
return res
327327

328328
@staticmethod
329-
def parse_internal(x):
329+
def parse_internal(code):
330330
try:
331-
res = asttokens.ASTTokens(x, parse=True)
332-
return (res, res._tree)
331+
return Dispatcher.parse(code)
333332
except Exception as e:
334333
raise InstructorError(
335334
"Something went wrong when parsing PEC or solution code: %s" % str(e)
@@ -344,55 +343,61 @@ def parse(self, text, test=True):
344343
return parse_method(text)
345344

346345

347-
# add property methods for retrieving parser outputs --------------------------
348-
# note that this code is an alternative means of using something like..
349-
# @property
350-
# def student_withs(self): ...
351-
# when defining the State class.
352-
def getx(tree_name, Parser, ext_attr, self):
353-
"""getter for Parser outputs"""
354-
# return cached output if possible
355-
cache_key = tree_name + Parser.__name__
356-
if self._parser_cache.get(cache_key):
357-
p = self._parser_cache[cache_key]
358-
else:
359-
# otherwise, run parser over tree
360-
p = Parser()
361-
# set mappings for parsers that inspect attribute access
362-
if ext_attr != "mappings" and Parser in [FunctionParser, ObjectAccessParser]:
363-
p.mappings = self.pre_exercise_mappings.copy()
364-
# run parser
365-
p.visit(getattr(self, tree_name))
366-
# cache
367-
self._parser_cache[cache_key] = p
368-
return getattr(p, ext_attr)
369-
370-
371-
# put a property getter on state for each parsed ast tree output.
372-
# since the getter takes only one argument, self, partial functions
373-
# are used to set all other arguments on getx
374-
for s in ["student", "solution"]:
375-
tree_name = s + "_tree"
376-
for k, Parser in parser_dict.items():
377-
setattr(State, s + "_" + k, property(partial(getx, tree_name, Parser, "out")))
378-
379-
# mappings from ObjectAccessParser
380-
prop_oa_map = property(partial(getx, tree_name, ObjectAccessParser, "mappings"))
381-
setattr(State, s + "_oa_mappings", prop_oa_map)
382-
383-
# mappings from FunctionParser
384-
prop_map = property(partial(getx, tree_name, FunctionParser, "mappings"))
385-
setattr(State, s + "_mappings", prop_map)
346+
class Dispatcher:
347+
def __init__(self, pre_exercise_tree):
348+
self._parser_cache = dict()
349+
self.pre_exercise_mappings = self._getx(FunctionParser, "mappings", pre_exercise_tree)
350+
351+
def __call__(self, name, node):
352+
return getattr(self, name)(node)
353+
354+
@staticmethod
355+
def parse(code):
356+
res = asttokens.ASTTokens(code, parse=True)
357+
return res, res.tree
358+
359+
# add methods for retrieving parser outputs --------------------------
360+
def _getx(self, Parser, ext_attr, tree):
361+
"""getter for Parser outputs"""
362+
# return cached output if possible
363+
cache_key = Parser.__name__ + str(hash(tree))
364+
if self._parser_cache.get(cache_key):
365+
p = self._parser_cache[cache_key]
366+
else:
367+
# otherwise, run parser over tree
368+
p = Parser()
369+
# set mappings for parsers that inspect attribute access
370+
if ext_attr != "mappings" and Parser in [FunctionParser, ObjectAccessParser]:
371+
p.mappings = self.pre_exercise_mappings.copy()
372+
# run parser
373+
p.visit(tree)
374+
# cache
375+
self._parser_cache[cache_key] = p
376+
return getattr(p, ext_attr)
377+
378+
379+
# put a function on the dispatcher
380+
for k, Parser in parser_dict.items():
381+
setattr(Dispatcher, k, partialmethod(Dispatcher._getx, Parser, "out"))
382+
383+
# mappings from ObjectAccessParser
384+
prop_oa_map = partialmethod(Dispatcher._getx, ObjectAccessParser, "mappings")
385+
setattr(Dispatcher, "oa_mappings", prop_oa_map)
386+
387+
# mappings from FunctionParser
388+
prop_map = partialmethod(Dispatcher._getx, FunctionParser, "mappings")
389+
setattr(Dispatcher, "mappings", prop_map)
386390

387391
# mappings for pre exercise code from FunctionParser
388-
pec_prop_map = property(partial(getx, "pre_exercise_tree", FunctionParser, "mappings"))
389-
setattr(State, "pre_exercise_mappings", pec_prop_map)
392+
pec_prop_map = partialmethod(Dispatcher._getx, FunctionParser, "mappings")
393+
setattr(Dispatcher, "pre_exercise_mappings", pec_prop_map)
390394

391395
# State subclasses based on parsed output -------------------------------------
392396
State.SUBCLASSES = {
393397
node_name: type(node_name, (State,), {}) for node_name in parser_dict
394398
}
395399

400+
396401
# global setters on State -----------------------------------------------------
397402
def set_converter(key, fundef):
398403
# note that root state is set on the State class in test_exercise

pythonwhat/checks/check_funcs.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -123,8 +123,8 @@ def check_node(
123123
if expand_msg is None:
124124
expand_msg = "Check the {{typestr}}. "
125125

126-
stu_out = getattr(state, "student_" + name)
127-
sol_out = getattr(state, "solution_" + name)
126+
stu_out = state.ast_dispatcher(name, state.student_tree)
127+
sol_out = state.ast_dispatcher(name, state.solution_tree)
128128

129129
# check if there are enough nodes for index
130130
fmt_kwargs = {

pythonwhat/checks/check_function.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -99,10 +99,10 @@ def check_function(
9999
if params_not_matched_msg is None:
100100
params_not_matched_msg = SIG_ISSUE_MSG
101101

102-
stu_out = state.student_function_calls
103-
sol_out = state.solution_function_calls
102+
stu_out = state.ast_dispatcher("function_calls", state.student_tree)
103+
sol_out = state.ast_dispatcher("function_calls", state.solution_tree)
104104

105-
student_mappings = state.student_mappings
105+
student_mappings = state.ast_dispatcher("mappings", state.student_tree)
106106

107107
fmt_kwargs = {
108108
"times": get_times(index + 1),

pythonwhat/checks/check_object.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -182,8 +182,8 @@ def __init__(self, n):
182182

183183
# create child state, using either parser output, or create part from name
184184
fallback = lambda: ObjectAssignmentParser.get_part(index)
185-
stu_part = state.student_object_assignments.get(index, fallback())
186-
sol_part = state.solution_object_assignments.get(index, fallback())
185+
stu_part = state.ast_dispatcher("object_assignments", state.student_tree).get(index, fallback())
186+
sol_part = state.ast_dispatcher("object_assignments", state.solution_tree).get(index, fallback())
187187

188188
# test object exists
189189
_msg = state.build_message(missing_msg, append_message["kwargs"])

pythonwhat/checks/has_funcs.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -511,8 +511,8 @@ def has_import(
511511
import matplotlib.pyplot as pltttt
512512
513513
"""
514-
student_imports = state.student_imports
515-
solution_imports = state.solution_imports
514+
student_imports = state.ast_dispatcher("imports", state.student_tree)
515+
solution_imports = state.ast_dispatcher("imports", state.solution_tree)
516516

517517
if name not in solution_imports:
518518
raise InstructorError(
@@ -660,7 +660,7 @@ def has_printout(
660660
)
661661

662662
try:
663-
sol_call_ast = state.solution_function_calls["print"][index]["node"]
663+
sol_call_ast = state.ast_dispatcher("function_calls", state.solution_tree)["print"][index]["node"]
664664
except (KeyError, IndexError):
665665
raise InstructorError(
666666
"`has_printout({})` couldn't find the {} print call in your solution.".format(

pythonwhat/test_funcs/test_compound_statement.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -439,7 +439,7 @@ def test_object_after_expression(
439439
pre_code=None,
440440
**kwargs
441441
):
442-
state.highlight = state.student_object_assignments.get(name, {}).get("highlight")
442+
state.highlight = state.ast_dispatcher("object_assignments", state.student_tree).get(name, {}).get("highlight")
443443
has_equal_value(
444444
state,
445445
incorrect_msg=incorrect_msg,

pythonwhat/test_funcs/test_object_accessed.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,8 +34,8 @@ def test_object_accessed(state, name, times=1, not_accessed_msg=None):
3434
| ``test_object_accessed("arr.shape")``: pass.
3535
| ``test_object_accessed("arr.dtype")``: fail.
3636
"""
37-
student_object_accesses = state.student_object_accesses
38-
student_mappings = state.student_oa_mappings
37+
student_object_accesses = state.ast_dispatcher("object_accesses", state.student_tree)
38+
student_mappings = state.ast_dispatcher("oa_mappings", state.student_tree)
3939

4040
if not not_accessed_msg:
4141
stud_name = name

tests/test_check_function.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@ def test_bind_args():
8686

8787
pec = "def my_fun(a, b, *args, **kwargs): pass"
8888
s = setup_state(pec=pec, stu_code="my_fun(1, 2, 3, 4, c = 5)")
89-
args = s._state.student_function_calls["my_fun"][0]["args"]
89+
args = s._state.ast_dispatcher("function_calls", s._state.student_tree)["my_fun"][0]["args"]
9090
sig = signature(s._state.student_process.shell.user_ns["my_fun"])
9191
binded_args = bind_args(sig, args)
9292
assert binded_args["a"]["node"].n == 1

0 commit comments

Comments
 (0)