Skip to content

Commit 266243b

Browse files
fix: preserve list meanings (#575)
1 parent 6db7e7c commit 266243b

File tree

2 files changed

+176
-38
lines changed

2 files changed

+176
-38
lines changed

google/cloud/datastore/helpers.py

+34-32
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@
1818
"""
1919

2020
import datetime
21-
import itertools
2221

2322
from google.protobuf import struct_pb2
2423
from google.type import latlng_pb2
@@ -43,36 +42,29 @@ def _get_meaning(value_pb, is_list=False):
4342
:param is_list: Boolean indicating if the ``value_pb`` contains
4443
a list value.
4544
46-
:rtype: int
45+
:rtype: int | Tuple[Optional[int], Optional[list[int | None]]] | None
4746
:returns: The meaning for the ``value_pb`` if one is set, else
48-
:data:`None`. For a list value, if there are disagreeing
49-
means it just returns a list of meanings. If all the
50-
list meanings agree, it just condenses them.
47+
:data:`None`. For a list value, returns a tuple of
48+
the root meaning of the list, and a list of meanings
49+
of each sub-value. If subvalues are all empty, returns
50+
:data:`None` instead of a list.
5151
"""
5252
if is_list:
53+
root_meaning = value_pb.meaning or None
5354
values = value_pb.array_value.values
5455

55-
# An empty list will have no values, hence no shared meaning
56-
# set among them.
57-
if len(values) == 0:
58-
return None
59-
6056
# We check among all the meanings, some of which may be None,
6157
# the rest which may be enum/int values.
62-
all_meanings = [_get_meaning(sub_value_pb) for sub_value_pb in values]
63-
unique_meanings = set(all_meanings)
64-
65-
if len(unique_meanings) == 1:
66-
# If there is a unique meaning, we preserve it.
67-
return 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-
return all_meanings
71-
72-
elif value_pb.meaning: # Simple field (int32).
73-
return value_pb.meaning
74-
75-
return None
58+
sub_meanings = [sub_value_pb.meaning or None for sub_value_pb in values]
59+
if not any(meaning is not None for meaning in sub_meanings):
60+
sub_meanings = None
61+
if root_meaning is None and sub_meanings is None:
62+
# no meanings to save
63+
return None
64+
else:
65+
return root_meaning, sub_meanings
66+
else:
67+
return value_pb.meaning or None
7668

7769

7870
def _new_value_pb(entity_pb, name):
@@ -156,6 +148,10 @@ def entity_from_protobuf(pb):
156148
def _set_pb_meaning_from_entity(entity, name, value, value_pb, is_list=False):
157149
"""Add meaning information (from an entity) to a protobuf.
158150
151+
value_pb is assumed to have no `meaning` data currently present.
152+
This means if the entity's meaning data is None, this function will do nothing,
153+
rather than removing any existing data.
154+
159155
:type entity: :class:`google.cloud.datastore.entity.Entity`
160156
:param entity: The entity to be turned into a protobuf.
161157
@@ -181,14 +177,20 @@ def _set_pb_meaning_from_entity(entity, name, value, value_pb, is_list=False):
181177
if orig_value is not value:
182178
return
183179

184-
# For lists, we set meaning on each sub-element.
185-
if is_list:
186-
if not isinstance(meaning, list):
187-
meaning = itertools.repeat(meaning)
188-
val_iter = zip(value_pb.array_value.values, meaning)
189-
for sub_value_pb, sub_meaning in val_iter:
190-
if sub_meaning is not None:
191-
sub_value_pb.meaning = sub_meaning
180+
if meaning is None:
181+
# no meaning data to set
182+
return
183+
elif is_list:
184+
# for lists, set meaning on the root pb and on each sub-element
185+
root_meaning, sub_meaning_list = meaning
186+
if root_meaning is not None:
187+
value_pb.meaning = root_meaning
188+
if sub_meaning_list:
189+
for sub_value_pb, sub_meaning in zip(
190+
value_pb.array_value.values, sub_meaning_list
191+
):
192+
if sub_meaning is not None:
193+
sub_value_pb.meaning = sub_meaning
192194
else:
193195
value_pb.meaning = meaning
194196

tests/unit/test_helpers.py

+142-6
Original file line numberDiff line numberDiff line change
@@ -361,19 +361,21 @@ def test_entity_to_protobuf_w_variable_meanings():
361361
entity = Entity()
362362
name = "quux"
363363
entity[name] = values = [1, 20, 300]
364-
meaning = 9
365-
entity._meanings[name] = ([None, meaning, None], values)
364+
root_meaning = 31
365+
sub_meaning = 9
366+
entity._meanings[name] = ((root_meaning, [None, sub_meaning, None]), values)
366367
entity_pb = entity_to_protobuf(entity)
367368

368369
# Construct the expected protobuf.
369370
expected_pb = entity_pb2.Entity()
370371
value_pb = _new_value_pb(expected_pb, name)
372+
value_pb.meaning = root_meaning
371373
value0 = value_pb.array_value.values.add()
372374
value0.integer_value = values[0]
373375
# The only array entry with a meaning is the middle one.
374376
value1 = value_pb.array_value.values.add()
375377
value1.integer_value = values[1]
376-
value1.meaning = meaning
378+
value1.meaning = sub_meaning
377379
value2 = value_pb.array_value.values.add()
378380
value2.integer_value = values[2]
379381

@@ -1179,7 +1181,46 @@ def test__get_meaning_w_array_value():
11791181
sub_value_pb2.string_value = "bye"
11801182

11811183
result = _get_meaning(value_pb, is_list=True)
1182-
assert meaning == result
1184+
# should preserve sub-value meanings as list
1185+
assert (None, [meaning, meaning]) == result
1186+
1187+
1188+
def test__get_meaning_w_array_value_root_meaning():
1189+
from google.cloud.datastore_v1.types import entity as entity_pb2
1190+
from google.cloud.datastore.helpers import _get_meaning
1191+
1192+
value_pb = entity_pb2.Value()
1193+
meaning = 9
1194+
value_pb.meaning = meaning
1195+
sub_value_pb1 = value_pb._pb.array_value.values.add()
1196+
sub_value_pb2 = value_pb._pb.array_value.values.add()
1197+
1198+
sub_value_pb1.string_value = "hi"
1199+
sub_value_pb2.string_value = "bye"
1200+
1201+
result = _get_meaning(value_pb, is_list=True)
1202+
# should preserve sub-value meanings as list
1203+
assert (meaning, None) == result
1204+
1205+
1206+
def test__get_meaning_w_array_value_root_and_sub_meanings():
1207+
from google.cloud.datastore_v1.types import entity as entity_pb2
1208+
from google.cloud.datastore.helpers import _get_meaning
1209+
1210+
value_pb = entity_pb2.Value()
1211+
root_meaning = 9
1212+
sub_meaning = 3
1213+
value_pb.meaning = root_meaning
1214+
sub_value_pb1 = value_pb._pb.array_value.values.add()
1215+
sub_value_pb2 = value_pb._pb.array_value.values.add()
1216+
1217+
sub_value_pb1.meaning = sub_value_pb2.meaning = sub_meaning
1218+
sub_value_pb1.string_value = "hi"
1219+
sub_value_pb2.string_value = "bye"
1220+
1221+
result = _get_meaning(value_pb, is_list=True)
1222+
# should preserve sub-value meanings as list
1223+
assert (root_meaning, [sub_meaning, sub_meaning]) == result
11831224

11841225

11851226
def test__get_meaning_w_array_value_multiple_meanings():
@@ -1198,7 +1239,7 @@ def test__get_meaning_w_array_value_multiple_meanings():
11981239
sub_value_pb2.string_value = "bye"
11991240

12001241
result = _get_meaning(value_pb, is_list=True)
1201-
assert result == [meaning1, meaning2]
1242+
assert result == (None, [meaning1, meaning2])
12021243

12031244

12041245
def test__get_meaning_w_array_value_meaning_partially_unset():
@@ -1215,7 +1256,102 @@ def test__get_meaning_w_array_value_meaning_partially_unset():
12151256
sub_value_pb2.string_value = "bye"
12161257

12171258
result = _get_meaning(value_pb, is_list=True)
1218-
assert result == [meaning1, None]
1259+
assert result == (None, [meaning1, None])
1260+
1261+
1262+
def test__get_meaning_w_array_value_meaning_fully_unset():
1263+
from google.cloud.datastore_v1.types import entity as entity_pb2
1264+
from google.cloud.datastore.helpers import _get_meaning
1265+
1266+
value_pb = entity_pb2.Value()
1267+
sub_value_pb1 = value_pb._pb.array_value.values.add()
1268+
sub_value_pb2 = value_pb._pb.array_value.values.add()
1269+
1270+
sub_value_pb1.string_value = "hi"
1271+
sub_value_pb2.string_value = "bye"
1272+
1273+
result = _get_meaning(value_pb, is_list=True)
1274+
assert result is None
1275+
1276+
1277+
@pytest.mark.parametrize("orig_root_meaning", [0, 1])
1278+
@pytest.mark.parametrize("orig_sub_meaning", [0, 1])
1279+
def test__set_pb_meaning_w_array_value_fully_unset(orig_root_meaning, orig_sub_meaning):
1280+
"""
1281+
call _set_pb_meaning_from_entity with meaning=None data.
1282+
Should not touch proto's meaning field
1283+
"""
1284+
from google.cloud.datastore_v1.types import entity as entity_pb2
1285+
from google.cloud.datastore.helpers import _set_pb_meaning_from_entity
1286+
from google.cloud.datastore.entity import Entity
1287+
1288+
orig_pb = entity_pb2.Entity()
1289+
value_pb = orig_pb._pb.properties.get_or_create("value")
1290+
value_pb.meaning = orig_root_meaning
1291+
sub_value_pb1 = value_pb.array_value.values.add()
1292+
sub_value_pb1.meaning = orig_sub_meaning
1293+
1294+
entity = Entity(key="key")
1295+
entity._meanings = {"value": ((None, None), None)}
1296+
_set_pb_meaning_from_entity(entity, "value", None, value_pb, is_list=True)
1297+
assert value_pb.meaning == orig_root_meaning
1298+
assert value_pb.array_value.values[0].meaning == orig_sub_meaning
1299+
1300+
1301+
@pytest.mark.parametrize("orig_meaning", [0, 1])
1302+
def test__set_pb_meaning_w_value_unset(orig_meaning):
1303+
"""
1304+
call _set_pb_meaning_from_entity with meaning=None data.
1305+
Should not touch proto's meaning field
1306+
"""
1307+
from google.cloud.datastore_v1.types import entity as entity_pb2
1308+
from google.cloud.datastore.helpers import _set_pb_meaning_from_entity
1309+
from google.cloud.datastore.entity import Entity
1310+
1311+
orig_pb = entity_pb2.Entity()
1312+
value_pb = orig_pb._pb.properties.get_or_create("value")
1313+
value_pb.meaning = orig_meaning
1314+
1315+
entity = Entity(key="key")
1316+
entity._meanings = {"value": (None, None)}
1317+
_set_pb_meaning_from_entity(entity, "value", None, value_pb, is_list=False)
1318+
assert value_pb.meaning == orig_meaning
1319+
1320+
1321+
def test__array_w_meaning_end_to_end():
1322+
"""
1323+
Test proto->entity->proto with an array with a meaning field
1324+
"""
1325+
from google.cloud.datastore_v1.types import entity as entity_pb2
1326+
from google.cloud.datastore.helpers import entity_from_protobuf
1327+
from google.cloud.datastore.helpers import entity_to_protobuf
1328+
1329+
orig_pb = entity_pb2.Entity()
1330+
value_pb = orig_pb._pb.properties.get_or_create("value")
1331+
value_pb.meaning = 31
1332+
sub_value_pb1 = value_pb.array_value.values.add()
1333+
sub_value_pb1.double_value = 1
1334+
sub_value_pb1.meaning = 1
1335+
sub_value_pb2 = value_pb.array_value.values.add()
1336+
sub_value_pb2.double_value = 2
1337+
sub_value_pb3 = value_pb.array_value.values.add()
1338+
sub_value_pb3.double_value = 3
1339+
sub_value_pb3.meaning = 3
1340+
# convert to entity
1341+
entity = entity_from_protobuf(orig_pb._pb)
1342+
assert entity._meanings["value"][0] == (31, [1, None, 3])
1343+
assert entity._meanings["value"][1] == [1, 2, 3]
1344+
# convert back to pb
1345+
output_entity_pb = entity_to_protobuf(entity)
1346+
final_pb = output_entity_pb._pb.properties["value"]
1347+
assert final_pb.meaning == 31
1348+
assert len(final_pb.array_value.values) == 3
1349+
assert final_pb.array_value.values[0].meaning == 1
1350+
assert final_pb.array_value.values[0].double_value == 1
1351+
assert final_pb.array_value.values[1].meaning == 0
1352+
assert final_pb.array_value.values[1].double_value == 2
1353+
assert final_pb.array_value.values[2].meaning == 3
1354+
assert final_pb.array_value.values[2].double_value == 3
12191355

12201356

12211357
def _make_geopoint(*args, **kwargs):

0 commit comments

Comments
 (0)