Skip to content
Snippets Groups Projects
Unverified Commit 0725e776 authored by Ned Batchelder's avatar Ned Batchelder Committed by GitHub
Browse files

Merge pull request #17372 from mitodl/pdpinch/catch-mismated-parens

Add mismatched parenthesis detection to formula parsing
parents 312f5a44 ed90c80c
No related merge requests found
......@@ -100,6 +100,13 @@ class UndefinedVariable(Exception):
pass
class UnmatchedParenthesis(Exception):
"""
Indicate when a student inputs a formula with mismatched parentheses.
"""
pass
def lower_dict(input_dict):
"""
Convert all keys in a dictionary to lowercase; keep their original values.
......@@ -249,6 +256,7 @@ def evaluator(variables, functions, math_expr, case_sensitive=False):
return float('nan')
# Parse the tree.
check_parens(math_expr)
math_interpreter = ParseAugmenter(math_expr, case_sensitive)
math_interpreter.parse_algebra()
......@@ -278,6 +286,30 @@ def evaluator(variables, functions, math_expr, case_sensitive=False):
return math_interpreter.reduce_tree(evaluate_actions)
def check_parens(formula):
"""
Check that any open parentheses are closed
Otherwise, raise an UnmatchedParenthesis exception
"""
count = 0
delta = {
'(': +1,
')': -1
}
for index, char in enumerate(formula):
if char in delta:
count += delta[char]
if count < 0:
msg = "Invalid Input: A closing parenthesis was found after segment " + \
"{}, but there is no matching opening parenthesis before it."
raise UnmatchedParenthesis(msg.format(formula[0:index]))
if count > 0:
msg = "Invalid Input: Parentheses are unmatched. " + \
"{} parentheses were opened but never closed."
raise UnmatchedParenthesis(msg.format(count))
class ParseAugmenter(object):
"""
Holds the data for a particular parse.
......
......@@ -554,3 +554,12 @@ class EvaluatorTest(unittest.TestCase):
calc.evaluator({'r1': 5}, {}, "r1+r2")
with self.assertRaisesRegexp(calc.UndefinedVariable, 'r1 r3'):
calc.evaluator(variables, {}, "r1*r3", case_sensitive=True)
def test_mismatched_parens(self):
"""
Check to see if the evaluator catches mismatched parens
"""
with self.assertRaisesRegexp(calc.UnmatchedParenthesis, 'opened but never closed'):
calc.evaluator({}, {}, "(1+2")
with self.assertRaisesRegexp(calc.UnmatchedParenthesis, 'no matching opening parenthesis'):
calc.evaluator({}, {}, "(1+2))")
......@@ -40,7 +40,7 @@ import capa.safe_exec as safe_exec
import capa.xqueue_interface as xqueue_interface
import dogstats_wrapper as dog_stats_api
# specific library imports
from calc import UndefinedVariable, evaluator
from calc import UndefinedVariable, UnmatchedParenthesis, evaluator
from cmath import isnan
from openedx.core.djangolib.markup import HTML, Text
......@@ -1604,6 +1604,10 @@ class NumericalResponse(LoncapaResponse):
bad_variables=text_type(undef_var),
)
)
except UnmatchedParenthesis as err:
raise StudentInputError(
err.args[0]
)
except ValueError as val_err:
if 'factorial' in text_type(val_err):
# This is thrown when fact() or factorial() is used in an answer
......@@ -1770,7 +1774,7 @@ class NumericalResponse(LoncapaResponse):
try:
evaluator(dict(), dict(), answer)
return True
except (StudentInputError, UndefinedVariable):
except (StudentInputError, UndefinedVariable, UnmatchedParenthesis):
return False
def get_answers(self):
......@@ -3108,6 +3112,14 @@ class FormulaResponse(LoncapaResponse):
raise StudentInputError(
_("Invalid input: {bad_input} not permitted in answer.").format(bad_input=text_type(err))
)
except UnmatchedParenthesis as err:
log.debug(
'formularesponse: unmatched parenthesis in formula=%s',
cgi.escape(answer)
)
raise StudentInputError(
err.args[0]
)
except ValueError as err:
if 'factorial' in text_type(err):
# This is thrown when fact() or factorial() is used in a formularesponse answer
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment