Skip to content

Commit 2f0bc4f

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 ac999e8 commit 2f0bc4f

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
@@ -103,58 +103,51 @@ def to_protobuf(self):
103103
"""
104104
return self._pb
105105

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

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

159152
# Build a composite filter AND'd together.
160153
composite_filter = clone._pb.filter.composite_filter
@@ -320,7 +313,7 @@ def fetch(self, limit=None):
320313
321314
>>> from gcloud import datastore
322315
>>> dataset = datastore.get_dataset('dataset-id')
323-
>>> query = dataset.query('Person').filter('name =', 'Sally')
316+
>>> query = dataset.query('Person').filter('name', '=', 'Sally')
324317
>>> query.fetch()
325318
[<Entity object>, <Entity object>, ...]
326319
>>> query.fetch(1)

gcloud/datastore/test_query.py

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

80-
def test_filter_w_no_operator(self):
81-
query = self._makeOne()
82-
self.assertRaises(ValueError, query.filter, 'firstname', 'John')
83-
8480
def test_filter_w_unknown_operator(self):
8581
query = self._makeOne()
86-
self.assertRaises(ValueError, query.filter, 'firstname ~~', 'John')
82+
self.assertRaises(ValueError, query.filter, 'firstname', '~~', 'John')
8783

8884
def test_filter_w_known_operator(self):
8985
from gcloud.datastore import datastore_v1_pb2 as datastore_pb
9086

9187
query = self._makeOne()
92-
after = query.filter('firstname =', u'John')
88+
after = query.filter('firstname', '=', u'John')
9389
self.assertFalse(after is query)
9490
self.assertTrue(isinstance(after, self._getTargetClass()))
9591
q_pb = after.to_protobuf()
@@ -105,11 +101,11 @@ def test_filter_w_all_operators(self):
105101
from gcloud.datastore import datastore_v1_pb2 as datastore_pb
106102

107103
query = self._makeOne()
108-
query = query.filter('leq_prop <=', u'val1')
109-
query = query.filter('geq_prop >=', u'val2')
110-
query = query.filter('lt_prop <', u'val3')
111-
query = query.filter('gt_prop >', u'val4')
112-
query = query.filter('eq_prop =', u'val5')
104+
query = query.filter('leq_prop', '<=', u'val1')
105+
query = query.filter('geq_prop', '>=', u'val2')
106+
query = query.filter('lt_prop', '<', u'val3')
107+
query = query.filter('gt_prop', '>', u'val4')
108+
query = query.filter('eq_prop', '=', u'val5')
113109

114110
query_pb = query.to_protobuf()
115111
pb_values = [
@@ -136,7 +132,7 @@ def test_filter_w_known_operator_and_entity(self):
136132
other = Entity()
137133
other['firstname'] = u'John'
138134
other['lastname'] = u'Smith'
139-
after = query.filter('other =', other)
135+
after = query.filter('other', '=', other)
140136
self.assertFalse(after is query)
141137
self.assertTrue(isinstance(after, self._getTargetClass()))
142138
q_pb = after.to_protobuf()
@@ -152,6 +148,23 @@ def test_filter_w_known_operator_and_entity(self):
152148
self.assertEqual(props[1].name, 'lastname')
153149
self.assertEqual(props[1].value.string_value, u'Smith')
154150

151+
def test_filter_w_whitespace_property_name(self):
152+
from gcloud.datastore import datastore_v1_pb2 as datastore_pb
153+
154+
query = self._makeOne()
155+
PROPERTY_NAME = ' property with lots of space '
156+
after = query.filter(PROPERTY_NAME, '=', u'John')
157+
self.assertFalse(after is query)
158+
self.assertTrue(isinstance(after, self._getTargetClass()))
159+
q_pb = after.to_protobuf()
160+
self.assertEqual(q_pb.filter.composite_filter.operator,
161+
datastore_pb.CompositeFilter.AND)
162+
f_pb, = list(q_pb.filter.composite_filter.filter)
163+
p_pb = f_pb.property_filter
164+
self.assertEqual(p_pb.property.name, PROPERTY_NAME)
165+
self.assertEqual(p_pb.value.string_value, u'John')
166+
self.assertEqual(p_pb.operator, datastore_pb.PropertyFilter.EQUAL)
167+
155168
def test_ancestor_w_non_key_non_list(self):
156169
query = self._makeOne()
157170
self.assertRaises(TypeError, query.ancestor, object())
@@ -162,7 +175,7 @@ def test_ancestor_wo_existing_ancestor_query_w_key_and_propfilter(self):
162175
_ID = 123
163176
_NAME = u'NAME'
164177
key = Key(path=[{'kind': _KIND, 'id': _ID}])
165-
query = self._makeOne().filter('name =', _NAME)
178+
query = self._makeOne().filter('name', '=', _NAME)
166179
after = query.ancestor(key)
167180
self.assertFalse(after is query)
168181
self.assertTrue(isinstance(after, self._getTargetClass()))
@@ -223,7 +236,7 @@ def test_ancestor_clears_existing_ancestor_query_w_others(self):
223236
_KIND = 'KIND'
224237
_ID = 123
225238
_NAME = u'NAME'
226-
query = self._makeOne().filter('name =', _NAME)
239+
query = self._makeOne().filter('name', '=', _NAME)
227240
between = query.ancestor([_KIND, _ID])
228241
after = between.ancestor(None)
229242
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)