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]

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