Skip to content

Commit

Permalink
Allowing heterogeneous meanings for datastore list properties.
Browse files Browse the repository at this point in the history
  • Loading branch information
dhermes committed Apr 27, 2016
1 parent d857d20 commit de913a1
Show file tree
Hide file tree
Showing 2 changed files with 89 additions and 30 deletions.
81 changes: 57 additions & 24 deletions gcloud/datastore/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
"""

import datetime
import itertools

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

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

Expand Down Expand Up @@ -155,6 +155,48 @@ def entity_from_protobuf(pb):
return entity


def _set_pb_meaning_from_entity(entity, name, value, value_pb,
is_list=False):
"""Add meaning information (from an entity) to a protobuf.
:type entity: :class:`gcloud.datastore.entity.Entity`
:param entity: The entity to be turned into a protobuf.
:type name: string
:param name: The name of the property.
:type value: object
:param value: The current value stored as property ``name``.
:type value_pb: :class:`gcloud.datastore._generated.entity_pb2.Value`
:param value_pb: The protobuf value to add meaning / meanings to.
:type is_list: bool
:param is_list: (Optional) Boolean indicating if the ``value`` is
a list value.
"""
if name not in entity._meanings:
return

meaning, orig_value = entity._meanings[name]
# Only add the meaning back to the protobuf if the value is
# unchanged from when it was originally read from the API.
if orig_value is not value:
return

# For lists, we set meaning on each sub-element.
if is_list:
if not isinstance(meaning, list):
meaning = itertools.repeat(meaning)
val_iter = six.moves.zip(value_pb.array_value.values,
meaning)
for sub_value_pb, sub_meaning in val_iter:
if sub_meaning is not None:
sub_value_pb.meaning = sub_meaning
else:
value_pb.meaning = meaning


def entity_to_protobuf(entity):
"""Converts an entity into a protobuf.
Expand Down Expand Up @@ -187,17 +229,8 @@ def entity_to_protobuf(entity):
sub_value.exclude_from_indexes = True

# Add meaning information to protobuf.
if name in entity._meanings:
meaning, orig_value = entity._meanings[name]
# Only add the meaning back to the protobuf if the value is
# unchanged from when it was originally read from the API.
if orig_value is value:
# For lists, we set meaning on each sub-element.
if value_is_list:
for sub_value_pb in value_pb.array_value.values:
sub_value_pb.meaning = meaning
else:
value_pb.meaning = meaning
_set_pb_meaning_from_entity(entity, name, value, value_pb,
is_list=value_is_list)

return entity_pb

Expand Down
38 changes: 32 additions & 6 deletions gcloud/datastore/test_helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -341,6 +341,32 @@ def test_meaning_with_change(self):
# value stored.
self._compareEntityProto(entity_pb, expected_pb)

def test_variable_meanings(self):
from gcloud.datastore._generated import entity_pb2
from gcloud.datastore.entity import Entity
from gcloud.datastore.helpers import _new_value_pb

entity = Entity()
name = 'quux'
entity[name] = values = [1, 20, 300]
meaning = 9
entity._meanings[name] = ([None, meaning, None], values)
entity_pb = self._callFUT(entity)

# Construct the expected protobuf.
expected_pb = entity_pb2.Entity()
value_pb = _new_value_pb(expected_pb, name)
value0 = value_pb.array_value.values.add()
value0.integer_value = values[0]
# The only array entry with a meaning is the middle one.
value1 = value_pb.array_value.values.add()
value1.integer_value = values[1]
value1.meaning = meaning
value2 = value_pb.array_value.values.add()
value2.integer_value = values[2]

self._compareEntityProto(entity_pb, expected_pb)


class Test_key_from_protobuf(unittest2.TestCase):

Expand Down Expand Up @@ -813,7 +839,7 @@ def test_array_value(self):
result = self._callFUT(value_pb, is_list=True)
self.assertEqual(meaning, result)

def test_array_value_disagreeing(self):
def test_array_value_multiple_meanings(self):
from gcloud.datastore._generated import entity_pb2

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

with self.assertRaises(ValueError):
self._callFUT(value_pb, is_list=True)
result = self._callFUT(value_pb, is_list=True)
self.assertEqual(result, [meaning1, meaning2])

def test_array_value_partially_unset(self):
def test_array_value_meaning_partially_unset(self):
from gcloud.datastore._generated import entity_pb2

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

with self.assertRaises(ValueError):
self._callFUT(value_pb, is_list=True)
result = self._callFUT(value_pb, is_list=True)
self.assertEqual(result, [meaning1, None])


class TestGeoPoint(unittest2.TestCase):
Expand Down

0 comments on commit de913a1

Please sign in to comment.