From ed90c80ccab59fbbf1ae2d00b0844ade79f0d70d Mon Sep 17 00:00:00 2001 From: Peter Pinch Date: Wed, 31 Jan 2018 11:18:59 -0500 Subject: [PATCH] catch mismatched parens --- common/lib/calc/calc/calc.py | 32 +++++++++++++++++++++++++ common/lib/calc/calc/tests/test_calc.py | 9 +++++++ common/lib/capa/capa/responsetypes.py | 16 +++++++++++-- 3 files changed, 55 insertions(+), 2 deletions(-) diff --git a/common/lib/calc/calc/calc.py b/common/lib/calc/calc/calc.py index 9024b9fc6d..fc45f107bc 100644 --- a/common/lib/calc/calc/calc.py +++ b/common/lib/calc/calc/calc.py @@ -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. diff --git a/common/lib/calc/calc/tests/test_calc.py b/common/lib/calc/calc/tests/test_calc.py index 2519bfc171..d8da18c93f 100644 --- a/common/lib/calc/calc/tests/test_calc.py +++ b/common/lib/calc/calc/tests/test_calc.py @@ -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))") diff --git a/common/lib/capa/capa/responsetypes.py b/common/lib/capa/capa/responsetypes.py index 993b8dc0f2..4a5ce19692 100644 --- a/common/lib/capa/capa/responsetypes.py +++ b/common/lib/capa/capa/responsetypes.py @@ -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