Mercurial > p > roundup > code
comparison 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 |
comparison
equal
deleted
inserted
replaced
| 8240:1189c742e4b3 | 8241:741ea8a86012 |
|---|---|
| 5 | 5 |
| 6 # This module is Free Software under the Roundup licensing, | 6 # This module is Free Software under the Roundup licensing, |
| 7 # see the COPYING.txt file coming with Roundup. | 7 # see the COPYING.txt file coming with Roundup. |
| 8 # | 8 # |
| 9 | 9 |
| 10 from roundup.exceptions import RoundupException | |
| 11 from roundup.i18n import _ | |
| 12 | |
| 13 opcode_names = { | |
| 14 -2: "not", | |
| 15 -3: "and", | |
| 16 -4: "or", | |
| 17 } | |
| 18 | |
| 19 | |
| 20 class ExpressionError(RoundupException): | |
| 21 """Takes two arguments. | |
| 22 | |
| 23 ExpressionError(template, context={}) | |
| 24 | |
| 25 The repr of ExpressionError is: | |
| 26 | |
| 27 template % context | |
| 28 | |
| 29 """ | |
| 30 | |
| 31 # only works on python 3 | |
| 32 #def __init__(self, *args, context=None): | |
| 33 # super().__init__(*args) | |
| 34 # self.context = context if isinstance(context, dict) else {} | |
| 35 | |
| 36 # works python 2 and 3 | |
| 37 def __init__(self, *args, **kwargs): | |
| 38 super(RoundupException, self).__init__(*args) | |
| 39 self.context = {} | |
| 40 if 'context' in kwargs and isinstance(kwargs['context'], dict): | |
| 41 self.context = kwargs['context'] | |
| 42 | |
| 43 # Skip testing for a bad call to ExpressionError | |
| 44 # keywords = [x for x in list(kwargs) if x != "context"] | |
| 45 #if len(keywords) != 0: | |
| 46 # raise ValueError("unknown keyword argument(s) passed to ExpressionError: %s" % keywords) | |
| 47 | |
| 48 def __str__(self): | |
| 49 try: | |
| 50 return self.args[0] % self.context | |
| 51 except KeyError: | |
| 52 return "%s: context=%s" % (self.args[0], self.context) | |
| 53 | |
| 54 def __repr__(self): | |
| 55 try: | |
| 56 return self.args[0] % self.context | |
| 57 except KeyError: | |
| 58 return "%s: context=%s" % (self.args[0], self.context) | |
| 59 | |
| 60 | |
| 10 class Binary: | 61 class Binary: |
| 11 | 62 |
| 12 def __init__(self, x, y): | 63 def __init__(self, x, y): |
| 13 self.x = x | 64 self.x = x |
| 14 self.y = y | 65 self.y = y |
| 36 return self.x in v | 87 return self.x in v |
| 37 | 88 |
| 38 def visit(self, visitor): | 89 def visit(self, visitor): |
| 39 visitor(self) | 90 visitor(self) |
| 40 | 91 |
| 92 def __repr__(self): | |
| 93 return "Value %s" % self.x | |
| 94 | |
| 41 | 95 |
| 42 class Empty(Unary): | 96 class Empty(Unary): |
| 43 | 97 |
| 44 def evaluate(self, v): | 98 def evaluate(self, v): |
| 45 return not v | 99 return not v |
| 46 | 100 |
| 47 def visit(self, visitor): | 101 def visit(self, visitor): |
| 48 visitor(self) | 102 visitor(self) |
| 49 | 103 |
| 104 def __repr__(self): | |
| 105 return "ISEMPTY(-1)" | |
| 106 | |
| 50 | 107 |
| 51 class Not(Unary): | 108 class Not(Unary): |
| 52 | 109 |
| 53 def evaluate(self, v): | 110 def evaluate(self, v): |
| 54 return not self.x.evaluate(v) | 111 return not self.x.evaluate(v) |
| 55 | 112 |
| 56 def generate(self, atom): | 113 def generate(self, atom): |
| 57 return "NOT(%s)" % self.x.generate(atom) | 114 return "NOT(%s)" % self.x.generate(atom) |
| 115 | |
| 116 def __repr__(self): | |
| 117 return "NOT(%s)" % self.x | |
| 58 | 118 |
| 59 | 119 |
| 60 class Or(Binary): | 120 class Or(Binary): |
| 61 | 121 |
| 62 def evaluate(self, v): | 122 def evaluate(self, v): |
| 65 def generate(self, atom): | 125 def generate(self, atom): |
| 66 return "(%s)OR(%s)" % ( | 126 return "(%s)OR(%s)" % ( |
| 67 self.x.generate(atom), | 127 self.x.generate(atom), |
| 68 self.y.generate(atom)) | 128 self.y.generate(atom)) |
| 69 | 129 |
| 130 def __repr__(self): | |
| 131 return "(%s OR %s)" % (self.y, self.x) | |
| 132 | |
| 70 | 133 |
| 71 class And(Binary): | 134 class And(Binary): |
| 72 | 135 |
| 73 def evaluate(self, v): | 136 def evaluate(self, v): |
| 74 return self.x.evaluate(v) and self.y.evaluate(v) | 137 return self.x.evaluate(v) and self.y.evaluate(v) |
| 76 def generate(self, atom): | 139 def generate(self, atom): |
| 77 return "(%s)AND(%s)" % ( | 140 return "(%s)AND(%s)" % ( |
| 78 self.x.generate(atom), | 141 self.x.generate(atom), |
| 79 self.y.generate(atom)) | 142 self.y.generate(atom)) |
| 80 | 143 |
| 144 def __repr__(self): | |
| 145 return "(%s AND %s)" % (self.y, self.x) | |
| 146 | |
| 81 | 147 |
| 82 def compile_expression(opcodes): | 148 def compile_expression(opcodes): |
| 83 | 149 |
| 84 stack = [] | 150 stack = [] |
| 85 push, pop = stack.append, stack.pop | 151 push, pop = stack.append, stack.pop |
| 86 for opcode in opcodes: | 152 try: |
| 87 if opcode == -1: push(Empty(opcode)) # noqa: E271,E701 | 153 for position, opcode in enumerate(opcodes): # noqa: B007 |
| 88 elif opcode == -2: push(Not(pop())) # noqa: E701 | 154 if opcode == -1: push(Empty(opcode)) # noqa: E271,E701 |
| 89 elif opcode == -3: push(And(pop(), pop())) # noqa: E701 | 155 elif opcode == -2: push(Not(pop())) # noqa: E701 |
| 90 elif opcode == -4: push(Or(pop(), pop())) # noqa: E701 | 156 elif opcode == -3: push(And(pop(), pop())) # noqa: E701 |
| 91 else: push(Equals(opcode)) # noqa: E701 | 157 elif opcode == -4: push(Or(pop(), pop())) # noqa: E701 |
| 158 else: push(Equals(opcode)) # noqa: E701 | |
| 159 except IndexError: | |
| 160 raise ExpressionError( | |
| 161 _("There was an error searching %(class)s by %(attr)s using: " | |
| 162 "%(opcodes)s. " | |
| 163 "The operator %(opcode)s (%(opcodename)s) at position " | |
| 164 "%(position)d has too few arguments."), | |
| 165 context={ | |
| 166 "opcode": opcode, | |
| 167 "opcodename": opcode_names[opcode], | |
| 168 "position": position + 1, | |
| 169 "opcodes": opcodes, | |
| 170 }) | |
| 171 if len(stack) != 1: | |
| 172 # Too many arguments - I don't think stack can be zero length | |
| 173 raise ExpressionError( | |
| 174 _("There was an error searching %(class)s by %(attr)s using: " | |
| 175 "%(opcodes)s. " | |
| 176 "There are too many arguments for the existing operators. The " | |
| 177 "values on the stack are: %(stack)s"), | |
| 178 context={ | |
| 179 "opcodes": opcodes, | |
| 180 "stack": stack, | |
| 181 }) | |
| 92 | 182 |
| 93 return pop() | 183 return pop() |
| 94 | 184 |
| 95 | 185 |
| 96 class Expression: | 186 class Expression: |
| 102 raise ValueError() | 192 raise ValueError() |
| 103 | 193 |
| 104 compiled = compile_expression(opcodes) | 194 compiled = compile_expression(opcodes) |
| 105 if is_link: | 195 if is_link: |
| 106 self.evaluate = lambda x: compiled.evaluate( | 196 self.evaluate = lambda x: compiled.evaluate( |
| 107 x and [int(x)] or []) | 197 (x and [int(x)]) or []) |
| 108 else: | 198 else: |
| 109 self.evaluate = lambda x: compiled.evaluate([int(y) for y in x]) | 199 self.evaluate = lambda x: compiled.evaluate([int(y) for y in x]) |
| 110 except (ValueError, TypeError): | 200 except (ValueError, TypeError): |
| 111 if is_link: | 201 if is_link: |
| 112 v = [None if x == '-1' else x for x in v] | 202 v = [None if x == '-1' else x for x in v] |
