Skip to content

Commit 0850558

Browse files
committed
Changing Query.filter to ask for operator and prop. name separately.
Also adding a test for property names with spaces in them. Fixes #424.
1 parent 1b9f1c2 commit 0850558

File tree

5 files changed

+52
-45
lines changed

5 files changed

+52
-45
lines changed

gcloud/datastore/connection.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -250,7 +250,8 @@ def run_query(self, dataset_id, query_pb, namespace=None):
250250
>>> from gcloud import datastore
251251
>>> connection = datastore.get_connection()
252252
>>> dataset = connection.dataset('dataset-id')
253-
>>> query = dataset.query().kind('MyKind').filter('property =', 'val')
253+
>>> query = dataset.query().kind('MyKind').filter(
254+
... 'property', '=', 'val')
254255
255256
Using the `fetch`` method...
256257

gcloud/datastore/demo/demo.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,11 +46,11 @@
4646
print(query.limit(2).fetch())
4747

4848
# Now let's check for Thing entities named 'Computer'
49-
print(query.filter('name =', 'Computer').fetch())
49+
print(query.filter('name', '=', 'Computer').fetch())
5050

5151
# If you want to filter by multiple attributes,
5252
# you can string .filter() calls together.
53-
print(query.filter('name =', 'Computer').filter('age =', 10).fetch())
53+
print(query.filter('name', '=', 'Computer').filter('age', '=', 10).fetch())
5454

5555
# You can also work inside a transaction.
5656
# (Check the official docs for explanations of what's happening here.)

gcloud/datastore/query.py

Lines changed: 17 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -104,58 +104,51 @@ def to_protobuf(self):
104104
"""
105105
return self._pb
106106

107-
def filter(self, expression, value):
108-
"""Filter the query based on an expression and a value.
107+
def filter(self, property_name, operator, value):
108+
"""Filter the query based on a property name, operator and a value.
109109
110110
This will return a clone of the current :class:`Query`
111111
filtered by the expression and value provided.
112112
113113
Expressions take the form of::
114114
115-
.filter('<property> <operator>', <value>)
115+
.filter('<property>', '<operator>', <value>)
116116
117117
where property is a property stored on the entity in the datastore
118118
and operator is one of ``OPERATORS``
119119
(ie, ``=``, ``<``, ``<=``, ``>``, ``>=``)::
120120
121121
>>> query = Query('Person')
122-
>>> filtered_query = query.filter('name =', 'James')
123-
>>> filtered_query = query.filter('age >', 50)
122+
>>> filtered_query = query.filter('name', '=', 'James')
123+
>>> filtered_query = query.filter('age', '>', 50)
124124
125125
Because each call to ``.filter()`` returns a cloned ``Query`` object
126126
we are able to string these together::
127127
128128
>>> query = Query('Person').filter(
129-
... 'name =', 'James').filter('age >', 50)
129+
... 'name', '=', 'James').filter('age', '>', 50)
130130
131-
:type expression: string
132-
:param expression: An expression of a property and an
133-
operator (ie, ``=``).
131+
:type property_name: string
132+
:param property_name: A property name.
133+
134+
:type operator: string
135+
:param operator: One of ``=``, ``<``, ``<=``, ``>``, ``>=``.
134136
135137
:type value: integer, string, boolean, float, None, datetime
136138
:param value: The value to filter on.
137139
138140
:rtype: :class:`Query`
139141
:returns: A Query filtered by the expression and value provided.
142+
:raises: `ValueError` if `operation` is not one of the specified
143+
values.
140144
"""
141145
clone = self._clone()
142146

143-
# Take an expression like 'property >=', and parse it into
144-
# useful pieces.
145-
property_name, operator = None, None
146-
expression = expression.strip()
147-
148-
# Use None to split on *any* whitespace.
149-
expr_pieces = expression.rsplit(None, 1)
150-
if len(expr_pieces) == 2:
151-
property_name, operator = expr_pieces
152-
property_name = property_name.strip()
153-
154-
# If no whitespace in `expression`, `operator` will be `None` and
155-
# self.OPERATORS[None] will be `None` as well.
156147
pb_op_enum = self.OPERATORS.get(operator)
157148
if pb_op_enum is None:
158-
raise ValueError('Invalid expression: "%s"' % expression)
149+
error_message = 'Invalid expression: "%s"' % (operator,)
150+
choices_message = 'Please use one of: =, <, <=, >, >=.'
151+
raise ValueError(error_message, choices_message)
159152

160153
# Build a composite filter AND'd together.
161154
composite_filter = clone._pb.filter.composite_filter
@@ -321,7 +314,7 @@ def fetch(self, limit=None):
321314
322315
>>> from gcloud import datastore
323316
>>> dataset = datastore.get_dataset('dataset-id')
324-
>>> query = dataset.query('Person').filter('name =', 'Sally')
317+
>>> query = dataset.query('Person').filter('name', '=', 'Sally')
325318
>>> query.fetch()
326319
[<Entity object>, <Entity object>, ...]
327320
>>> query.fetch(1)

gcloud/datastore/test_query.py

Lines changed: 27 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -80,19 +80,15 @@ def test_to_protobuf_w_kind(self):
8080
kq_pb, = list(q_pb.kind)
8181
self.assertEqual(kq_pb.name, _KIND)
8282

83-
def test_filter_w_no_operator(self):
84-
query = self._makeOne()
85-
self.assertRaises(ValueError, query.filter, 'firstname', 'John')
86-
8783
def test_filter_w_unknown_operator(self):
8884
query = self._makeOne()
89-
self.assertRaises(ValueError, query.filter, 'firstname ~~', 'John')
85+
self.assertRaises(ValueError, query.filter, 'firstname', '~~', 'John')
9086

9187
def test_filter_w_known_operator(self):
9288
from gcloud.datastore import datastore_v1_pb2 as datastore_pb
9389

9490
query = self._makeOne()
95-
after = query.filter('firstname =', u'John')
91+
after = query.filter('firstname', '=', u'John')
9692
self.assertFalse(after is query)
9793
self.assertTrue(isinstance(after, self._getTargetClass()))
9894
q_pb = after.to_protobuf()
@@ -108,11 +104,11 @@ def test_filter_w_all_operators(self):
108104
from gcloud.datastore import datastore_v1_pb2 as datastore_pb
109105

110106
query = self._makeOne()
111-
query = query.filter('leq_prop <=', u'val1')
112-
query = query.filter('geq_prop >=', u'val2')
113-
query = query.filter('lt_prop <', u'val3')
114-
query = query.filter('gt_prop >', u'val4')
115-
query = query.filter('eq_prop =', u'val5')
107+
query = query.filter('leq_prop', '<=', u'val1')
108+
query = query.filter('geq_prop', '>=', u'val2')
109+
query = query.filter('lt_prop', '<', u'val3')
110+
query = query.filter('gt_prop', '>', u'val4')
111+
query = query.filter('eq_prop', '=', u'val5')
116112

117113
query_pb = query.to_protobuf()
118114
pb_values = [
@@ -139,7 +135,7 @@ def test_filter_w_known_operator_and_entity(self):
139135
other = Entity()
140136
other['firstname'] = u'John'
141137
other['lastname'] = u'Smith'
142-
after = query.filter('other =', other)
138+
after = query.filter('other', '=', other)
143139
self.assertFalse(after is query)
144140
self.assertTrue(isinstance(after, self._getTargetClass()))
145141
q_pb = after.to_protobuf()
@@ -155,6 +151,23 @@ def test_filter_w_known_operator_and_entity(self):
155151
self.assertEqual(props[1].name, 'lastname')
156152
self.assertEqual(props[1].value.string_value, u'Smith')
157153

154+
def test_filter_w_whitespace_property_name(self):
155+
from gcloud.datastore import datastore_v1_pb2 as datastore_pb
156+
157+
query = self._makeOne()
158+
PROPERTY_NAME = ' property with lots of space '
159+
after = query.filter(PROPERTY_NAME, '=', u'John')
160+
self.assertFalse(after is query)
161+
self.assertTrue(isinstance(after, self._getTargetClass()))
162+
q_pb = after.to_protobuf()
163+
self.assertEqual(q_pb.filter.composite_filter.operator,
164+
datastore_pb.CompositeFilter.AND)
165+
f_pb, = list(q_pb.filter.composite_filter.filter)
166+
p_pb = f_pb.property_filter
167+
self.assertEqual(p_pb.property.name, PROPERTY_NAME)
168+
self.assertEqual(p_pb.value.string_value, u'John')
169+
self.assertEqual(p_pb.operator, datastore_pb.PropertyFilter.EQUAL)
170+
158171
def test_ancestor_w_non_key_non_list(self):
159172
query = self._makeOne()
160173
self.assertRaises(TypeError, query.ancestor, object())
@@ -165,7 +178,7 @@ def test_ancestor_wo_existing_ancestor_query_w_key_and_propfilter(self):
165178
_ID = 123
166179
_NAME = u'NAME'
167180
key = Key(path=[{'kind': _KIND, 'id': _ID}])
168-
query = self._makeOne().filter('name =', _NAME)
181+
query = self._makeOne().filter('name', '=', _NAME)
169182
after = query.ancestor(key)
170183
self.assertFalse(after is query)
171184
self.assertTrue(isinstance(after, self._getTargetClass()))
@@ -226,7 +239,7 @@ def test_ancestor_clears_existing_ancestor_query_w_others(self):
226239
_KIND = 'KIND'
227240
_ID = 123
228241
_NAME = u'NAME'
229-
query = self._makeOne().filter('name =', _NAME)
242+
query = self._makeOne().filter('name', '=', _NAME)
230243
between = query.ancestor([_KIND, _ID])
231244
after = between.ancestor(None)
232245
self.assertFalse(after is query)

regression/datastore.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -155,7 +155,7 @@ def test_save_key_self_reference(self):
155155
self.case_entities_to_delete.append(entity)
156156

157157
query = self.dataset.query('Person').filter(
158-
'linkedTo =', key).limit(2)
158+
'linkedTo', '=', key).limit(2)
159159

160160
stored_persons = query.fetch()
161161
self.assertEqual(len(stored_persons), 1)
@@ -199,15 +199,15 @@ def test_limit_queries(self):
199199
self.assertEqual(len(new_character_entities), characters_remaining)
200200

201201
def test_query_simple_filter(self):
202-
query = self._base_query().filter('appearances >=', 20)
202+
query = self._base_query().filter('appearances', '>=', 20)
203203
expected_matches = 6
204204
# We expect 6, but allow the query to get 1 extra.
205205
entities = query.fetch(limit=expected_matches + 1)
206206
self.assertEqual(len(entities), expected_matches)
207207

208208
def test_query_multiple_filters(self):
209209
query = self._base_query().filter(
210-
'appearances >=', 26).filter('family =', 'Stark')
210+
'appearances', '>=', 26).filter('family', '=', 'Stark')
211211
expected_matches = 4
212212
# We expect 4, but allow the query to get 1 extra.
213213
entities = query.fetch(limit=expected_matches + 1)
@@ -225,7 +225,7 @@ def test_query___key___filter(self):
225225
rickard_key = datastore.key.Key(
226226
path=[populate_datastore.ANCESTOR, populate_datastore.RICKARD])
227227

228-
query = self._base_query().filter('__key__ =', rickard_key)
228+
query = self._base_query().filter('__key__', '=', rickard_key)
229229
expected_matches = 1
230230
# We expect 1, but allow the query to get 1 extra.
231231
entities = query.fetch(limit=expected_matches + 1)

0 commit comments

Comments
 (0)