Skip to content

Commit c3331eb

Browse files
committed
Merge pull request #1758 from dhermes/fix-1649
Allowing heterogeneous meanings for datastore list properties.
2 parents 023f316 + de913a1 commit c3331eb

File tree

2 files changed

+89
-30
lines changed

2 files changed

+89
-30
lines changed

gcloud/datastore/helpers.py

Lines changed: 57 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
"""
1919

2020
import datetime
21+
import itertools
2122

2223
from google.protobuf import struct_pb2
2324
from google.type import latlng_pb2
@@ -45,10 +46,9 @@ def _get_meaning(value_pb, is_list=False):
4546
4647
:rtype: int
4748
:returns: The meaning for the ``value_pb`` if one is set, else
48-
:data:`None`.
49-
:raises: :class:`ValueError <exceptions.ValueError>` if a list value
50-
has disagreeing meanings (in sub-elements) or has some
51-
elements with meanings and some without.
49+
:data:`None`. For a list value, if there are disagreeing
50+
means it just returns a list of meanings. If all the
51+
list meanings agree, it just condenses them.
5252
"""
5353
meaning = None
5454
if is_list:
@@ -59,15 +59,15 @@ def _get_meaning(value_pb, is_list=False):
5959

6060
# We check among all the meanings, some of which may be None,
6161
# the rest which may be enum/int values.
62-
all_meanings = set(_get_meaning(sub_value_pb)
63-
for sub_value_pb in value_pb.array_value.values)
64-
meaning = all_meanings.pop()
65-
# The value we popped off should have been unique. If not
66-
# then we can't handle a list with values that have more
67-
# than one meaning.
68-
if all_meanings:
69-
raise ValueError('Different meanings set on values '
70-
'within an array_value')
62+
all_meanings = [_get_meaning(sub_value_pb)
63+
for sub_value_pb in value_pb.array_value.values]
64+
unique_meanings = set(all_meanings)
65+
if len(unique_meanings) == 1:
66+
# If there is a unique meaning, we preserve it.
67+
meaning = unique_meanings.pop()
68+
else: # We know len(value_pb.array_value.values) > 0.
69+
# If the meaning is not unique, just return all of them.
70+
meaning = all_meanings
7171
elif value_pb.meaning: # Simple field (int32)
7272
meaning = value_pb.meaning
7373

@@ -155,6 +155,48 @@ def entity_from_protobuf(pb):
155155
return entity
156156

157157

158+
def _set_pb_meaning_from_entity(entity, name, value, value_pb,
159+
is_list=False):
160+
"""Add meaning information (from an entity) to a protobuf.
161+
162+
:type entity: :class:`gcloud.datastore.entity.Entity`
163+
:param entity: The entity to be turned into a protobuf.
164+
165+
:type name: string
166+
:param name: The name of the property.
167+
168+
:type value: object
169+
:param value: The current value stored as property ``name``.
170+
171+
:type value_pb: :class:`gcloud.datastore._generated.entity_pb2.Value`
172+
:param value_pb: The protobuf value to add meaning / meanings to.
173+
174+
:type is_list: bool
175+
:param is_list: (Optional) Boolean indicating if the ``value`` is
176+
a list value.
177+
"""
178+
if name not in entity._meanings:
179+
return
180+
181+
meaning, orig_value = entity._meanings[name]
182+
# Only add the meaning back to the protobuf if the value is
183+
# unchanged from when it was originally read from the API.
184+
if orig_value is not value:
185+
return
186+
187+
# For lists, we set meaning on each sub-element.
188+
if is_list:
189+
if not isinstance(meaning, list):
190+
meaning = itertools.repeat(meaning)
191+
val_iter = six.moves.zip(value_pb.array_value.values,
192+
meaning)
193+
for sub_value_pb, sub_meaning in val_iter:
194+
if sub_meaning is not None:
195+
sub_value_pb.meaning = sub_meaning
196+
else:
197+
value_pb.meaning = meaning
198+
199+
158200
def entity_to_protobuf(entity):
159201
"""Converts an entity into a protobuf.
160202
@@ -187,17 +229,8 @@ def entity_to_protobuf(entity):
187229
sub_value.exclude_from_indexes = True
188230

189231
# Add meaning information to protobuf.
190-
if name in entity._meanings:
191-
meaning, orig_value = entity._meanings[name]
192-
# Only add the meaning back to the protobuf if the value is
193-
# unchanged from when it was originally read from the API.
194-
if orig_value is value:
195-
# For lists, we set meaning on each sub-element.
196-
if value_is_list:
197-
for sub_value_pb in value_pb.array_value.values:
198-
sub_value_pb.meaning = meaning
199-
else:
200-
value_pb.meaning = meaning
232+
_set_pb_meaning_from_entity(entity, name, value, value_pb,
233+
is_list=value_is_list)
201234

202235
return entity_pb
203236

gcloud/datastore/test_helpers.py

Lines changed: 32 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -341,6 +341,32 @@ def test_meaning_with_change(self):
341341
# value stored.
342342
self._compareEntityProto(entity_pb, expected_pb)
343343

344+
def test_variable_meanings(self):
345+
from gcloud.datastore._generated import entity_pb2
346+
from gcloud.datastore.entity import Entity
347+
from gcloud.datastore.helpers import _new_value_pb
348+
349+
entity = Entity()
350+
name = 'quux'
351+
entity[name] = values = [1, 20, 300]
352+
meaning = 9
353+
entity._meanings[name] = ([None, meaning, None], values)
354+
entity_pb = self._callFUT(entity)
355+
356+
# Construct the expected protobuf.
357+
expected_pb = entity_pb2.Entity()
358+
value_pb = _new_value_pb(expected_pb, name)
359+
value0 = value_pb.array_value.values.add()
360+
value0.integer_value = values[0]
361+
# The only array entry with a meaning is the middle one.
362+
value1 = value_pb.array_value.values.add()
363+
value1.integer_value = values[1]
364+
value1.meaning = meaning
365+
value2 = value_pb.array_value.values.add()
366+
value2.integer_value = values[2]
367+
368+
self._compareEntityProto(entity_pb, expected_pb)
369+
344370

345371
class Test_key_from_protobuf(unittest2.TestCase):
346372

@@ -813,7 +839,7 @@ def test_array_value(self):
813839
result = self._callFUT(value_pb, is_list=True)
814840
self.assertEqual(meaning, result)
815841

816-
def test_array_value_disagreeing(self):
842+
def test_array_value_multiple_meanings(self):
817843
from gcloud.datastore._generated import entity_pb2
818844

819845
value_pb = entity_pb2.Value()
@@ -827,10 +853,10 @@ def test_array_value_disagreeing(self):
827853
sub_value_pb1.string_value = u'hi'
828854
sub_value_pb2.string_value = u'bye'
829855

830-
with self.assertRaises(ValueError):
831-
self._callFUT(value_pb, is_list=True)
856+
result = self._callFUT(value_pb, is_list=True)
857+
self.assertEqual(result, [meaning1, meaning2])
832858

833-
def test_array_value_partially_unset(self):
859+
def test_array_value_meaning_partially_unset(self):
834860
from gcloud.datastore._generated import entity_pb2
835861

836862
value_pb = entity_pb2.Value()
@@ -842,8 +868,8 @@ def test_array_value_partially_unset(self):
842868
sub_value_pb1.string_value = u'hi'
843869
sub_value_pb2.string_value = u'bye'
844870

845-
with self.assertRaises(ValueError):
846-
self._callFUT(value_pb, is_list=True)
871+
result = self._callFUT(value_pb, is_list=True)
872+
self.assertEqual(result, [meaning1, None])
847873

848874

849875
class TestGeoPoint(unittest2.TestCase):

0 commit comments

Comments
 (0)