Skip to content

Commit 2116ba4

Browse files
committed
Make rev multilink expressions work for anydbm
1 parent 27c5bc8 commit 2116ba4

File tree

5 files changed

+147
-113
lines changed

5 files changed

+147
-113
lines changed

roundup/backends/back_anydbm.py

Lines changed: 1 addition & 95 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
from roundup.anypy.strings import b2s, bs2b, repr_export, eval_import, is_us
2929

3030
from roundup import hyperdb, date, password, roundupdb, security, support
31+
from roundup.mlink_expr import Expression
3132
from roundup.backends import locking
3233
from roundup.i18n import _
3334

@@ -48,101 +49,6 @@ def db_exists(config):
4849
def db_nuke(config):
4950
shutil.rmtree(config.DATABASE)
5051

51-
class Binary:
52-
53-
def __init__(self, x, y):
54-
self.x = x
55-
self.y = y
56-
57-
def visit(self, visitor):
58-
self.x.visit(visitor)
59-
self.y.visit(visitor)
60-
61-
class Unary:
62-
63-
def __init__(self, x):
64-
self.x = x
65-
66-
def generate(self, atom):
67-
return atom(self)
68-
69-
def visit(self, visitor):
70-
self.x.visit(visitor)
71-
72-
class Equals(Unary):
73-
74-
def evaluate(self, v):
75-
return self.x in v
76-
77-
def visit(self, visitor):
78-
visitor(self)
79-
80-
class Empty(Unary):
81-
82-
def evaluate(self, v):
83-
return not v
84-
85-
def visit(self, visitor):
86-
visitor(self)
87-
88-
class Not(Unary):
89-
90-
def evaluate(self, v):
91-
return not self.x.evaluate(v)
92-
93-
def generate(self, atom):
94-
return "NOT(%s)" % self.x.generate(atom)
95-
96-
class Or(Binary):
97-
98-
def evaluate(self, v):
99-
return self.x.evaluate(v) or self.y.evaluate(v)
100-
101-
def generate(self, atom):
102-
return "(%s)OR(%s)" % (
103-
self.x.generate(atom),
104-
self.y.generate(atom))
105-
106-
class And(Binary):
107-
108-
def evaluate(self, v):
109-
return self.x.evaluate(v) and self.y.evaluate(v)
110-
111-
def generate(self, atom):
112-
return "(%s)AND(%s)" % (
113-
self.x.generate(atom),
114-
self.y.generate(atom))
115-
116-
def compile_expression(opcodes):
117-
118-
stack = []
119-
push, pop = stack.append, stack.pop
120-
for opcode in opcodes:
121-
if opcode == -1: push(Empty(opcode))
122-
elif opcode == -2: push(Not(pop()))
123-
elif opcode == -3: push(And(pop(), pop()))
124-
elif opcode == -4: push(Or(pop(), pop()))
125-
else: push(Equals(opcode))
126-
127-
return pop()
128-
129-
class Expression:
130-
131-
def __init__(self, v):
132-
try:
133-
opcodes = [int(x) for x in v]
134-
if min(opcodes) >= -1:
135-
raise ValueError()
136-
137-
compiled = compile_expression(opcodes)
138-
self.evaluate = lambda x: compiled.evaluate([int(y) for y in x])
139-
except:
140-
if '-1' in v:
141-
v = [x for x in v if int(x) > 0]
142-
self.evaluate = lambda x: bool(set(x) & set(v)) or not x
143-
else:
144-
self.evaluate = lambda x: bool(set(x) & set(v))
145-
14652
#
14753
# Now the database
14854
#

roundup/backends/rdbms_common.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@
6666
from roundup.backends.sessions_rdbms import Sessions, OneTimeKeys
6767
from roundup.date import Range
6868

69-
from roundup.backends.back_anydbm import compile_expression
69+
from roundup.mlink_expr import compile_expression
7070
from roundup.anypy.strings import b2s, bs2b, us2s, repr_export, eval_import
7171

7272
from hashlib import md5

roundup/hyperdb.py

Lines changed: 42 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
# roundup modules
2929
from . import date, password
3030
from .support import ensureParentsExist, PrioList
31+
from roundup.mlink_expr import Expression
3132
from roundup.i18n import _
3233
from roundup.cgi.exceptions import DetectorError
3334
from roundup.anypy.cmp_ import NoneAndDictComparable
@@ -590,24 +591,50 @@ def search(self, search_matches=None, sort=True, retired=False):
590591
cl = p.propclass.rev_property.cls
591592
if not isinstance(p.val, type([])):
592593
p.val = [p.val]
593-
if p.val == ['-1'] :
594-
s1 = set(self.cls.getnodeids(retired=False))
595-
s2 = set()
594+
nval = [int(i) for i in p.val]
595+
pval = [str(i) for i in nval if i >= 0]
596+
items = set()
597+
if not nval or min(nval) >= -1:
598+
if -1 in nval:
599+
s1 = set(self.cls.getnodeids(retired=False))
600+
s2 = set()
601+
for id in cl.getnodeids(retired=False):
602+
node = cl.getnode(id)
603+
if node[pn]:
604+
if isinstance(node[pn], type([])):
605+
s2.update(node[pn])
606+
else:
607+
s2.add(node[pn])
608+
items |= s1.difference(s2)
609+
if isinstance(p.propclass.rev_property, Link):
610+
items |= set(cl.get(x, pn) for x in pval
611+
if not cl.is_retired(x))
612+
else:
613+
items |= set().union(*(cl.get(x, pn) for x in pval
614+
if not cl.is_retired(x)))
615+
else:
616+
# Expression: materialize rev multilinks and run
617+
# expression on them
618+
expr = Expression(nval)
619+
by_id = {}
596620
for id in cl.getnodeids(retired=False):
621+
by_id[id] = set()
622+
items = set()
623+
for id in self.cls.getnodeids(retired=False):
597624
node = cl.getnode(id)
598625
if node[pn]:
599-
if isinstance(node [pn], type([])):
600-
s2.update(node [pn])
601-
else:
602-
s2.add(node [pn])
603-
items = s1.difference(s2)
604-
elif isinstance(p.propclass.rev_property, Link):
605-
items = set(cl.get(x, pn) for x in p.val
606-
if not cl.is_retired(x))
607-
else:
608-
items = set().union(*(cl.get(x, pn) for x in p.val
609-
if not cl.is_retired(x)))
610-
filterspec[p.name] = list(sorted(items))
626+
v = node[pn]
627+
if not isinstance(v, type([])):
628+
v = [v]
629+
for x in v:
630+
if x not in by_id:
631+
by_id[x] = set()
632+
by_id[x].add(id)
633+
for k in by_id:
634+
if expr.evaluate(by_id[k]):
635+
items.add(k)
636+
637+
filterspec[p.name] = list(sorted(items, key=int))
611638
elif isinstance(p.val, type([])):
612639
exact = []
613640
subst = []

roundup/mlink_expr.py

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
#
2+
# Copyright: 2010 Intevation GmbH.
3+
# 2021 Ralf Schlatterbeck, [email protected].
4+
#
5+
6+
# This module is Free Software under the Roundup licensing,
7+
# see the COPYING.txt file coming with Roundup.
8+
#
9+
10+
class Binary:
11+
12+
def __init__(self, x, y):
13+
self.x = x
14+
self.y = y
15+
16+
def visit(self, visitor):
17+
self.x.visit(visitor)
18+
self.y.visit(visitor)
19+
20+
class Unary:
21+
22+
def __init__(self, x):
23+
self.x = x
24+
25+
def generate(self, atom):
26+
return atom(self)
27+
28+
def visit(self, visitor):
29+
self.x.visit(visitor)
30+
31+
class Equals(Unary):
32+
33+
def evaluate(self, v):
34+
return self.x in v
35+
36+
def visit(self, visitor):
37+
visitor(self)
38+
39+
class Empty(Unary):
40+
41+
def evaluate(self, v):
42+
return not v
43+
44+
def visit(self, visitor):
45+
visitor(self)
46+
47+
class Not(Unary):
48+
49+
def evaluate(self, v):
50+
return not self.x.evaluate(v)
51+
52+
def generate(self, atom):
53+
return "NOT(%s)" % self.x.generate(atom)
54+
55+
class Or(Binary):
56+
57+
def evaluate(self, v):
58+
return self.x.evaluate(v) or self.y.evaluate(v)
59+
60+
def generate(self, atom):
61+
return "(%s)OR(%s)" % (
62+
self.x.generate(atom),
63+
self.y.generate(atom))
64+
65+
class And(Binary):
66+
67+
def evaluate(self, v):
68+
return self.x.evaluate(v) and self.y.evaluate(v)
69+
70+
def generate(self, atom):
71+
return "(%s)AND(%s)" % (
72+
self.x.generate(atom),
73+
self.y.generate(atom))
74+
75+
def compile_expression(opcodes):
76+
77+
stack = []
78+
push, pop = stack.append, stack.pop
79+
for opcode in opcodes:
80+
if opcode == -1: push(Empty(opcode))
81+
elif opcode == -2: push(Not(pop()))
82+
elif opcode == -3: push(And(pop(), pop()))
83+
elif opcode == -4: push(Or(pop(), pop()))
84+
else: push(Equals(opcode))
85+
86+
return pop()
87+
88+
class Expression:
89+
90+
def __init__(self, v):
91+
try:
92+
opcodes = [int(x) for x in v]
93+
if min(opcodes) >= -1:
94+
raise ValueError()
95+
96+
compiled = compile_expression(opcodes)
97+
self.evaluate = lambda x: compiled.evaluate([int(y) for y in x])
98+
except:
99+
if '-1' in v:
100+
v = [x for x in v if int(x) > 0]
101+
self.evaluate = lambda x: bool(set(x) & set(v)) or not x
102+
else:
103+
self.evaluate = lambda x: bool(set(x) & set(v))

test/db_test_base.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2038,8 +2038,6 @@ def ls(x):
20382038
self.assertEqual(ls(self.db.user.get('4', ni)), ['1'])
20392039
self.assertEqual(ls(self.db.user.get('5', ni)), ['7'])
20402040

2041-
# Currently only works for sql databases
2042-
@pytest.mark.xfail
20432041
def testFilteringRevMultilinkExpression(self):
20442042
ae, iiter = self.filteringSetupTransitiveSearch('user')
20452043
ni = 'nosy_issues'

0 commit comments

Comments
 (0)