diff roundup/mlink_expr.py @ 8241:741ea8a86012

fix: issue2551374. Error handling for filter expressions. Errors in filter expressions are now reported. The UI needs some work but even the current code is helpful when debugging filter expressions. mlink_expr: defines/raises ExpressionError(error string template, context=dict()) raises ExpressionError when it detects errors when popping arguments off stack raises ExpressionError when more than one element left on the stack before returning also ruff fix to group boolean expression with parens back_anydbm.py, rdbms_common.py: catches ExpressionError, augments context with class and attribute being searched. raises the exception for both link and multilink relations client.py catches ExpressionError returning a basic error page. The page is a dead end. There are no links or anything for the user to move forward. The user has to go back, possibly refresh the page (because the submit button may be disalbled) re-enter the query and try again. This needs to be improved. test_liveserver.py test the error page generated by client.py db_test_base unit tests for filter with too few arguments, too many arguments, check all repr and str formats.
author John Rouillard <rouilj@ieee.org>
date Mon, 30 Dec 2024 20:22:55 -0500
parents 87af08c75695
children 224ccb8b49ca
line wrap: on
line diff
--- a/roundup/mlink_expr.py	Mon Dec 30 02:59:27 2024 -0500
+++ b/roundup/mlink_expr.py	Mon Dec 30 20:22:55 2024 -0500
@@ -7,6 +7,57 @@
 # see the COPYING.txt file coming with Roundup.
 #
 
+from roundup.exceptions import RoundupException
+from roundup.i18n import _
+
+opcode_names = {
+    -2: "not",
+    -3: "and",
+    -4: "or",
+}
+
+
+class ExpressionError(RoundupException):
+    """Takes two arguments.
+
+        ExpressionError(template, context={})
+
+    The repr of ExpressionError is:
+
+        template % context
+
+    """
+
+    # only works on python 3
+    #def __init__(self, *args, context=None):
+    #    super().__init__(*args)
+    #    self.context = context if isinstance(context, dict) else {}
+
+    # works python 2 and 3
+    def __init__(self, *args, **kwargs):
+        super(RoundupException, self).__init__(*args)
+        self.context = {}
+        if 'context' in kwargs and isinstance(kwargs['context'], dict):
+            self.context = kwargs['context']
+
+        # Skip testing for a bad call to ExpressionError
+        # keywords = [x for x in list(kwargs) if x != "context"]
+        #if len(keywords) != 0:
+        #        raise ValueError("unknown keyword argument(s) passed to ExpressionError: %s" % keywords)
+
+    def __str__(self):
+        try:
+            return self.args[0] % self.context
+        except KeyError:
+            return "%s: context=%s" % (self.args[0], self.context)
+
+    def __repr__(self):
+        try:
+            return self.args[0] % self.context
+        except KeyError:
+            return "%s: context=%s" % (self.args[0], self.context)
+
+
 class Binary:
 
     def __init__(self, x, y):
@@ -38,6 +89,9 @@
     def visit(self, visitor):
         visitor(self)
 
+    def __repr__(self):
+        return "Value %s" % self.x
+
 
 class Empty(Unary):
 
@@ -47,6 +101,9 @@
     def visit(self, visitor):
         visitor(self)
 
+    def __repr__(self):
+        return "ISEMPTY(-1)"
+
 
 class Not(Unary):
 
@@ -56,6 +113,9 @@
     def generate(self, atom):
         return "NOT(%s)" % self.x.generate(atom)
 
+    def __repr__(self):
+        return "NOT(%s)" % self.x
+
 
 class Or(Binary):
 
@@ -67,6 +127,9 @@
             self.x.generate(atom),
             self.y.generate(atom))
 
+    def __repr__(self):
+        return "(%s OR %s)" % (self.y, self.x)
+
 
 class And(Binary):
 
@@ -78,17 +141,44 @@
             self.x.generate(atom),
             self.y.generate(atom))
 
+    def __repr__(self):
+        return "(%s AND %s)" % (self.y, self.x)
+
 
 def compile_expression(opcodes):
 
     stack = []
     push, pop = stack.append, stack.pop
-    for opcode in opcodes:
-        if   opcode == -1: push(Empty(opcode))      # noqa: E271,E701
-        elif opcode == -2: push(Not(pop()))         # noqa: E701
-        elif opcode == -3: push(And(pop(), pop()))  # noqa: E701
-        elif opcode == -4: push(Or(pop(), pop()))   # noqa: E701
-        else:              push(Equals(opcode))     # noqa: E701
+    try:
+        for position, opcode in enumerate(opcodes):     # noqa: B007
+            if   opcode == -1: push(Empty(opcode))      # noqa: E271,E701
+            elif opcode == -2: push(Not(pop()))         # noqa: E701
+            elif opcode == -3: push(And(pop(), pop()))  # noqa: E701
+            elif opcode == -4: push(Or(pop(), pop()))   # noqa: E701
+            else:              push(Equals(opcode))     # noqa: E701
+    except IndexError:
+        raise ExpressionError(
+            _("There was an error searching %(class)s by %(attr)s using: "
+              "%(opcodes)s. "
+              "The operator %(opcode)s (%(opcodename)s) at position "
+              "%(position)d has too few arguments."),
+            context={
+                "opcode": opcode,
+                "opcodename": opcode_names[opcode],
+                "position": position + 1,
+                "opcodes": opcodes,
+            })
+    if len(stack) != 1:
+        # Too many arguments - I don't think stack can be zero length
+        raise ExpressionError(
+            _("There was an error searching %(class)s by %(attr)s using: "
+              "%(opcodes)s. "
+              "There are too many arguments for the existing operators. The "
+              "values on the stack are: %(stack)s"),
+            context={
+                "opcodes": opcodes,
+                "stack": stack,
+            })
 
     return pop()
 
@@ -104,7 +194,7 @@
             compiled = compile_expression(opcodes)
             if is_link:
                 self.evaluate = lambda x: compiled.evaluate(
-                    x and [int(x)] or [])
+                    (x and [int(x)]) or [])
             else:
                 self.evaluate = lambda x: compiled.evaluate([int(y) for y in x])
         except (ValueError, TypeError):

Roundup Issue Tracker: http://roundup-tracker.org/