Merge lp:~thisfred/u1db/combine-mapping-1 into lp:u1db

Proposed by Eric Casteleijn
Status: Rejected
Rejected by: Eric Casteleijn
Proposed branch: lp:~thisfred/u1db/combine-mapping-1
Merge into: lp:u1db
Diff against target: 286 lines (+168/-34)
4 files modified
CMakeLists.txt (+5/-0)
u1db/query_parser.py (+94/-34)
u1db/tests/test_backends.py (+28/-0)
u1db/tests/test_query_parser.py (+41/-0)
To merge this branch: bzr merge lp:~thisfred/u1db/combine-mapping-1
Reviewer Review Type Date Requested Status
Ubuntu One hackers Pending
Review via email: mp+115433@code.launchpad.net

Commit message

Changed the Python query parser to be a little more versatile. It now allows for arbitrary numbers and types of arguments, which combine() will need.

Description of the change

Changed the Python query parser to be a little more versatile. It now allows for arbitrary numbers and types of arguments, which combine() will need.

Next step: do the same for C, which will definitely be a more interesting challenge.

To post a comment you must log in.
lp:~thisfred/u1db/combine-mapping-1 updated
356. By Eric Casteleijn

better error msges

Revision history for this message
Eric Casteleijn (thisfred) wrote :

Rejecting this, in favor of newer branch.

Unmerged revisions

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'CMakeLists.txt'
2--- CMakeLists.txt 2012-07-16 16:20:53 +0000
3+++ CMakeLists.txt 2012-07-17 20:51:27 +0000
4@@ -44,6 +44,11 @@
5 COMMAND cd html-docs && make doctest && cd ..
6 )
7
8+add_custom_target(check-nodoc
9+ COMMAND python -m testtools.run discover
10+ DEPENDS build-inplace
11+)
12+
13 add_custom_target(check
14 COMMAND python -m testtools.run discover
15 DEPENDS build-inplace doctests
16
17=== modified file 'u1db/query_parser.py'
18--- u1db/query_parser.py 2012-05-15 16:23:19 +0000
19+++ u1db/query_parser.py 2012-07-17 20:51:27 +0000
20@@ -21,9 +21,66 @@
21 )
22
23
24+def make_subtree(expression, start, idx, open_parens):
25+ tree = []
26+ while idx < len(expression):
27+ char = expression[idx]
28+ if char == '(':
29+ open_parens.append(1)
30+ term = expression[start:idx].strip()
31+ if term:
32+ tree.append(term)
33+ else:
34+ raise errors.IndexDefinitionParseError(
35+ "Missing operator name in: \n%s\n%s^" %
36+ (expression, " " * idx))
37+ idx += 1
38+ start = idx
39+ idx, start, subtree = make_subtree(
40+ expression, start, idx, open_parens)
41+ tree.append(subtree)
42+ elif char == ')':
43+ try:
44+ open_parens.pop()
45+ except IndexError:
46+ raise errors.IndexDefinitionParseError(
47+ "Encountered ')' before '(' when parsing:\n%s\n%s^" %
48+ (expression, " " * idx))
49+ term = expression[start:idx].strip()
50+ if term:
51+ tree.append(term)
52+ idx += 1
53+ start = idx
54+ return idx, start, tree
55+ elif char == ',':
56+ term = expression[start:idx].strip()
57+ if term:
58+ tree.append(term)
59+ idx += 1
60+ start = idx
61+ else:
62+ idx += 1
63+ if start < len(expression):
64+ tree.append(expression[start:])
65+ return idx, start, tree
66+
67+
68+def make_tree(expression):
69+ open_parens = []
70+ tree = make_subtree(expression, 0, 0, open_parens)[2]
71+ if open_parens:
72+ raise errors.IndexDefinitionParseError(
73+ "%d missing ')'s when parsing '%s'." %
74+ (len(open_parens), expression))
75+ return tree
76+
77+
78 class Getter(object):
79 """Get values from a document based on a specification."""
80
81+ arity = 1
82+ args = ['expr']
83+
84 def get(self, raw_doc):
85 """Get a value from the document.
86
87@@ -93,12 +150,11 @@
88 """A transformation on a value from another Getter."""
89
90 name = None
91- """The name that the transform has in a query string."""
92
93 def __init__(self, inner):
94 """Create a transformation.
95
96- :param inner: the Getter to transform the value for.
97+ :param inner: the argument(s) to the transformation.
98 """
99 self.inner = inner
100
101@@ -148,6 +204,8 @@
102 """
103
104 name = 'number'
105+ arity = 2
106+ args = ['expr', int]
107
108 def __init__(self, inner, number):
109 super(Number, self).__init__(inner)
110@@ -230,44 +288,46 @@
111 return partial, ''
112
113 def parse(self, field):
114- inner = self._inner_parse(field)
115+ tree = make_tree(field)
116+ inner = self._inner_parse(tree)
117 return inner
118
119- def _inner_parse(self, field):
120- word, field = self._take_word(field)
121- if field.startswith("("):
122+ def _inner_parse(self, tree):
123+ if len(tree) > 1:
124 # We have an operation
125- if not field.endswith(")"):
126- raise errors.IndexDefinitionParseError(
127- "Invalid transformation function: %s" % field)
128- op = self._transformations.get(word, None)
129+ op_name = tree[0]
130+ args = tree[1]
131+ op = self._transformations.get(op_name, None)
132 if op is None:
133 raise errors.IndexDefinitionParseError(
134- "Unknown operation: %s" % word)
135- if ',' in field:
136- # XXX: The arguments should probably be cast to whatever types
137- # they represent, but short of evaling them, I don't see an
138- # easy way to do that without adding a lot of complexity.
139- # Since there is only one operation with an extra argument, I'm
140- # punting on this until we grow some more.
141- args = [a.strip() for a in field[1:-1].split(',')]
142- extracted = args[0]
143- else:
144- args = []
145- extracted = field[1:-1]
146- inner = self._inner_parse(extracted)
147- return op(inner, *args[1:])
148+ "Unknown operation: %s" % op_name)
149+ if op.arity >= 0 and len(args) != op.arity:
150+ raise errors.IndexDefinitionParseError(
151+ "Invalid number of arguments for transformation function:"
152+ " %s, %r" % (op_name, args))
153+ parsed = []
154+ for i, arg in enumerate(args):
155+ arg_type = op.args[i % len(op.args)]
156+ if arg_type == 'expr':
157+ inner = self._inner_parse([arg])
158+ else:
159+ try:
160+ inner = arg_type(arg)
161+ except ValueError, e:
162+ raise errors.IndexDefinitionParseError(
163+ "Invalid value %r for argument type %r (%r)." %
164+ (arg, arg_type, e))
165+ parsed.append(inner)
166+ return op(*parsed)
167 else:
168- if len(field) != 0:
169- raise errors.IndexDefinitionParseError(
170- "Unhandled characters: %s" % (field,))
171- if len(word) == 0:
172- raise errors.IndexDefinitionParseError(
173- "Missing field specifier")
174- if word.endswith("."):
175- raise errors.IndexDefinitionParseError(
176- "Invalid field specifier: %s" % word)
177- return ExtractField(word)
178+ if len(tree) == 0:
179+ raise errors.IndexDefinitionParseError(
180+ "Expected fieldname or expression.")
181+ fieldname = tree[0]
182+ if fieldname.endswith("."):
183+ raise errors.IndexDefinitionParseError(
184+ "Invalid field specifier: %s" % fieldname)
185+ return ExtractField(fieldname)
186
187 def parse_all(self, fields):
188 return [self.parse(field) for field in fields]
189
190=== modified file 'u1db/tests/test_backends.py'
191--- u1db/tests/test_backends.py 2012-07-13 22:09:11 +0000
192+++ u1db/tests/test_backends.py 2012-07-17 20:51:27 +0000
193@@ -1076,6 +1076,34 @@
194 self.assertEqual(
195 [doc], self.db.get_from_index('test-idx', 'value', 'value2'))
196
197+ def test_get_from_index_multi_list(self):
198+ doc = self.db.create_doc(
199+ '{"key": "value", "key2": ["value2-1", "value2-2", "value2-3"]}')
200+ self.db.create_index('test-idx', 'key', 'key2')
201+ self.assertEqual(
202+ [doc], self.db.get_from_index('test-idx', 'value', 'value2-1'))
203+ self.assertEqual(
204+ [doc], self.db.get_from_index('test-idx', 'value', 'value2-2'))
205+ self.assertEqual(
206+ [doc], self.db.get_from_index('test-idx', 'value', 'value2-3'))
207+ self.assertEqual(
208+ [('value', 'value2-1'), ('value', 'value2-2'),
209+ ('value', 'value2-3')],
210+ sorted(self.db.get_index_keys('test-idx')))
211+
212+ def test_get_index_keys_multi_list_list(self):
213+ self.db.create_doc(
214+ '{"key": "value1-1 value1-2 value1-3", '
215+ '"key2": ["value2-1", "value2-2", "value2-3"]}')
216+ self.db.create_index('test-idx', 'split_words(key)', 'key2')
217+ self.assertEqual(
218+ [(u'value1-1', u'value2-1'), (u'value1-1', u'value2-2'),
219+ (u'value1-1', u'value2-3'), (u'value1-2', u'value2-1'),
220+ (u'value1-2', u'value2-2'), (u'value1-2', u'value2-3'),
221+ (u'value1-3', u'value2-1'), (u'value1-3', u'value2-2'),
222+ (u'value1-3', u'value2-3')],
223+ sorted(self.db.get_index_keys('test-idx')))
224+
225 def test_get_from_index_multi_ordered(self):
226 doc1 = self.db.create_doc('{"key": "value3", "key2": "value4"}')
227 doc2 = self.db.create_doc('{"key": "value2", "key2": "value3"}')
228
229=== modified file 'u1db/tests/test_query_parser.py'
230--- u1db/tests/test_query_parser.py 2012-05-15 16:23:19 +0000
231+++ u1db/tests/test_query_parser.py 2012-07-17 20:51:27 +0000
232@@ -24,6 +24,44 @@
233 trivial_raw_doc = {}
234
235
236+class TestMakeTree(tests.TestCase):
237+
238+ def assertParseError(self, definition):
239+ self.assertRaises(
240+ errors.IndexDefinitionParseError, query_parser.make_tree,
241+ definition)
242+
243+ def test_single_field(self):
244+ self.assertEqual(['f'], query_parser.make_tree('f'))
245+
246+ def test_single_mapping(self):
247+ query_parser.make_tree('mapping(field1)')
248+ self.assertEqual(
249+ ['mapping', ['field1']], query_parser.make_tree('mapping(field1)'))
250+
251+ def test_single_mapping_multiple_fields(self):
252+ self.assertEqual(
253+ ['mapping', ['field1', 'field2', 'field3']],
254+ query_parser.make_tree('mapping(field1, field2, field3)'))
255+
256+ def test_nested_mapping(self):
257+ self.assertEqual(
258+ ['mapping1', ['mapping2', ['field1', 'field2'],
259+ 'mapping3', ['field3', 'field4']]],
260+ query_parser.make_tree(
261+ 'mapping1(mapping2(field1, field2), '
262+ 'mapping3(field3, field4))'))
263+
264+ def test_parse_missing_close_paren(self):
265+ self.assertParseError("lower(a")
266+
267+ def test_parse_trailing_chars(self):
268+ self.assertParseError("lower(ab))")
269+
270+ def test_parse_empty_op(self):
271+ self.assertParseError("(ab)")
272+
273+
274 class TestStaticGetter(tests.TestCase):
275
276 def test_returns_string(self):
277@@ -322,6 +360,9 @@
278 def test_parse_unknown_op(self):
279 self.assertParseError("no_such_operation(field)")
280
281+ def test_parse_wrong_arg_type(self):
282+ self.assertParseError("number(field, fnord)")
283+
284 def test_parse_transformation(self):
285 getter = self.parse("lower(a)")
286 self.assertIsInstance(getter, query_parser.Lower)

Subscribers

People subscribed via source and target branches