Skip to content

Commit 1778430

Browse files
committed
Implement Key.compare_to_proto to check pb keys against existing.
Addresses sixth part of #451.
1 parent 1442143 commit 1778430

4 files changed

Lines changed: 170 additions & 22 deletions

File tree

gcloud/datastore/entity.py

Lines changed: 2 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -254,22 +254,8 @@ def save(self):
254254
transaction.add_auto_id_entity(self)
255255

256256
if isinstance(key_pb, datastore_pb.Key):
257-
# Update the path (which may have been altered).
258-
# NOTE: The underlying namespace can't have changed in a save().
259-
# The value of the dataset ID may have changed from implicit
260-
# (i.e. None, with the ID implied from the dataset.Dataset
261-
# object associated with the Entity/Key), but if it was
262-
# implicit before the save() we leave it as implicit.
263-
path = []
264-
for element in key_pb.path_element:
265-
key_part = {}
266-
for descriptor, value in element._fields.items():
267-
key_part[descriptor.name] = value
268-
path.append(key_part)
269-
# This is temporary. Will be addressed throughout #451.
270-
clone = key._clone()
271-
clone._path = path
272-
self._key = clone
257+
# Update the key (which may have been altered).
258+
self.key(self.key().compare_to_proto(key_pb))
273259

274260
return self
275261

gcloud/datastore/key.py

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,92 @@ def complete_key(self, id_or_name):
152152
new_key._flat_path += (id_or_name,)
153153
return new_key
154154

155+
def _validate_protobuf_dataset_id(self, protobuf):
156+
"""Checks that dataset ID on protobuf matches current one.
157+
158+
The value of the dataset ID may have changed from unprefixed
159+
(e.g. 'foo') to prefixed (e.g. 's~foo' or 'e~foo').
160+
161+
:type protobuf: :class:`gcloud.datastore.datastore_v1_pb2.Key`
162+
:param protobuf: A protobuf representation of the key. Expected to be
163+
returned after a datastore operation.
164+
165+
:rtype: :class:`str`
166+
"""
167+
proto_dataset_id = protobuf.partition_id.dataset_id
168+
if proto_dataset_id == self.dataset_id:
169+
return
170+
171+
# Since they don't match, we check to see if `proto_dataset_id` has a
172+
# prefix.
173+
unprefixed = None
174+
prefix = proto_dataset_id[:2]
175+
if prefix in ('s~', 'e~'):
176+
unprefixed = proto_dataset_id[2:]
177+
178+
if unprefixed != self.dataset_id:
179+
raise ValueError('Dataset ID on protobuf does not match.',
180+
proto_dataset_id, self.dataset_id)
181+
182+
def compare_to_proto(self, protobuf):
183+
"""Checks current key against a protobuf; updates if partial.
184+
185+
If the current key is partial, returns a new key that has been
186+
completed otherwise returns the current key.
187+
188+
The value of the dataset ID may have changed from implicit (i.e. None,
189+
with the ID implied from the dataset.Dataset object associated with the
190+
Entity/Key), but if it was implicit before, we leave it as implicit.
191+
192+
:type protobuf: :class:`gcloud.datastore.datastore_v1_pb2.Key`
193+
:param protobuf: A protobuf representation of the key. Expected to be
194+
returned after a datastore operation.
195+
196+
:rtype: :class:`gcloud.datastore.key.Key`
197+
:returns: The current key if not partial.
198+
:raises: `ValueError` if the namespace or dataset ID of `protobuf`
199+
don't match the current values or if the path from `protobuf`
200+
doesn't match.
201+
"""
202+
if self.namespace is None:
203+
if protobuf.partition_id.HasField('namespace'):
204+
raise ValueError('Namespace unset on key but set on protobuf.')
205+
elif protobuf.partition_id.namespace != self.namespace:
206+
raise ValueError('Namespace on protobuf does not match.',
207+
protobuf.partition_id.namespace, self.namespace)
208+
209+
# Check that dataset IDs match if not implicit.
210+
if self.dataset_id is not None:
211+
self._validate_protobuf_dataset_id(protobuf)
212+
213+
path = []
214+
for element in protobuf.path_element:
215+
key_part = {}
216+
for descriptor, value in element._fields.items():
217+
key_part[descriptor.name] = value
218+
path.append(key_part)
219+
220+
if path == self.path:
221+
return self
222+
223+
if not self.is_partial:
224+
raise ValueError('Proto path does not match completed key.',
225+
path, self.path)
226+
227+
last_part = path[-1]
228+
id_or_name = None
229+
if 'id' in last_part:
230+
id_or_name = last_part.pop('id')
231+
elif 'name' in last_part:
232+
id_or_name = last_part.pop('name')
233+
234+
# We have edited path by popping from the last part, so check again.
235+
if path != self.path:
236+
raise ValueError('Proto path does not match partial key.',
237+
path, self.path)
238+
239+
return self.complete_key(id_or_name)
240+
155241
def to_protobuf(self):
156242
"""Return a protobuf corresponding to the key.
157243

gcloud/datastore/test_entity.py

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -287,12 +287,7 @@ def get_entities(self, keys):
287287
return [self.get(key) for key in keys]
288288

289289
def allocate_ids(self, incomplete_key, num_ids):
290-
def clone_with_new_id(key, new_id):
291-
clone = key._clone()
292-
clone._path[-1]['id'] = new_id
293-
return clone
294-
return [clone_with_new_id(incomplete_key, i + 1)
295-
for i in range(num_ids)]
290+
return [incomplete_key.complete_key(i + 1) for i in range(num_ids)]
296291

297292

298293
class _Connection(object):

gcloud/datastore/test_key.py

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,87 @@ def test_complete_key_on_complete(self):
8484
key = self._makeOne('KIND', 1234)
8585
self.assertRaises(ValueError, key.complete_key, 5678)
8686

87+
def test_compare_to_proto_incomplete_w_id(self):
88+
_ID = 1234
89+
key = self._makeOne('KIND')
90+
pb = key.to_protobuf()
91+
pb.path_element[0].id = _ID
92+
new_key = key.compare_to_proto(pb)
93+
self.assertFalse(new_key is key)
94+
self.assertEqual(new_key.id, _ID)
95+
self.assertEqual(new_key.name, None)
96+
97+
def test_compare_to_proto_incomplete_w_name(self):
98+
_NAME = 'NAME'
99+
key = self._makeOne('KIND')
100+
pb = key.to_protobuf()
101+
pb.path_element[0].name = _NAME
102+
new_key = key.compare_to_proto(pb)
103+
self.assertFalse(new_key is key)
104+
self.assertEqual(new_key.id, None)
105+
self.assertEqual(new_key.name, _NAME)
106+
107+
def test_compare_to_proto_incomplete_w_incomplete(self):
108+
key = self._makeOne('KIND')
109+
pb = key.to_protobuf()
110+
new_key = key.compare_to_proto(pb)
111+
self.assertTrue(new_key is key)
112+
113+
def test_compare_to_proto_incomplete_w_bad_path(self):
114+
key = self._makeOne('KIND1', 1234, 'KIND2')
115+
pb = key.to_protobuf()
116+
pb.path_element[0].kind = 'NO_KIND'
117+
self.assertRaises(ValueError, key.compare_to_proto, pb)
118+
119+
def test_compare_to_proto_complete_w_id(self):
120+
key = self._makeOne('KIND', 1234)
121+
pb = key.to_protobuf()
122+
pb.path_element[0].id = 5678
123+
self.assertRaises(ValueError, key.compare_to_proto, pb)
124+
125+
def test_compare_to_proto_complete_w_name(self):
126+
key = self._makeOne('KIND', 1234)
127+
pb = key.to_protobuf()
128+
pb.path_element[0].name = 'NAME'
129+
self.assertRaises(ValueError, key.compare_to_proto, pb)
130+
131+
def test_compare_to_proto_complete_w_incomplete(self):
132+
key = self._makeOne('KIND', 1234)
133+
pb = key.to_protobuf()
134+
pb.path_element[0].ClearField('id')
135+
self.assertRaises(ValueError, key.compare_to_proto, pb)
136+
137+
def test_compare_to_proto_complete_diff_dataset(self):
138+
key = self._makeOne('KIND', 1234, dataset_id='DATASET')
139+
pb = key.to_protobuf()
140+
pb.partition_id.dataset_id = 's~' + key.dataset_id
141+
new_key = key.compare_to_proto(pb)
142+
self.assertTrue(new_key is key)
143+
144+
def test_compare_to_proto_complete_bad_dataset(self):
145+
key = self._makeOne('KIND', 1234, dataset_id='DATASET')
146+
pb = key.to_protobuf()
147+
pb.partition_id.dataset_id = 'BAD_PRE~' + key.dataset_id
148+
self.assertRaises(ValueError, key.compare_to_proto, pb)
149+
150+
def test_compare_to_proto_complete_valid_namespace(self):
151+
key = self._makeOne('KIND', 1234, namespace='NAMESPACE')
152+
pb = key.to_protobuf()
153+
new_key = key.compare_to_proto(pb)
154+
self.assertTrue(new_key is key)
155+
156+
def test_compare_to_proto_complete_namespace_unset_on_pb(self):
157+
key = self._makeOne('KIND', 1234, namespace='NAMESPACE')
158+
pb = key.to_protobuf()
159+
pb.partition_id.ClearField('namespace')
160+
self.assertRaises(ValueError, key.compare_to_proto, pb)
161+
162+
def test_compare_to_proto_complete_namespace_unset_on_key(self):
163+
key = self._makeOne('KIND', 1234)
164+
pb = key.to_protobuf()
165+
pb.partition_id.namespace = 'NAMESPACE'
166+
self.assertRaises(ValueError, key.compare_to_proto, pb)
167+
87168
def test_to_protobuf_defaults(self):
88169
from gcloud.datastore.datastore_v1_pb2 import Key as KeyPB
89170
_KIND = 'KIND'

0 commit comments

Comments
 (0)